From cbac134f796adcd9cbab4d108b5fe7c38f2cb6de Mon Sep 17 00:00:00 2001 From: Ean Milligan Date: Mon, 7 Jul 2025 02:12:53 -0400 Subject: [PATCH] Add initial group support, only support SUM mode (ie no modifiers allowed) --- src/artigen/cmdTokenizer.ts | 65 +++++++++++++---- src/artigen/dice/groupHandler.ts | 112 ++++++++++++++++++++++++++++++ src/artigen/math/mathTokenizer.ts | 21 +++++- src/artigen/utils/escape.ts | 5 ++ src/artigen/utils/parenBalance.ts | 3 +- 5 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 src/artigen/dice/groupHandler.ts diff --git a/src/artigen/cmdTokenizer.ts b/src/artigen/cmdTokenizer.ts index e0bee29..2325bf1 100644 --- a/src/artigen/cmdTokenizer.ts +++ b/src/artigen/cmdTokenizer.ts @@ -5,15 +5,16 @@ import config from '~config'; import { ReturnData } from 'artigen/artigen.d.ts'; import { CountDetails, RollDistributionMap, RollModifiers } from 'artigen/dice/dice.d.ts'; +import { handleGroup } from 'artigen/dice/groupHandler.ts'; import { loopCountCheck } from 'artigen/managers/loopManager.ts'; import { tokenizeMath } from 'artigen/math/mathTokenizer.ts'; import { reduceCountDetails } from 'artigen/utils/counter.ts'; -import { closeInternal, cmdSplitRegex, internalWrapRegex, openInternal } from 'artigen/utils/escape.ts'; +import { closeInternal, closeInternalGrp, internalGrpWrapRegex, internalWrapRegex, openInternal, openInternalGrp } from 'artigen/utils/escape.ts'; import { loggingEnabled } from 'artigen/utils/logFlag.ts'; -import { assertGroupBalance, getMatchingGroupIdx, getMatchingInternalIdx, getMatchingPostfixIdx } from 'artigen/utils/parenBalance.ts'; +import { assertGroupBalance, getMatchingGroupIdx, getMatchingInternalGrpIdx, getMatchingInternalIdx, getMatchingPostfixIdx } from 'artigen/utils/parenBalance.ts'; import { basicReducer } from 'artigen/utils/reducers.ts'; // tokenizeCmd expects a string[] of items that are either config.prefix/config.postfix or some text that contains math and/or dice rolls @@ -131,38 +132,78 @@ export const tokenizeCmd = ( } return [returnData, countDetails, rollDists]; } else { - // Check for any groups and handle them? + // Check for any groups and handle them const groupParts = cmd .join('') - .split(/([{,}])/g) + .split(/([{}])/g) .filter((x) => x); + const groupResults: ReturnData[] = []; if (groupParts.includes('{')) { assertGroupBalance(groupParts); } while (groupParts.includes('{')) { loggingEnabled && log(LT.LOG, `Handling Groups | Current cmd: ${JSON.stringify(groupParts)}`); - const openIdx = groupParts.indexOf('}'); - const closeIdx = getMatchingGroupIdx; - const temp = cmd.join('').replaceAll('{', '').replaceAll('}', '').replaceAll(',', ''); - cmd = temp.split(cmdSplitRegex); + + const openIdx = groupParts.indexOf('{'); + const closeIdx = getMatchingGroupIdx(groupParts, openIdx); + + const currentGrp = groupParts.slice(openIdx + 1, closeIdx); + + const [tempData, tempCounts, tempDists] = handleGroup(currentGrp, '', modifiers, previousResults); + const data = tempData[0]; + log(LT.LOG, `Solved Group is back ${JSON.stringify(data)} | ${JSON.stringify(returnData)} ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`); + + countDetails.push(...tempCounts); + rollDists.push(...tempDists); + + // Merge result back into groupParts + groupParts.splice(openIdx, closeIdx - openIdx + 1, `${openInternalGrp}${groupResults.length}${closeInternalGrp}`); + groupResults.push(data); } const cmdForMath = groupParts.join(''); loggingEnabled && log(LT.LOG, `Tokenizing math ${cmdForMath}`); // Solve the math and rolls for this cmd - const [tempData, tempCounts, tempDists] = tokenizeMath(cmdForMath, modifiers, previousResults); + const [tempData, tempCounts, tempDists] = tokenizeMath(cmdForMath, modifiers, previousResults, groupResults); const data = tempData[0]; loggingEnabled && - log(LT.LOG, `Solved math is back ${JSON.stringify(data)} | ${JSON.stringify(returnData)} ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`); + log( + LT.LOG, + `Solved math is back ${JSON.stringify(data)} | ${JSON.stringify(returnData)} ${JSON.stringify(groupResults)} ${ + JSON.stringify( + tempCounts, + ) + } ${JSON.stringify(tempDists)}`, + ); // Merge counts countDetails.push(...tempCounts); rollDists.push(...tempDists); + // Handle merging group data into initConfig first since a group could "smuggle" a returnData in it + const tempInitConf = data.initConfig.split(internalGrpWrapRegex).filter((x) => x); + loggingEnabled && log(LT.LOG, `Split solved math into tempInitConf ${JSON.stringify(tempInitConf)}`); + while (tempInitConf.includes(openInternalGrp)) { + loopCountCheck(); + + const openIdx = tempInitConf.indexOf(openInternalGrp); + const closeIdx = getMatchingInternalGrpIdx(tempInitConf, openIdx); + + // Take first groupResult out of array + const dataToMerge = groupResults.shift(); + + // Replace the found pair with the nested tempInitConfig and result + tempInitConf.splice(openIdx, closeIdx - openIdx + 1, `${dataToMerge?.initConfig}`); + loggingEnabled && log(LT.LOG, `Current tempInitConf state ${JSON.stringify(tempInitConf)}`); + } + // 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)}`); + const initConf = tempInitConf + .join('') + .split(internalWrapRegex) + .filter((x) => x); + loggingEnabled && log(LT.LOG, `Split tempInitConfig into initConf ${JSON.stringify(initConf)}`); while (initConf.includes(openInternal)) { loopCountCheck(); diff --git a/src/artigen/dice/groupHandler.ts b/src/artigen/dice/groupHandler.ts new file mode 100644 index 0000000..093a1f5 --- /dev/null +++ b/src/artigen/dice/groupHandler.ts @@ -0,0 +1,112 @@ +import { log, LogTypes as LT } from '@Log4Deno'; + +import { ReturnData } from 'artigen/artigen.d.ts'; + +import { CountDetails, RollDistributionMap, RollModifiers } from 'artigen/dice/dice.d.ts'; + +import { loopCountCheck } from 'artigen/managers/loopManager.ts'; + +import { tokenizeMath } from 'artigen/math/mathTokenizer.ts'; + +import { closeInternalGrp, openInternalGrp } from 'artigen/utils/escape.ts'; +import { loggingEnabled } from 'artigen/utils/logFlag.ts'; +import { getMatchingGroupIdx } from 'artigen/utils/parenBalance.ts'; + +export const handleGroup = ( + groupParts: string[], + groupModifiers: string, + modifiers: RollModifiers, + previousResults: number[], +): [ReturnData[], CountDetails[], RollDistributionMap[]] => { + const returnData: ReturnData[] = []; + const countDetails: CountDetails[] = []; + const rollDists: RollDistributionMap[] = []; + + // Nested groups still exist, unwrap them + while (groupParts.includes('{')) { + loopCountCheck(); + + loggingEnabled && log(LT.LOG, `Handling Nested Groups | Current cmd: ${JSON.stringify(groupParts)}`); + + const openIdx = groupParts.indexOf('}'); + const closeIdx = getMatchingGroupIdx(groupParts, openIdx); + + const currentGrp = groupParts.slice(openIdx + 1, closeIdx); + + const [tempData, tempCounts, tempDists] = handleGroup(currentGrp, '', modifiers, previousResults); + const data = tempData[0]; + loggingEnabled && log(LT.LOG, `Solved Nested Group is back ${JSON.stringify(data)} | ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`); + + countDetails.push(...tempCounts); + rollDists.push(...tempDists); + + // Merge result back into groupParts + groupParts.splice(openIdx, closeIdx - openIdx + 1, `${openInternalGrp}${data.rollTotal}${closeInternalGrp}`); + } + + // Handle the items in the groups + const commaParts = groupParts + .join('') + .split(',') + .filter((x) => x); + + if (commaParts.length > 1) { + loggingEnabled && log(LT.LOG, `In multi-mode ${JSON.stringify(commaParts)} ${groupModifiers}`); + // Handle "normal operation" of group + const groupResults: ReturnData[] = []; + + for (const part of commaParts) { + loopCountCheck(); + + loggingEnabled && log(LT.LOG, `Solving commaPart: ${part}`); + const [tempData, tempCounts, tempDists] = tokenizeMath(part, modifiers, previousResults, []); + const data = tempData[0]; + + loggingEnabled && log(LT.LOG, `Solved Math for Group is back ${JSON.stringify(data)} | ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`); + + countDetails.push(...tempCounts); + rollDists.push(...tempDists); + groupResults.push(data); + } + + if (groupModifiers) { + // Handle the provided modifiers + } else { + // Sum mode + const data = groupResults.reduce((prev, cur) => ({ + rollTotal: prev.rollTotal + cur.rollTotal, + rollPreFormat: '', + rollPostFormat: '', + rollDetails: prev.rollDetails + ' + ' + cur.rollDetails, + containsCrit: prev.containsCrit || cur.containsCrit, + containsFail: prev.containsFail || cur.containsFail, + initConfig: prev.initConfig + ', ' + cur.initConfig, + isComplex: prev.isComplex || cur.isComplex, + })); + data.initConfig = `{${data.initConfig}}`; + data.rollDetails = `{${data.rollDetails}}`; + returnData.push(data); + } + } else { + loggingEnabled && log(LT.LOG, `In single-mode ${JSON.stringify(commaParts)} ${groupModifiers}`); + if (groupModifiers) { + // Handle special case where the group modifiers are applied across the dice rolled + // ex from roll20 docs: {4d6+3d8}k4 - Roll 4 d6's and 3 d8's, out of those 7 dice the highest 4 are kept and summed up. + } else { + // why did you put this in a group, that was entirely pointless + loggingEnabled && log(LT.LOG, `Solving commaPart: ${commaParts[0]}`); + const [tempData, tempCounts, tempDists] = tokenizeMath(commaParts[0], modifiers, previousResults, []); + const data = tempData[0]; + + loggingEnabled && log(LT.LOG, `Solved Math for Group is back ${JSON.stringify(data)} | ${JSON.stringify(tempCounts)} ${JSON.stringify(tempDists)}`); + + countDetails.push(...tempCounts); + rollDists.push(...tempDists); + data.initConfig = `{${data.initConfig}}`; + data.rollDetails = `{${data.rollDetails}}`; + returnData.push(data); + } + } + + return [returnData, countDetails, rollDists]; +}; diff --git a/src/artigen/math/mathTokenizer.ts b/src/artigen/math/mathTokenizer.ts index 32891f9..1bfc360 100644 --- a/src/artigen/math/mathTokenizer.ts +++ b/src/artigen/math/mathTokenizer.ts @@ -11,7 +11,7 @@ import { loopCountCheck } from 'artigen/managers/loopManager.ts'; import { mathSolver } from 'artigen/math/mathSolver.ts'; -import { cmdSplitRegex, internalWrapRegex } from 'artigen/utils/escape.ts'; +import { closeInternalGrp, cmdSplitRegex, internalWrapRegex, 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'; @@ -20,7 +20,12 @@ import { assertParenBalance } from 'artigen/utils/parenBalance.ts'; const minusOps = ['(', '^', '**', '*', '/', '%', '+', '-']; const allOps = [...minusOps, ')']; -export const tokenizeMath = (cmd: string, modifiers: RollModifiers, previousResults: number[]): [ReturnData[], CountDetails[], RollDistributionMap[]] => { +export const tokenizeMath = ( + cmd: string, + modifiers: RollModifiers, + previousResults: number[], + groupResults: ReturnData[], +): [ReturnData[], CountDetails[], RollDistributionMap[]] => { const countDetails: CountDetails[] = []; const rollDists: RollDistributionMap[] = []; @@ -54,6 +59,18 @@ export const tokenizeMath = (cmd: string, modifiers: RollModifiers, previousResu } 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] = { diff --git a/src/artigen/utils/escape.ts b/src/artigen/utils/escape.ts index 65e8b3c..873f7ca 100644 --- a/src/artigen/utils/escape.ts +++ b/src/artigen/utils/escape.ts @@ -30,3 +30,8 @@ export const cmdSplitRegex = new RegExp(`(${escapePrefixPostfix(config.prefix)}) export const openInternal = '\u2045'; export const closeInternal = '\u2046'; export const internalWrapRegex = new RegExp(`([${openInternal}${closeInternal}])`, 'g'); + +// Internal Group is used for marking handled groups +export const openInternalGrp = '\u2e20'; +export const closeInternalGrp = '\u2e21'; +export const internalGrpWrapRegex = new RegExp(`([${openInternalGrp}${closeInternalGrp}])`, 'g'); diff --git a/src/artigen/utils/parenBalance.ts b/src/artigen/utils/parenBalance.ts index 76b41ea..8f217c7 100644 --- a/src/artigen/utils/parenBalance.ts +++ b/src/artigen/utils/parenBalance.ts @@ -6,7 +6,7 @@ import { loopCountCheck } from 'artigen/managers/loopManager.ts'; import { MathConf } from 'artigen/math/math.d.ts'; -import { closeInternal, openInternal } from 'artigen/utils/escape.ts'; +import { closeInternal, closeInternalGrp, openInternal, openInternalGrp } 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 => { @@ -60,5 +60,6 @@ export const assertPrePostBalance = (conf: MathConf[]) => checkBalance(conf, con // getMatchingXIdx gets the matching X, also partially verifies the conf has balanced X export const getMatchingGroupIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, '{', '}', 'Group', true, openIdx); export const getMatchingInternalIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, openInternal, closeInternal, 'Internal', true, openIdx); +export const getMatchingInternalGrpIdx = (conf: MathConf[], openIdx: number): number => checkBalance(conf, openInternalGrp, closeInternalGrp, 'InternalGrp', 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);