'use strict'; const config = require('config'); const timers = require('timers/promises'); const style = require('../utils/style'); const getLeaders = require('../utils/get-leaders'); const getWordKey = require('../utils/get-word-key'); const pickRandom = require('../utils/pick-random'); const words = require('../../assets/mash-words.json'); const availableLetters = { vowel: ['a', 'e', 'i', 'o', 'u'], consonant: ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'], // Countdown regards y as a consonant }; const types = { v: 'vowel', c: 'consonant' }; const settings = { ...config.letters }; const games = new Map(); function getBoard(context) { const game = games.get(context.room.id); return `${style.grey(style.code('['))}${game.word.split('').concat(Array.from({ length: config.letters.length - game.word.length })).map((letter) => style.bold(style.code(letter?.toUpperCase() || ' '))).join(style.grey(style.code('|')))}${style.grey(style.code(']'))}`; // eslint-disable-line no-irregular-whitespace } function countLetters(word) { return word.split('').reduce((counts, letter) => ({ ...counts, [letter]: (counts[letter] || 0) + 1 }), {}); } function playWord(rawWord, context) { const game = games.get(context.room.id); const word = rawWord.trim().toLowerCase(); if (!word || word.length < 3 || word.length > game.word.length) { return; } if (game.played.has(word)) { context.logger.debug(`${context.user.username} played a word that was already played: ${word}`); return; } if (!words[word.length]?.[getWordKey(word)]?.some((definition) => definition.word === word)) { context.logger.debug(`${context.user.username} played word not in the dictionary: ${word}`); return; } const counts = countLetters(word); const invalid = Object.entries(counts).filter(([letter, count]) => !game.counts[letter] || game.counts[letter] < count).map(([letter]) => letter); if (invalid.length > 0) { context.logger.debug(`${context.user.username} played '${word}' containing letters that are not on the board: ${invalid.join(' ')}`); return; } context.setPoints(context.user, word.length); game.points[context.user.username] = (game.points[context.user.username] || 0) + word.length; game.played.add(word); context.sendMessage(`${context.user.username} played ${style.bold(style.pink(word))} for ${style.bold(word.length)} points!`, context.room.id); } function stop(context, aborted) { const game = games.get(context.room.id); game.ac.abort(); games.delete(context.room.id); if (aborted) { context.sendMessage(`The game was stopped by ${style.cyan(`${config.usernamePrefix}${context.user.username}`)}. Best players: ${getLeaders(game.points)}`, context.room.id); } } async function play(context) { const game = games.get(context.room.id); game.state = 'words'; game.counts = countLetters(game.word); context.sendMessage(`${getBoard(context)} Let's start!`, context.room.id); try { await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal }); context.sendMessage(`${getBoard(context)} ${style.bold(style.green(`${Math.round((settings.timeout / 3) * 2)} seconds`))} left`, context.room.id); await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal }); context.sendMessage(`${getBoard(context)} ${style.bold(style.green(`${Math.round(settings.timeout / 3)} seconds`))} left`, context.room.id); await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal }); context.sendMessage(`${style.bold('Time\'s up!')} Best players: ${getLeaders(game.points)}`, context.room.id); stop(context); } catch (error) { // abort expected, probably not an error } } function pickLetters(type, context) { const game = games.get(context.room.id); if (!game || game.word.length === config.letters.length) { return; } if (type === 'consonant' || type === 'vowel') { game.word = `${game.word}${pickRandom(availableLetters[type])}`; } else { type.toLowerCase().slice(0, config.letters.length - game.word.length).split('').forEach((typeKey) => { game.word = `${game.word}${pickRandom(availableLetters[types[typeKey]])}`; }); } if (game.word.length === config.letters.length) { play(context); return; } context.sendMessage(`${getBoard(context)} Would you like a consonant or a vowel?`, context.room.id); } function start(context, letters) { if (games.has(context.room.id)) { context.sendMessage(`${getBoard(context)} This is the current board. Use ${config.prefix}letters:stop to reset.`, context.room.id); return; } games.set(context.room.id, { state: 'letters', word: '', played: new Set(), points: {}, ac: new AbortController(), // eslint-disable-line no-undef }); if (letters) { pickLetters(letters.toLowerCase(), context); return; } context.sendMessage('Let\'s play the letters! Would you like a consonant or a vowel?', context.room.id); } function onCommand(args, context) { if (context.subcommand === 'stop') { stop(context, true); return; } if (context.command === 'letsgo' || ['start', 'go', 'auto', 'random'].includes(context.subcommand)) { start(context, Array.from({ length: config.letters.length }, () => (Math.random() < 0.55 ? 'c' : 'v')).join('')); // a slight bias towards consonants seems to give better boards return; } if (!context.subcommand) { start(context); } } function onMessage(message, context) { const game = games.get(context.room?.id); if (message.type !== 'message' || context.user?.id === config.user?.id) { // stop listening to yourself return; } if (game?.state === 'letters') { const multi = message.body.match(/\b[vc]{2,}\b/i)?.[0]; if (multi) { pickLetters(multi.toLowerCase(), context); return; } if (/(^|\b)cons?(onant)?($|\b)/i.test(message.body)) { pickLetters('consonant', context); return; } if (/(^|\b)vow(el)?($|\b)/i.test(message.body)) { pickLetters('vowel', context); return; } } if (game?.state === 'words') { playWord(message.body, context); } } module.exports = { name: 'Letters', help: `Make the longest word from the ${config.letters.length} letters on the board. Quick-start with ${config.prefix}letsgo, or use ${config.prefix}letters to fill the board by manually selecting a vow(el), a con(sonant), or multiple at once like CCVVCCVVC.`, commands: ['letsgo'], onCommand, onMessage, };