Compare commits

..

No commits in common. "dev" and "main" have entirely different histories.
dev ... main

4 changed files with 56 additions and 529 deletions

View File

@ -1,210 +0,0 @@
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)
// )
)
)

View File

@ -1,204 +0,0 @@
// 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))
)
)

View File

@ -1,5 +1,5 @@
export default {
rollRegex: /^(?<count>\d+)?(?<mode>[dhlfb])(?<size>\d+)(?<operation>\s*([+\-*x\/])\s*(?<modifier>\d+))?(\s*\<(?<table>[\w\-\s]+)?(\s*:(?<exclusions>.*?))?\>)?/,
rollRegex: /^(\d+)?([dhlfb])(\d+)(\s*([+\-*x\/])\s*(\d+))?/,
optionRollRegex: /^(\d+)?(([dhlfb])(\d+))?(\s*([+\-*x\/])\s*(\d+))?/,
descriptionRegex: /\s*((\d*-\d*)|(\d+))?([^;\n]+)/g,
macroNameRegex: /^[a-z0-9]+$/,

View File

@ -50,43 +50,25 @@ 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,
table,
exclusions,
modifierString,
operation,
modifier
} = match.groups
console.log(count)
] = match.slice(1)
return {
count: ParseRollInt(count, 1),
mode,
size: ParseRollInt(size),
table,
exclusions: exclusions == null ? [] : ParseExclusions(exclusions),
operation,
modifier: ParseRollInt(modifier),
descriptionConditions: PullDescription(expression, match)
@ -141,13 +123,28 @@ const OnMessage = (message, respond) => {
if(dice == undefined)
return // No dice
Roll(dice, respond)
RollDice(dice, respond)
}
const Result = (dice) => {
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
}
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':
@ -197,48 +194,6 @@ const Result = (dice) => {
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)
@ -249,7 +204,6 @@ const Roll = (dice, respond) => {
response += `\` ${result} \` \u27F5 [${rolls.join(', ')}] ${dice.count + dice.mode + dice.size}`
if(dice.operation) {
let operationSymbol = dice.operation
response += ' ' + operationSymbol + ' ' + dice.modifier
}
@ -311,6 +265,39 @@ 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
@ -366,7 +353,7 @@ const HandleCommand = async interaction => {
}
}
Roll(dice, content => interaction.followUp(content) )
RollDice(dice, content => interaction.followUp(content) )
}
}
@ -435,50 +422,6 @@ 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()
@ -502,7 +445,7 @@ const AddMacro = async (interaction) => {
await interaction.deferReply({ ephemeral: true })
await Promise.all([
MacroLevel(interaction.guild.id).put(name, dice),
OpenMacros(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.`)
@ -530,8 +473,6 @@ const RemoveMacro = async (interaction) => {
await interaction.followUp(`Removed \`${name}\`, its dice expression was: \`\`\`${dice}\`\`\``)
}
const Start = async () => {
Command(
constants.commands.about,