Added numbers game.

This commit is contained in:
Niels Simenon 2023-04-10 05:54:27 +02:00
parent 8c34fe5013
commit 4fbd366bb9
7 changed files with 2120 additions and 16 deletions

View File

@ -28,7 +28,7 @@ module.exports = {
'mash', 'mash',
'trivia', 'trivia',
'letters', 'letters',
// 'numbers', 'numbers',
'hunt', 'hunt',
'8ball', '8ball',
'geo', 'geo',
@ -101,6 +101,11 @@ module.exports = {
length: 9, length: 9,
timeout: 60, timeout: 60,
}, },
numbers: {
length: 6,
timeout: 60,
points: [10, 7, 7, 7, 7, 7, 5, 5, 5, 5, 5], // points by distance
},
riddle: { riddle: {
timeout: 30, timeout: 30,
score: false, score: false,

View File

@ -66,6 +66,11 @@ function playWord(rawWord, context) {
function stop(context, aborted) { function stop(context, aborted) {
const game = games.get(context.room.id); const game = games.get(context.room.id);
if (!game) {
context.sendMessage(`There is no letters game going on, ${context.user.prefixedUsername}. You can start one with ${config.prefix}letters!`, context.room.id);
return;
}
game.ac.abort(); game.ac.abort();
games.delete(context.room.id); games.delete(context.room.id);
@ -124,7 +129,7 @@ function pickLetters(type, context) {
function start(context, letters) { function start(context, letters) {
if (games.has(context.room.id)) { 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); context.sendMessage(`${getBoard(context)} is the current board. Use ${config.prefix}letters:stop to reset.`, context.room.id);
return; return;
} }

View File

@ -1,14 +1,142 @@
'use strict'; 'use strict';
const config = require('config'); const config = require('config');
const { evaluate } = require('mathjs'); const crypto = require('crypto');
const timers = require('timers/promises');
const {
create,
addDependencies,
divideDependencies,
evaluateDependencies,
} = require('mathjs');
const getLeaders = require('../utils/get-leaders');
const pickRandom = require('../utils/pick-random');
const style = require('../utils/style');
const { solveAll } = require('../utils/numbers-solver');
const games = new Map(); const games = new Map();
const math = create({
addDependencies,
divideDependencies,
evaluateDependencies,
});
const settings = config.numbers;
/* eslint-disable no-irregular-whitespace */
function padNumber(number) {
if (!number) {
return '';
}
const string = String(number);
if (string.length === 3) {
return string;
}
if (string.length === 2) {
return `${string}`;
}
return `${string}`;
}
function getTarget(game) {
return `${style.grey(style.code('['))}${style.bold(style.code(game.target))}${style.grey(style.code(']'))}`;
}
function getBoardNumbers(game) {
// return `\`[\`${game.numbers.concat(Array.from({ length: config.numbers.length - game.numbers.length })).map((number) => style.bold(`\`${padNumber(number)}\``)).join('`|`')}\`]\``;
return `${style.grey(style.code('['))}${game.numbers.concat(Array.from({ length: config.numbers.length - game.numbers.length })).map((number) => style.bold(style.code(padNumber(number)))).join(style.grey(style.code('|')))}${style.grey(style.code(']'))}`;
}
function getBoard(context) { function getBoard(context) {
const game = games.get(context.room.id); const game = games.get(context.room.id);
return game; if (game.target) {
return `${getBoardNumbers(game)} and the target is ${getTarget(game)}`;
}
return getBoardNumbers(game);
}
function countNumbers(numbers) {
return numbers.reduce((acc, number) => ({ ...acc, [number]: (acc[number] || 0) + 1 }), {});
}
function stop(context, aborted) {
const game = games.get(context.room.id);
if (!game) {
context.sendMessage(`There is no numbers game going on, ${context.user.prefixedUsername}. You can start one with ${config.prefix}numbers!`, context.room.id);
return;
}
game.ac.abort();
games.delete(context.room.id);
if (game.winner) {
context.setPoints(game.winner, game.points[game.winner.username]);
}
const wrapText = aborted
? `The game was stopped by ${style.cyan(`${config.usernamePrefix}${context.user.username}`)}.`
: style.bold('Time\'s up!');
const winnerText = game.winner
? `The winner is ${style.bold(game.winner.prefixedUsername)}, who gets ${style.bold(game.points[game.winner.username])} points.`
: 'No one found a solution.';
const leaders = Object.keys(game.points).length > 1
? `Honorary mentions: ${getLeaders(game.points, context.user, { skip: [game.winner.username] })}.`
: null;
const solution = game.solution.answer === game.target
? `The target ${getTarget(game)} ${game.points[game.winner?.username] === 10 ? 'was' : 'could be'} solved as follows: ${style.bold(style.code(game.winner?.solution || game.solution.solution))}.`
: `The target ${getTarget(game)} was impossible, the closest answer is ${style.bold(game.solution.answer)} as follows: ${style.bold(style.code(game.solution.solution))}.`;
context.sendMessage([wrapText, winnerText, leaders, solution].filter(Boolean).join(' '), context.room.id);
}
async function play(context) {
const game = games.get(context.room.id);
game.counts = countNumbers(game.numbers);
game.target = crypto.randomInt(100, 999);
game.solutions = await solveAll(game.numbers, game.target, 3);
game.solution = game.solutions.reduce((closest, solution) => (!closest || Math.abs(game.target - solution.answer) < Math.abs(game.target - closest.answer) ? solution : closest), null);
game.state = 'solutions';
context.sendMessage(`${getBoard(context)}. You have ${style.bold(style.green(settings.timeout))} seconds, let's start!`, context.room.id);
context.logger.info(`Numbers game started by ${context.user.username}, numbers ${game.numbers.join('|')}, target ${game.target}, ${game.solution.answer === game.target ? 'exact' : 'closest'} solution for ${game.solution.answer} is: ${game.solution.solution}`);
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 });
stop(context);
} catch (error) {
// abort expected, probably not an error
}
}
function pickBig() {
return pickRandom([100, 75, 50, 25]);
}
function pickSmall() {
return pickRandom([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
} }
function pickNumbers(type, context) { function pickNumbers(type, context) {
@ -18,62 +146,161 @@ function pickNumbers(type, context) {
return; return;
} }
console.log('pick', type); if (type === 'big') {
game.numbers = game.numbers.concat(pickBig());
}
if (type === 'small') {
game.numbers = game.numbers.concat(pickSmall());
}
if (type !== 'small' && type !== 'big') {
type.toLowerCase().slice(0, config.numbers.length - game.numbers.length).split('').forEach((typeKey) => {
game.numbers = game.numbers.concat(typeKey === 'b' ? pickBig() : pickSmall());
});
}
if (game.numbers.length === config.numbers.length) {
play(context);
return;
}
context.sendMessage(`${getBoard(context)} Would you like a big number or a small one?`, context.room.id);
} }
function playSolution(solution, context) { function playSolution(solution, context) {
const game = games.get(context.room.id);
try {
const parsed = math.parse(solution.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
const numbers = parsed.filter((node) => node.type === 'ConstantNode').map((node) => node.value);
const counts = countNumbers(numbers);
const imagined = Object.keys(counts).filter((number) => !game.counts[number]);
const overused = Object.entries(counts).filter(([number, count]) => count > game.counts[number]).map(([number]) => number);
if (imagined.length > 0) {
context.sendMessage(`You are using ${imagined.map((number) => style.bold(number)).join(' and ')} not on the board, ${context.user.prefixedUsername}`, context.room.id);
return;
} }
function start(context) { if (overused.length > 0) {
context.sendMessage(`You are using ${overused.map((number) => style.bold(number)).join(' and ')} too often, ${context.user.prefixedUsername}`, context.room.id);
return;
}
const answer = parsed.evaluate();
const distance = Math.abs(game.target - answer);
const points = config.numbers.points[distance];
const lastScore = game.points[context.user.username] || 0;
const highestScore = Object.values(game.points).reduce((acc, userPoints) => (userPoints > acc ? userPoints : acc), 0);
const summary = distance === 0
? `Your solution ${style.bold(style.code(parsed))} results in ${style.bold(answer)}, which is ${style.bold('exactly on target')}`
: `Your solution ${style.bold(style.code(parsed))} results in ${style.bold(answer)}, which is ${style.bold(distance)} away from target ${style.bold(game.target)}`;
if (points && points > lastScore) {
game.points[context.user.username] = points;
}
if (points && points > highestScore) {
game.winner = {
...context.user,
solution: parsed,
};
context.sendMessage(`${summary} for ${style.bold(points)} points, the current winner, ${context.user.prefixedUsername}`, context.room.id);
return;
}
if (points && points > lastScore) {
context.sendMessage(`${summary} for ${style.bold(points)} points, your personal best, but not beating the winning ${style.bold(highestScore)} points, ${context.user.prefixedUsername}`, context.room.id);
return;
}
if (points) {
context.sendMessage(`${summary} for ${style.bold(points)} points, but does not beat your personal best of ${style.bold(lastScore)} points, ${context.user.prefixedUsername}`, context.room.id);
return;
}
context.sendMessage(`${summary} for ${style.bold('no')} points, ${context.user.prefixedUsername}`, context.room.id);
} catch (error) {
// invalid answer
context.logger.error(error);
}
}
function start(context, numbers) {
if (games.has(context.room.id)) { if (games.has(context.room.id)) {
context.sendMessage(`${getBoard(context)} This is the current board. Use ${config.prefix}numbers:stop to reset.`); context.sendMessage(`${getBoard(context)} is the current board. Use ${config.prefix}numbers:stop to reset.`, context.room.id);
return; return;
} }
games.set(context.room.id, { games.set(context.room.id, {
state: 'pick', state: 'pick',
numbers: [],
target: null,
points: {},
winner: null,
ac: new AbortController(), // eslint-disable-line no-undef
}); });
if (numbers) {
pickNumbers(numbers, context);
return;
}
context.sendMessage('Let\'s play the numbers! Would you like a big number or a small one?', context.room.id); context.sendMessage('Let\'s play the numbers! Would you like a big number or a small one?', context.room.id);
} }
function onCommand(args, context) { function onCommand(args, context) {
if (context.subcommand === 'stop') {
stop(context, true);
return;
}
if (['numsgo', 'numgo'].includes(context.command) || ['start', 'go', 'auto', 'random'].includes(context.subcommand)) {
start(context, 'bbssss'); // two from the top, four from the bottom, please Rachel
return;
}
if (!args.subcommand) { if (!args.subcommand) {
start(context); start(context);
} }
} }
function onMessage(message, context) { function onMessage(message, context) {
console.log('message', message.body);
const game = games.get(context.room.id); const game = games.get(context.room.id);
const body = message.originalBody || message.body; // * gets resolved to <em>
if (game?.state === 'pick') { if (game?.state === 'pick') {
const multi = message.body.match(/\b[bs]{2,}\b/i)?.[0]; const multi = body.match(/\b[bs]{2,}\b/i)?.[0];
if (multi) { if (multi) {
pickNumbers(multi.toLowerCase(), context); pickNumbers(multi.toLowerCase(), context);
return; return;
} }
if (/big/i.test(message.body)) { if (/big/i.test(body)) {
pickNumbers('big', context); pickNumbers('big', context);
return; return;
} }
if (/small/i.test(message.body)) { if (/small/i.test(body)) {
pickNumbers('small', context); pickNumbers('small', context);
return; return;
} }
} }
if (game?.state === 'solutions') { if (game?.state === 'solutions') {
playSolution(message.body); playSolution(body, context);
} }
} }
module.exports = { module.exports = {
onCommand, onCommand,
onMessage, onMessage,
commands: ['numsgo', 'numgo'],
}; };

View File

@ -73,7 +73,7 @@ function getLeaderboard(game, { user, room, command }) {
return; return;
} }
game.sendMessage(`The top ${Math.min(Object.keys(leaderboard).length, 10)} ${style.italic(game.name)} players are: ${getLeaders(leaderboard, user, false, 10)}`, room.id); game.sendMessage(`The top ${Math.min(Object.keys(leaderboard).length, 10)} ${style.italic(game.name)} players are: ${getLeaders(leaderboard, user, { ping: false, limit: 10 })}`, room.id);
} }
/* eslint-disable no-irregular-whitespace */ /* eslint-disable no-irregular-whitespace */

View File

@ -3,8 +3,9 @@
const config = require('config'); const config = require('config');
const style = require('./style'); const style = require('./style');
function getLeaders(points, user, ping = true, limit = Infinity) { function getLeaders(points, user, { ping = true, limit = 20, skip = [] } = {}) {
return Object.entries(points) return Object.entries(points)
.filter(([userKey]) => !skip.includes(userKey))
.sort(([, scoreA], [, scoreB]) => scoreB - scoreA) .sort(([, scoreA], [, scoreB]) => scoreB - scoreA)
.slice(0, limit) .slice(0, limit)
.map(([userKey, score], index) => { .map(([userKey, score], index) => {

1866
src/utils/numbers-solver.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ function schatCode(text) {
function curate(fn) { function curate(fn) {
return (text) => { return (text) => {
if (text) { if (typeof text !== 'undefined' && text !== null) {
return fn(text); return fn(text);
} }