mirror of
https://github.com/Burn-E99/TheArtificer.git
synced 2026-01-08 21:37:54 -05:00
Auto stash before merge of "master" and "origin/master"
This commit is contained in:
24
src/solver.d.ts
vendored
Normal file
24
src/solver.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
export type RollSet = {
|
||||
origidx: number,
|
||||
roll: number,
|
||||
dropped: boolean,
|
||||
rerolled: boolean,
|
||||
exploding: boolean,
|
||||
critHit: boolean,
|
||||
critFail: boolean
|
||||
};
|
||||
|
||||
export type SolvedStep = {
|
||||
total: number,
|
||||
details: string,
|
||||
containsCrit: boolean,
|
||||
containsFail: boolean
|
||||
};
|
||||
|
||||
export type SolvedRoll = {
|
||||
error: boolean,
|
||||
errorMsg: string,
|
||||
line1: string,
|
||||
line2: string,
|
||||
line3: string
|
||||
};
|
||||
768
src/solver.ts
Normal file
768
src/solver.ts
Normal file
@ -0,0 +1,768 @@
|
||||
import { RollSet, SolvedStep, SolvedRoll } from "./solver.d.ts";
|
||||
|
||||
const MAXLOOPS = 5000000;
|
||||
|
||||
const genRoll = (size: number): number => {
|
||||
return Math.floor((Math.random() * size) + 1);
|
||||
};
|
||||
|
||||
const compareRolls = (a: RollSet, b: RollSet): number => {
|
||||
if (a.roll < b.roll) {
|
||||
return -1;
|
||||
}
|
||||
if (a.roll > b.roll) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const compareOrigidx = (a: RollSet, b: RollSet): number => {
|
||||
if (a.origidx < b.origidx) {
|
||||
return -1;
|
||||
}
|
||||
if (a.origidx > b.origidx) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const escapeCharacters = (str: string, esc: string): string => {
|
||||
for (let i = 0; i < esc.length; i++) {
|
||||
const temprgx = new RegExp(`[${esc[i]}]`, "g");
|
||||
str = str.replace(temprgx, ("\\" + esc[i]));
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
const roll = (rollStr: string, maximiseRoll: boolean, nominalRoll: boolean): RollSet[] => {
|
||||
/* Roll const Capabilities ==>
|
||||
* Deciphers and rolls a single dice roll set
|
||||
* xdydzracsq!
|
||||
*
|
||||
* x [OPT] - number of dice to roll, if omitted, 1 is used
|
||||
* dy [REQ] - size of dice to roll, d20 = 20 sided die
|
||||
* dz [OPT] - drops the lowest z dice, cannot be used with kz
|
||||
* kz [OPT] - keeps the highest z dice, cannot be used with dz
|
||||
* ra [OPT] - rerolls any rolls that match a, r3 will reroll any dice that land on 3, throwing out old rolls
|
||||
* csq [OPT] - changes crit score to q, where q can be a single number or a range formatted as q-u
|
||||
* ! [OPT] - exploding, rolls another dy for every crit roll
|
||||
*/
|
||||
|
||||
rollStr = rollStr.toLowerCase();
|
||||
|
||||
const dpts = rollStr.split("d");
|
||||
|
||||
const rollConf = {
|
||||
dieCount: 0,
|
||||
dieSize: 0,
|
||||
drop: {
|
||||
on: false,
|
||||
count: 0
|
||||
},
|
||||
keep: {
|
||||
on: false,
|
||||
count: 0
|
||||
},
|
||||
dropHigh: {
|
||||
on: false,
|
||||
count: 0
|
||||
},
|
||||
keepLow: {
|
||||
on: false,
|
||||
count: 0
|
||||
},
|
||||
reroll: {
|
||||
on: false,
|
||||
nums: [0]
|
||||
},
|
||||
critScore: {
|
||||
on: false,
|
||||
range: [0]
|
||||
},
|
||||
critFail: {
|
||||
on: false,
|
||||
range: [0]
|
||||
},
|
||||
exploding: false
|
||||
};
|
||||
|
||||
if (dpts.length < 2) {
|
||||
throw new Error("YouNeedAD");
|
||||
}
|
||||
|
||||
const tempDC = dpts.shift();
|
||||
rollConf.dieCount = parseInt(tempDC || "1");
|
||||
|
||||
let afterDieIdx = dpts[0].search(/\D/);
|
||||
if (afterDieIdx === -1) {
|
||||
afterDieIdx = dpts[0].length;
|
||||
}
|
||||
|
||||
let remains = dpts.join("");
|
||||
rollConf.dieSize = parseInt(remains.slice(0, afterDieIdx));
|
||||
remains = remains.slice(afterDieIdx);
|
||||
|
||||
// Finish parsing the roll
|
||||
if (remains.length > 0) {
|
||||
if (remains.search(/\D/) !== 0 || remains.indexOf("l") === 0 || remains.indexOf("h") === 0) {
|
||||
remains = "d" + remains;
|
||||
}
|
||||
|
||||
while (remains.length > 0) {
|
||||
let afterSepIdx = remains.search(/\d/);
|
||||
if (afterSepIdx < 0) {
|
||||
afterSepIdx = remains.length;
|
||||
}
|
||||
const tSep = remains.slice(0, afterSepIdx);
|
||||
remains = remains.slice(afterSepIdx);
|
||||
let afterNumIdx = remains.search(/\D/);
|
||||
if (afterNumIdx < 0) {
|
||||
afterNumIdx = remains.length;
|
||||
}
|
||||
const tNum = parseInt(remains.slice(0, afterNumIdx));
|
||||
|
||||
switch (tSep) {
|
||||
case "dl":
|
||||
case "d":
|
||||
rollConf.drop.on = true;
|
||||
rollConf.drop.count = tNum;
|
||||
break;
|
||||
case "kh":
|
||||
case "k":
|
||||
rollConf.keep.on = true;
|
||||
rollConf.keep.count = tNum;
|
||||
break;
|
||||
case "dh":
|
||||
rollConf.dropHigh.on = true;
|
||||
rollConf.dropHigh.count = tNum;
|
||||
break;
|
||||
case "kl":
|
||||
rollConf.keepLow.on = true;
|
||||
rollConf.keepLow.count = tNum;
|
||||
break;
|
||||
case "r":
|
||||
rollConf.reroll.on = true;
|
||||
rollConf.reroll.nums.push(tNum);
|
||||
break;
|
||||
case "cs":
|
||||
case "cs=":
|
||||
rollConf.critScore.on = true;
|
||||
rollConf.critScore.range.push(tNum);
|
||||
break;
|
||||
case "cs>":
|
||||
rollConf.critScore.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
rollConf.critScore.range.push(i);
|
||||
}
|
||||
break;
|
||||
case "cs<":
|
||||
rollConf.critScore.on = true;
|
||||
for (let i = 0; i <= tNum; i++) {
|
||||
rollConf.critScore.range.push(i);
|
||||
}
|
||||
break;
|
||||
case "cf":
|
||||
case "cf=":
|
||||
rollConf.critFail.on = true;
|
||||
rollConf.critFail.range.push(tNum);
|
||||
break;
|
||||
case "cf>":
|
||||
rollConf.critFail.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
rollConf.critFail.range.push(i);
|
||||
}
|
||||
break;
|
||||
case "cf<":
|
||||
rollConf.critFail.on = true;
|
||||
for (let i = 0; i <= tNum; i++) {
|
||||
rollConf.critFail.range.push(i);
|
||||
}
|
||||
break;
|
||||
case "!":
|
||||
rollConf.exploding = true;
|
||||
afterNumIdx = 1;
|
||||
break;
|
||||
default:
|
||||
throw new Error("UnknownOperation_" + tSep);
|
||||
}
|
||||
remains = remains.slice(afterNumIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the parse
|
||||
if (rollConf.dieCount < 0) {
|
||||
throw new Error("NoZerosAllowed_base");
|
||||
}
|
||||
if (rollConf.dieCount === 0 || rollConf.dieSize === 0) {
|
||||
throw new Error("NoZerosAllowed_base");
|
||||
}
|
||||
let dkdkCnt = 0;
|
||||
[rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach(e => {
|
||||
if (e) {
|
||||
dkdkCnt++;
|
||||
}
|
||||
});
|
||||
if (dkdkCnt > 1) {
|
||||
throw new Error("FormattingError_dk");
|
||||
}
|
||||
if (rollConf.drop.on && rollConf.drop.count === 0) {
|
||||
throw new Error("NoZerosAllowed_drop");
|
||||
}
|
||||
if (rollConf.keep.on && rollConf.keep.count === 0) {
|
||||
throw new Error("NoZerosAllowed_keep");
|
||||
}
|
||||
if (rollConf.dropHigh.on && rollConf.dropHigh.count === 0) {
|
||||
throw new Error("NoZerosAllowed_dropHigh");
|
||||
}
|
||||
if (rollConf.keepLow.on && rollConf.keepLow.count === 0) {
|
||||
throw new Error("NoZerosAllowed_keepLow");
|
||||
}
|
||||
if (rollConf.reroll.on && rollConf.reroll.nums.indexOf(0) >= 0) {
|
||||
throw new Error("NoZerosAllowed_reroll");
|
||||
}
|
||||
|
||||
// Roll the roll
|
||||
const rollSet = [];
|
||||
/* Roll will contain objects of the following format:
|
||||
* {
|
||||
* origidx: 0,
|
||||
* roll: 0,
|
||||
* dropped: false,
|
||||
* rerolled: false,
|
||||
* exploding: false,
|
||||
* critHit: false,
|
||||
* critFail: false
|
||||
* }
|
||||
*
|
||||
* Each of these is defined as following:
|
||||
* {
|
||||
* origidx: The original index of the roll
|
||||
* roll: The resulting roll on this die in the set
|
||||
* dropped: This die is to be dropped as it was one of the dy lowest dice
|
||||
* rerolled: This die has been rerolled as it matched rz, it is replaced by the very next die in the set
|
||||
* exploding: This die was rolled as the previous die exploded (was a crit hit)
|
||||
* critHit: This die matched csq[-u], max die value used if cs not used
|
||||
* critFail: This die rolled a nat 1, a critical failure
|
||||
* }
|
||||
*/
|
||||
|
||||
const templateRoll = {
|
||||
origidx: 0,
|
||||
roll: 0,
|
||||
dropped: false,
|
||||
rerolled: false,
|
||||
exploding: false,
|
||||
critHit: false,
|
||||
critFail: false
|
||||
};
|
||||
|
||||
let loopCount = 0;
|
||||
|
||||
for (let i = 0; i < rollConf.dieCount; i++) {
|
||||
if (loopCount > MAXLOOPS) {
|
||||
throw new Error("MaxLoopsExceeded");
|
||||
}
|
||||
|
||||
const rolling = JSON.parse(JSON.stringify(templateRoll));
|
||||
rolling.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
|
||||
|
||||
if (rollConf.critScore.on && rollConf.critScore.range.indexOf(rolling.roll) >= 0) {
|
||||
rolling.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
rolling.critHit = (rolling.roll === rollConf.dieSize);
|
||||
}
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.indexOf(rolling.roll) >= 0) {
|
||||
rolling.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
rolling.critFail = (rolling.roll === 1);
|
||||
}
|
||||
|
||||
rollSet.push(rolling);
|
||||
loopCount++;
|
||||
}
|
||||
|
||||
if (rollConf.reroll.on || rollConf.exploding) {
|
||||
for (let i = 0; i < rollSet.length; i++) {
|
||||
if (loopCount > MAXLOOPS) {
|
||||
throw new Error("MaxLoopsExceeded");
|
||||
}
|
||||
|
||||
if (rollConf.reroll.on && rollConf.reroll.nums.indexOf(rollSet[i].roll) >= 0) {
|
||||
rollSet[i].rerolled = true;
|
||||
|
||||
const newRoll = JSON.parse(JSON.stringify(templateRoll));
|
||||
newRoll.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
|
||||
|
||||
if (rollConf.critScore.on && rollConf.critScore.range.indexOf(newRoll.roll) >= 0) {
|
||||
newRoll.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
newRoll.critHit = (newRoll.roll === rollConf.dieSize);
|
||||
}
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.indexOf(newRoll.roll) >= 0) {
|
||||
newRoll.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
newRoll.critFail = (newRoll.roll === 1);
|
||||
}
|
||||
|
||||
rollSet.splice(i + 1, 0, newRoll);
|
||||
} else if (rollConf.exploding && !rollSet[i].rerolled && rollSet[i].critHit) {
|
||||
const newRoll = JSON.parse(JSON.stringify(templateRoll));
|
||||
newRoll.roll = maximiseRoll ? rollConf.dieSize : (nominalRoll ? ((rollConf.dieSize / 2) + 0.5) : genRoll(rollConf.dieSize));
|
||||
newRoll.exploding = true;
|
||||
|
||||
if (rollConf.critScore.on && rollConf.critScore.range.indexOf(newRoll.roll) >= 0) {
|
||||
newRoll.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
newRoll.critHit = (newRoll.roll === rollConf.dieSize);
|
||||
}
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.indexOf(newRoll.roll) >= 0) {
|
||||
newRoll.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
newRoll.critFail = (newRoll.roll === 1);
|
||||
}
|
||||
|
||||
rollSet.splice(i + 1, 0, newRoll);
|
||||
}
|
||||
|
||||
loopCount++;
|
||||
}
|
||||
}
|
||||
|
||||
let rerollCount = 0;
|
||||
for (let i = 0; i < rollSet.length; i++) {
|
||||
rollSet[i].origidx = i;
|
||||
|
||||
if (rollSet[i].rerolled) {
|
||||
rerollCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (rollConf.drop.on || rollConf.keep.on || rollConf.dropHigh.on || rollConf.keepLow.on) {
|
||||
rollSet.sort(compareRolls);
|
||||
|
||||
let dropCount = 0;
|
||||
const validRolls = rollSet.length - rerollCount;
|
||||
|
||||
if (rollConf.drop.on) {
|
||||
dropCount = rollConf.drop.count;
|
||||
if (dropCount > validRolls) {
|
||||
dropCount = validRolls;
|
||||
}
|
||||
}
|
||||
|
||||
if (rollConf.keep.on) {
|
||||
dropCount = validRolls - rollConf.keep.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (rollConf.dropHigh.on) {
|
||||
rollSet.reverse();
|
||||
dropCount = rollConf.dropHigh.count;
|
||||
if (dropCount > validRolls) {
|
||||
dropCount = validRolls;
|
||||
}
|
||||
}
|
||||
|
||||
if (rollConf.keepLow.on) {
|
||||
rollSet.reverse();
|
||||
dropCount = validRolls - rollConf.keepLow.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
while (dropCount > 0 && i < rollSet.length) {
|
||||
if (!rollSet[i].rerolled) {
|
||||
rollSet[i].dropped = true;
|
||||
dropCount--;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
rollSet.sort(compareOrigidx);
|
||||
}
|
||||
|
||||
return rollSet;
|
||||
};
|
||||
|
||||
const formatRoll = (rollConf: string, maximiseRoll: boolean, nominalRoll: boolean): SolvedStep => {
|
||||
let tempTotal = 0;
|
||||
let tempDetails = "[";
|
||||
let tempCrit = false;
|
||||
let tempFail = false;
|
||||
|
||||
const tempRollSet = roll(rollConf, maximiseRoll, nominalRoll);
|
||||
tempRollSet.forEach(e => {
|
||||
let preFormat = "";
|
||||
let postFormat = "";
|
||||
|
||||
if (!e.dropped && !e.rerolled) {
|
||||
tempTotal += e.roll;
|
||||
if (e.critHit) {
|
||||
tempCrit = true;
|
||||
}
|
||||
if (e.critFail) {
|
||||
tempFail = true;
|
||||
}
|
||||
}
|
||||
if (e.critHit) {
|
||||
preFormat = "**" + preFormat;
|
||||
postFormat = postFormat + "**";
|
||||
}
|
||||
if (e.critFail) {
|
||||
preFormat = "__" + preFormat;
|
||||
postFormat = postFormat + "__";
|
||||
}
|
||||
if (e.dropped || e.rerolled) {
|
||||
preFormat = "~~" + preFormat;
|
||||
postFormat = postFormat + "~~";
|
||||
}
|
||||
|
||||
tempDetails += preFormat + e.roll + postFormat + " + ";
|
||||
});
|
||||
tempDetails = tempDetails.substr(0, (tempDetails.length - 3));
|
||||
tempDetails += "]";
|
||||
|
||||
return {
|
||||
total: tempTotal,
|
||||
details: tempDetails,
|
||||
containsCrit: tempCrit,
|
||||
containsFail: tempFail
|
||||
};
|
||||
};
|
||||
|
||||
const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean): SolvedStep => {
|
||||
const signs = ["^", "*", "/", "%", "+", "-"];
|
||||
const stepSolve = {
|
||||
total: 0,
|
||||
details: "",
|
||||
containsCrit: false,
|
||||
containsFail: false
|
||||
};
|
||||
|
||||
let singleNum = false;
|
||||
if (conf.length === 1) {
|
||||
singleNum = true;
|
||||
}
|
||||
|
||||
// Evaluate all parenthesis
|
||||
while (conf.indexOf("(") > -1) {
|
||||
const openParen = conf.indexOf("(");
|
||||
let closeParen = -1;
|
||||
let nextParen = 0;
|
||||
|
||||
for (let i = openParen; i < conf.length; i++) {
|
||||
if (conf[i] === "(") {
|
||||
nextParen++;
|
||||
} else if (conf[i] === ")") {
|
||||
nextParen--;
|
||||
}
|
||||
|
||||
if (nextParen === 0) {
|
||||
closeParen = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (closeParen === -1 || closeParen < openParen) {
|
||||
throw new Error("UnbalancedParens");
|
||||
}
|
||||
|
||||
conf.splice(openParen, closeParen, fullSolver(conf.slice((openParen + 1), closeParen), true));
|
||||
let insertedMult = false;
|
||||
if (((openParen - 1) > -1) && (signs.indexOf(conf[openParen - 1].toString()) === -1)) {
|
||||
insertedMult = true;
|
||||
conf.splice(openParen, 0, "*");
|
||||
}
|
||||
if (!insertedMult && (((openParen + 1) < conf.length) && (signs.indexOf(conf[openParen + 1].toString()) === -1))) {
|
||||
conf.splice((openParen + 1), 0, "*");
|
||||
} else if (insertedMult && (((openParen + 2) < conf.length) && (signs.indexOf(conf[openParen + 2].toString()) === -1))) {
|
||||
conf.splice((openParen + 2), 0, "*");
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate all EMMDAS
|
||||
const allCurOps = [["^"], ["*", "/", "%"], ["+", "-"]];
|
||||
allCurOps.forEach(curOps => {
|
||||
for (let i = 0; i < conf.length; i++) {
|
||||
if (curOps.indexOf(conf[i].toString()) > -1) {
|
||||
const operand1 = conf[i - 1];
|
||||
const operand2 = conf[i + 1];
|
||||
let oper1 = NaN;
|
||||
let oper2 = NaN;
|
||||
const subStepSolve = {
|
||||
total: NaN,
|
||||
details: "",
|
||||
containsCrit: false,
|
||||
containsFail: false
|
||||
};
|
||||
|
||||
if (typeof operand1 === "object") {
|
||||
oper1 = operand1.total;
|
||||
subStepSolve.details = operand1.details + "\\" + conf[i];
|
||||
subStepSolve.containsCrit = operand1.containsCrit;
|
||||
subStepSolve.containsFail = operand1.containsFail;
|
||||
} else {
|
||||
oper1 = parseFloat(operand1.toString());
|
||||
subStepSolve.details = oper1.toString() + conf[i];
|
||||
}
|
||||
|
||||
if (typeof operand2 === "object") {
|
||||
oper2 = operand2.total;
|
||||
subStepSolve.details += operand2.details;
|
||||
subStepSolve.containsCrit = subStepSolve.containsCrit || operand2.containsCrit;
|
||||
subStepSolve.containsFail = subStepSolve.containsFail || operand2.containsFail;
|
||||
} else {
|
||||
oper2 = parseFloat(operand2.toString());
|
||||
subStepSolve.details += oper2;
|
||||
}
|
||||
|
||||
if (isNaN(oper1) || isNaN(oper2)) {
|
||||
throw new Error("OperandNaN");
|
||||
}
|
||||
|
||||
if ((typeof oper1 === "number") && (typeof oper2 === "number")) {
|
||||
switch (conf[i]) {
|
||||
case "^":
|
||||
subStepSolve.total = Math.pow(oper1, oper2);
|
||||
break;
|
||||
case "*":
|
||||
subStepSolve.total = oper1 * oper2;
|
||||
break;
|
||||
case "/":
|
||||
subStepSolve.total = oper1 / oper2;
|
||||
break;
|
||||
case "%":
|
||||
subStepSolve.total = oper1 % oper2;
|
||||
break;
|
||||
case "+":
|
||||
subStepSolve.total = oper1 + oper2;
|
||||
break;
|
||||
case "-":
|
||||
subStepSolve.total = oper1 - oper2;
|
||||
break;
|
||||
default:
|
||||
throw new Error("OperatorWhat");
|
||||
}
|
||||
} else {
|
||||
throw new Error("EMDASNotNumber");
|
||||
}
|
||||
|
||||
conf.splice((i - 1), (i + 2), subStepSolve);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (conf.length > 1) {
|
||||
throw new Error("ConfWhat");
|
||||
} else if (singleNum && (typeof (conf[0]) === "number")) {
|
||||
stepSolve.total = conf[0];
|
||||
stepSolve.details = conf[0].toString();
|
||||
} else {
|
||||
stepSolve.total = (<SolvedStep>conf[0]).total;
|
||||
stepSolve.details = (<SolvedStep>conf[0]).details;
|
||||
stepSolve.containsCrit = (<SolvedStep>conf[0]).containsCrit;
|
||||
stepSolve.containsFail = (<SolvedStep>conf[0]).containsFail;
|
||||
}
|
||||
|
||||
if (wrapDetails) {
|
||||
stepSolve.details = "(" + stepSolve.details + ")";
|
||||
}
|
||||
|
||||
if (stepSolve.total === undefined) {
|
||||
throw new Error("UndefinedStep");
|
||||
}
|
||||
|
||||
return stepSolve;
|
||||
};
|
||||
|
||||
const parseRoll = (fullCmd: string, localPrefix: string, localPostfix: string, maximiseRoll: boolean, nominalRoll: boolean): SolvedRoll => {
|
||||
const returnmsg = {
|
||||
error: false,
|
||||
errorMsg: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
line3: ""
|
||||
};
|
||||
|
||||
try {
|
||||
const sepRolls = fullCmd.split(localPrefix);
|
||||
|
||||
const tempReturnData = [];
|
||||
|
||||
for (let i = 0; i < sepRolls.length; i++) {
|
||||
const [tempConf, tempFormat] = sepRolls[i].split(localPostfix);
|
||||
|
||||
const mathConf: (string | number | SolvedStep)[] = <(string | number | SolvedStep)[]>tempConf.replace(/ /g, "").split(/([-+()*/%^])/g);
|
||||
|
||||
let parenCnt = 0;
|
||||
mathConf.forEach(e => {
|
||||
if (e === "(") {
|
||||
parenCnt++;
|
||||
} else if (e === ")") {
|
||||
parenCnt--;
|
||||
}
|
||||
});
|
||||
|
||||
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++) {
|
||||
if (mathConf[i].toString().length === 0) {
|
||||
mathConf.splice(i, 1);
|
||||
i--;
|
||||
} else if (mathConf[i] == parseFloat(mathConf[i].toString())) {
|
||||
mathConf[i] = parseFloat(mathConf[i].toString());
|
||||
} else if (/([0123456789])/g.test(mathConf[i].toString())) {
|
||||
mathConf[i] = formatRoll(mathConf[i].toString(), maximiseRoll, nominalRoll);
|
||||
}
|
||||
}
|
||||
|
||||
const tempSolved = fullSolver(mathConf, false);
|
||||
|
||||
tempReturnData.push({
|
||||
rollTotal: tempSolved.total,
|
||||
rollPostFormat: tempFormat,
|
||||
rollDetails: tempSolved.details,
|
||||
containsCrit: tempSolved.containsCrit,
|
||||
containsFail: tempSolved.containsFail,
|
||||
initConfig: tempConf
|
||||
});
|
||||
}
|
||||
|
||||
if (fullCmd[fullCmd.length - 1] === " ") {
|
||||
fullCmd = escapeCharacters(fullCmd.substr(0, (fullCmd.length - 1)), "|");
|
||||
}
|
||||
|
||||
let line1 = "";
|
||||
let line2 = "";
|
||||
let line3 = "";
|
||||
|
||||
if (maximiseRoll) {
|
||||
line1 = " requested the theoretical maximum of: `[[" + fullCmd + "`";
|
||||
line2 = "Theoretical Maximum Results: ";
|
||||
} else if (nominalRoll) {
|
||||
line1 = " requested the theoretical nominal of: `[[" + fullCmd + "`";
|
||||
line2 = "Theoretical Nominal Results: ";
|
||||
} else {
|
||||
line1 = " rolled: `[[" + fullCmd + "`";
|
||||
line2 = "Results: ";
|
||||
}
|
||||
|
||||
tempReturnData.forEach(e => {
|
||||
let preFormat = "";
|
||||
let postFormat = "";
|
||||
if (e.containsCrit) {
|
||||
preFormat = "**" + preFormat;
|
||||
postFormat = postFormat + "**";
|
||||
}
|
||||
if (e.containsFail) {
|
||||
preFormat = "__" + preFormat;
|
||||
postFormat = postFormat + "__";
|
||||
}
|
||||
|
||||
line2 += preFormat + e.rollTotal + postFormat + escapeCharacters(e.rollPostFormat, "|*_~`");
|
||||
|
||||
line3 += "`" + e.initConfig + "` = " + e.rollDetails + " = " + preFormat + e.rollTotal + postFormat + "\n";
|
||||
});
|
||||
|
||||
returnmsg.line1 = line1;
|
||||
returnmsg.line2 = line2;
|
||||
returnmsg.line3 = line3;
|
||||
|
||||
} catch (solverError) {
|
||||
const [errorName, errorDetails] = solverError.message.split("_");
|
||||
|
||||
let 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 " + localPostfix;
|
||||
}
|
||||
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 ore more operands are not a roll or a number, check input";
|
||||
break;
|
||||
default:
|
||||
console.error(errorName, errorDetails);
|
||||
errorMsg = "Unhandled Error: " + solverError.message;
|
||||
break;
|
||||
}
|
||||
|
||||
returnmsg.error = true;
|
||||
returnmsg.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
return returnmsg;
|
||||
};
|
||||
|
||||
export default { parseRoll };
|
||||
91
src/utils.ts
Normal file
91
src/utils.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { Message } from "https://deno.land/x/discordeno@10.0.0/mod.ts";
|
||||
|
||||
const split2k = (chunk: string): string[] => {
|
||||
chunk = chunk.replace(/\\n/g, "\n");
|
||||
const bites = [];
|
||||
while (chunk.length > 2000) {
|
||||
// take 2001 chars to see if word magically ends on char 2000
|
||||
let bite = chunk.substr(0, 2001);
|
||||
const etib = bite.split("").reverse().join("");
|
||||
const lastI = etib.indexOf(" "); // might be able to do lastIndexOf now
|
||||
if (lastI > 0) {
|
||||
bite = bite.substr(0, 2000 - lastI);
|
||||
} else {
|
||||
bite = bite.substr(0, 2000);
|
||||
}
|
||||
bites.push(bite);
|
||||
chunk = chunk.slice(bite.length);
|
||||
}
|
||||
// Push leftovers into bites
|
||||
bites.push(chunk);
|
||||
|
||||
return bites;
|
||||
};
|
||||
|
||||
const ask = async (question: string, stdin = Deno.stdin, stdout = Deno.stdout) => {
|
||||
const buf = new Uint8Array(1024);
|
||||
|
||||
// Write question to console
|
||||
await stdout.write(new TextEncoder().encode(question));
|
||||
|
||||
// Read console's input into answer
|
||||
const n = <number>await stdin.read(buf);
|
||||
const answer = new TextDecoder().decode(buf.subarray(0, n));
|
||||
|
||||
return answer.trim();
|
||||
};
|
||||
|
||||
const cmdPrompt = async (logChannel: string, botName: string, sendMessage: (c: string, m: string) => Promise<Message>): Promise<void> => {
|
||||
let done = false;
|
||||
|
||||
while (!done) {
|
||||
const fullCmd = await ask("cmd> ");
|
||||
|
||||
const args = fullCmd.split(" ");
|
||||
const command = args.shift()?.toLowerCase();
|
||||
if (command === "exit" || command === "e") {
|
||||
console.log(`${botName} Shutting down.\n\nGoodbye.`);
|
||||
done = true;
|
||||
Deno.exit(0);
|
||||
} else if (command === "stop") {
|
||||
console.log(`Closing ${botName} CLI. Bot will continue to run.\n\nGoodbye.`);
|
||||
done = true;
|
||||
} else if (command === "m") {
|
||||
try {
|
||||
const channelID = args.shift() || "";
|
||||
const message = args.join(" ");
|
||||
const messages = split2k(message);
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
sendMessage(channelID, messages[i]).catch(reason => {
|
||||
console.error(reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else if (command === "ml") {
|
||||
const message = args.join(" ");
|
||||
const messages = split2k(message);
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
sendMessage(logChannel, messages[i]).catch(reason => {
|
||||
console.error(reason);
|
||||
});
|
||||
}
|
||||
} else if (command === "help" || command === "h") {
|
||||
console.log(`${botName} CLI Help:\n\nAvailable Commands:\n exit - closes bot\n stop - closes the CLI\n m [ChannelID] [messgae] - sends message to specific ChannelID as the bot\n ml [message] sends a message to the specified botlog\n help - this message`);
|
||||
} else {
|
||||
console.log("undefined command");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendIndirectMessage = async (message: Message, messageContent: string, sendMessage: (c: string, m: string) => Promise<Message>, sendDirectMessage: (c: string, m: string) => Promise<Message>): Promise<Message> => {
|
||||
if (message.guildID === "") {
|
||||
return await sendDirectMessage(message.author.id, messageContent);
|
||||
} else {
|
||||
return await sendMessage(message.channelID, messageContent);
|
||||
}
|
||||
};
|
||||
|
||||
export default { split2k, cmdPrompt, sendIndirectMessage };
|
||||
Reference in New Issue
Block a user