204 lines
8.2 KiB
TypeScript
204 lines
8.2 KiB
TypeScript
/* The Artificer was built in memory of Babka
|
|
* With love, Ean
|
|
*
|
|
* December 21, 2020
|
|
*/
|
|
|
|
import {
|
|
log,
|
|
// Log4Deno deps
|
|
LT,
|
|
} from '../../deps.ts';
|
|
|
|
import { SolvedStep } from './solver.d.ts';
|
|
import { loggingEnabled } from './rollUtils.ts';
|
|
|
|
// fullSolver(conf, wrapDetails) returns one condensed SolvedStep
|
|
// fullSolver is a function that recursively solves the full roll and math
|
|
export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean): SolvedStep => {
|
|
// Initialize PEMDAS
|
|
const signs = ['^', '*', '/', '%', '+', '-'];
|
|
const stepSolve = {
|
|
total: 0,
|
|
details: '',
|
|
containsCrit: false,
|
|
containsFail: false,
|
|
};
|
|
|
|
// If entering with a single number, note it now
|
|
let singleNum = false;
|
|
if (conf.length === 1) {
|
|
singleNum = true;
|
|
}
|
|
|
|
// Evaluate all parenthesis
|
|
while (conf.includes('(')) {
|
|
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for (`);
|
|
// Get first open parenthesis
|
|
const openParen = conf.indexOf('(');
|
|
let closeParen = -1;
|
|
let nextParen = 0;
|
|
|
|
// Using nextParen to count the opening/closing parens, find the matching paren to openParen above
|
|
for (let i = openParen; i < conf.length; i++) {
|
|
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for matching ) openIdx: ${openParen} checking: ${i}`);
|
|
// If we hit an open, add one (this includes the openParen we start with), if we hit a close, subtract one
|
|
if (conf[i] === '(') {
|
|
nextParen++;
|
|
} else if (conf[i] === ')') {
|
|
nextParen--;
|
|
}
|
|
|
|
// When nextParen reaches 0 again, we will have found the matching closing parenthesis and can safely exit the for loop
|
|
if (nextParen === 0) {
|
|
closeParen = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Make sure we did find the correct closing paren, if not, error out now
|
|
if (closeParen === -1 || closeParen < openParen) {
|
|
throw new Error('UnbalancedParens');
|
|
}
|
|
|
|
// Call the solver on the items between openParen and closeParen (excluding the parens)
|
|
const parenSolve = fullSolver(conf.slice(openParen + 1, closeParen), true);
|
|
// Replace the items between openParen and closeParen (including the parens) with its solved equivalent
|
|
conf.splice(openParen, closeParen - openParen + 1, parenSolve);
|
|
|
|
// Determining if we need to add in a multiplication sign to handle implicit multiplication (like "(4)2" = 8)
|
|
// insertedMult flags if there was a multiplication sign inserted before the parens
|
|
let insertedMult = false;
|
|
// Check if a number was directly before openParen and slip in the "*" if needed
|
|
if (openParen - 1 > -1 && signs.indexOf(conf[openParen - 1].toString()) === -1) {
|
|
insertedMult = true;
|
|
conf.splice(openParen, 0, '*');
|
|
}
|
|
// Check if a number is directly after closeParen and slip in the "*" if needed
|
|
if (!insertedMult && openParen + 1 < conf.length && signs.indexOf(conf[openParen + 1].toString()) === -1) {
|
|
conf.splice(openParen + 1, 0, '*');
|
|
} else if (insertedMult && openParen + 2 < conf.length && signs.indexOf(conf[openParen + 2].toString()) === -1) {
|
|
// insertedMult is utilized here to let us account for an additional item being inserted into the array (the "*" from before openParen)
|
|
conf.splice(openParen + 2, 0, '*');
|
|
}
|
|
}
|
|
|
|
// Evaluate all EMDAS by looping thru each tier of operators (exponential is the highest tier, addition/subtraction the lowest)
|
|
const allCurOps = [['^'], ['*', '/', '%'], ['+', '-']];
|
|
allCurOps.forEach((curOps) => {
|
|
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)}`);
|
|
// Iterate thru all operators/operands in the conf
|
|
for (let i = 0; i < conf.length; i++) {
|
|
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)} | Checking ${JSON.stringify(conf[i])}`);
|
|
// Check if the current index is in the active tier of operators
|
|
if (curOps.includes(conf[i].toString())) {
|
|
// Grab the operands from before and after the operator
|
|
const operand1 = conf[i - 1];
|
|
const operand2 = conf[i + 1];
|
|
// Init temp math to NaN to catch bad parsing
|
|
let oper1 = NaN;
|
|
let oper2 = NaN;
|
|
const subStepSolve = {
|
|
total: NaN,
|
|
details: '',
|
|
containsCrit: false,
|
|
containsFail: false,
|
|
};
|
|
|
|
// If operand1 is a SolvedStep, populate our subStepSolve with its details and crit/fail flags
|
|
if (typeof operand1 === 'object') {
|
|
oper1 = operand1.total;
|
|
subStepSolve.details = `${operand1.details}\\${conf[i]}`;
|
|
subStepSolve.containsCrit = operand1.containsCrit;
|
|
subStepSolve.containsFail = operand1.containsFail;
|
|
} else {
|
|
// else parse it as a number and add it to the subStep details
|
|
if (operand1 || operand1 == 0) {
|
|
oper1 = parseFloat(operand1.toString());
|
|
subStepSolve.details = `${oper1.toString()}\\${conf[i]}`;
|
|
}
|
|
}
|
|
|
|
// If operand2 is a SolvedStep, populate our subStepSolve with its details without overriding what operand1 filled in
|
|
if (typeof operand2 === 'object') {
|
|
oper2 = operand2.total;
|
|
subStepSolve.details += operand2.details;
|
|
subStepSolve.containsCrit = subStepSolve.containsCrit || operand2.containsCrit;
|
|
subStepSolve.containsFail = subStepSolve.containsFail || operand2.containsFail;
|
|
} else {
|
|
// else parse it as a number and add it to the subStep details
|
|
oper2 = parseFloat(operand2.toString());
|
|
subStepSolve.details += oper2;
|
|
}
|
|
|
|
// Make sure neither operand is NaN before continuing
|
|
if (isNaN(oper1) || isNaN(oper2)) {
|
|
throw new Error('OperandNaN');
|
|
}
|
|
|
|
// Verify a second time that both are numbers before doing math, throwing an error if necessary
|
|
if (typeof oper1 === 'number' && typeof oper2 === 'number') {
|
|
// Finally do the operator on the operands, throw an error if the operator is not found
|
|
switch (conf[i]) {
|
|
case '^':
|
|
subStepSolve.total = Math.pow(oper1, oper2);
|
|
break;
|
|
case '*':
|
|
subStepSolve.total = oper1 * oper2;
|
|
break;
|
|
case '/':
|
|
subStepSolve.total = oper1 / oper2;
|
|
break;
|
|
case '%':
|
|
subStepSolve.total = oper1 % oper2;
|
|
break;
|
|
case '+':
|
|
subStepSolve.total = oper1 + oper2;
|
|
break;
|
|
case '-':
|
|
subStepSolve.total = oper1 - oper2;
|
|
break;
|
|
default:
|
|
throw new Error('OperatorWhat');
|
|
}
|
|
} else {
|
|
throw new Error('EMDASNotNumber');
|
|
}
|
|
|
|
// Replace the two operands and their operator with our subStepSolve
|
|
conf.splice(i - 1, 3, subStepSolve);
|
|
// Because we are messing around with the array we are iterating thru, we need to back up one idx to make sure every operator gets processed
|
|
i--;
|
|
}
|
|
}
|
|
});
|
|
|
|
// If we somehow have more than one item left in conf at this point, something broke, throw an error
|
|
if (conf.length > 1) {
|
|
loggingEnabled && log(LT.LOG, `ConfWHAT? ${JSON.stringify(conf)}`);
|
|
throw new Error('ConfWhat');
|
|
} else if (singleNum && typeof conf[0] === 'number') {
|
|
// If we are only left with a number, populate the stepSolve with it
|
|
stepSolve.total = conf[0];
|
|
stepSolve.details = conf[0].toString();
|
|
} else {
|
|
// Else fully populate the stepSolve with what was computed
|
|
stepSolve.total = (<SolvedStep> conf[0]).total;
|
|
stepSolve.details = (<SolvedStep> conf[0]).details;
|
|
stepSolve.containsCrit = (<SolvedStep> conf[0]).containsCrit;
|
|
stepSolve.containsFail = (<SolvedStep> conf[0]).containsFail;
|
|
}
|
|
|
|
// If this was a nested call, add on parens around the details to show what math we've done
|
|
if (wrapDetails) {
|
|
stepSolve.details = `(${stepSolve.details})`;
|
|
}
|
|
|
|
// If our total has reached undefined for some reason, error out now
|
|
if (stepSolve.total === undefined) {
|
|
throw new Error('UndefinedStep');
|
|
}
|
|
|
|
return stepSolve;
|
|
};
|