TheArtificer/src/solver/parser.ts

288 lines
10 KiB
TypeScript

import {
log,
// Log4Deno deps
LT,
} from '../../deps.ts';
import config from '../../config.ts';
import { RollModifiers } from '../mod.d.ts';
import { ReturnData, SolvedRoll, SolvedStep } from './solver.d.ts';
import { compareTotalRolls, escapeCharacters } from './rollUtils.ts';
import { formatRoll } from './rollFormatter.ts';
import { fullSolver } from './solver.ts';
// parseRoll(fullCmd, modifiers)
// parseRoll handles converting fullCmd into a computer readable format for processing, and finally executes the solving
export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll => {
const returnmsg = {
error: false,
errorMsg: '',
errorCode: '',
line1: '',
line2: '',
line3: '',
};
// Whole function lives in a try-catch to allow safe throwing of errors on purpose
try {
// Split the fullCmd by the command prefix to allow every roll/math op to be handled individually
const sepRolls = fullCmd.split(config.prefix);
const tempReturnData: ReturnData[] = [];
// Loop thru all roll/math ops
for (let i = 0; i < sepRolls.length; i++) {
log(LT.LOG, `Parsing roll ${fullCmd} | Working ${sepRolls[i]}`);
// Split the current iteration on the command postfix to separate the operation to be parsed and the text formatting after the opertaion
const [tempConf, tempFormat] = sepRolls[i].split(config.postfix);
// 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: (string | number | SolvedStep)[] = <(string | number | SolvedStep)[]> tempConf.replace(/ /g, '').split(/([-+()*/%^])/g);
// Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens
let parenCnt = 0;
mathConf.forEach((e) => {
log(LT.LOG, `Parsing roll ${fullCmd} | Checking parenthesis balance ${e}`);
if (e === '(') {
parenCnt++;
} else if (e === ')') {
parenCnt--;
}
});
// If the parenCnt is not 0, then we do not have balanced parens and need to error out now
if (parenCnt !== 0) {
throw new Error('UnbalancedParens');
}
// Evaluate all rolls into stepSolve format and all numbers into floats
for (let i = 0; i < mathConf.length; i++) {
log(LT.LOG, `Parsing roll ${fullCmd} | Evaluating rolls into mathable items ${JSON.stringify(mathConf[i])}`);
if (mathConf[i].toString().length === 0) {
// If its an empty string, get it out of here
mathConf.splice(i, 1);
i--;
} else if (mathConf[i] == parseFloat(mathConf[i].toString())) {
// If its a number, parse the number out
mathConf[i] = parseFloat(mathConf[i].toString());
} else if (/([0123456789])/g.test(mathConf[i].toString())) {
// If there is a number somewhere in mathconf[i] but there are also other characters preventing it from parsing correctly as a number, it should be a dice roll, parse it as such (if it for some reason is not a dice roll, formatRoll/roll will handle it)
mathConf[i] = formatRoll(mathConf[i].toString(), modifiers.maxRoll, modifiers.nominalRoll);
} else if (mathConf[i].toString().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,
};
} else if (mathConf[i].toString().toLowerCase() === 'pi' || mathConf[i].toString().toLowerCase() == '𝜋') {
// If the operand is the constant pi, create a SolvedStep for it
mathConf[i] = {
total: Math.PI,
details: '𝜋',
containsCrit: false,
containsFail: false,
};
} else if (mathConf[i].toString().toLowerCase() === 'pie') {
// 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,
};
mathConf.splice(i + 1, 0, ...['*', {
total: Math.E,
details: '*e*',
containsCrit: false,
containsFail: false,
}]);
}
}
// Now that mathConf is parsed, send it into the solver
const tempSolved = fullSolver(mathConf, false);
// Push all of this step's solved data into the temp array
tempReturnData.push({
rollTotal: tempSolved.total,
rollPostFormat: tempFormat,
rollDetails: tempSolved.details,
containsCrit: tempSolved.containsCrit,
containsFail: tempSolved.containsFail,
initConfig: tempConf,
});
}
// Parsing/Solving done, time to format the output for Discord
// Remove any floating spaces from fullCmd
if (fullCmd[fullCmd.length - 1] === ' ') {
fullCmd = fullCmd.substring(0, fullCmd.length - 1);
}
// Escape any | and ` chars in fullCmd to prevent spoilers and code blocks from acting up
fullCmd = escapeCharacters(fullCmd, '|');
fullCmd = fullCmd.replace(/`/g, '');
let line1 = '';
let line2 = '';
let line3 = '';
// If maximiseRoll or nominalRoll are on, mark the output as such, else use default formatting
if (modifiers.maxRoll) {
line1 = ` requested the theoretical maximum of: \`${config.prefix}${fullCmd}\``;
line2 = 'Theoretical Maximum Results: ';
} else if (modifiers.nominalRoll) {
line1 = ` requested the theoretical nominal of: \`${config.prefix}${fullCmd}\``;
line2 = 'Theoretical Nominal Results: ';
} else if (modifiers.order === 'a') {
line1 = ` requested the following rolls to be ordered from least to greatest: \`${config.prefix}${fullCmd}\``;
line2 = 'Results: ';
tempReturnData.sort(compareTotalRolls);
} else if (modifiers.order === 'd') {
line1 = ` requested the following rolls to be ordered from greatest to least: \`${config.prefix}${fullCmd}\``;
line2 = 'Results: ';
tempReturnData.sort(compareTotalRolls);
tempReturnData.reverse();
} else {
line1 = ` rolled: \`${config.prefix}${fullCmd}\``;
line2 = 'Results: ';
}
// Fill out all of the details and results now
tempReturnData.forEach((e) => {
log(LT.LOG, `Parsing roll ${fullCmd} | Making return text ${JSON.stringify(e)}`);
let preFormat = '';
let postFormat = '';
// If the roll containted a crit success or fail, set the formatting around it
if (e.containsCrit) {
preFormat = `**${preFormat}`;
postFormat = `${postFormat}**`;
}
if (e.containsFail) {
preFormat = `__${preFormat}`;
postFormat = `${postFormat}__`;
}
// Populate line2 (the results) and line3 (the details) with their data
if (modifiers.order === '') {
line2 += `${preFormat}${e.rollTotal}${postFormat}${escapeCharacters(e.rollPostFormat, '|*_~`')}`;
} else {
// If order is on, turn rolls into csv without formatting
line2 += `${preFormat}${e.rollTotal}${postFormat}, `;
}
line2 = line2.replace(/\*\*\*\*/g, '** **').replace(/____/g, '__ __').replace(/~~~~/g, '~~ ~~');
line3 += `\`${e.initConfig}\` = ${e.rollDetails} = ${preFormat}${e.rollTotal}${postFormat}\n`;
});
// If order is on, remove trailing ", "
if (modifiers.order !== '') {
line2 = line2.substring(0, line2.length - 2);
}
// Fill in the return block
returnmsg.line1 = line1;
returnmsg.line2 = line2;
returnmsg.line3 = line3;
} catch (solverError) {
// Welp, the unthinkable happened, we hit an error
// Split on _ for the error messages that have more info than just their name
const [errorName, errorDetails] = solverError.message.split('_');
let errorMsg = '';
// Translate the errorName to a specific errorMsg
switch (errorName) {
case 'YouNeedAD':
errorMsg = 'Formatting Error: Missing die size and count config';
break;
case 'FormattingError':
errorMsg = 'Formatting Error: Cannot use Keep and Drop at the same time, remove all but one and repeat roll';
break;
case 'NoMaxWithDash':
errorMsg = 'Formatting Error: CritScore range specified without a maximum, remove - or add maximum to correct';
break;
case 'UnknownOperation':
errorMsg = `Error: Unknown Operation ${errorDetails}`;
if (errorDetails === '-') {
errorMsg += '\nNote: Negative numbers are not supported';
} else if (errorDetails === ' ') {
errorMsg += `\nNote: Every roll must be closed by ${config.postfix}`;
}
break;
case 'NoZerosAllowed':
errorMsg = 'Formatting Error: ';
switch (errorDetails) {
case 'base':
errorMsg += 'Die Size and Die Count';
break;
case 'drop':
errorMsg += 'Drop (d or dl)';
break;
case 'keep':
errorMsg += 'Keep (k or kh)';
break;
case 'dropHigh':
errorMsg += 'Drop Highest (dh)';
break;
case 'keepLow':
errorMsg += 'Keep Lowest (kl)';
break;
case 'reroll':
errorMsg += 'Reroll (r)';
break;
case 'critScore':
errorMsg += 'Crit Score (cs)';
break;
default:
errorMsg += `Unhandled - ${errorDetails}`;
break;
}
errorMsg += ' cannot be zero';
break;
case 'CritScoreMinGtrMax':
errorMsg = 'Formatting Error: CritScore maximum cannot be greater than minimum, check formatting and flip min/max';
break;
case 'MaxLoopsExceeded':
errorMsg = 'Error: Roll is too complex or reaches infinity';
break;
case 'UnbalancedParens':
errorMsg = 'Formatting Error: At least one of the equations contains unbalanced parenthesis';
break;
case 'EMDASNotNumber':
errorMsg = 'Error: One or more operands is not a number';
break;
case 'ConfWhat':
errorMsg = 'Error: Not all values got processed, please report the command used';
break;
case 'OperatorWhat':
errorMsg = 'Error: Something really broke with the Operator, try again';
break;
case 'OperandNaN':
errorMsg = 'Error: One or more operands reached NaN, check input';
break;
case 'UndefinedStep':
errorMsg = 'Error: Roll became undefined, one or more operands are not a roll or a number, check input';
break;
default:
log(LT.ERROR, `Undangled Error: ${errorName}, ${errorDetails}`);
errorMsg = `Unhandled Error: ${solverError.message}\nCheck input and try again, if issue persists, please use \`${config.prefix}report\` to alert the devs of the issue`;
break;
}
// Fill in the return block
returnmsg.error = true;
returnmsg.errorCode = solverError.message;
returnmsg.errorMsg = errorMsg;
}
return returnmsg;
};