|
|
@@ -1,7 +1,66 @@
|
|
|
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
|
|
|
+const advRegex = () => /\b(disadvantage|advantage)\b/i
|
|
|
+const percentRegex = () => /\b(percentile|percentiles|percent)\b/i
|
|
|
+const dicebotRegex = () => /\b(dicebot|dice bot)\b/i
|
|
|
+const unicoder = require('./unicoder')
|
|
|
+
|
|
|
+const d6 = {
|
|
|
+ stringify: n => unicoder.codesets.dice[n],
|
|
|
+ total: (ns, bonus) => ns.reduce((a, b) => a + b, bonus).toString()
|
|
|
+}
|
|
|
+
|
|
|
+const coin = {
|
|
|
+ stringify: (n, i, total) => `${n === 1 ? unicoder.codesets.circles.red : n === 2 ? unicoder.codesets.circles.blue : unicoder.codesets.circles.black}${total === 1 ? (n === 1 ? ' Heads' : n === 2 ? ' Tails' : ' Sides') : ''}`,
|
|
|
+ total: ns => {
|
|
|
+ const heads = ns.filter(x => x === 1).length
|
|
|
+ const tails = ns.filter(x => x === 2).length
|
|
|
+
|
|
|
+ if (heads === 1 && tails === 0) return 'Heads'
|
|
|
+ if (heads === 0 && tails === 1) return 'Tails'
|
|
|
+ if (heads === 0 && tails > 0) return `${tails} Tails and no Heads`
|
|
|
+ if (heads > 0 && tails === 0) return `${heads} Heads and no Tails`
|
|
|
+ if (heads > 0 && tails > 0) return `${heads} Heads and ${tails} Tails`
|
|
|
+ return 'The coin disappears'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const d10 = {
|
|
|
+ stringify: (n, i, total, { percent = false } = {}) => {
|
|
|
+ if (total === 2 && percent) {
|
|
|
+ if (i === 0) {
|
|
|
+ return n === 10 ? '00' : `${n}0`
|
|
|
+ } else {
|
|
|
+ return n === 10 ? '0' : n.toString()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return unicoder.codesets.negRound[n === 10 ? '0' : n.toString()]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ total: (ns, bonus, { percent = false } = {}) => {
|
|
|
+ if (ns.length === 2 & percent) {
|
|
|
+ if (ns[0] === 0 && ns[1] === 0) return '100'
|
|
|
+ return ((ns[0] === 10 ? 0 : ns[0]) * 10 + (ns[1] % 10)).toString()
|
|
|
+ }
|
|
|
+ return ns.reduce((a, b) => a + b, bonus).toString()
|
|
|
+ },
|
|
|
+ bonusText: (x) => {
|
|
|
+ const {percent = false, values = []} = x || {}
|
|
|
+
|
|
|
+ if (values.length === 2 && !percent) return '_Did you mean "percentile"?_'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const d20 = {
|
|
|
+ stringify: n => unicoder.codesets.negRound[n],
|
|
|
+ total: (ns, bonus) => ns.reduce((a, b) => a + b, bonus).toString()
|
|
|
+}
|
|
|
+
|
|
|
+const ascii = {
|
|
|
+ stringify: n => n.toString(),
|
|
|
+ total: (ns, bonus) => ns.reduce((a, b) => a + b, bonus).toString()
|
|
|
+}
|
|
|
|
|
|
class Roll {
|
|
|
constructor(roll) {
|
|
|
@@ -11,14 +70,14 @@ class Roll {
|
|
|
return this.values.reduce((a, b) => a + b, this.bonus || 0)
|
|
|
}
|
|
|
toString() {
|
|
|
- let str = this.values.map(val => `[${val}]`).join(' + ')
|
|
|
+ let str = this.values.map((val, i) => this.formatter && this.formatter.stringify ? this.formatter.stringify(val, i, this.values.length, this) : `[${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()}`
|
|
|
+ str += ` = ${this.formatter && this.formatter.total ? this.formatter.total(this.values, this.bonus || 0, this) : this.value()}`
|
|
|
}
|
|
|
if (this.rerolls === 1) {
|
|
|
str += ` with 1 reroll`
|
|
|
@@ -38,11 +97,19 @@ class BadRoll extends Roll {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-const random = n => Math.floor(Math.random() * n) + 1
|
|
|
+const random = (rng, n) => Math.floor(rng() * n) + 1
|
|
|
|
|
|
class Dice {
|
|
|
constructor(dice) {
|
|
|
if (dice) Object.assign(this, dice)
|
|
|
+ if (!this.formatter) {
|
|
|
+ if (this.sides === 2) this.formatter = coin
|
|
|
+ else if (this.sides === 6) this.formatter = d6
|
|
|
+ else if (this.sides === 10) this.formatter = d10
|
|
|
+ else if (this.sides <= 20) this.formatter = d20
|
|
|
+ else this.formatter = ascii
|
|
|
+ }
|
|
|
+ if (!this.rng) this.rng = () => Math.random()
|
|
|
}
|
|
|
roll() {
|
|
|
if (this.count > 100) return new BadRoll(`I lost count rolling ${this}`)
|
|
|
@@ -55,11 +122,11 @@ class Dice {
|
|
|
let values = []
|
|
|
for (let i = 0; i < this.count; i++) {
|
|
|
if (this.modifier === 'advantage') {
|
|
|
- values.push(Math.max(random(this.sides), random(this.sides)))
|
|
|
+ values.push(Math.max(random(this.rng, this.sides), random(this.rng, this.sides)))
|
|
|
} else if (this.modifier === 'disadvantage') {
|
|
|
- values.push(Math.min(random(this.sides), random(this.sides)))
|
|
|
+ values.push(Math.min(random(this.rng, this.sides), random(this.rng, this.sides)))
|
|
|
} else {
|
|
|
- values.push(random(this.sides))
|
|
|
+ values.push(random(this.rng, this.sides))
|
|
|
}
|
|
|
}
|
|
|
let bonus = 0
|
|
|
@@ -73,7 +140,7 @@ class Dice {
|
|
|
for (let d = 0; d < values.length; d++) {
|
|
|
const v = values[d]
|
|
|
if (this.rerollNumbers && this.rerollNumbers.includes(v)) {
|
|
|
- values[d] = random(this.sides)
|
|
|
+ values[d] = random(this.rng, this.sides)
|
|
|
foundAny = true
|
|
|
rerolls++
|
|
|
}
|
|
|
@@ -83,11 +150,20 @@ class Dice {
|
|
|
return new Roll({
|
|
|
values,
|
|
|
bonus,
|
|
|
- rerolls
|
|
|
+ rerolls,
|
|
|
+ percent: this.percent,
|
|
|
+ formatter: this.formatter
|
|
|
})
|
|
|
}
|
|
|
toString() {
|
|
|
- let str = `${this.count}d${this.sides}`
|
|
|
+ let str =
|
|
|
+ this.sides === 2
|
|
|
+ ? (
|
|
|
+ this.count === 1 ? 'a coin' : `${this.count} coins`
|
|
|
+ )
|
|
|
+ : (this.count === 2 && this.percent)
|
|
|
+ ? 'percentile dice'
|
|
|
+ :`${this.count}d${this.sides}`
|
|
|
if (this.bonusSign) {
|
|
|
str += this.bonusSign + this.bonus
|
|
|
}
|
|
|
@@ -105,7 +181,7 @@ class Dice {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-Dice.parse = str => {
|
|
|
+Dice.parse = (str, opts) => {
|
|
|
const rrRegex = rerollRegex()
|
|
|
const rerolls = []
|
|
|
const reroll = rrRegex.exec(str)
|
|
|
@@ -165,14 +241,24 @@ Dice.parse = str => {
|
|
|
|
|
|
const aRegex = advRegex()
|
|
|
const ad = aRegex.exec(str)
|
|
|
+ const dicebot = dicebotRegex().test(str)
|
|
|
|
|
|
const modifier = ad ? (
|
|
|
ad[1].toLowerCase()[0] === 'a' ? 'advantage' : 'disadvantage'
|
|
|
) : null
|
|
|
|
|
|
+ const percent = percentRegex().test(str)
|
|
|
+
|
|
|
const regex = diceRegex()
|
|
|
const ret = []
|
|
|
let dice
|
|
|
+ if (percent) {
|
|
|
+ ret.push(new Dice({
|
|
|
+ count: 2,
|
|
|
+ sides: 10,
|
|
|
+ percent: true
|
|
|
+ }))
|
|
|
+ }
|
|
|
while (dice = regex.exec(str)) {
|
|
|
ret.push(new Dice({
|
|
|
match: dice,
|
|
|
@@ -181,22 +267,33 @@ Dice.parse = str => {
|
|
|
bonusSign: dice[3] || null,
|
|
|
bonus: dice[4] === undefined ? null : parseFloat(dice[4]),
|
|
|
rerollNumbers,
|
|
|
- modifier
|
|
|
+ modifier,
|
|
|
+ percent,
|
|
|
+ dicebot,
|
|
|
+ rng: opts && opts.rng
|
|
|
}))
|
|
|
}
|
|
|
return ret
|
|
|
}
|
|
|
|
|
|
-Dice.chat = chat => {
|
|
|
- const dice = Dice.parse(chat)
|
|
|
+Dice.chat = (chat, opts) => {
|
|
|
+ const dice = Dice.parse(chat, opts)
|
|
|
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(', ')}.`
|
|
|
+ const isCoin = dice.length === 1 && dice[0].sides === 2
|
|
|
+ const rollingString = `${isCoin ? 'Flipping' : 'Rolling'} ${diceStrings.join(', ')}...`
|
|
|
+ const rolls = dice.map(die => die.roll())
|
|
|
+ const rollsStrings = rolls.map(roll => roll.toString())
|
|
|
+ const bonusString = rolls
|
|
|
+ .filter(x => x.formatter && x.formatter.bonusText)
|
|
|
+ .map(x => x.formatter.bonusText(x))
|
|
|
+ .filter(x => x)
|
|
|
+ .map(x => '\n' + x)
|
|
|
+ .join('')
|
|
|
+ return `${rollingString} ${rollsStrings.join(', ')}.${bonusString}`
|
|
|
}
|
|
|
}
|
|
|
|