const diceRegex = () => /\b(\d*)[dD](\d+)\b(?:\s?([+-])\s?(\d+))?/g const rerollRegex = () => /\b(reroll|re-roll)\s+((?:(?:\S*|\d+s|\d+'s|\d+)\s*)+)/ const numberRegex = () => /\b(one|two|three|four|five|six|seven|eight|nine|ten|ones|twos|threes|fours|fives|sixes|sevens|eights|nines|tens|one's|two's|three's|four's|five's|seven's|eight's|nine's|ten's|\d+)\b/gi const advRegex = () => /(disadvantage|advantage)/i class Roll { constructor(roll) { if (roll) Object.assign(this, roll) } value() { return this.values.reduce((a, b) => a + b, this.bonus || 0) } toString() { let str = this.values.map(val => `[${val}]`).join(' + ') if (this.bonus < 0) { str += ` - ${Math.abs(this.bonus)}` } else if (this.bonus > 0) { str += ` + ${this.bonus}` } if (this.values.length > 1 || this.bonus) { str += ` = ${this.value()}` } if (this.rerolls === 1) { str += ` with 1 reroll` } else if (this.rerolls > 1) { str += ` with ${this.rerolls} rerolls` } return str } } class BadRoll extends Roll { constructor(text) { super({text}) } toString() { return this.text } } const random = n => Math.floor(Math.random() * n) + 1 class Dice { constructor(dice) { if (dice) Object.assign(this, dice) } roll() { if (this.count > 100) return new BadRoll(`I lost count rolling ${this}`) if (this.sides > 256) return new BadRoll(`The faces are too small to read on ${this}`) if (this.count === 0) return new BadRoll(`I finished before I started rolling ${this}`) if (this.sides === 0) return new BadRoll(`I lost my grip on ${this}`) if (this.sides === 1) return new BadRoll(`The mobius ${this} never stopped rolling`) if (this.bonusSign === '-' && this.bonus > this.sides) return new BadRoll(`It doesn't seem fair to roll ${this}`) let values = [] for (let i = 0; i < this.count; i++) { if (this.modifier === 'advantage') { values.push(Math.max(random(this.sides), random(this.sides))) } else if (this.modifier === 'disadvantage') { values.push(Math.min(random(this.sides), random(this.sides))) } else { values.push(random(this.sides)) } } let bonus = 0 switch (this.bonusSign) { case '-': bonus = -this.bonus; break case '+': bonus = this.bonus; break } let rerolls = 0 for (let i = 0; i < 10; i++) { let foundAny = false for (let d = 0; d < values.length; d++) { const v = values[d] if (this.rerollNumbers && this.rerollNumbers.includes(v)) { values[d] = random(this.sides) foundAny = true rerolls++ } } if (!foundAny) break } return new Roll({ values, bonus, rerolls }) } toString() { let str = `${this.count}d${this.sides}` if (this.bonusSign) { str += this.bonusSign + this.bonus } if (this.modifier) { str += ` with ${this.modifier}` } if (this.rerollNumbers && this.rerollNumbers.length) { if (this.rerollNumbers.length === 1) { str += ` and reroll ${this.rerollNumbers[0]}'s` } else { str += ` and reroll ${this.rerollNumbers.slice(0, this.rerollNumbers.length - 1).join(`'s, `)}'s, and ${this.rerollNumbers[this.rerollNumbers.length - 1]}'s` } } return str } } Dice.parse = str => { const rrRegex = rerollRegex() const rerolls = [] const reroll = rrRegex.exec(str) const rerollNumbers = [] if (reroll) { const nRegex = numberRegex() while (number = nRegex.exec(str)) { switch (number[1]) { case 'ones': case 'one': rerollNumbers.push(1) break case 'twos': case 'two': rerollNumbers.push(2) break case 'threes': case 'three': rerollNumbers.push(3) break case 'fours': case 'four': rerollNumbers.push(4) break case 'fives': case 'five': rerollNumbers.push(5) break case 'sixes': case 'six': rerollNumbers.push(6) break case 'sevens': case 'seven': rerollNumbers.push(7) break case 'eights': case 'eight': rerollNumbers.push(8) break case 'nines': case 'nine': rerollNumbers.push(9) break case 'tens': case 'ten': rerollNumbers.push(10) break default: const n = parseFloat(number) if (!isNaN(n)) { rerollNumbers.push(n) } } } } const aRegex = advRegex() const ad = aRegex.exec(str) const modifier = ad ? ( ad[1].toLowerCase()[0] === 'a' ? 'advantage' : 'disadvantage' ) : null const regex = diceRegex() const ret = [] let dice while (dice = regex.exec(str)) { ret.push(new Dice({ match: dice, count: dice[1] === '' ? 1 : parseFloat(dice[1]), sides: parseFloat(dice[2]), bonusSign: dice[3] || null, bonus: dice[4] === undefined ? null : parseFloat(dice[4]), rerollNumbers, modifier })) } return ret } Dice.chat = chat => { const dice = Dice.parse(chat) if (dice.length) { const diceStrings = dice.map(x => x.toString()) if (diceStrings.length > 1) { diceStrings[diceStrings.length - 1] = `and ${diceStrings[diceStrings.length - 1]}` } const rollingString = `Rolling ${diceStrings.join(', ')}...` const rollsStrings = dice.map(die => die.roll().toString()) return `${rollingString} ${rollsStrings.join(', ')}.` } } module.exports = { Dice, Roll, diceRegex, rerollRegex, advRegex }