From 920c8824feac5758408847422769614c31e8ad15 Mon Sep 17 00:00:00 2001 From: "Ean Milligan (Bastion)" Date: Thu, 7 Jan 2021 08:34:14 -0500 Subject: [PATCH] Auto stash before merge of "master" and "origin/master" --- .gitignore | 4 + .vscode/settings.json | 8 + config.example.ts | 39 +++ emojis/popcat.gif | Bin 0 -> 13036 bytes mod.ts | 241 +++++++++++++ src/solver.d.ts | 24 ++ src/solver.ts | 768 ++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 91 +++++ 8 files changed, 1175 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 config.example.ts create mode 100644 emojis/popcat.gif create mode 100644 mod.ts create mode 100644 src/solver.d.ts create mode 100644 src/solver.ts create mode 100644 src/utils.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a0fdc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +config.json +config.ts +brokennode/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..98e5a49 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true, + "deno.import_intellisense_origins": { + "https://deno.land": true + } +} \ No newline at end of file diff --git a/config.example.ts b/config.example.ts new file mode 100644 index 0000000..1e47033 --- /dev/null +++ b/config.example.ts @@ -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; diff --git a/emojis/popcat.gif b/emojis/popcat.gif new file mode 100644 index 0000000000000000000000000000000000000000..61ce11324266c64c5af0f267527386bba94487fc GIT binary patch literal 13036 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 R@##`NbRGGzoC*LY{{uKTq>=yt literal 0 HcmV?d00001 diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..cce96ac --- /dev/null +++ b/mod.ts @@ -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: [] + }; + 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 = []; + 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); diff --git a/src/solver.d.ts b/src/solver.d.ts new file mode 100644 index 0000000..fb91da6 --- /dev/null +++ b/src/solver.d.ts @@ -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 +}; diff --git a/src/solver.ts b/src/solver.ts new file mode 100644 index 0000000..a15916c --- /dev/null +++ b/src/solver.ts @@ -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 = (conf[0]).total; + stepSolve.details = (conf[0]).details; + stepSolve.containsCrit = (conf[0]).containsCrit; + stepSolve.containsFail = (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 }; \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..f3d4efc --- /dev/null +++ b/src/utils.ts @@ -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 = 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): Promise => { + 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, sendDirectMessage: (c: string, m: string) => Promise): Promise => { + if (message.guildID === "") { + return await sendDirectMessage(message.author.id, messageContent); + } else { + return await sendMessage(message.channelID, messageContent); + } +}; + +export default { split2k, cmdPrompt, sendIndirectMessage };