Added playable round to letters game.

This commit is contained in:
ThePendulum 2022-10-21 05:07:32 +02:00
parent 4534a1debe
commit 55343d5de7
7 changed files with 160 additions and 48 deletions

View File

@ -35,5 +35,6 @@ module.exports = {
},
letters: {
length: 9,
timeout: 30,
},
};

View File

@ -50,7 +50,7 @@ function onCommand(args, context) {
}
const messages = [
`How could you miss *that*, ${config.usernamePrefix}${context.user.username}...?!`,
`How could you miss ${style.italic('that')}, ${config.usernamePrefix}${context.user.username}...?!`,
`That's a miss! Better luck next time, ${config.usernamePrefix}${context.user.username}.`,
`The duck outsmarted you, ${config.usernamePrefix}${context.user.username}`,
`Channeling Gareth Southgate, ${config.usernamePrefix}${context.user.username}? You missed!`,

View File

@ -1,13 +1,19 @@
'use strict';
const config = require('config');
const timers = require('timers/promises');
const shuffle = require('../utils/shuffle');
const style = require('../utils/style');
const getLeaders = require('../utils/get-leaders');
const getWordKey = require('../utils/get-word-key');
const words = require('../../assets/mash-words.json');
const availableVowels = ['a', 'e', 'i', 'o', 'u'];
const availableConsonants = ['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: 'vowels', c: 'consonants' };
const settings = { ...config.letters };
const games = new Map();
function getBoard(context) {
@ -16,6 +22,10 @@ function getBoard(context) {
return `${game.word.split('').map((letter) => `${style.grey('[')}${letter.toUpperCase()}${style.grey(']')}`).join('')}${`${style.silver('[')} ${style.silver(']')}`.repeat(config.letters.length - game.word.length)}`;
}
function countLetters(word) {
return word.split('').reduce((counts, letter) => ({ ...counts, [letter]: (counts[letter] || 0) + 1 }), {});
}
function shuffleLetters(letters, acc = []) {
const shuffled = shuffle(letters, config.letters.length);
@ -26,21 +36,93 @@ function shuffleLetters(letters, acc = []) {
return [...acc, ...shuffled].slice(0, config.letters.length);
}
function playRound(context) {
context.sendMessage(`${getBoard(context)} Let's start!`, context.room.id);
function playWord(rawWord, context) {
const game = games.get(context.room.id);
const word = rawWord.trim().toLowerCase();
if (!word || 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 pickLetter(type, context) {
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(`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;
}
game.word = `${game.word}${game[type].pop()}`;
if (type === 'consonants' || type === 'vowels') {
game.word = `${game.word}${game[type].pop()}`;
} else {
type.toLowerCase().slice(0, config.letters.length - game.word.length).split('').forEach((typeKey) => {
game.word = `${game.word}${game[types[typeKey]].pop()}`;
});
}
if (game.word.length === config.letters.length) {
playRound(context);
play(context);
return;
}
@ -54,43 +136,63 @@ function start(context) {
}
games.set(context.room.id, {
state: 'letters',
word: '',
vowels: shuffleLetters(availableVowels),
consonants: shuffleLetters(availableConsonants),
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 stop(context) {
games.delete(context.room.id);
context.sendMessage('The game is stopped and reset', context.room.id);
}
function onCommand(args, context) {
if (args.subcommand === 'stop') {
stop(context);
if (context.subcommand === 'stop') {
stop(context, true);
return;
}
if (!args.subcommand) {
if (['help', 'commands'].includes(context.subcommand)) {
context.sendMessage('Make the longest word using the available letters. To pick the letters, say con(sonant), vow(el) or supply multiple: CCCCCVVVV. Available subcommands: :stop', context.room.id);
return;
}
if (!context.subcommand) {
start(context);
}
}
function onMessage(message, context) {
if (/(^|\b)cons?(onant)?($|\b)/i.test(message.body)) {
pickLetter('consonants', context);
return;
const game = games.get(context.room.id);
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('consonants', context);
return;
}
if (/(^|\b)vow(el)?($|\b)/i.test(message.body)) {
pickLetters('vowels', context);
return;
}
}
if (/(^|\b)vow(el)?($|\b)/i.test(message.body)) {
pickLetter('vowels', context);
if (game?.state === 'words') {
playWord(message.body, context);
}
}
module.exports = {
name: 'Letters',
onCommand,
onMessage,
};

View File

@ -3,6 +3,7 @@
const config = require('config');
const style = require('../utils/style');
const getWordKey = require('../utils/get-word-key');
const words = require('../../assets/mash-words.json');
const mashes = new Map();
@ -10,10 +11,6 @@ const mashes = new Map();
const defineCommands = ['define', 'dict', 'dictionary'];
const resolveCommands = ['solve', 'resolve', 'lookup'];
function getWordKey(word) {
return word.split('').sort().join('');
}
function start(length, context, attempt = 0) {
const mash = mashes.get(context.room.id);
const lengthWords = words[length];
@ -51,13 +48,13 @@ function start(length, context, attempt = 0) {
const newMash = mashes.get(context.room.id);
context.sendMessage(`Stomp stomp, here's your mash: ${style.bold(style.purple(newMash.anagram))}`, context.room.id);
context.sendMessage(`Stomp stomp, here's your mash: ${style.bold(style.pink(newMash.anagram))}`, context.room.id);
context.logger.info(`Mash started, '${anagram}' with answers ${answers.map((answer) => `'${answer.word}'`).join(', ')}`);
}
function play(rawWord, context, shouted) {
const mash = mashes.get(context.room.id);
const word = rawWord.toLowerCase();
const word = rawWord.trim().toLowerCase();
const key = getWordKey(word);
const answer = mash.answers.find((answerX) => answerX.word === word);
@ -159,7 +156,7 @@ function hint(context) {
}
if (mash.anagram.length === 4) {
context.sendMessage(`Hints for ${style.bold(style.purple(mash.anagram))}, ${config.usernamePrefix}${context.user.username}: ${mash.answers.map((answer) => `${style.bold(`${answer.word.slice(0, 1)} ${'_ '.repeat(answer.word.length - 1).trim()}`)} (${answer.definitions[0]})`).join(', ')}`, context.room.id);
context.sendMessage(`Hints for ${style.bold(style.pink(mash.anagram))}, ${config.usernamePrefix}${context.user.username}: ${mash.answers.map((answer) => `${style.bold(`${answer.word.slice(0, 1)} ${'_ '.repeat(answer.word.length - 1).trim()}`)} (${answer.definitions[0]})`).join(', ')}`, context.room.id);
return;
}
@ -192,7 +189,7 @@ function onCommand(args, context) {
}
if (!word && mash) {
context.sendMessage(`The current mash is: ${style.bold(style.purple(mash.anagram))}`, context.room.id);
context.sendMessage(`The current mash is: ${style.bold(style.pink(mash.anagram))}`, context.room.id);
return;
}

View File

@ -7,6 +7,7 @@ const { decode } = require('html-entities');
const questions = require('../../assets/jeopardy.json');
const shuffle = require('../utils/shuffle');
const style = require('../utils/style');
const getLeaders = require('../utils/get-leaders');
const settings = { ...config.trivia };
const help = {
@ -37,18 +38,6 @@ function scoreRound(context, round) {
}).filter(Boolean).join(', ');
}
function getLeaders(context) {
const game = games.get(context.room.id);
return Object.entries(game.points).sort(([, scoreA], [, scoreB]) => scoreB - scoreA).map(([username, score], index) => {
if (index === 0) {
return `${style.bold(`${config.usernamePrefix}${username}`)} with ${style.bold(`${score}`)} points`;
}
return `${style.bold(style.cyan(`${config.usernamePrefix}${username}`))} with ${style.bold(`${score}`)} points`;
}).join(', ');
}
async function playRound(context, round = 0) {
const game = games.get(context.room.id);
const ac = new AbortController(); // eslint-disable-line no-undef
@ -60,7 +49,7 @@ async function playRound(context, round = 0) {
const question = game.questions[round];
context.sendMessage(`${style.bold(style.purple(`Question ${round + 1}/${game.questions.length}`))} ${style.silver(`(${question.category})`)}: ${question.question}`, context.room.id);
context.sendMessage(`${style.bold(style.pink(`Question ${round + 1}/${game.questions.length}`))} ${style.silver(`(${question.category})`)}: ${question.question}`, context.room.id);
context.logger.info(`Trivia asked "${question.question}" with answer: ${question.answer}`);
try {
@ -70,10 +59,10 @@ async function playRound(context, round = 0) {
// replace space with U+2003 Em Space to separate words, since a single space separates the placeholders, and double spaces are removed during Markdown render
if (question.answer.length >= 3) {
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3) * 2} seconds`))} left, first hint for ${style.bold(style.purple(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${question.answer.slice(0, 1)} ${question.answer.slice(1).replace(/\s/g, '').replace(/[^\s]/g, '_ ').trim()}`)}`, context.room.id);
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3) * 2} seconds`))} left, first hint for ${style.bold(style.pink(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${question.answer.slice(0, 1)} ${question.answer.slice(1).replace(/\s/g, '').replace(/[^\s]/g, '_ ').trim()}`)}`, context.room.id);
} else {
// giving the first letter gives too much away, only give the placeholders
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3) * 2} seconds`))} left, first hint for ${style.bold(style.purple(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${question.answer.replace(/\s/g, '').replace(/[^\s]/g, '_ ').trim()}`)}`, context.room.id);
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3) * 2} seconds`))} left, first hint for ${style.bold(style.pink(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${question.answer.replace(/\s/g, '').replace(/[^\s]/g, '_ ').trim()}`)}`, context.room.id);
}
await timers.setTimeout((game.timeout / 3) * 1000, null, {
@ -81,7 +70,7 @@ async function playRound(context, round = 0) {
});
if (question.answer.length >= 4) {
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3)} seconds`))} left, second hint for ${style.bold(style.purple(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${question.answer.slice(0, 1)} ${question.answer.slice(1, -1).replace(/\s/g, '').replace(/[^\s]/g, '_ ')}${question.answer.slice(-1)}`)}`, context.room.id);
context.sendMessage(`${style.bold(style.green(`${Math.floor(game.timeout / 3)} seconds`))} left, second hint for ${style.bold(style.pink(`question ${round + 1}/${game.questions.length}`))}: ${style.bold(`${question.answer.slice(0, 1)} ${question.answer.slice(1, -1).replace(/\s/g, '').replace(/[^\s]/g, '_ ')}${question.answer.slice(-1)}`)}`, context.room.id);
}
await timers.setTimeout((game.timeout / 3) * 1000, null, {
@ -96,7 +85,7 @@ async function playRound(context, round = 0) {
}
if (game.stopped) {
context.sendMessage(`The game was stopped by ${style.cyan(`${config.usernamePrefix}${game.stopped.username}`)}. The answer to the last question was: ${style.bold(question.answer)}. Best players: ${getLeaders(context)}`, context.room.id);
context.sendMessage(`The game was stopped by ${style.cyan(`${config.usernamePrefix}${game.stopped.username}`)}. The answer to the last question was: ${style.bold(question.answer)}. Best players: ${getLeaders(game.points)}`, context.room.id);
games.delete(context.room.id);
return;
@ -120,7 +109,7 @@ async function playRound(context, round = 0) {
await timers.setTimeout(5000);
if (game.stopped) {
context.sendMessage(`The game was stopped by ${config.usernamePrefix}${game.stopped.username}. The answer to the last question was: ${style.bold(question.answer)}. Best players: ${getLeaders(context)}`, context.room.id);
context.sendMessage(`The game was stopped by ${config.usernamePrefix}${game.stopped.username}. The answer to the last question was: ${style.bold(question.answer)}. Best players: ${getLeaders(game.points)}`, context.room.id);
games.delete(context.room.id);
return;
@ -132,7 +121,7 @@ async function playRound(context, round = 0) {
await timers.setTimeout(2000);
context.sendMessage(`That's the end of the game! Best players: ${getLeaders(context)}`, context.room.id);
context.sendMessage(`That's the end of the game! Best players: ${getLeaders(game.points)}`, context.room.id);
games.delete(context.room.id);
}
@ -143,7 +132,7 @@ async function start(context) {
games.set(context.room.id, {
round: 0,
questions: roundQuestions,
answers: [],
answers: new Map(),
points: {},
...settings,
});

16
src/utils/get-leaders.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
const config = require('config');
const style = require('./style');
function getLeaders(points) {
return Object.entries(points).sort(([, scoreA], [, scoreB]) => scoreB - scoreA).map(([username, score], index) => {
if (index === 0) {
return `${style.bold(`${config.usernamePrefix}${username}`)} with ${style.bold(`${score}`)} points`;
}
return `${style.bold(style.cyan(`${config.usernamePrefix}${username}`))} with ${style.bold(`${score}`)} points`;
}).join(', ');
}
module.exports = getLeaders;

View File

@ -0,0 +1,7 @@
'use strict';
function getWordKey(word) {
return word.split('').sort().join('');
}
module.exports = getWordKey;