201 lines
6.3 KiB
JavaScript
Executable File
201 lines
6.3 KiB
JavaScript
Executable File
'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,
|
||
};
|