Disallowing functions and non-arithmetic operators in numbers.
This commit is contained in:
parent
b791147ce0
commit
27a82b9cdd
|
@ -135,8 +135,8 @@ module.exports = {
|
||||||
},
|
},
|
||||||
numbers: {
|
numbers: {
|
||||||
length: 6,
|
length: 6,
|
||||||
timeout: 60,
|
timeout: 90,
|
||||||
points: [10, 7, 7, 7, 7, 7, 5, 5, 5, 5, 5], // points by distance
|
points: [3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1], // points by distance
|
||||||
},
|
},
|
||||||
riddle: {
|
riddle: {
|
||||||
timeout: 30,
|
timeout: 30,
|
||||||
|
|
|
@ -1,46 +1,42 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const timers = require('timers/promises');
|
const timers = require('timers/promises');
|
||||||
|
|
||||||
const {
|
const math = require('mathjs');
|
||||||
create,
|
|
||||||
parse,
|
|
||||||
addDependencies,
|
|
||||||
divideDependencies,
|
|
||||||
evaluateDependencies,
|
|
||||||
} = require('mathjs');
|
|
||||||
|
|
||||||
const getLeaders = require('../utils/get-leaders');
|
const getLeaders = require('../utils/get-leaders');
|
||||||
const pickRandom = require('../utils/pick-random');
|
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 { parse: limitedParse, divide: mathDivide, import: mathImport } = create({
|
const limitedMath = math.create({
|
||||||
addDependencies,
|
addDependencies: math.addDependencies,
|
||||||
divideDependencies,
|
divideDependencies: math.divideDependencies,
|
||||||
evaluateDependencies,
|
evaluateDependencies: math.evaluateDependencies,
|
||||||
});
|
});
|
||||||
|
|
||||||
class FractionError extends Error {
|
class RuleError extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = 'FractionError';
|
this.name = 'RuleError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function divide(a, b) {
|
function divide(a, b) {
|
||||||
const result = mathDivide(a, b);
|
const result = math.divide(a, b);
|
||||||
|
|
||||||
if (result % 1) {
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
mathImport({
|
limitedMath.import({
|
||||||
divide,
|
divide,
|
||||||
}, { override: true });
|
}, { override: true });
|
||||||
|
|
||||||
|
@ -120,11 +116,11 @@ function getWinnerText(game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSolutionText(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))}.`;
|
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
|
// 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!`;
|
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') {
|
if (type !== 'small' && type !== 'large') {
|
||||||
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' || 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);
|
const game = games.get(context.room.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = limitedParse(solution.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
|
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) => node.type === 'ConstantNode').map((node) => node.value);
|
|
||||||
|
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 counts = countNumbers(numbers);
|
||||||
const imagined = Object.keys(counts).filter((number) => !game.counts[number]);
|
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);
|
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);
|
context.sendMessage(`${summary} for ${style.bold('no')} points, ${context.user.prefixedUsername}`, context.room.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// invalid solution
|
// invalid solution
|
||||||
if (error.name === 'FractionError') {
|
if (error.name === 'RuleError') {
|
||||||
context.sendMessage(`${error.message}, which is not allowed, ${context.user.prefixedUsername}`, context.room.id);
|
context.sendMessage(`${error.message}, ${context.user.prefixedUsername}`, context.room.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +338,7 @@ function start(context, numbers) {
|
||||||
|
|
||||||
function solve(calculation, context) {
|
function solve(calculation, context) {
|
||||||
try {
|
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();
|
const answer = parsed.evaluate();
|
||||||
|
|
||||||
context.sendMessage(`${style.bold(style.code(parsed))} = ${style.bold(answer)}`, context.room.id);
|
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>
|
const body = message.originalBody || message.body; // * gets resolved to <em>
|
||||||
|
|
||||||
if (game?.state === 'pick') {
|
if (game?.state === 'pick') {
|
||||||
const multi = body.match(/\b[bls]{2,}$/i)?.[0];
|
const multi = body.match(/\b[blts]{2,}$/i)?.[0];
|
||||||
|
|
||||||
if (multi) {
|
if (multi) {
|
||||||
pickNumbers(multi.toLowerCase(), context);
|
pickNumbers(multi.toLowerCase(), context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/large|big/i.test(body)) {
|
if (/large|big|top/i.test(body)) {
|
||||||
pickNumbers('large', context);
|
pickNumbers('large', context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/small/i.test(body)) {
|
if (/small|bottom/i.test(body)) {
|
||||||
pickNumbers('small', context);
|
pickNumbers('small', context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -397,5 +405,5 @@ module.exports = {
|
||||||
onCommand,
|
onCommand,
|
||||||
onMessage,
|
onMessage,
|
||||||
commands: ['nums', 'numsgo', 'numgo', 'calculate', 'calc', 'solve'],
|
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
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue