|
|
|
@ -1,42 +1,46 @@
|
|
|
|
|
/* eslint-disable max-classes-per-file */
|
|
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const config = require('config');
|
|
|
|
|
const crypto = require('crypto');
|
|
|
|
|
const timers = require('timers/promises');
|
|
|
|
|
|
|
|
|
|
const math = require('mathjs');
|
|
|
|
|
const {
|
|
|
|
|
create,
|
|
|
|
|
parse,
|
|
|
|
|
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 limitedMath = math.create({
|
|
|
|
|
addDependencies: math.addDependencies,
|
|
|
|
|
divideDependencies: math.divideDependencies,
|
|
|
|
|
evaluateDependencies: math.evaluateDependencies,
|
|
|
|
|
const { parse: limitedParse, divide: mathDivide, import: mathImport } = create({
|
|
|
|
|
addDependencies,
|
|
|
|
|
divideDependencies,
|
|
|
|
|
evaluateDependencies,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
class RuleError extends Error {
|
|
|
|
|
class FractionError extends Error {
|
|
|
|
|
constructor(message) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = 'RuleError';
|
|
|
|
|
this.name = 'FractionError';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function divide(a, b) {
|
|
|
|
|
const result = math.divide(a, b);
|
|
|
|
|
const result = mathDivide(a, b);
|
|
|
|
|
|
|
|
|
|
if (result % 1) {
|
|
|
|
|
throw new RuleError(`The division ${style.bold(style.code(`${a} / ${b}`))} resulting in a fraction is not allowed`);
|
|
|
|
|
throw new FractionError(`${style.bold(style.code(`${a} / ${b}`))} results in a fraction`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
limitedMath.import({
|
|
|
|
|
mathImport({
|
|
|
|
|
divide,
|
|
|
|
|
}, { override: true });
|
|
|
|
|
|
|
|
|
@ -116,11 +120,11 @@ function getWinnerText(game) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSolutionText(game) {
|
|
|
|
|
if (game.points[game.winner?.username] === config.numbers.points[0] && game.solution?.answer === game.target) {
|
|
|
|
|
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] === config.numbers.points[0]) {
|
|
|
|
|
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!`;
|
|
|
|
|
}
|
|
|
|
@ -220,7 +224,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(['b', 'l', 't'].includes(typeKey) ? pickLarge() : pickSmall());
|
|
|
|
|
game.numbers = game.numbers.concat(typeKey === 'b' || typeKey === 'l' ? pickLarge() : pickSmall());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -236,20 +240,8 @@ function playSolution(solution, context) {
|
|
|
|
|
const game = games.get(context.room.id);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
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 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 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);
|
|
|
|
@ -304,8 +296,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 === 'RuleError') {
|
|
|
|
|
context.sendMessage(`${error.message}, ${context.user.prefixedUsername}`, context.room.id);
|
|
|
|
|
if (error.name === 'FractionError') {
|
|
|
|
|
context.sendMessage(`${error.message}, which is not allowed, ${context.user.prefixedUsername}`, context.room.id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -338,7 +330,7 @@ function start(context, numbers) {
|
|
|
|
|
|
|
|
|
|
function solve(calculation, context) {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = math.parse(calculation.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
|
|
|
|
|
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);
|
|
|
|
@ -378,19 +370,19 @@ function onMessage(message, context) {
|
|
|
|
|
const body = message.originalBody || message.body; // * gets resolved to <em>
|
|
|
|
|
|
|
|
|
|
if (game?.state === 'pick') {
|
|
|
|
|
const multi = body.match(/\b[blts]{2,}$/i)?.[0];
|
|
|
|
|
const multi = body.match(/\b[bls]{2,}$/i)?.[0];
|
|
|
|
|
|
|
|
|
|
if (multi) {
|
|
|
|
|
pickNumbers(multi.toLowerCase(), context);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/large|big|top/i.test(body)) {
|
|
|
|
|
if (/large|big/i.test(body)) {
|
|
|
|
|
pickNumbers('large', context);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/small|bottom/i.test(body)) {
|
|
|
|
|
if (/small/i.test(body)) {
|
|
|
|
|
pickNumbers('small', context);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@ -405,5 +397,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 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
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|