From 27a82b9cddbf15e065c0b87c9c05d5a49f2fb1b2 Mon Sep 17 00:00:00 2001 From: Niels Simenon Date: Wed, 12 Apr 2023 17:13:24 +0200 Subject: [PATCH] Disallowing functions and non-arithmetic operators in numbers. --- config/default.js | 4 +-- src/games/numbers.js | 64 +++++++++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/config/default.js b/config/default.js index b972b8f..dc7b19e 100755 --- a/config/default.js +++ b/config/default.js @@ -135,8 +135,8 @@ module.exports = { }, numbers: { length: 6, - timeout: 60, - points: [10, 7, 7, 7, 7, 7, 5, 5, 5, 5, 5], // points by distance + timeout: 90, + points: [3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1], // points by distance }, riddle: { timeout: 30, diff --git a/src/games/numbers.js b/src/games/numbers.js index 5e5fca9..892305d 100644 --- a/src/games/numbers.js +++ b/src/games/numbers.js @@ -1,46 +1,42 @@ +/* eslint-disable max-classes-per-file */ + 'use strict'; const config = require('config'); const crypto = require('crypto'); const timers = require('timers/promises'); -const { - create, - parse, - addDependencies, - divideDependencies, - evaluateDependencies, -} = require('mathjs'); +const math = 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 { parse: limitedParse, divide: mathDivide, import: mathImport } = create({ - addDependencies, - divideDependencies, - evaluateDependencies, +const limitedMath = math.create({ + addDependencies: math.addDependencies, + divideDependencies: math.divideDependencies, + evaluateDependencies: math.evaluateDependencies, }); -class FractionError extends Error { +class RuleError extends Error { constructor(message) { super(message); - this.name = 'FractionError'; + this.name = 'RuleError'; } } function divide(a, b) { - const result = mathDivide(a, b); + const result = math.divide(a, b); if (result % 1) { - throw new FractionError(`${style.bold(style.code(`${a} / ${b}`))} results in a fraction`); + throw new RuleError(`The division ${style.bold(style.code(`${a} / ${b}`))} resulting in a fraction is not allowed`); } return result; } -mathImport({ +limitedMath.import({ divide, }, { override: true }); @@ -120,11 +116,11 @@ function getWinnerText(game) { } function getSolutionText(game) { - if (game.points[game.winner?.username] === 10 && game.solution?.answer === game.target) { + if (game.points[game.winner?.username] === config.numbers.points[0] && 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) { + if (game.points[game.winner?.username] === config.numbers.points[0]) { // 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!`; } @@ -224,7 +220,7 @@ function pickNumbers(type, context) { if (type !== 'small' && type !== 'large') { type.toLowerCase().slice(0, config.numbers.length - game.numbers.length).split('').forEach((typeKey) => { - game.numbers = game.numbers.concat(typeKey === 'b' || typeKey === 'l' ? pickLarge() : pickSmall()); + game.numbers = game.numbers.concat(['b', 'l', 't'].includes(typeKey) ? pickLarge() : pickSmall()); }); } @@ -240,8 +236,20 @@ function playSolution(solution, context) { const game = games.get(context.room.id); try { - 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 parsed = limitedMath.parse(solution.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat + + const numbers = parsed.filter((node) => { + if (node.type === 'FunctionNode') { + throw new RuleError(`The ${style.bold(node.name)} function is not allowed`); + } + + if (node.op && !['add', 'subtract', 'multiply', 'divide'].includes(node.fn)) { + throw new RuleError(`The ${style.bold(node.op)} operator is not allowed`); + } + + return 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); @@ -296,8 +304,8 @@ function playSolution(solution, context) { context.sendMessage(`${summary} for ${style.bold('no')} points, ${context.user.prefixedUsername}`, context.room.id); } catch (error) { // invalid solution - if (error.name === 'FractionError') { - context.sendMessage(`${error.message}, which is not allowed, ${context.user.prefixedUsername}`, context.room.id); + if (error.name === 'RuleError') { + context.sendMessage(`${error.message}, ${context.user.prefixedUsername}`, context.room.id); return; } @@ -330,7 +338,7 @@ function start(context, numbers) { 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 parsed = math.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); @@ -370,19 +378,19 @@ function onMessage(message, context) { const body = message.originalBody || message.body; // * gets resolved to if (game?.state === 'pick') { - const multi = body.match(/\b[bls]{2,}$/i)?.[0]; + const multi = body.match(/\b[blts]{2,}$/i)?.[0]; if (multi) { pickNumbers(multi.toLowerCase(), context); return; } - if (/large|big/i.test(body)) { + if (/large|big|top/i.test(body)) { pickNumbers('large', context); return; } - if (/small/i.test(body)) { + if (/small|bottom/i.test(body)) { pickNumbers('small', context); return; } @@ -397,5 +405,5 @@ module.exports = { onCommand, onMessage, commands: ['nums', '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 - 1} 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 + help: `Reach the target number using only the ${config.numbers.length} numbers on the board. You can use each number only once, but do not have to use all of them. You may use addition, subtraction, multiplication and divisions, but each intermediate calculation must result in a whole number. You can score ${Array.from(new Set(config.numbers.points)).slice(0, -1).join(', ')} or ${config.numbers.points.at(-1)} points for solutions up to ${config.numbers.points.length - 1} away, 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 };