Auto stash before merge of "master" and "origin/master"

This commit is contained in:
Ean Milligan (Bastion) 2021-01-07 08:34:14 -05:00
parent bfff1de45b
commit 920c8824fe
8 changed files with 1175 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
config.json
config.ts
brokennode/

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": true,
"deno.import_intellisense_origins": {
"https://deno.land": true
}
}

39
config.example.ts Normal file
View File

@ -0,0 +1,39 @@
export const config = {
"name": "The Artificer",
"version": "1.0.0",
"token": "the_bot_token",
"prefix": "[[",
"postfix": "]]",
"logChannel": "the_log_channel",
"help": [
"```fix",
"The Artificer Help",
"```",
"__**Commands:**__",
"```",
"[[? - This Command",
"[[ping - Pings the bot to check connectivity",
"[[version - Prints the bots version",
"[[popcat - Popcat",
"[[report [text] - Report a command that failed to run",
"[[stats - Statistics on the bot",
"[[xdydzracsq!]] ... - Rolls all configs requested, you may repeat the command multiple times in the same message (just ensure you close each roll with ]])",
" * 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",
"```"
],
"emojis": {
"popcat": {
"name": "popcat",
"id": "796340018377523221",
animated: true
}
}
};
export default config;

BIN
emojis/popcat.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

241
mod.ts Normal file
View File

