diff --git a/distribution.js b/distribution.js new file mode 100644 index 0000000..f9753ef --- /dev/null +++ b/distribution.js @@ -0,0 +1,210 @@ +export class DenominatorTooLargeError extends Error {} + +export function Distribution(floor, ceil, denominator, fill) { + if(denominator > Number.MAX_SAFE_INTEGER) { + throw DenominatorTooLargeError("Distribution denominator unsafe") + } + + let length = (ceil + 1) - floor + let buffer = new ArrayBuffer(length * 4) + let data = new Uint32Array(buffer) + + if(Number.isInteger(fill)) { + data.fill(fill) + } + + return { + floor, + ceil, + denominator, + data + } +} + +export function Die(size) { + return Distribution(1, size, size, 1) +} + +export function Index(dist, result) { + return result - dist.floor +} + +export function Set(dist, result, prob) { + return dist.data.set([ prob ], Index(dist, result)) +} + +export function Get(dist, result) { + return dist.data.at(Index(dist, result)) +} + +export function Inc(dist, result, amount = 1) { + let i = Index(dist, result) + return dist.data.set([ dist.data.at(i) + amount ], i) +} + +export function Includes(dist, result) { + return dist.floor <= result && result <= dist.ceil +} + +export function Convolve(d1, d2) { + let ax1 = d1.floor + d2.floor + let ax2 = d1.ceil + d2.ceil + let Pc = Distribution(ax1, ax2, d1.denominator * d2.denominator, 0) + + for(let S = ax1; S <= ax2; S++) { + for(let r1 = d1.floor; r1 <= d1.ceil; r1++) { + let r2 = S - r1 + + if(Includes(d2, r2)) { + Inc(Pc, S, Get(d1, r1) * Get(d2, r2)) + } + } + } + + return Pc +} + +export function Highest(d1, d2) { + let hd, ld + if(d1.ceil >= d2.ceil) { + hd = d1, ld = d2 + } else { + hd = d2, ld = d1 + } + + let Ph = Distribution(Math.max(d1.floor, d2.floor), hd.ceil, d1.denominator * d2.denominator, 0) + + for(let r1 = hd.floor; r1 <= hd.ceil; r1++) { + for(let r2 = ld.floor; r2 <= ld.ceil; r2++) { + let S = Math.max(r2, r1) + + Inc(Ph, S, Get(hd, r1) * Get(ld, r2)) + } + } + + return Ph +} + +export function Lowest(d1, d2) { + let ld, hd + if(d1.floor <= d2.floor) { + ld = d1, hd = d2 + } else { + ld = d2, hd = d1 + } + + let Ph = Distribution(ld.floor, Math.min(d1.ceil, d2.ceil), d1.denominator * d2.denominator, 0) + + for(let r1 = ld.floor; r1 <= ld.ceil; r1++) { + for(let r2 = hd.floor; r2 <= hd.ceil; r2++) { + let S = Math.min(r2, r1) + + Inc(Ph, S, Get(ld, r1) * Get(hd, r2)) + } + } + + return Ph +} + +export function ForEach(dist, func) { + for(let i = 0; i < dist.data.length; i++) { + func(i + dist.floor, dist.data.at(i)) + } +} + +export function Floats(dist) { + let buf = new ArrayBuffer(dist.data.length * 4) + let view = new Float32Array(buf) + + ForEach(dist, (i) => { + view[Index(dist, i)] = Get(dist, i) / dist.denominator + }) + + return view +} + +export function Count(dist, amount) { + let counter = amount + + for(let i = 0; i < dist.data.length; i++) { + counter -= dist.data.at(i) + if(counter <= 0) { + return i + dist.floor + } + } +} + +const MAX_32_BIT_INT = 4294967295 +export function Roll(dist) { + let i = crypto.getRandomValues(new Uint32Array(1))[0] + i = Math.ceil((i / MAX_32_BIT_INT) * dist.denominator) + + return Count(dist, i) +} + +/* + 2 11.11% ████████████ + 3 22.22% ████████████ +# 4 33.33% ████████████ + 5 22.22% ████████████ + 6 1.11% ████████████ +*/ + +export function Visualize(dist, highlight = 13, tableWidth = 13) { + let largestProb = 0 + let items = [] + ForEach(dist, (i, v) => { + if(v > largestProb) largestProb = v + items.push([ i, v ]) + }) + + let v = '' + let probUnits = tableWidth / largestProb + + for(let [ res, prob ] of items) { + let percentage = prob / dist.denominator + percentage *= 100 + + v += res === highlight ? '#' : ' ' + v += res.toString().padStart(3, ' ') + v += ' ' + v += percentage.toFixed(2).padStart(5) + '%' + v += ' ' + let u = prob * probUnits + let fb = Math.floor(u) + let r = u - fb + v += ('\u2588'.repeat(fb) + fractionalBlockCharacter(r)).padEnd(tableWidth, '\u2500') + v += '\n' + } + + return v +} + +export function fractionalBlockCharacter(float) { + if(float >= 7/8) return '\u2589' + if(float >= 3/4) return '\u258A' + if(float >= 5/8) return '\u258B' + if(float >= 1/2) return '\u258C' + if(float >= 3/8) return '\u258D' + if(float >= 1/4) return '\u258E' + if(float >= 1/8) return '\u258F' + return '' +} + +// console.log( +// ) + +let exD = Die(6) +exD.floor += 3 +exD.ceil += 3 +// TODO: Change ceil back to a function so this is less weird + +console.log( + // Die(6).data.length + Visualize( + // Convolve( + // Die(4), + Highest(Die(8), exD) + // ) + ) +) \ No newline at end of file diff --git a/experiment.js b/experiment.js new file mode 100644 index 0000000..0ff2f73 --- /dev/null +++ b/experiment.js @@ -0,0 +1,204 @@ +// export function Weights(size) { +// let set = new Set() + +// for(let i = 0; i < size; i++) { +// set.add(i + 1) +// } + +// return new Map([ +// [ 1, set ] +// ]) +// } + +export function Weights(size) { + let out = [] + + for(let i = 0; i < size; i++) { + out.push(1/size) + } + + return out +} + +export function Add(left, right, out = new Map()) { + for(let i = 0; i < left.size * right.size; i++) { + let li = parseInt(i / left.size) + 1 + let ri = (i % left.size) + 1 + + let key = li + ri + let base = out.get(key) ?? 0 + out.set(key, base + left.get(li) + right.get(ri)) + } + // for(let li = 0; li < left.size; li++) { + // for(let ri = 0; ri < right.size; ri++) { + // let i = li + ri + // let b = out.get(i) ?? 0 + // out.set(i, b + left.get(i) + right.get(i)) + // } + // } + + return out + +} + +export function Mul(left, to) { + let out = left + + for(let i = 1; i < to; i++) { + out = Add(out, left) + } + + return out +} + +// console.log(Weights(6)) +// console.log(Add(Weights(6), Weights(6))) +// console.log(Mul(Weights(6), 3)) + +export function WeightOf(size) { + return 1 / size +} + +function probSum2dN(N, S) { + let P = 0 + for(let r1 = 1; r1 < N + 1; r1++) { + let r2 = S - r1 + if(1 <= r2 && r2 <= N) { + P += 1 + } + } + return [ P, Math.pow(N, 2) ] +} + +// function conv(a, b) { +// let bf = b.reverse() +// let len = (a.length) + (b.length) +// let P = new Map() + +// for(let i = 1; i < len; i++) { +// let topSliceRange = [ +// Math.max(i - a.length, 0), +// Math.min(i, b.length) +// ] +// let bottomSliceRange = [ +// Math.max(b.length - i, 0), +// Math.min(b.length, len - i) +// ] +// let topSlice = a.slice(...topSliceRange) +// let bottomSlice = bf.slice(...bottomSliceRange) + +// P.set( +// i + 1, +// topSlice.reduce( +// (a, v, i) => a + (v * bottomSlice[i]), +// 0 +// ) +// ) +// } + +// return P +// } + +// const conv = (vec1, vec2) => { +// if (vec1.length === 0 || vec2.length === 0) { +// throw new Error('Vectors can not be empty!'); +// } +// const volume = vec1; +// const kernel = vec2; +// /* Initialized to zero by default */ +// const convVec = new Float32Array(volume.length + kernel.length); + +// let i = 0; +// for (let j = 0; j < kernel.length; ++j) { +// convVec[j] = volume[0] * kernel[j]; +// } + +// for (i = 1; i < volume.length; ++i) { +// for (let j = 0; j < kernel.length; ++j) { +// convVec[i + j] += volume[i] * kernel[j]; +// } +// } + +// return convVec; +// }; + +// console.log( +// conv( +// Weights(6), +// Weights(6) +// // conv(Weights(6), Weights(6)) +// ) +// ) + +class Distribution extends Float32Array { + static Die(size) { + return new Distribution(1, size, 1 / size) + } + + constructor(first, last, fill) { + + let len = (last + 1) - first + let b = new ArrayBuffer(len * 4) + super(b) + + this.first = first + this.last = last + if(fill) { + this.fill(fill) + } + } + + forEach(func) { + for(let i = this.first; i <= this.last; i++) { + func(this.get(i), i) + } + } + + set(i, value) { + super.set([ value ], i - this.first) + } + + increase(i, value) { + let index = i - this.first + super.set([ this.at(index) + value ], index) + } + + get(i) { + return this.at(i - this.first) + } + + contains(indice) { + return indice >= this.first && indice <= this.last + } +} + +function conv(d1, d2) { + let ax1 = d1.first + d2.first + let ax2 = d1.last + d2.last + console.log(ax1, ax2, ax2 - ax1) + let Pc = new Distribution(ax1, ax2, 0) + + for(let S = ax1; S <= ax2; S++) { + for(let r1 = d1.first; r1 <= d1.last; r1++) { + let r2 = S - r1 + + if(d2.contains(r2)) { + Pc.increase(S, d1.get(r1) * d2.get(r2)) + } + } + } + + return Pc +} + +// console.log(Distribution.Die(6)) +// let a = Distribution.Die(12) +// a.set(2, 0.000069) +// a.forEach(console.log) + +console.log( + conv( + Distribution.Die(6), + conv(Distribution.Die(6), Distribution.Die(6)) + ) +) \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index 860f90a..b68c36f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,5 +1,5 @@ export default { - rollRegex: /^(\d+)?([dhlfb])(\d+)(\s*([+\-*x\/])\s*(\d+))?/, + rollRegex: /^(?\d+)?(?[dhlfb])(?\d+)(?\s*([+\-*x\/])\s*(?\d+))?(\s*\<(?[\w\-\s]+)?(\s*:(?.*?))?\>)?/, optionRollRegex: /^(\d+)?(([dhlfb])(\d+))?(\s*([+\-*x\/])\s*(\d+))?/, descriptionRegex: /\s*((\d*-\d*)|(\d+))?([^;\n]+)/g, macroNameRegex: /^[a-z0-9]+$/, diff --git a/src/index.js b/src/index.js index 40b8e96..4ca99ea 100644 --- a/src/index.js +++ b/src/index.js @@ -50,25 +50,43 @@ const ParseOptionRoll = expression => { } } +const ParseExclusions = (exclusions) => { + let numbers = [] + + for(let part of exclusions.split(/\s*,\s*/)) { + let int = parseInt(part) + if(int !== NaN) { + numbers.push(int) + } + } + + return numbers +} + const ParseRoll = expression => { let match = constants.rollRegex.exec(expression.trim()) if(match == null) return - let [ + let { count, mode, size, - modifierString, + table, + exclusions, operation, modifier - ] = match.slice(1) + } = match.groups + + console.log(count) return { count: ParseRollInt(count, 1), mode, size: ParseRollInt(size), + table, + exclusions: exclusions == null ? [] : ParseExclusions(exclusions), operation, modifier: ParseRollInt(modifier), descriptionConditions: PullDescription(expression, match) @@ -123,28 +141,13 @@ const OnMessage = (message, respond) => { if(dice == undefined) return // No dice - RollDice(dice, respond) + Roll(dice, respond) } -const RollDice = (dice, respond) => { - if(dice.size > 255) { - respond('That die is way too big... .-.') - return - } else if(dice.size < 2) { - respond('I cannot even fathom a die with that geometry ;-;') - return - } - - if(dice.count > 100) { - respond('I don\'t have that many dice O_O') - return - } - +const Result = (dice) => { let rolls = [ ...crypto.getRandomValues(new Uint8Array(dice.count) ) ] .map(n => Math.ceil((n / 256) * dice.size)) let result = 0 - let operationSymbol = dice.operation - let response = '' switch(dice.mode.toLowerCase()) { case 'd': @@ -194,6 +197,48 @@ const RollDice = (dice, respond) => { break } + return { + result, + rolls + } +} + +const Roll = (dice, respond) => { + if(dice.size > 255) { + respond('That die is way too big... .-.') + return + } else if(dice.size < 2) { + respond('I cannot even fathom a die with that geometry ;-;') + return + } + + if(dice.count > 100) { + respond('I don\'t have that many dice O_O') + return + } + + let result, rolls + let tries = 0 + const maxTries = dice.size * dice.count * 256 + + const roll = () => { + ({ result, rolls } = Result(dice)) + tries++ + } + + roll() + while(dice.exclusions.includes(result)) { + roll() + + if(tries >= maxTries) { + // TODO: Buttons to rerun the command and reset table, if applicable + respond(`I've rolled ${tries} times, but no unique results are left in this table.`) + return + } + } + + let response = '' + if(dice.descriptionConditions) { for(let { range, content } of dice.descriptionConditions) { if(!range || result >= range.lower && result <= range.upper) @@ -204,6 +249,7 @@ const RollDice = (dice, respond) => { response += `\` ${result} \` \u27F5 [${rolls.join(', ')}] ${dice.count + dice.mode + dice.size}` if(dice.operation) { + let operationSymbol = dice.operation response += ' ' + operationSymbol + ' ' + dice.modifier } @@ -265,39 +311,6 @@ const Subcommands = (data, subcommandCallbacks) => return subcommandCallbacks[interaction.options.getSubcommand()](interaction) }) -const OpenMacros = guildId => - db.sublevel(guildId).sublevel('macros') - -const ReloadMacros = async guildId => { - let commands = [] - let macros = OpenMacros(guildId) - let cacheEntry = {} - - for await (let [ name, dice ] of macros.iterator() ) { - cacheEntry[name] = dice - - commands.push({ - name, - description: Elipsify("Roll " + dice.replaceAll('\n', ';'), 100), - options: [ - { - name: "options", - description: "Dice, modifiers, or descriptions to apply over the macro", - type: 3 - } - ] - }) - } - - globalThis.macroCache.set(guildId, cacheEntry) - - await rest.put( - Routes.applicationGuildCommands(process.env.DISCORD_ID, guildId), - { body: globalThis.commands } - ) - .catch(err => console.error('Failed to reload macros:', err) ) -} - const Elipsify = (string, maxLength) => string.length > maxLength ? string.slice(0, maxLength - 3) + '...' : string @@ -353,7 +366,7 @@ const HandleCommand = async interaction => { } } - RollDice(dice, content => interaction.followUp(content) ) + Roll(dice, content => interaction.followUp(content) ) } } @@ -422,6 +435,50 @@ const About = async (interaction) => { }) } + + +const TableLevel = guildId => + db.sublevel(guildId).sublevel('tables') + +const Table = (guildId, name, excludedNumbers = new Set()) => { + TableLevel(guildId).put(name, excludedNumbers) +} + + + +const MacroLevel = guildId => + db.sublevel(guildId).sublevel('macros') + +const ReloadMacros = async guildId => { + let commands = [] + let macros = MacroLevel(guildId) + let cacheEntry = {} + + for await (let [ name, dice ] of macros.iterator() ) { + cacheEntry[name] = dice + + commands.push({ + name, + description: Elipsify("Roll " + dice.replaceAll('\n', ';'), 100), + options: [ + { + name: "options", + description: "Dice, modifiers, or descriptions to apply over the macro", + type: 3 + } + ] + }) + } + + globalThis.macroCache.set(guildId, cacheEntry) + + await rest.put( + Routes.applicationGuildCommands(process.env.DISCORD_ID, guildId), + { body: globalThis.commands } + ) + .catch(err => console.error('Failed to reload macros:', err) ) +} + const AddMacro = async (interaction) => { let respond = Responses(interaction, true) let name = interaction.options.get('name').value.toLowerCase() @@ -445,7 +502,7 @@ const AddMacro = async (interaction) => { await interaction.deferReply({ ephemeral: true }) await Promise.all([ - OpenMacros(interaction.guild.id).put(name, dice), + MacroLevel(interaction.guild.id).put(name, dice), ReloadMacros(interaction.guild.id) ]) interaction.followUp(`Macro added! Try \`/${name}\`! You might need to switch to a different server and back or reopen Discord in order for it to recognize the new command.`) @@ -473,6 +530,8 @@ const RemoveMacro = async (interaction) => { await interaction.followUp(`Removed \`${name}\`, its dice expression was: \`\`\`${dice}\`\`\``) } + + const Start = async () => { Command( constants.commands.about,