TheArtificer/src/solver/solver.ts

202 lines
8.1 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 openParenIdx = conf.indexOf('(');
let closeParenIdx = -1;
let nextParenIdx = 0;
// Using nextParenIdx to count the opening/closing parens, find the matching paren to openParenIdx above
closingParenLocator: for (let i = openParenIdx; i < conf.length; i++) {
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for matching ) openIdx: ${openParenIdx} checking: ${i}`);
// If we hit an open, add one (this includes the openParenIdx we start with), if we hit a close, subtract one
if (conf[i] === '(') {
nextParenIdx++;
} else if (conf[i] === ')') {
nextParenIdx--;
}
// When nextParenIdx reaches 0 again, we will have found the matching closing parenthesis and can safely exit the for loop
if (nextParenIdx === 0) {
closeParenIdx = i;
break closingParenLocator;
}
}
// Make sure we did find the correct closing paren, if not, error out now
if (closeParenIdx === -1 || closeParenIdx < openParenIdx) {
throw new Error('UnbalancedParens');
}
// Call the solver on the items between openParenIdx and closeParenIdx (excluding the parens)
const parenSolve = fullSolver(conf.slice(openParenIdx + 1, closeParenIdx), true);
// Replace the items between openParenIdx and closeParenIdx (including the parens) with its solved equivalent
conf.splice(openParenIdx, closeParenIdx - openParenIdx + 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 = 0;
// Check if a number was directly before openParenIdx and slip in the "*" if needed
if (openParenIdx - 1 > -1 && !signs.includes(conf[openParenIdx - 1].toString())) {
insertedMult = 1;
conf.splice(openParenIdx, 0, '*');
}
// Check if a number is directly after the closing paren and slip in the "*" if needed
// openParenIdx is used here as the conf array has already been collapsed down
if (openParenIdx + 1 + insertedMult < conf.length && !signs.includes(conf[openParenIdx + 1 + insertedMult].toString())) {
conf.splice(openParenIdx + 1 + insertedMult, 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;
};