Disallowing fractions in numbers game, fixed stop output, added generic calculator to numbers.
This commit is contained in:
parent
ea672df4c3
commit
cfcb48224b
|
@ -85,14 +85,14 @@ async function play(context) {
|
||||||
game.state = 'words';
|
game.state = 'words';
|
||||||
game.counts = countLetters(game.word);
|
game.counts = countLetters(game.word);
|
||||||
|
|
||||||
context.sendMessage(`${getBoard(context)} Let's start!`, context.room.id);
|
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(settings.timeout))}, let's start!`, context.room.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
|
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);
|
context.sendMessage(`${getBoard(context)} You have ${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 });
|
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);
|
context.sendMessage(`${getBoard(context)} You have ${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 });
|
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);
|
context.sendMessage(`${style.bold('Time\'s up!')} Best players: ${getLeaders(game.points)}`, context.room.id);
|
||||||
|
|
|
@ -6,6 +6,7 @@ const timers = require('timers/promises');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
create,
|
create,
|
||||||
|
parse,
|
||||||
addDependencies,
|
addDependencies,
|
||||||
divideDependencies,
|
divideDependencies,
|
||||||
evaluateDependencies,
|
evaluateDependencies,
|
||||||
|
@ -16,15 +17,35 @@ const pickRandom = require('../utils/pick-random');
|
||||||
const style = require('../utils/style');
|
const style = require('../utils/style');
|
||||||
const { solveAll } = require('../utils/numbers-solver');
|
const { solveAll } = require('../utils/numbers-solver');
|
||||||
|
|
||||||
const games = new Map();
|
const { parse: limitedParse, divide: mathDivide, import: mathImport } = create({
|
||||||
|
|
||||||
const math = create({
|
|
||||||
addDependencies,
|
addDependencies,
|
||||||
divideDependencies,
|
divideDependencies,
|
||||||
evaluateDependencies,
|
evaluateDependencies,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class FractionError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'FractionError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function divide(a, b) {
|
||||||
|
const result = mathDivide(a, b);
|
||||||
|
|
||||||
|
if (result % 1) {
|
||||||
|
throw new FractionError(`${style.bold(style.code(`${a} / ${b}`))} results in a fraction`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
mathImport({
|
||||||
|
divide,
|
||||||
|
}, { override: true });
|
||||||
|
|
||||||
const settings = config.numbers;
|
const settings = config.numbers;
|
||||||
|
const games = new Map();
|
||||||
|
|
||||||
/* eslint-disable no-irregular-whitespace */
|
/* eslint-disable no-irregular-whitespace */
|
||||||
function padNumber(number) {
|
function padNumber(number) {
|
||||||
|
@ -68,6 +89,39 @@ function countNumbers(numbers) {
|
||||||
return numbers.reduce((acc, number) => ({ ...acc, [number]: (acc[number] || 0) + 1 }), {});
|
return numbers.reduce((acc, number) => ({ ...acc, [number]: (acc[number] || 0) + 1 }), {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWinnerText(game) {
|
||||||
|
if (game.state === 'pick') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.winner) {
|
||||||
|
return `The winner is ${style.bold(game.winner.prefixedUsername)}, who gets ${style.bold(game.points[game.winner.username])} points.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'No one found a solution.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSolutionText(game) {
|
||||||
|
if (game.points[game.winner?.username] === 10 && game.solution?.answer === game.target) {
|
||||||
|
return `The target ${getTarget(game)} was solved as follows: ${style.bold(style.code(game.winner.solution))}. My solution was ${style.bold(style.code(game.solution.solution))}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.points[game.winner?.username] === 10) {
|
||||||
|
// the winner somehow found a solution the solver did not
|
||||||
|
return `The target ${getTarget(game)} was solved as follows: ${style.bold(style.code(game.winner.solution))}. Even I couldn't get that, well done!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.solution?.answer === game.target) {
|
||||||
|
return `The target ${getTarget(game)} could be solved as follows: ${style.bold(style.code(game.solution.solution))}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.solution) {
|
||||||
|
return `The target ${getTarget(game)} was impossible, the closest answer is ${style.bold(game.solution.answer)} as follows: ${style.bold(style.code(game.solution.solution))}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function stop(context, aborted) {
|
function stop(context, aborted) {
|
||||||
const game = games.get(context.room.id);
|
const game = games.get(context.room.id);
|
||||||
|
|
||||||
|
@ -87,19 +141,15 @@ function stop(context, aborted) {
|
||||||
? `The game was stopped by ${style.cyan(`${config.usernamePrefix}${context.user.username}`)}.`
|
? `The game was stopped by ${style.cyan(`${config.usernamePrefix}${context.user.username}`)}.`
|
||||||
: style.bold('Time\'s up!');
|
: style.bold('Time\'s up!');
|
||||||
|
|
||||||
const winnerText = game.winner
|
const winnerText = getWinnerText(game);
|
||||||
? `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
|
const leaders = Object.keys(game.points).length > 1
|
||||||
? `Honorary mentions: ${getLeaders(game.points, context.user, { skip: [game.winner.username] })}.`
|
? `Honorary mentions: ${getLeaders(game.points, context.user, { skip: [game.winner.username] })}.`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const solution = game.solution.answer === game.target
|
const solutionText = getSolutionText(game);
|
||||||
? `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);
|
context.sendMessage([wrapText, winnerText, leaders, solutionText].filter(Boolean).join(' '), context.room.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function play(context) {
|
async function play(context) {
|
||||||
|
@ -118,10 +168,10 @@ async function play(context) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
|
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);
|
context.sendMessage(`${getBoard(context)}. You have ${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 });
|
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);
|
context.sendMessage(`${getBoard(context)}. You have ${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 });
|
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
|
||||||
|
|
||||||
|
@ -156,7 +206,7 @@ function pickNumbers(type, context) {
|
||||||
|
|
||||||
if (type !== 'small' && type !== 'big') {
|
if (type !== 'small' && type !== 'big') {
|
||||||
type.toLowerCase().slice(0, config.numbers.length - game.numbers.length).split('').forEach((typeKey) => {
|
type.toLowerCase().slice(0, config.numbers.length - game.numbers.length).split('').forEach((typeKey) => {
|
||||||
game.numbers = game.numbers.concat(typeKey === 'b' ? pickBig() : pickSmall());
|
game.numbers = game.numbers.concat(typeKey === 'b' || typeKey === 'l' ? pickBig() : pickSmall());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +222,7 @@ function playSolution(solution, context) {
|
||||||
const game = games.get(context.room.id);
|
const game = games.get(context.room.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = math.parse(solution.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
|
const parsed = limitedParse(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 numbers = parsed.filter((node) => node.type === 'ConstantNode').map((node) => node.value);
|
||||||
const counts = countNumbers(numbers);
|
const counts = countNumbers(numbers);
|
||||||
const imagined = Object.keys(counts).filter((number) => !game.counts[number]);
|
const imagined = Object.keys(counts).filter((number) => !game.counts[number]);
|
||||||
|
@ -196,7 +246,7 @@ function playSolution(solution, context) {
|
||||||
|
|
||||||
const summary = distance === 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('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)}`;
|
: `Your solution ${style.bold(style.code(parsed))} results in ${style.bold(answer)}, which is ${style.bold(distance)} away from target ${getTarget(game)}`;
|
||||||
|
|
||||||
if (points && points > lastScore) {
|
if (points && points > lastScore) {
|
||||||
game.points[context.user.username] = points;
|
game.points[context.user.username] = points;
|
||||||
|
@ -227,7 +277,12 @@ function playSolution(solution, context) {
|
||||||
|
|
||||||
context.sendMessage(`${summary} for ${style.bold('no')} points, ${context.user.prefixedUsername}`, context.room.id);
|
context.sendMessage(`${summary} for ${style.bold('no')} points, ${context.user.prefixedUsername}`, context.room.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// invalid answer
|
// invalid solution
|
||||||
|
if (error.name === 'FractionError') {
|
||||||
|
context.sendMessage(`${error.message}, which is not allowed, ${context.user.prefixedUsername}`, context.room.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
context.logger.error(error);
|
context.logger.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,12 +310,28 @@ function start(context, numbers) {
|
||||||
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 solve(calculation, context) {
|
||||||
|
try {
|
||||||
|
const parsed = parse(calculation.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
|
||||||
|
const answer = parsed.evaluate();
|
||||||
|
|
||||||
|
context.sendMessage(`${style.bold(style.code(parsed))} = ${style.bold(answer)}`, context.room.id);
|
||||||
|
} catch (error) {
|
||||||
|
context.sendMessage(`Failed to solve ${style.bold(style.code(calculation))}: ${error.message}`, context.room.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onCommand(args, context) {
|
function onCommand(args, context) {
|
||||||
if (context.subcommand === 'stop') {
|
if (context.subcommand === 'stop') {
|
||||||
stop(context, true);
|
stop(context, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['calculate', 'calc', 'solve'].includes(context.subcommand || context.command)) {
|
||||||
|
solve(args.join(' '), context); // two from the top, four from the bottom, please Rachel
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (['numsgo', 'numgo'].includes(context.command) || ['start', 'go', 'auto', 'random'].includes(context.subcommand)) {
|
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
|
start(context, 'bbssss'); // two from the top, four from the bottom, please Rachel
|
||||||
return;
|
return;
|
||||||
|
@ -276,7 +347,7 @@ function onMessage(message, context) {
|
||||||
const body = message.originalBody || message.body; // * gets resolved to <em>
|
const body = message.originalBody || message.body; // * gets resolved to <em>
|
||||||
|
|
||||||
if (game?.state === 'pick') {
|
if (game?.state === 'pick') {
|
||||||
const multi = body.match(/\b[bs]{2,}\b/i)?.[0];
|
const multi = body.match(/\b[bls]{2,}\b/i)?.[0];
|
||||||
|
|
||||||
if (multi) {
|
if (multi) {
|
||||||
pickNumbers(multi.toLowerCase(), context);
|
pickNumbers(multi.toLowerCase(), context);
|
||||||
|
@ -302,5 +373,6 @@ function onMessage(message, context) {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
onCommand,
|
onCommand,
|
||||||
onMessage,
|
onMessage,
|
||||||
commands: ['numsgo', 'numgo'],
|
commands: ['numsgo', 'numgo', 'calculate', 'calc', 'solve'],
|
||||||
|
help: `Reach the target number using only the ${config.numbers.length} numbers on the board; you do not have to use all of them. You may use addition, subtraction, multiplication and divisions, but each calculation must result in a whole number. You can score points for solutions up to ${config.numbers.points.length} away from the target, but only the first best solution gets the points. Quick-start with ${config.prefix}numsgo, or use ${config.prefix}numbers to fill the board by manually selecting a random *large* or *big* number (100, 75, 50, 25), a random *small* number (1-10), or multiple at once like LLSSSS.`, // eslint-disable-line max-len
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue