schat2-clive/src/games/letters.js

190 lines
5.9 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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) {
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
});
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.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. Start with ${config.prefix}letters, and fill the board by selecting a vow(el), a con(sonant), or multiple at once like CCVVCCVVC.`,
onCommand,
onMessage,
};