import { log, LogTypes as LT } from '@Log4Deno'; import { ReturnData } from 'artigen/artigen.d.ts'; import { MathConf, SolvedStep } from 'artigen/math/math.d.ts'; import { CountDetails, ExecutedRoll, GroupConf, RollDistributionMap, RollModifiers, RollSet } from 'artigen/dice/dice.d.ts'; import { formatRoll } from 'artigen/dice/generateFormattedRoll.ts'; import { loopCountCheck } from 'artigen/managers/loopManager.ts'; import { mathSolver } from 'artigen/math/mathSolver.ts'; import { closeInternalGrp, cmdSplitRegex, internalWrapRegex, mathSplitRegex, openInternalGrp } from 'artigen/utils/escape.ts'; import { legalMathOperators } from 'artigen/utils/legalMath.ts'; import { loggingEnabled } from 'artigen/utils/logFlag.ts'; import { assertParenBalance } from 'artigen/utils/parenBalance.ts'; import { executeRoll } from 'artigen/dice/executeRoll.ts'; import { compareOrigIdx, compareRolls } from 'artigen/utils/sortFuncs.ts'; // minusOps are operators that will cause a negative sign to collapse into a number (in cases like + - 1) const minusOps = ['(', '^', '**', '*', '/', '%', '+', '-']; const allOps = [...minusOps, ')']; export const tokenizeMath = ( cmd: string, modifiers: RollModifiers, previousResults: number[], groupResults: ReturnData[], groupConf: GroupConf | null = null, ): [ReturnData[], CountDetails[], RollDistributionMap[]] => { const countDetails: CountDetails[] = []; const rollDists: RollDistributionMap[] = []; const executedRolls: Map = new Map(); loggingEnabled && log(LT.LOG, `Parsing roll ${cmd} | ${JSON.stringify(modifiers)} | ${JSON.stringify(previousResults)}`); // Remove all spaces from the operation config and split it by any operator (keeping the operator in mathConf for fullSolver to do math on) const mathConf: MathConf[] = cmd .replace(cmdSplitRegex, '') .replace(internalWrapRegex, '') .replace(/ /g, '') .split(mathSplitRegex) .filter((x) => x); loggingEnabled && log(LT.LOG, `Split roll into mathConf ${JSON.stringify(mathConf)}`); // Verify balanced parens before doing anything assertParenBalance(mathConf); // Evaluate all rolls into stepSolve format and all numbers into floats for (let i = 0; i < mathConf.length; i++) { loopCountCheck(); loggingEnabled && log(LT.LOG, `Parsing roll ${JSON.stringify(cmd)} | Evaluating rolls into math-able items ${JSON.stringify(mathConf[i])}`); const curMathConfStr = mathConf[i].toString(); if (curMathConfStr.length === 0) { // If its an empty string, get it out of here mathConf.splice(i, 1); i--; } else if (mathConf[i] == parseFloat(curMathConfStr)) { // If its a number, parse the number out mathConf[i] = parseFloat(curMathConfStr); } else if (curMathConfStr.startsWith(openInternalGrp)) { const groupIdx = parseInt(curMathConfStr.substring(1, curMathConfStr.indexOf(closeInternalGrp))); if (groupIdx >= groupResults.length) { throw new Error('InternalGroupMachineBroke'); } mathConf[i] = { total: groupResults[groupIdx].rollTotal, details: groupResults[groupIdx].rollDetails, containsCrit: groupResults[groupIdx].containsCrit, containsFail: groupResults[groupIdx].containsFail, isComplex: groupResults[groupIdx].isComplex, }; } else if (curMathConfStr.toLowerCase() === 'e') { // If the operand is the constant e, create a SolvedStep for it mathConf[i] = { total: Math.E, details: '*e*', containsCrit: false, containsFail: false, isComplex: false, }; } else if (curMathConfStr.toLowerCase() === 'lemon' || curMathConfStr.toLowerCase() === '๐Ÿ‹') { mathConf[i] = { total: 5, details: '๐Ÿ‹', containsCrit: false, containsFail: false, isComplex: false, }; } else if (curMathConfStr.toLowerCase() === 'horse' || curMathConfStr.toLowerCase() === '๐Ÿด') { mathConf[i] = { total: Math.sqrt(3), details: '๐Ÿด', containsCrit: false, containsFail: false, isComplex: false, }; } else if (curMathConfStr.toLowerCase() === 'fart' || curMathConfStr.toLowerCase() === '๐Ÿ’ฉ') { mathConf[i] = { total: 7, details: '๐Ÿ’ฉ', containsCrit: false, containsFail: false, isComplex: false, }; } else if (curMathConfStr.toLowerCase() === 'sex' || curMathConfStr.toLowerCase() === '๐Ÿ†๐Ÿ‘' || curMathConfStr.toLowerCase() === '๐Ÿ‘๐Ÿ†') { mathConf[i] = { total: 69, details: '( อกยฐ อœส– อกยฐ)', containsCrit: false, containsFail: false, isComplex: false, }; } else if (curMathConfStr.toLowerCase() === 'inf' || curMathConfStr.toLowerCase() === 'infinity' || curMathConfStr.toLowerCase() === 'โˆž') { // If the operand is the constant Infinity, create a SolvedStep for it mathConf[i] = { total: Infinity, details: 'โˆž', containsCrit: false, containsFail: false, isComplex: false, }; } else if (curMathConfStr.toLowerCase() === 'pi' || curMathConfStr.toLowerCase() === '๐œ‹') { // If the operand is the constant pi, create a SolvedStep for it mathConf[i] = { total: Math.PI, details: '๐œ‹', containsCrit: false, containsFail: false, isComplex: false, }; } else if (curMathConfStr.toLowerCase() === 'pie' || curMathConfStr.toLowerCase() === '๐Ÿฅง') { // If the operand is pie, pi*e, create a SolvedStep for e and pi (and the multiplication symbol between them) mathConf[i] = { total: Math.PI, details: '๐œ‹', containsCrit: false, containsFail: false, isComplex: false, }; mathConf.splice( i + 1, 0, ...[ '*', { total: Math.E, details: '*e*', containsCrit: false, containsFail: false, isComplex: false, }, ], ); i += 2; } else if (!legalMathOperators.includes(curMathConfStr) && legalMathOperators.some((mathOp) => curMathConfStr.endsWith(mathOp))) { // Identify when someone does something weird like 4floor(2.5) and split 4 and floor const matchedMathOp = legalMathOperators.filter((mathOp) => curMathConfStr.endsWith(mathOp))[0]; mathConf[i] = parseFloat(curMathConfStr.replace(matchedMathOp, '')); mathConf.splice(i + 1, 0, ...['*', matchedMathOp]); i += 2; } else if (/(x\d+(\.\d*)?)/.test(curMathConfStr)) { // Identify when someone is using a variable from previous commands if (curMathConfStr.includes('.')) { // Verify someone did not enter x1.1 as a variable throw new Error(`IllegalVariable_${curMathConfStr}`); } const varIdx = parseInt(curMathConfStr.replaceAll('x', '')); // Get the index from the variable and attempt to use it to query the previousResults if (previousResults.length > varIdx) { mathConf[i] = parseFloat(previousResults[varIdx].toString()); } else { throw new Error(`IllegalVariable_${curMathConfStr}`); } } else if (![...allOps, ...legalMathOperators].includes(curMathConfStr)) { // If nothing else has handled it by now, try it as a roll const executedRoll = executeRoll(curMathConfStr, modifiers); if (groupConf) { executedRolls.set(i, executedRoll); } else { const formattedRoll = formatRoll(executedRoll, modifiers); mathConf[i] = formattedRoll.solvedStep; countDetails.push(formattedRoll.countDetails); rollDists.push(formattedRoll.rollDistributions); } } // Identify if we are in a state where the current number is a negative number if (mathConf[i - 1] === '-' && ((!mathConf[i - 2] && mathConf[i - 2] !== 0) || minusOps.includes( mathConf[i - 2]))) { if (typeof mathConf[i] === 'string') { // Current item is a mathOp, need to insert a "-1 *" before it mathConf.splice(i - 1, 1, ...[parseFloat('-1'), '*']); i += 2; } else { // Handle normally, just set current item to negative if (typeof mathConf[i] === 'number') { mathConf[i] = mathConf[i] * -1; } else { ( mathConf[i]).total = ( mathConf[i]).total * -1; ( mathConf[i]).details = `-${( mathConf[i]).details}`; } mathConf.splice(i - 1, 1); i--; } } } // Handle applying the group config if (groupConf) { loggingEnabled && log(LT.LOG, `Applying groupConf to executedRolls | ${JSON.stringify(groupConf)} ${JSON.stringify(executedRolls.entries().toArray())}`); // Merge all rollSets into one array, adding the idx into each rollSet to allow separating them back out const allRollSets: RollSet[] = []; const executedRollArr = executedRolls.entries().toArray(); executedRollArr.forEach(([rollGroupIdx, executedRoll]) => { executedRoll.rollSet.forEach((roll) => (roll.rollGrpIdx = rollGroupIdx)); allRollSets.push(...executedRoll.rollSet); }); loggingEnabled && log(LT.LOG, `raw rollSets: ${JSON.stringify(allRollSets)}`); // Handle drop or keep operations if (groupConf.drop.on || groupConf.keep.on || groupConf.dropHigh.on || groupConf.keepLow.on) { allRollSets.sort(compareRolls); let dropCount = 0; // For normal drop and keep, simple subtraction is enough to determine how many to drop // Protections are in to prevent the dropCount from going below 0 or more than the valid rolls to drop if (groupConf.drop.on) { dropCount = groupConf.drop.count; if (dropCount > allRollSets.length) { dropCount = allRollSets.length; } } else if (groupConf.keep.on) { dropCount = allRollSets.length - groupConf.keep.count; if (dropCount < 0) { dropCount = 0; } } // For inverted drop and keep, order must be flipped to greatest to least before the simple subtraction can determine how many to drop // Protections are in to prevent the dropCount from going below 0 or more than the valid rolls to drop else if (groupConf.dropHigh.on) { allRollSets.reverse(); dropCount = groupConf.dropHigh.count; if (dropCount > allRollSets.length) { dropCount = allRollSets.length; } } else if (groupConf.keepLow.on) { allRollSets.reverse(); dropCount = allRollSets.length - groupConf.keepLow.count; if (dropCount < 0) { dropCount = 0; } } let i = 0; while (dropCount > 0 && i < allRollSets.length) { loopCountCheck(); loggingEnabled && log(LT.LOG, `Handling group dropping | Dropping ${dropCount}, looking at ${JSON.stringify(allRollSets[i])}`); if (!allRollSets[i].dropped && !allRollSets[i].rerolled) { allRollSets[i].dropped = true; allRollSets[i].success = false; allRollSets[i].fail = false; allRollSets[i].matchLabel = ''; dropCount--; } i++; } allRollSets.sort(compareOrigIdx); } // Handle marking new successes/fails if (groupConf.success.on || groupConf.fail.on) { allRollSets.forEach((rs) => { loopCountCheck(); if (!rs.dropped && !rs.rerolled) { if (groupConf.success.on && groupConf.success.range.includes(rs.roll)) { rs.success = true; rs.matchLabel = 'S'; } if (groupConf.fail.on && groupConf.fail.range.includes(rs.roll)) { rs.fail = true; rs.matchLabel = 'F'; } } }); } // Handle separating the rollSets back out, recalculating the success/fail count, assigning them to the correct mathConf slots executedRollArr.forEach(([rollGroupIdx, executedRoll]) => { // Update flags on executedRoll executedRoll.countSuccessOverride = executedRoll.countSuccessOverride || groupConf.success.on; executedRoll.countFailOverride = executedRoll.countFailOverride || groupConf.fail.on; executedRoll.rollSet = allRollSets.filter((rs) => rs.rollGrpIdx === rollGroupIdx); const formattedRoll = formatRoll(executedRoll, modifiers); mathConf[rollGroupIdx] = formattedRoll.solvedStep; countDetails.push(formattedRoll.countDetails); rollDists.push(formattedRoll.rollDistributions); }); } // Now that mathConf is parsed, send it into the solver loggingEnabled && log(LT.LOG, `Sending mathConf to solver ${JSON.stringify(mathConf)}`); const tempSolved = mathSolver(mathConf); loggingEnabled && log(LT.LOG, `SolvedStep back from mathSolver ${JSON.stringify(tempSolved)}`); // Push all of this step's solved data into the temp array return [ [ { rollTotal: tempSolved.total, rollPreFormat: '', rollPostFormat: '', rollDetails: tempSolved.details, containsCrit: tempSolved.containsCrit, containsFail: tempSolved.containsFail, initConfig: cmd, isComplex: tempSolved.isComplex, }, ], countDetails, rollDists, ]; };