BIIIIIG change here: NESTED ROLLS :D
Also, start the reorganization of artigen to support nesting better, make smaller more readable files, and better named functions
This commit is contained in:
parent
bdec5e7850
commit
03a2acc386
|
@ -0,0 +1,85 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { tokenizeMath } from 'artigen/mathTokenizer.ts';
|
||||
import { CountDetails, ReturnData } from 'artigen/solver.d.ts';
|
||||
|
||||
import { closeInternal, internalWrapRegex, openInternal } from 'artigen/utils/escape.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { getMatchingInternalIdx, getMatchingPostfixIdx } from 'artigen/utils/parenBalance.ts';
|
||||
|
||||
import { RollModifiers } from 'src/mod.d.ts';
|
||||
|
||||
// tokenizeCmd expects a string[] of items that are either config.prefix/config.postfix or some text that contains math and/or dice rolls
|
||||
export const tokenizeCmd = (cmd: string[], modifiers: RollModifiers, topLevel: boolean): [ReturnData[], CountDetails[]] => {
|
||||
loggingEnabled && log(LT.LOG, `Tokenizing command ${JSON.stringify(cmd)}`);
|
||||
|
||||
const returnData: ReturnData[] = [];
|
||||
const countDetails: CountDetails[] = [];
|
||||
|
||||
// Wrapped commands still exist, unwrap them
|
||||
while (cmd.includes(config.prefix)) {
|
||||
const openIdx = cmd.indexOf(config.prefix);
|
||||
const closeIdx = getMatchingPostfixIdx(cmd, openIdx);
|
||||
|
||||
// Handle any nested commands
|
||||
const [tempData, tempCounts] = tokenizeCmd(cmd.slice(openIdx + 1, closeIdx), modifiers, false);
|
||||
const data = tempData[0];
|
||||
|
||||
if (topLevel) {
|
||||
// Handle saving any formatting between dice
|
||||
if (openIdx !== 0) {
|
||||
data.rollPreFormat = cmd.slice(0, openIdx).join('');
|
||||
}
|
||||
|
||||
// Chop off all formatting between cmds along with the processed cmd
|
||||
cmd.splice(0, closeIdx + 1);
|
||||
} else {
|
||||
// We're handling something nested, replace [[cmd]] with the cmd's result
|
||||
cmd.splice(openIdx, closeIdx - openIdx + 1, `${openInternal}${data.rollTotal}${closeInternal}`);
|
||||
}
|
||||
|
||||
// Store results
|
||||
returnData.push(data);
|
||||
countDetails.push(...tempCounts);
|
||||
}
|
||||
|
||||
if (topLevel) {
|
||||
if (cmd.length) {
|
||||
loggingEnabled && log(LT.LOG, `Adding leftover formatting to last returnData ${JSON.stringify(cmd)}`);
|
||||
returnData[returnData.length - 1].rollPostFormat = cmd.join('');
|
||||
}
|
||||
return [returnData, countDetails];
|
||||
} else {
|
||||
loggingEnabled && log(LT.LOG, `Tokenizing math ${JSON.stringify(cmd)}`);
|
||||
|
||||
// Solve the math and rolls for this cmd
|
||||
const [tempData, tempCounts] = tokenizeMath(cmd.join(''), modifiers);
|
||||
const data = tempData[0];
|
||||
loggingEnabled && log(LT.LOG, `Solved math is back ${JSON.stringify(data)} | ${JSON.stringify(returnData)}`);
|
||||
|
||||
// Merge counts
|
||||
countDetails.push(...tempCounts);
|
||||
|
||||
// Handle merging returnData into tempData
|
||||
const initConf = data.initConfig.split(internalWrapRegex).filter((x) => x);
|
||||
loggingEnabled && log(LT.LOG, `Split solved math initConfig ${JSON.stringify(initConf)}`);
|
||||
while (initConf.includes(openInternal)) {
|
||||
const openIdx = initConf.indexOf(openInternal);
|
||||
const closeIdx = getMatchingInternalIdx(initConf, openIdx);
|
||||
|
||||
// Take first returnData out of array
|
||||
const dataToMerge = returnData.shift();
|
||||
|
||||
// Replace the found pair with the nested initConfig and result
|
||||
initConf.splice(openIdx, closeIdx - openIdx + 1, `${config.prefix}${dataToMerge?.initConfig}=${dataToMerge?.rollTotal}${config.postfix}`);
|
||||
loggingEnabled && log(LT.LOG, `Current initConf state ${JSON.stringify(initConf)}`);
|
||||
}
|
||||
|
||||
// Join all parts/remainders
|
||||
data.initConfig = initConf.join('');
|
||||
loggingEnabled && log(LT.LOG, `ReturnData merged into solved math ${JSON.stringify(data)} | ${JSON.stringify(countDetails)}`);
|
||||
return [[data], countDetails];
|
||||
}
|
||||
};
|
|
@ -0,0 +1,158 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { formatRoll } from 'artigen/rollFormatter.ts';
|
||||
import { fullSolver } from 'artigen/solver.ts';
|
||||
import { CountDetails, MathConf, ReturnData, SolvedStep } from 'artigen/solver.d.ts';
|
||||
|
||||
import { cmdSplitRegex, internalWrapRegex } 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 { RollModifiers } from 'src/mod.d.ts';
|
||||
|
||||
const operators = ['(', ')', '^', '*', '/', '%', '+', '-'];
|
||||
|
||||
export const tokenizeMath = (cmd: string, modifiers: RollModifiers): [ReturnData[], CountDetails[]] => {
|
||||
const countDetails: CountDetails[] = [];
|
||||
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${cmd}`);
|
||||
|
||||
// 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[] = <MathConf[]> cmd
|
||||
.replace(cmdSplitRegex, '')
|
||||
.replace(internalWrapRegex, '')
|
||||
.replace(/ /g, '')
|
||||
.split(/([-+()*/^]|(?<![d%])%)/g)
|
||||
.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++) {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${JSON.stringify(cmd)} | Evaluating rolls into math-able items ${JSON.stringify(mathConf[i])}`);
|
||||
|
||||
const strMathConfI = mathConf[i].toString();
|
||||
|
||||
if (strMathConfI.length === 0) {
|
||||
// If its an empty string, get it out of here
|
||||
mathConf.splice(i, 1);
|
||||
i--;
|
||||
} else if (mathConf[i] == parseFloat(strMathConfI)) {
|
||||
// If its a number, parse the number out
|
||||
mathConf[i] = parseFloat(strMathConfI);
|
||||
} else if (strMathConfI.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 (strMathConfI.toLowerCase() === 'fart' || strMathConfI.toLowerCase() === '💩') {
|
||||
mathConf[i] = {
|
||||
total: 7,
|
||||
details: '💩',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'sex') {
|
||||
mathConf[i] = {
|
||||
total: 69,
|
||||
details: '( ͡° ͜ʖ ͡°)',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'inf' || strMathConfI.toLowerCase() === 'infinity' || strMathConfI.toLowerCase() === '∞') {
|
||||
// If the operand is the constant Infinity, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Infinity,
|
||||
details: '∞',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'pi' || strMathConfI.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 (strMathConfI.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,
|
||||
},
|
||||
],
|
||||
);
|
||||
i += 2;
|
||||
} else if (!legalMathOperators.includes(strMathConfI) && legalMathOperators.some((mathOp) => strMathConfI.endsWith(mathOp))) {
|
||||
// Identify when someone does something weird like 4floor(2.5) and split 4 and floor
|
||||
const matchedMathOp = legalMathOperators.filter((mathOp) => strMathConfI.endsWith(mathOp))[0];
|
||||
mathConf[i] = parseFloat(strMathConfI.replace(matchedMathOp, ''));
|
||||
|
||||
mathConf.splice(i + 1, 0, ...['*', matchedMathOp]);
|
||||
i += 2;
|
||||
} else if (![...operators, ...legalMathOperators].includes(strMathConfI)) {
|
||||
// If nothing else has handled it by now, try it as a roll
|
||||
const formattedRoll = formatRoll(strMathConfI, modifiers);
|
||||
mathConf[i] = formattedRoll.solvedStep;
|
||||
countDetails.push(formattedRoll.countDetails);
|
||||
}
|
||||
|
||||
// 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) || 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] = <number> mathConf[i] * -1;
|
||||
} else {
|
||||
(<SolvedStep> mathConf[i]).total = (<SolvedStep> mathConf[i]).total * -1;
|
||||
(<SolvedStep> mathConf[i]).details = `-${(<SolvedStep> mathConf[i]).details}`;
|
||||
}
|
||||
mathConf.splice(i - 1, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that mathConf is parsed, send it into the solver
|
||||
const tempSolved = fullSolver(mathConf);
|
||||
|
||||
// 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,
|
||||
},
|
||||
],
|
||||
countDetails,
|
||||
];
|
||||
};
|
|
@ -1,22 +1,19 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
import { tokenizeCmd } from 'artigen/cmdTokenizer.ts';
|
||||
import { SolvedRoll } from 'artigen/solver.d.ts';
|
||||
|
||||
import { formatRoll } from 'artigen/rollFormatter.ts';
|
||||
import { fullSolver } from 'artigen/solver.ts';
|
||||
import { CountDetails, ReturnData, SolvedRoll, SolvedStep } from 'artigen/solver.d.ts';
|
||||
|
||||
import { escapeCharacters } from 'artigen/utils/escape.ts';
|
||||
import { legalMathOperators } from 'artigen/utils/legalMath.ts';
|
||||
import { cmdSplitRegex, escapeCharacters } from 'artigen/utils/escape.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { compareTotalRolls, compareTotalRollsReverse } from 'artigen/utils/sortFuncs.ts';
|
||||
import { translateError } from 'artigen/utils/translateError.ts';
|
||||
|
||||
import { RollModifiers } from 'src/mod.d.ts';
|
||||
import { assertPrePostBalance } from 'src/artigen/utils/parenBalance.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 operators = ['(', ')', '^', '*', '/', '%', '+', '-'];
|
||||
const returnMsg = <SolvedRoll> {
|
||||
error: false,
|
||||
errorCode: '',
|
||||
|
@ -34,181 +31,21 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
|
|||
},
|
||||
};
|
||||
|
||||
// Whole function lives in a try-catch to allow safe throwing of errors on purpose
|
||||
// Whole processor lives in a try-catch to catch artigen's intentional error conditions
|
||||
try {
|
||||
// Split the fullCmd by the command prefix to allow every roll/math op to be handled individually
|
||||
const sepRolls = fullCmd.split(config.prefix);
|
||||
// TODO(@burn-e99): HERE for the [[ ]] nesting stuff
|
||||
// filter removes all null/empty strings since we don't care about them
|
||||
const sepCmds = fullCmd.split(cmdSplitRegex).filter((x) => x);
|
||||
loggingEnabled && log(LT.LOG, `Split cmd into parts ${JSON.stringify(sepCmds)}`);
|
||||
|
||||
const tempReturnData: ReturnData[] = [];
|
||||
const tempCountDetails: CountDetails[] = [
|
||||
{
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
},
|
||||
];
|
||||
// Verify prefix/postfix balance
|
||||
assertPrePostBalance(sepCmds);
|
||||
|
||||
// Loop thru all roll/math ops
|
||||
for (const sepRoll of sepRolls) {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Working ${sepRoll}`);
|
||||
// Split the current iteration on the command postfix to separate the operation to be parsed and the text formatting after the operation
|
||||
const [tempConf, tempFormat] = sepRoll.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(/([-+()*/^]|(?<![d%])%)/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) => {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Checking parenthesis balance ${e}`);
|
||||
if (e === '(') {
|
||||
parenCnt++;
|
||||
} else if (e === ')') {
|
||||
parenCnt--;
|
||||
}
|
||||
|
||||
// If parenCnt ever goes below 0, that means too many closing paren appeared before opening parens
|
||||
if (parenCnt < 0) {
|
||||
throw new Error('UnbalancedParens');
|
||||
}
|
||||
});
|
||||
|
||||
// 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++) {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Evaluating rolls into math-able items ${JSON.stringify(mathConf[i])}`);
|
||||
|
||||
const strMathConfI = mathConf[i].toString();
|
||||
|
||||
if (strMathConfI.length === 0) {
|
||||
// If its an empty string, get it out of here
|
||||
mathConf.splice(i, 1);
|
||||
i--;
|
||||
} else if (mathConf[i] == parseFloat(strMathConfI)) {
|
||||
// If its a number, parse the number out
|
||||
mathConf[i] = parseFloat(strMathConfI);
|
||||
} else if (strMathConfI.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 (strMathConfI.toLowerCase() === 'fart' || strMathConfI.toLowerCase() === '💩') {
|
||||
mathConf[i] = {
|
||||
total: 7,
|
||||
details: '💩',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'sex') {
|
||||
mathConf[i] = {
|
||||
total: 69,
|
||||
details: '( ͡° ͜ʖ ͡°)',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'inf' || strMathConfI.toLowerCase() === 'infinity' || strMathConfI.toLowerCase() === '∞') {
|
||||
// If the operand is the constant Infinity, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Infinity,
|
||||
details: '∞',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'pi' || strMathConfI.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 (strMathConfI.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,
|
||||
},
|
||||
],
|
||||
);
|
||||
i += 2;
|
||||
} else if (!legalMathOperators.includes(strMathConfI) && legalMathOperators.some((mathOp) => strMathConfI.endsWith(mathOp))) {
|
||||
// Identify when someone does something weird like 4floor(2.5) and split 4 and floor
|
||||
const matchedMathOp = legalMathOperators.filter((mathOp) => strMathConfI.endsWith(mathOp))[0];
|
||||
mathConf[i] = parseFloat(strMathConfI.replace(matchedMathOp, ''));
|
||||
|
||||
mathConf.splice(i + 1, 0, ...['*', matchedMathOp]);
|
||||
i += 2;
|
||||
} else if (![...operators, ...legalMathOperators].includes(strMathConfI)) {
|
||||
// If nothing else has handled it by now, try it as a roll
|
||||
const formattedRoll = formatRoll(strMathConfI, modifiers);
|
||||
mathConf[i] = formattedRoll.solvedStep;
|
||||
tempCountDetails.push(formattedRoll.countDetails);
|
||||
}
|
||||
|
||||
// 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) || 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] = <number> mathConf[i] * -1;
|
||||
} else {
|
||||
(<SolvedStep> mathConf[i]).total = (<SolvedStep> mathConf[i]).total * -1;
|
||||
(<SolvedStep> mathConf[i]).details = `-${(<SolvedStep> mathConf[i]).details}`;
|
||||
}
|
||||
mathConf.splice(i - 1, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Send the split roll into the command tokenizer to get raw response data
|
||||
const [tempReturnData, tempCountDetails] = tokenizeCmd(sepCmds, modifiers, true);
|
||||
loggingEnabled && log(LT.LOG, `Return data is back ${JSON.stringify(tempReturnData)}`);
|
||||
|
||||
// Remove any floating spaces from fullCmd
|
||||
if (fullCmd[fullCmd.length - 1] === ' ') {
|
||||
fullCmd = fullCmd.substring(0, fullCmd.length - 1);
|
||||
}
|
||||
fullCmd = fullCmd.trim();
|
||||
|
||||
// Escape any | and ` chars in fullCmd to prevent spoilers and code blocks from acting up
|
||||
fullCmd = escapeCharacters(fullCmd, '|');
|
||||
|
@ -220,6 +57,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
|
|||
|
||||
// The ': ' is used by generateRollEmbed to split line 2 up
|
||||
const resultStr = tempReturnData.length > 1 ? 'Results: ' : 'Result: ';
|
||||
line2 = resultStr;
|
||||
|
||||
// If a theoretical roll is requested, mark the output as such, else use default formatting
|
||||
if (modifiers.maxRoll || modifiers.minRoll || modifiers.nominalRoll) {
|
||||
|
@ -227,19 +65,16 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
|
|||
const theoreticalBools = [modifiers.maxRoll, modifiers.minRoll, modifiers.nominalRoll];
|
||||
const theoreticalText = theoreticalTexts[theoreticalBools.indexOf(true)];
|
||||
|
||||
line1 = ` requested the Theoretical ${theoreticalText} of:\n\`${config.prefix}${fullCmd}\``;
|
||||
line1 = ` requested the Theoretical ${theoreticalText} of:\n\`${fullCmd}\``;
|
||||
line2 = `Theoretical ${theoreticalText} ${resultStr}`;
|
||||
} else if (modifiers.order === 'a') {
|
||||
line1 = ` requested the following rolls to be ordered from least to greatest:\n\`${config.prefix}${fullCmd}\``;
|
||||
line2 = resultStr;
|
||||
line1 = ` requested the following rolls to be ordered from least to greatest:\n\`${fullCmd}\``;
|
||||
tempReturnData.sort(compareTotalRolls);
|
||||
} else if (modifiers.order === 'd') {
|
||||
line1 = ` requested the following rolls to be ordered from greatest to least:\n\`${config.prefix}${fullCmd}\``;
|
||||
line2 = resultStr;
|
||||
line1 = ` requested the following rolls to be ordered from greatest to least:\n\`${fullCmd}\``;
|
||||
tempReturnData.sort(compareTotalRollsReverse);
|
||||
} else {
|
||||
line1 = ` rolled:\n\`${config.prefix}${fullCmd}\``;
|
||||
line2 = resultStr;
|
||||
line1 = ` rolled:\n\`${fullCmd}\``;
|
||||
}
|
||||
|
||||
// Fill out all of the details and results now
|
||||
|
@ -260,11 +95,8 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
|
|||
|
||||
// Populate line2 (the results) and line3 (the details) with their data
|
||||
if (modifiers.order === '') {
|
||||
line2 += `${preFormat}${modifiers.commaTotals ? e.rollTotal.toLocaleString() : e.rollTotal}${postFormat}${
|
||||
escapeCharacters(
|
||||
e.rollPostFormat,
|
||||
'|*_~`',
|
||||
)
|
||||
line2 += `${e.rollPreFormat ? escapeCharacters(e.rollPreFormat, '|*_~`') : ' '}${preFormat}${modifiers.commaTotals ? e.rollTotal.toLocaleString() : e.rollTotal}${postFormat}${
|
||||
e.rollPostFormat ? escapeCharacters(e.rollPostFormat, '|*_~`') : ''
|
||||
}`;
|
||||
} else {
|
||||
// If order is on, turn rolls into csv without formatting
|
||||
|
@ -294,114 +126,9 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
|
|||
exploded: acc.exploded + cnt.exploded,
|
||||
}));
|
||||
} catch (e) {
|
||||
const solverError = e as Error;
|
||||
// Welp, the unthinkable happened, we hit an error
|
||||
|
||||
// Split on _ for the error messages that have more info than just their name
|
||||
const errorSplits = solverError.message.split('_');
|
||||
const errorName = errorSplits.shift();
|
||||
const errorDetails = errorSplits.join('_');
|
||||
|
||||
let errorMsg = '';
|
||||
|
||||
// Translate the errorName to a specific errorMsg
|
||||
switch (errorName) {
|
||||
case 'WholeDieCountSizeOnly':
|
||||
errorMsg = 'Error: Die Size and Die Count must be whole numbers';
|
||||
break;
|
||||
case 'YouNeedAD':
|
||||
errorMsg = 'Formatting Error: Missing die size and count config';
|
||||
break;
|
||||
case 'CannotParseDieCount':
|
||||
errorMsg = `Formatting Error: Cannot parse \`${errorDetails}\` as a number`;
|
||||
break;
|
||||
case 'DoubleSeparator':
|
||||
errorMsg = `Formatting Error: \`${errorDetails}\` should only be specified once per roll, remove all but one and repeat roll`;
|
||||
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;
|
||||
case 'critFail':
|
||||
errorMsg += 'Crit Fail (`cf`)';
|
||||
break;
|
||||
default:
|
||||
errorMsg += `Unhandled - ${errorDetails}`;
|
||||
break;
|
||||
}
|
||||
errorMsg += ' cannot be zero';
|
||||
break;
|
||||
case 'NoRerollOnAllSides':
|
||||
errorMsg = 'Error: Cannot reroll all sides of a die, must have at least one side that does not get rerolled';
|
||||
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, `Unhandled Parser 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;
|
||||
[returnMsg.errorCode, returnMsg.errorMsg] = translateError(e as Error);
|
||||
}
|
||||
|
||||
return returnMsg;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
// Available Roll Types
|
||||
export type RollType = '' | 'roll20' | 'fate' | 'cwod' | 'ova';
|
||||
|
||||
export type MathConf = string | number | SolvedStep;
|
||||
|
||||
// RollSet is used to preserve all information about a calculated roll
|
||||
export type RollSet = {
|
||||
type: RollType;
|
||||
|
@ -24,6 +26,7 @@ export type SolvedStep = {
|
|||
// ReturnData is the temporary internal type used before getting turned into SolvedRoll
|
||||
export type ReturnData = {
|
||||
rollTotal: number;
|
||||
rollPreFormat: string;
|
||||
rollPostFormat: string;
|
||||
rollDetails: string;
|
||||
containsCrit: boolean;
|
||||
|
|
|
@ -5,14 +5,15 @@
|
|||
*/
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { SolvedStep } from 'artigen/solver.d.ts';
|
||||
import { MathConf, SolvedStep } from 'artigen/solver.d.ts';
|
||||
|
||||
import { legalMath, legalMathOperators } from 'artigen/utils/legalMath.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
import { getMatchingParenIdx } from 'artigen/utils/parenBalance.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 => {
|
||||
export const fullSolver = (conf: MathConf[], wrapDetails = false): SolvedStep => {
|
||||
// Initialize PEMDAS
|
||||
const signs = ['^', '*', '/', '%', '+', '-'];
|
||||
const stepSolve = {
|
||||
|
@ -33,30 +34,7 @@ export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails:
|
|||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for (`);
|
||||
// Get first open parenthesis
|
||||
let 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');
|
||||
}
|
||||
const closeParenIdx = getMatchingParenIdx(conf, openParenIdx);
|
||||
|
||||
// Call the solver on the items between openParenIdx and closeParenIdx (excluding the parens)
|
||||
const parenSolve = fullSolver(conf.slice(openParenIdx + 1, closeParenIdx), true);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
// escapeCharacters(str, esc) returns str
|
||||
|
@ -14,3 +16,13 @@ export const escapeCharacters = (str: string, esc: string): string => {
|
|||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
// escapePrefixPostfix(str) returns str
|
||||
// Escapes all characters that need escaped in a regex string to allow prefix/postfix to be configurable
|
||||
const escapePrefixPostfix = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
export const cmdSplitRegex = new RegExp(`(${escapePrefixPostfix(config.prefix)})|(${escapePrefixPostfix(config.postfix)})`, 'g');
|
||||
|
||||
// Internal is used for recursive text replacement, these will always be the top level as they get replaced with config.prefix/postfix when exiting each level
|
||||
export const openInternal = '\u2045';
|
||||
export const closeInternal = '\u2046';
|
||||
export const internalWrapRegex = new RegExp(`([${openInternal}${closeInternal}])`, 'g');
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
import { MathConf } from 'artigen/solver.d.ts';
|
||||
|
||||
import { closeInternal, openInternal } from 'artigen/utils/escape.ts';
|
||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||
|
||||
const checkBalance = (conf: MathConf[], openStr: string, closeStr: string, errorType: string, getMatching: boolean, openIdx: number): number => {
|
||||
let parenCnt = 0;
|
||||
|
||||
// Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens
|
||||
for (let i = openIdx; i < conf.length; i++) {
|
||||
loggingEnabled &&
|
||||
log(
|
||||
LT.LOG,
|
||||
`${getMatching ? 'Looking for matching' : 'Checking'} ${openStr}/${closeStr} ${getMatching ? '' : 'balance '}on ${
|
||||
JSON.stringify(
|
||||
conf,
|
||||
)
|
||||
} | at ${JSON.stringify(conf[i])}`,
|
||||
);
|
||||
if (conf[i] === openStr) {
|
||||
parenCnt++;
|
||||
} else if (conf[i] === closeStr) {
|
||||
parenCnt--;
|
||||
}
|
||||
|
||||
// If parenCnt ever goes below 0, that means too many closing paren appeared before opening parens
|
||||
if (parenCnt < 0) {
|
||||
throw new Error(`Unbalanced${errorType}`);
|
||||
}
|
||||
|
||||
// When parenCnt reaches 0 again, we will have found the matching closing parenthesis and can safely exit the for loop
|
||||
if (getMatching && parenCnt === 0) {
|
||||
loggingEnabled && log(LT.LOG, `Matching ${openStr}/${closeStr} found at "${i}" | ${JSON.stringify(conf[i])}`);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// If the parenCnt is not 0, then we do not have balanced parens and need to error out now
|
||||
// If getMatching flag is set and we have exited the loop, we did not find a matching paren
|
||||
if (parenCnt !== 0 || getMatching) {
|
||||
throw new Error(`Unbalanced${errorType}`);
|
||||
}
|
||||
|
||||
// getMatching flag not set, this value is unused
|
||||
return 0;
|
||||
};
|
||||
|
||||
// assertXBalance verifies the entire conf has balanced X
|
||||
export const assertParenBalance = (conf: MathConf[]) => checkBalance(conf, '(', ')', 'Paren', false, 0);
|
||||
export const assertPrePostBalance = (conf: MathConf[]) => checkBalance(conf, config.prefix, config.postfix, 'PrefixPostfix', false, 0);
|
||||
|
||||
// getMatchingXIdx gets the matching X, also partially verifies the conf has balanced X
|
||||
export const getMatchingInternalIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, openInternal, closeInternal, 'Internal', true, openIdx);
|
||||
export const getMatchingParenIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, '(', ')', 'Paren', true, openIdx);
|
||||
export const getMatchingPostfixIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, config.prefix, config.postfix, 'PrefixPostfix', true, openIdx);
|
|
@ -0,0 +1,114 @@
|
|||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '~config';
|
||||
|
||||
export const translateError = (solverError: Error): [string, string] => {
|
||||
// Welp, the unthinkable happened, we hit an error
|
||||
|
||||
// Split on _ for the error messages that have more info than just their name
|
||||
const errorSplits = solverError.message.split('_');
|
||||
const errorName = errorSplits.shift();
|
||||
const errorDetails = errorSplits.join('_');
|
||||
|
||||
let errorMsg = '';
|
||||
|
||||
// Translate the errorName to a specific errorMsg
|
||||
switch (errorName) {
|
||||
case 'WholeDieCountSizeOnly':
|
||||
errorMsg = 'Error: Die Size and Die Count must be whole numbers';
|
||||
break;
|
||||
case 'YouNeedAD':
|
||||
errorMsg = 'Formatting Error: Missing die size and count config';
|
||||
break;
|
||||
case 'CannotParseDieCount':
|
||||
errorMsg = `Formatting Error: Cannot parse \`${errorDetails}\` as a number`;
|
||||
break;
|
||||
case 'DoubleSeparator':
|
||||
errorMsg = `Formatting Error: \`${errorDetails}\` should only be specified once per roll, remove all but one and repeat roll`;
|
||||
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;
|
||||
case 'critFail':
|
||||
errorMsg += 'Crit Fail (`cf`)';
|
||||
break;
|
||||
default:
|
||||
errorMsg += `Unhandled - ${errorDetails}`;
|
||||
break;
|
||||
}
|
||||
errorMsg += ' cannot be zero';
|
||||
break;
|
||||
case 'NoRerollOnAllSides':
|
||||
errorMsg = 'Error: Cannot reroll all sides of a die, must have at least one side that does not get rerolled';
|
||||
break;
|
||||
case 'CritScoreMinGtrMax':
|
||||
errorMsg = 'Formatting Error: CritScore maximum cannot be greater than minimum, check formatting and flip min/max';
|
||||
break;
|
||||
case 'Invalid string length':
|
||||
case 'MaxLoopsExceeded':
|
||||
errorMsg = 'Error: Roll is too complex or reaches infinity';
|
||||
break;
|
||||
case 'UnbalancedParen':
|
||||
errorMsg = 'Formatting Error: At least one of the equations contains unbalanced `(`/`)`';
|
||||
break;
|
||||
case 'UnbalancedPrefixPostfix':
|
||||
errorMsg = `Formatting Error: At least one of the equations contains unbalanced \`${config.prefix}\`/\`${config.postfix}\``;
|
||||
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, `Unhandled Parser 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;
|
||||
}
|
||||
|
||||
return [solverError.message, errorMsg];
|
||||
};
|
|
@ -49,13 +49,10 @@ export const roll = async (message: DiscordenoMessage, args: string[], command:
|
|||
return;
|
||||
}
|
||||
|
||||
// Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituted in
|
||||
const rollCmd = message.content.substring(config.prefix.length);
|
||||
|
||||
sendRollRequest({
|
||||
apiRoll: false,
|
||||
dd: { myResponse: m, originalMessage: message },
|
||||
rollCmd,
|
||||
rollCmd: message.content,
|
||||
modifiers,
|
||||
originalCommand,
|
||||
});
|
||||
|
|
|
@ -78,7 +78,7 @@ export const apiRoll = async (query: Map<string, string>, apiUserid: bigint): Pr
|
|||
}
|
||||
|
||||
// Clip off the leading prefix. API calls must be formatted with a prefix at the start to match how commands are sent in Discord
|
||||
rollCmd = rollCmd.substring(rollCmd.indexOf(config.prefix) + 2).replace(/%20/g, ' ');
|
||||
rollCmd = rollCmd.replace(/%20/g, ' ').trim();
|
||||
|
||||
const modifiers: RollModifiers = {
|
||||
noDetails: query.has('nd'),
|
||||
|
|
Loading…
Reference in New Issue