Implemented queueing system for rollWorker to limit number of concurrent workers

Moved some consts to the config.ts file
Added flag to disable logging while rolling for a massive speed increase and to increase stability of the bot
This commit is contained in:
Ean Milligan (Bastion) 2022-06-19 01:51:16 -04:00
parent b58cf31edb
commit bb10d52506
9 changed files with 198 additions and 136 deletions

View File

@ -5,6 +5,11 @@ export const config = {
"localtoken": "local_testing_token", // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" "localtoken": "local_testing_token", // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token"
"prefix": "[[", // Prefix for all commands "prefix": "[[", // Prefix for all commands
"postfix": "]]", // Postfix for rolling command "postfix": "]]", // Postfix for rolling command
"limits": { // Limits for the bot functions
"maxLoops": 5000000, // Determines how long the bot will attempt a roll, number of loops before it kills a roll. Increase this at your own risk
"maxWorkers": 16, // Maximum number of worker threads to spawn at once (Set this to less than the number of threads your CPU has, Artificer will eat it all if too many rolls happen at once)
"workerTimeout": 300000 // Maximum time before the bot kills a worker thread in ms
},
"api": { // Setting for the built-in API "api": { // Setting for the built-in API
"enable": false, // Leave this off if you have no intention of using this/supporting it "enable": false, // Leave this off if you have no intention of using this/supporting it
"port": 8080, // Port for the API to listen on "port": 8080, // Port for the API to listen on

View File

@ -268,7 +268,7 @@ ${details}`,
details = 'Details have been ommitted from this message for being over 2000 characters.'; details = 'Details have been ommitted from this message for being over 2000 characters.';
if (b.size > 8388290) { if (b.size > 8388290) {
details += details +=
'Full details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.'; '\n\nFull details could not be attached to this messaged as a \`.txt\` file as the file would be too large for Discord to handle. If you would like to see the details of rolls, please send the rolls in multiple messages instead of bundled into one.';
return { return {
embed: { embed: {
color: infoColor2, color: infoColor2,
@ -283,7 +283,7 @@ ${details}`,
}, },
}; };
} else { } else {
details += 'Full details have been attached to this messaged as a \`.txt\` file for verification purposes.'; details += '\n\nFull details have been attached to this messaged as a \`.txt\` file for verification purposes.';
return { return {
embed: { embed: {
color: infoColor2, color: infoColor2,

View File

@ -11,10 +11,150 @@ import {
sendDirectMessage, sendDirectMessage,
} from '../../deps.ts'; } from '../../deps.ts';
import { SolvedRoll } from '../solver/solver.d.ts'; import { SolvedRoll } from '../solver/solver.d.ts';
import { RollModifiers } from '../mod.d.ts'; import { QueuedRoll, RollModifiers } from '../mod.d.ts';
import { generateCountDetailsEmbed, generateDMFailed, generateRollEmbed, infoColor1, warnColor } from '../commandUtils.ts'; import { generateCountDetailsEmbed, generateDMFailed, generateRollEmbed, infoColor1, infoColor2, warnColor } from '../commandUtils.ts';
import rollFuncs from './roll/_index.ts'; import rollFuncs from './roll/_index.ts';
let currentWorkers = 0;
const rollQueue: Array<QueuedRoll> = [];
const rollingEmbed = {
embeds: [{
color: infoColor1,
title: 'Rolling . . .',
}],
};
// Handle setting up and calling the rollWorker
const handleRollWorker = async (m: DiscordenoMessage, message: DiscordenoMessage, originalCommand: String, rollCmd: String, modifiers: RollModifiers) => {
currentWorkers++;
// gmModifiers used to create gmEmbed (basically just turn off the gmRoll)
const gmModifiers = JSON.parse(JSON.stringify(modifiers));
gmModifiers.gmRoll = false;
const rollWorker = new Worker(new URL('../solver/rollWorker.ts', import.meta.url).href, { type: 'module' });
const workerTimeout = setTimeout(async () => {
rollWorker.terminate();
currentWorkers--;
m.edit({
embeds: [
(await generateRollEmbed(
message.authorId,
<SolvedRoll> {
error: true,
errorCode: 'TooComplex',
errorMsg: 'Error: Roll took too long to process, try breaking roll down into simpler parts',
},
<RollModifiers> {},
)).embed,
],
});
}, config.limits.workerTimeout);
rollWorker.postMessage({
rollCmd,
modifiers,
});
rollWorker.addEventListener('message', async (workerMessage) => {
try {
currentWorkers--;
clearTimeout(workerTimeout);
const returnmsg = workerMessage.data;
const pubEmbedDetails = await generateRollEmbed(message.authorId, returnmsg, modifiers);
const gmEmbedDetails = await generateRollEmbed(message.authorId, returnmsg, gmModifiers);
const countEmbed = generateCountDetailsEmbed(returnmsg.counts);
// If there was an error, report it to the user in hopes that they can determine what they did wrong
if (returnmsg.error) {
m.edit({ embeds: [pubEmbedDetails.embed] });
if (DEVMODE && config.logRolls) {
// If enabled, log rolls so we can see what went wrong
dbClient.execute(queries.insertRollLogCmd(0, 1), [originalCommand, returnmsg.errorCode, m.id]).catch((e) => {
log(LT.ERROR, `Failed to insert into DB: ${JSON.stringify(e)}`);
});
}
} else {
// Determine if we are to send a GM roll or a normal roll
if (modifiers.gmRoll) {
// Send the public embed to correct channel
m.edit({ embeds: [pubEmbedDetails.embed] });
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
modifiers.gms.forEach(async (gm) => {
log(LT.LOG, `Messaging GM ${gm}`);
// Attempt to DM the GM and send a warning if it could not DM a GM
await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), {
embeds: modifiers.count ? [gmEmbedDetails.embed, countEmbed] : [gmEmbedDetails.embed],
}).then(async () => {
// Check if we need to attach a file and send it after the initial details sent
if (gmEmbedDetails.hasAttachment) {
await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), {
file: gmEmbedDetails.attachment,
}).catch(() => {
message.reply(generateDMFailed(gm));
});
}
}).catch(() => {
message.reply(generateDMFailed(gm));
});
});
} else {
// Not a gm roll, so just send normal embed to correct channel
const n = await m.edit({
embeds: modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed],
});
if (pubEmbedDetails.hasAttachment) {
// Attachment requires you to send a new message
n.reply({
file: pubEmbedDetails.attachment,
});
}
}
}
} catch (e) {
log(LT.ERROR, `Unddandled Error: ${JSON.stringify(e)}`);
}
});
};
// Runs the roll or queues it depending on how many workers are currently running
const queueRoll = async (m: DiscordenoMessage, message: DiscordenoMessage, originalCommand: String, rollCmd: String, modifiers: RollModifiers) => {
if (!rollQueue.length && currentWorkers < config.limits.maxWorkers) {
handleRollWorker(m, message, originalCommand, rollCmd, modifiers);
} else {
m.edit({
embeds: [{
color: infoColor2,
title: `${config.name} currently has its hands full and has queued your roll.`,
description: `There are currently ${currentWorkers + rollQueue.length} rolls ahead of this roll.
The results for this roll will replace this message when it is done.`,
}],
}).catch((e) => {
log(LT.ERROR, `Failed to send message: ${JSON.stringify(m)} | ${JSON.stringify(e)}`);
});
rollQueue.push({ m, message, originalCommand, rollCmd, modifiers });
}
};
// Checks the queue constantly to make sure the queue stays empty
setInterval(async () => {
log(LT.LOG, `Checking rollQueue for items, rollQueue length: ${rollQueue.length}, currentWorkers: ${currentWorkers}, config.limits.maxWorkers: ${config.limits.maxWorkers}`);
if (rollQueue.length && currentWorkers < config.limits.maxWorkers) {
const temp = rollQueue.shift();
if (temp) {
temp.m.edit(rollingEmbed).catch((e) => {
log(LT.ERROR, `Failed to send message: ${JSON.stringify(temp.m)} | ${JSON.stringify(e)}`);
});
handleRollWorker(temp.m, temp.message, temp.originalCommand, temp.rollCmd, temp.modifiers);
}
}
}, 1000);
export const roll = async (message: DiscordenoMessage, args: string[], command: string) => { export const roll = async (message: DiscordenoMessage, args: string[], command: string) => {
// Light telemetry to see how many times a command is being run // Light telemetry to see how many times a command is being run
dbClient.execute(`CALL INC_CNT("roll");`).catch((e) => { dbClient.execute(`CALL INC_CNT("roll");`).catch((e) => {
@ -38,20 +178,11 @@ export const roll = async (message: DiscordenoMessage, args: string[], command:
try { try {
const originalCommand = `${config.prefix}${command} ${args.join(' ')}`; const originalCommand = `${config.prefix}${command} ${args.join(' ')}`;
const m = await message.reply({ const m = await message.reply(rollingEmbed);
embeds: [{
color: infoColor1,
title: 'Rolling . . .',
}],
});
// Get modifiers from command // Get modifiers from command
const modifiers = rollFuncs.getModifiers(m, args, command, originalCommand); const modifiers = rollFuncs.getModifiers(m, args, command, originalCommand);
// gmModifiers used to create gmEmbed (basically just turn off the gmRoll)
const gmModifiers = JSON.parse(JSON.stringify(modifiers));
gmModifiers.gmRoll = false;
// Return early if the modifiers were invalid // Return early if the modifiers were invalid
if (!modifiers.valid) { if (!modifiers.valid) {
return; return;
@ -59,92 +190,8 @@ export const roll = async (message: DiscordenoMessage, args: string[], command:
// Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in // Rejoin all of the args and send it into the solver, if solver returns a falsy item, an error object will be substituded in
const rollCmd = `${command} ${args.join(' ')}`; const rollCmd = `${command} ${args.join(' ')}`;
// const returnmsg = solver.parseRoll(rollCmd, modifiers) || <SolvedRoll>{ error: true, errorCode: 'EmptyMessage', errorMsg: 'Error: Empty message' };
const rollWorker = new Worker(new URL('../solver/rollWorker.ts', import.meta.url).href, { type: 'module' }); queueRoll(m, message, originalCommand, rollCmd, modifiers);
const workerTimeout = setTimeout(async () => {
rollWorker.terminate();
m.edit({
embeds: [
(await generateRollEmbed(
message.authorId,
<SolvedRoll> {
error: true,
errorCode: 'TooComplex',
errorMsg: 'Error: Roll Too Complex, try breaking roll down into simpler parts',
},
<RollModifiers> {},
)).embed,
],
});
}, 60000);
rollWorker.postMessage({
rollCmd,
modifiers,
});
rollWorker.addEventListener('message', async (workerMessage) => {
try {
clearTimeout(workerTimeout);
const returnmsg = workerMessage.data;
const pubEmbedDetails = await generateRollEmbed(message.authorId, returnmsg, modifiers);
const gmEmbedDetails = await generateRollEmbed(message.authorId, returnmsg, gmModifiers);
const countEmbed = generateCountDetailsEmbed(returnmsg.counts);
// If there was an error, report it to the user in hopes that they can determine what they did wrong
if (returnmsg.error) {
m.edit({ embeds: [pubEmbedDetails.embed] });
if (DEVMODE && config.logRolls) {
// If enabled, log rolls so we can see what went wrong
dbClient.execute(queries.insertRollLogCmd(0, 1), [originalCommand, returnmsg.errorCode, m.id]).catch((e) => {
log(LT.ERROR, `Failed to insert into DB: ${JSON.stringify(e)}`);
});
}
} else {
// Determine if we are to send a GM roll or a normal roll
if (modifiers.gmRoll) {
// Send the public embed to correct channel
m.edit({ embeds: [pubEmbedDetails.embed] });
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
modifiers.gms.forEach(async (gm) => {
log(LT.LOG, `Messaging GM ${gm}`);
// Attempt to DM the GM and send a warning if it could not DM a GM
await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), {
embeds: modifiers.count ? [gmEmbedDetails.embed, countEmbed] : [gmEmbedDetails.embed],
}).then(async () => {
// Check if we need to attach a file and send it after the initial details sent
if (gmEmbedDetails.hasAttachment) {
await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), {
file: gmEmbedDetails.attachment,
}).catch(() => {
message.reply(generateDMFailed(gm));
});
}
}).catch(() => {
message.reply(generateDMFailed(gm));
});
});
} else {
// Not a gm roll, so just send normal embed to correct channel
const n = await m.edit({
embeds: modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed],
});
if (pubEmbedDetails.hasAttachment) {
// Attachment requires you to send a new message
n.reply({
file: pubEmbedDetails.attachment,
});
}
}
}
} catch (e) {
log(LT.ERROR, `Unddandled Error: ${JSON.stringify(e)}`);
}
});
} catch (e) { } catch (e) {
log(LT.ERROR, `Undandled Error: ${JSON.stringify(e)}`); log(LT.ERROR, `Undandled Error: ${JSON.stringify(e)}`);
} }

10
src/mod.d.ts vendored
View File

@ -1,4 +1,5 @@
// mod.d.ts custom types // mod.d.ts custom types
import { DiscordenoMessage } from '../deps.ts';
// EmojiConf is used as a structure for the emojis stored in config.ts // EmojiConf is used as a structure for the emojis stored in config.ts
export type EmojiConf = { export type EmojiConf = {
@ -22,3 +23,12 @@ export type RollModifiers = {
valid: boolean; valid: boolean;
count: boolean; count: boolean;
}; };
// QueuedRoll is the structure to track rolls we could not immediately handle
export type QueuedRoll = {
m: DiscordenoMessage;
message: DiscordenoMessage;
originalCommand: String;
rollCmd: String;
modifiers: RollModifiers;
};

View File

@ -8,7 +8,7 @@ import config from '../../config.ts';
import { RollModifiers } from '../mod.d.ts'; import { RollModifiers } from '../mod.d.ts';
import { CountDetails, ReturnData, SolvedRoll, SolvedStep } from './solver.d.ts'; import { CountDetails, ReturnData, SolvedRoll, SolvedStep } from './solver.d.ts';
import { compareTotalRolls, escapeCharacters } from './rollUtils.ts'; import { compareTotalRolls, escapeCharacters, loggingEnabled } from './rollUtils.ts';
import { formatRoll } from './rollFormatter.ts'; import { formatRoll } from './rollFormatter.ts';
import { fullSolver } from './solver.ts'; import { fullSolver } from './solver.ts';
@ -49,7 +49,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
// Loop thru all roll/math ops // Loop thru all roll/math ops
for (const sepRoll of sepRolls) { for (const sepRoll of sepRolls) {
log(LT.LOG, `Parsing roll ${fullCmd} | Working ${sepRoll}`); 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 opertaion // 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] = sepRoll.split(config.postfix); const [tempConf, tempFormat] = sepRoll.split(config.postfix);
@ -59,7 +59,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
// Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens // 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; let parenCnt = 0;
mathConf.forEach((e) => { mathConf.forEach((e) => {
log(LT.LOG, `Parsing roll ${fullCmd} | Checking parenthesis balance ${e}`); loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Checking parenthesis balance ${e}`);
if (e === '(') { if (e === '(') {
parenCnt++; parenCnt++;
} else if (e === ')') { } else if (e === ')') {
@ -74,7 +74,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
// Evaluate all rolls into stepSolve format and all numbers into floats // Evaluate all rolls into stepSolve format and all numbers into floats
for (let i = 0; i < mathConf.length; i++) { for (let i = 0; i < mathConf.length; i++) {
log(LT.LOG, `Parsing roll ${fullCmd} | Evaluating rolls into mathable items ${JSON.stringify(mathConf[i])}`); loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Evaluating rolls into mathable items ${JSON.stringify(mathConf[i])}`);
if (mathConf[i].toString().length === 0) { if (mathConf[i].toString().length === 0) {
// If its an empty string, get it out of here // If its an empty string, get it out of here
mathConf.splice(i, 1); mathConf.splice(i, 1);
@ -198,7 +198,7 @@ export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll
// Fill out all of the details and results now // Fill out all of the details and results now
tempReturnData.forEach((e) => { tempReturnData.forEach((e) => {
log(LT.LOG, `Parsing roll ${fullCmd} | Making return text ${JSON.stringify(e)}`); loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Making return text ${JSON.stringify(e)}`);
let preFormat = ''; let preFormat = '';
let postFormat = ''; let postFormat = '';

View File

@ -7,6 +7,7 @@ import {
import { roll } from './roller.ts'; import { roll } from './roller.ts';
import { rollCounter } from './counter.ts'; import { rollCounter } from './counter.ts';
import { RollFormat } from './solver.d.ts'; import { RollFormat } from './solver.d.ts';
import { loggingEnabled } from './rollUtils.ts';
// formatRoll(rollConf, maximiseRoll, nominalRoll) returns one SolvedStep // formatRoll(rollConf, maximiseRoll, nominalRoll) returns one SolvedStep
// formatRoll handles creating and formatting the completed rolls into the SolvedStep format // formatRoll handles creating and formatting the completed rolls into the SolvedStep format
@ -21,7 +22,7 @@ export const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll:
// Loop thru all parts of the roll to document everything that was done to create the total roll // Loop thru all parts of the roll to document everything that was done to create the total roll
tempRollSet.forEach((e) => { tempRollSet.forEach((e) => {
log(LT.LOG, `Formatting roll ${rollConf} | ${JSON.stringify(e)}`); loggingEnabled && log(LT.LOG, `Formatting roll ${rollConf} | ${JSON.stringify(e)}`);
let preFormat = ''; let preFormat = '';
let postFormat = ''; let postFormat = '';

View File

@ -6,10 +6,7 @@ import {
import { ReturnData, RollSet } from './solver.d.ts'; import { ReturnData, RollSet } from './solver.d.ts';
// MAXLOOPS determines how long the bot will attempt a roll export const loggingEnabled = false;
// Default is 5000000 (5 million), which results in at most a 10 second delay before the bot calls the roll infinite or too complex
// Increase at your own risk
export const MAXLOOPS = 5000000;
// genRoll(size) returns number // genRoll(size) returns number
// genRoll rolls a die of size size and returns the result // genRoll rolls a die of size size and returns the result
@ -63,7 +60,7 @@ export const compareOrigidx = (a: RollSet, b: RollSet): number => {
export const escapeCharacters = (str: string, esc: string): string => { export const escapeCharacters = (str: string, esc: string): string => {
// Loop thru each esc char one at a time // Loop thru each esc char one at a time
for (const e of esc) { for (const e of esc) {
log(LT.LOG, `Escaping character ${e} | ${str}, ${esc}`); loggingEnabled && log(LT.LOG, `Escaping character ${e} | ${str}, ${esc}`);
// Create a new regex to look for that char that needs replaced and escape it // Create a new regex to look for that char that needs replaced and escape it
const temprgx = new RegExp(`[${e}]`, 'g'); const temprgx = new RegExp(`[${e}]`, 'g');
str = str.replace(temprgx, `\\${e}`); str = str.replace(temprgx, `\\${e}`);

View File

@ -1,3 +1,4 @@
import config from '../../config.ts';
import { import {
log, log,
// Log4Deno deps // Log4Deno deps
@ -5,7 +6,7 @@ import {
} from '../../deps.ts'; } from '../../deps.ts';
import { RollSet } from './solver.d.ts'; import { RollSet } from './solver.d.ts';
import { compareOrigidx, compareRolls, genRoll, MAXLOOPS } from './rollUtils.ts'; import { compareOrigidx, compareRolls, genRoll, loggingEnabled } from './rollUtils.ts';
// roll(rollStr, maximiseRoll, nominalRoll) returns RollSet // roll(rollStr, maximiseRoll, nominalRoll) returns RollSet
// roll parses and executes the rollStr, if needed it will also make the roll the maximum or average // roll parses and executes the rollStr, if needed it will also make the roll the maximum or average
@ -87,8 +88,8 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
throw new Error('YouNeedAD'); throw new Error('YouNeedAD');
} }
log(LT.LOG, `Handling roll ${rollStr} | Parsed Die Count: ${rollConf.dieCount}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsed Die Count: ${rollConf.dieCount}`);
log(LT.LOG, `Handling roll ${rollStr} | Parsed Die Size: ${rollConf.dieSize}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsed Die Size: ${rollConf.dieSize}`);
// Finish parsing the roll // Finish parsing the roll
if (remains.length > 0) { if (remains.length > 0) {
@ -99,7 +100,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Loop until all remaining args are parsed // Loop until all remaining args are parsed
while (remains.length > 0) { while (remains.length > 0) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing remains ${remains}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing remains ${remains}`);
// Find the next number in the remains to be able to cut out the rule name // Find the next number in the remains to be able to cut out the rule name
let afterSepIdx = remains.search(/\d/); let afterSepIdx = remains.search(/\d/);
if (afterSepIdx < 0) { if (afterSepIdx < 0) {
@ -157,7 +158,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why) // Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why)
rollConf.reroll.on = true; rollConf.reroll.on = true;
for (let i = tNum; i <= rollConf.dieSize; i++) { for (let i = tNum; i <= rollConf.dieSize; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing r> ${i}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing r> ${i}`);
rollConf.reroll.nums.push(i); rollConf.reroll.nums.push(i);
} }
break; break;
@ -168,7 +169,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why) // Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why)
rollConf.reroll.on = true; rollConf.reroll.on = true;
for (let i = 1; i <= tNum; i++) { for (let i = 1; i <= tNum; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing r< ${i}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing r< ${i}`);
rollConf.reroll.nums.push(i); rollConf.reroll.nums.push(i);
} }
break; break;
@ -182,7 +183,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why) // Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
rollConf.critScore.on = true; rollConf.critScore.on = true;
for (let i = tNum; i <= rollConf.dieSize; i++) { for (let i = tNum; i <= rollConf.dieSize; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing cs> ${i}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing cs> ${i}`);
rollConf.critScore.range.push(i); rollConf.critScore.range.push(i);
} }
break; break;
@ -190,7 +191,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why) // Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
rollConf.critScore.on = true; rollConf.critScore.on = true;
for (let i = 0; i <= tNum; i++) { for (let i = 0; i <= tNum; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing cs< ${i}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing cs< ${i}`);
rollConf.critScore.range.push(i); rollConf.critScore.range.push(i);
} }
break; break;
@ -204,7 +205,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why) // Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
rollConf.critFail.on = true; rollConf.critFail.on = true;
for (let i = tNum; i <= rollConf.dieSize; i++) { for (let i = tNum; i <= rollConf.dieSize; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing cf> ${i}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing cf> ${i}`);
rollConf.critFail.range.push(i); rollConf.critFail.range.push(i);
} }
break; break;
@ -212,7 +213,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why) // Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
rollConf.critFail.on = true; rollConf.critFail.on = true;
for (let i = 0; i <= tNum; i++) { for (let i = 0; i <= tNum; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing cf< ${i}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing cf< ${i}`);
rollConf.critFail.range.push(i); rollConf.critFail.range.push(i);
} }
break; break;
@ -245,7 +246,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why) // Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why)
rollConf.exploding.on = true; rollConf.exploding.on = true;
for (let i = tNum; i <= rollConf.dieSize; i++) { for (let i = tNum; i <= rollConf.dieSize; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing !> ${i}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing !> ${i}`);
rollConf.exploding.nums.push(i); rollConf.exploding.nums.push(i);
} }
break; break;
@ -256,7 +257,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why) // Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why)
rollConf.exploding.on = true; rollConf.exploding.on = true;
for (let i = 1; i <= tNum; i++) { for (let i = 1; i <= tNum; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Parsing !< ${i}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Parsing !< ${i}`);
rollConf.exploding.nums.push(i); rollConf.exploding.nums.push(i);
} }
break; break;
@ -279,7 +280,7 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Since only one drop or keep option can be active, count how many are active to throw the right error // Since only one drop or keep option can be active, count how many are active to throw the right error
let dkdkCnt = 0; let dkdkCnt = 0;
[rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach((e) => { [rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach((e) => {
log(LT.LOG, `Handling roll ${rollStr} | Checking if drop/keep is on ${e}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Checking if drop/keep is on ${e}`);
if (e) { if (e) {
dkdkCnt++; dkdkCnt++;
} }
@ -344,9 +345,9 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// Initial rolling, not handling reroll or exploding here // Initial rolling, not handling reroll or exploding here
for (let i = 0; i < rollConf.dieCount; i++) { for (let i = 0; i < rollConf.dieCount; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Initial rolling ${i} of ${JSON.stringify(rollConf)}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Initial rolling ${i} of ${JSON.stringify(rollConf)}`);
// If loopCount gets too high, stop trying to calculate infinity // If loopCount gets too high, stop trying to calculate infinity
if (loopCount > MAXLOOPS) { if (loopCount > config.limits.maxLoops) {
throw new Error('MaxLoopsExceeded'); throw new Error('MaxLoopsExceeded');
} }
@ -378,9 +379,9 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
// If needed, handle rerolling and exploding dice now // If needed, handle rerolling and exploding dice now
if (rollConf.reroll.on || rollConf.exploding.on) { if (rollConf.reroll.on || rollConf.exploding.on) {
for (let i = 0; i < rollSet.length; i++) { for (let i = 0; i < rollSet.length; i++) {
log(LT.LOG, `Handling roll ${rollStr} | Handling rerolling and exploding ${JSON.stringify(rollSet[i])}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Handling rerolling and exploding ${JSON.stringify(rollSet[i])}`);
// If loopCount gets too high, stop trying to calculate infinity // If loopCount gets too high, stop trying to calculate infinity
if (loopCount > MAXLOOPS) { if (loopCount > config.limits.maxLoops) {
throw new Error('MaxLoopsExceeded'); throw new Error('MaxLoopsExceeded');
} }
@ -451,11 +452,11 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
if (rollConf.reroll.on) { if (rollConf.reroll.on) {
for (let j = 0; j < rollSet.length; j++) { for (let j = 0; j < rollSet.length; j++) {
// If loopCount gets too high, stop trying to calculate infinity // If loopCount gets too high, stop trying to calculate infinity
if (loopCount > MAXLOOPS) { if (loopCount > config.limits.maxLoops) {
throw new Error('MaxLoopsExceeded'); throw new Error('MaxLoopsExceeded');
} }
log(LT.LOG, `Handling roll ${rollStr} | Setting originalIdx on ${JSON.stringify(rollSet[j])}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Setting originalIdx on ${JSON.stringify(rollSet[j])}`);
rollSet[j].origidx = j; rollSet[j].origidx = j;
if (rollSet[j].rerolled) { if (rollSet[j].rerolled) {
@ -505,11 +506,11 @@ export const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolea
let i = 0; let i = 0;
while (dropCount > 0 && i < rollSet.length) { while (dropCount > 0 && i < rollSet.length) {
// If loopCount gets too high, stop trying to calculate infinity // If loopCount gets too high, stop trying to calculate infinity
if (loopCount > MAXLOOPS) { if (loopCount > config.limits.maxLoops) {
throw new Error('MaxLoopsExceeded'); throw new Error('MaxLoopsExceeded');
} }
log(LT.LOG, `Handling roll ${rollStr} | Dropping dice ${dropCount} ${JSON.stringify(rollSet[i])}`); loggingEnabled && log(LT.LOG, `Handling roll ${rollStr} | Dropping dice ${dropCount} ${JSON.stringify(rollSet[i])}`);
// Skip all rolls that were rerolled // Skip all rolls that were rerolled
if (!rollSet[i].rerolled) { if (!rollSet[i].rerolled) {
rollSet[i].dropped = true; rollSet[i].dropped = true;

View File

@ -11,6 +11,7 @@ import {
} from '../../deps.ts'; } from '../../deps.ts';
import { SolvedStep } from './solver.d.ts'; import { SolvedStep } from './solver.d.ts';
import { loggingEnabled } from './rollUtils.ts';
// fullSolver(conf, wrapDetails) returns one condensed SolvedStep // fullSolver(conf, wrapDetails) returns one condensed SolvedStep
// fullSolver is a function that recursively solves the full roll and math // fullSolver is a function that recursively solves the full roll and math
@ -32,7 +33,7 @@ export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails:
// Evaluate all parenthesis // Evaluate all parenthesis
while (conf.indexOf('(') > -1) { while (conf.indexOf('(') > -1) {
log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for (`); loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for (`);
// Get first open parenthesis // Get first open parenthesis
const openParen = conf.indexOf('('); const openParen = conf.indexOf('(');
let closeParen = -1; let closeParen = -1;
@ -40,7 +41,7 @@ export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails:
// Using nextParen to count the opening/closing parens, find the matching paren to openParen above // Using nextParen to count the opening/closing parens, find the matching paren to openParen above
for (let i = openParen; i < conf.length; i++) { for (let i = openParen; i < conf.length; i++) {
log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for matching ) openIdx: ${openParen} checking: ${i}`); loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for matching ) openIdx: ${openParen} checking: ${i}`);
// If we hit an open, add one (this includes the openParen we start with), if we hit a close, subtract one // If we hit an open, add one (this includes the openParen we start with), if we hit a close, subtract one
if (conf[i] === '(') { if (conf[i] === '(') {
nextParen++; nextParen++;
@ -83,10 +84,10 @@ export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails:
// Evaluate all EMMDAS by looping thru each teir of operators (exponential is the higehest teir, addition/subtraction the lowest) // Evaluate all EMMDAS by looping thru each teir of operators (exponential is the higehest teir, addition/subtraction the lowest)
const allCurOps = [['^'], ['*', '/', '%'], ['+', '-']]; const allCurOps = [['^'], ['*', '/', '%'], ['+', '-']];
allCurOps.forEach((curOps) => { allCurOps.forEach((curOps) => {
log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)}`); loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)}`);
// Iterate thru all operators/operands in the conf // Iterate thru all operators/operands in the conf
for (let i = 0; i < conf.length; i++) { for (let i = 0; i < conf.length; i++) {
log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)} | Checking ${JSON.stringify(conf[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 teir of operators // Check if the current index is in the active teir of operators
if (curOps.indexOf(conf[i].toString()) > -1) { if (curOps.indexOf(conf[i].toString()) > -1) {
// Grab the operands from before and after the operator // Grab the operands from before and after the operator
@ -172,7 +173,7 @@ export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails:
// If we somehow have more than one item left in conf at this point, something broke, throw an error // If we somehow have more than one item left in conf at this point, something broke, throw an error
if (conf.length > 1) { if (conf.length > 1) {
log(LT.LOG, `ConfWHAT? ${JSON.stringify(conf)}`); loggingEnabled && log(LT.LOG, `ConfWHAT? ${JSON.stringify(conf)}`);
throw new Error('ConfWhat'); throw new Error('ConfWhat');
} else if (singleNum && (typeof (conf[0]) === 'number')) { } else if (singleNum && (typeof (conf[0]) === 'number')) {
// If we are only left with a number, populate the stepSolve with it // If we are only left with a number, populate the stepSolve with it