@ -0,0 +1,241 @@
/* The Artificer was built in memory of Babka
* With love, Ean
*
* December 21, 2020
*/
const DEVMODE = false;
import {
startBot, editBotsStatus,
Intents, StatusTypes, ActivityType,
Message, Guild, sendMessage, sendDirectMessage,
cache
} from "https://deno.land/x/discordeno@10.0.0/mod.ts";
import utils from "./src/utils.ts";
import solver from "./src/solver.ts";
import config from "./config.ts";
startBot({
token: config.token,
intents: [Intents.GUILD_MESSAGES, Intents.DIRECT_MESSAGES, Intents.GUILDS],
eventHandlers: {
ready: () => {
console.log("Logged in!");
editBotsStatus(StatusTypes.Online, `${config.prefix}help for commands`, ActivityType.Game);
setTimeout(() => {
sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch(() => {
console.error("Failed to send message 00");
});
}, 1000);
},
guildCreate: (guild: Guild) => {
sendMessage(config.logChannel, `New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`).catch(() => {
console.error("Failed to send message 01");
});
},
guildDelete: (guild: Guild) => {
sendMessage(config.logChannel, `I have been removed from: ${guild.name} (id: ${guild.id})`).catch(() => {
console.error("Failed to send message 02");
});
},
debug: (DEVMODE ? console.error : () => { }),
messageCreate: async (message: Message) => {
// Ignore all other bots
if (message.author.bot) return;
// Ignore all messages that are not commands
if (message.content.indexOf(config.prefix) !== 0) return;
// Split into standard command + args format
const args = message.content.slice(config.prefix.length).trim().split(/ +/g);
const command = args.shift()?.toLowerCase();
// All commands below here
// [[ping
// Its a ping test, what else do you want.
if (command === "ping") {
// Calculates ping between sending a message and editing it, giving a nice round-trip latency.
// The second ping is an average latency between the bot and the websocket server (one-way, not round-trip)
try {
const m = await utils.sendIndirectMessage(message, "Ping?", sendMessage, sendDirectMessage);
m.edit(`Pong! Latency is ${m.timestamp - message.timestamp}ms.`);
} catch (err) {
console.error("Failed to send message 10", message, err);
}
}
// [[help or [[h or [[?
// Help command, prints from help file
else if (command === "help" || command === "h" || command === "?") {
utils.sendIndirectMessage(message, config.help.join("\n"), sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 20", message, err);
});
}
// [[v or [[version
// Returns version of the bot
else if (command === "version" || command === "v") {
utils.sendIndirectMessage(message, `My current version is ${config.version}.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 30", message, err);
});
}
// [[popcat
// popcat animated emoji
else if (command === "popcat") {
utils.sendIndirectMessage(message, `<${config.emojis.popcat.animated ? "a" : ""}:${config.emojis.popcat.name}:${config.emojis.popcat.id}>`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 40", message, err);
});
message.delete().catch(err => {
console.error("Failed to delete message 41", message, err);
});
}
// [[report or [[r (command that failed)
// Manually report a failed roll
else if (command === "report" || command === "r") {
sendMessage(config.logChannel, ("USER REPORT:\n" + args.join(" "))).catch(err => {
console.error("Failed to send message 50", message, err);
});
utils.sendIndirectMessage(message, "Failed command has been reported to my developer.", sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 51", message, err);
});
}
// [[stats or [[s
// Displays stats on the bot
else if (command === "stats" || command === "s") {
utils.sendIndirectMessage(message, `${config.name} is rolling dice for ${cache.members.size} users, in ${cache.channels.size} channels of ${cache.guilds.size} servers.`, sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 60", message, err);
});
}
// [[
// Dice rolling commence!
else {
if (DEVMODE && message.guildID !== "317852981733097473") {
utils.sendIndirectMessage(message, "Command is in development, please try again later.", sendMessage, sendDirectMessage).catch(err => {
console.error("Failed to send message 70", message, err);
});
return;
}
try {
const m = await utils.sendIndirectMessage(message, "Rolling...", sendMessage, sendDirectMessage);
const modifiers = {
noDetails: false,
spoiler: "",
maxRoll: false,
nominalRoll: false,
gmRoll: false,
gms: <string[]>[]
};
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "-nd":
modifiers.noDetails = true;
args.splice(i, 1);
i--;
break;
case "-s":
modifiers.spoiler = "||";
args.splice(i, 1);
i--;
break;
case "-m":
modifiers.maxRoll = true;
args.splice(i, 1);
i--;
break;
case "-n":
modifiers.nominalRoll = true;
args.splice(i, 1);
i--;
break;
case "-gm":
modifiers.gmRoll = true;
while (((i + 1) < args.length) && args[i + 1].startsWith("<@!")) {
modifiers.gms.push(args[i + 1]);
args.splice((i + 1), 1);
}
if (modifiers.gms.length < 1) {
m.edit("Error: Must specifiy at least one GM by mentioning them");
return;
}
args.splice(i, 1);
i--;
break;
default:
break;
}
}
const rollCmd = command + " " + args.join(" ");
const returnmsg = solver.parseRoll(rollCmd, config.prefix, config.postfix, modifiers.maxRoll, modifiers.nominalRoll) || { error: true, errorMsg: "Error: Empty message", line1: "", line2: "", line3: "" };
let returnText = "";
if (returnmsg.error) {
returnText = returnmsg.errorMsg;
} else {
returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2;
if (modifiers.noDetails) {
returnText += "\nDetails suppressed by -nd flag.";
} else {
returnText += "\nDetails:\n" + modifiers.spoiler + returnmsg.line3 + modifiers.spoiler;
}
}
if (modifiers.gmRoll) {
const normalText = "<@" + message.author.id + ">" + returnmsg.line1 + "\nResults have been messaged to the following GMs: " + modifiers.gms.join(" ");
modifiers.gms.forEach(async e => {
const msgs = utils.split2k(returnText);
const failedDMs = <string[]>[];
for (let i = 0; ((failedDMs.indexOf(e) === -1) && (i < msgs.length)); i++) {
await sendDirectMessage(e.substr(3, (e.length - 4)), msgs[i]).catch(() => {
failedDMs.push(e);
utils.sendIndirectMessage(message, "WARNING: " + e + " could not be messaged. If this issue persists, make sure direct messages are allowed from this server.", sendMessage, sendDirectMessage);
});
}
});
m.edit(normalText);
} else {
if (returnText.length > 2000) {
const msgs = utils.split2k(returnText);
let failed = false;
for (let i = 0; (!failed && (i < msgs.length)); i++) {
await sendDirectMessage(message.author.id, msgs[i]).catch(() => {
failed = true;
});
}
if (failed) {
returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. WARNING: <@" + message.author.id + "> could **NOT** be messaged full details for verification purposes.";
} else {
returnText = "<@" + message.author.id + ">" + returnmsg.line1 + "\n" + returnmsg.line2 + "\nDetails have been ommitted from this message for being over 2000 characters. Full details have been messaged to <@" + message.author.id + "> for verification purposes.";
}
}
m.edit(returnText);
}
} catch (err) {
console.error("Something failed 71");
}
}
}
}
});
utils.cmdPrompt(config.logChannel, config.name, sendMessage);

24
src/solver.d.ts vendored Normal file
View 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
View 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
View 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 };