Disallowing functions and non-arithmetic operators in numbers.

This commit is contained in:
Niels Simenon 2023-04-12 17:13:24 +02:00
parent b791147ce0
commit 27a82b9cdd
2 changed files with 38 additions and 30 deletions

View File

@ -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,

View File

@ -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 <em>
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
};