| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- const takeRegex = () => /\b(take|keep|drop|lose)\s+(?:the\s+)?(top|highest|high|best|bottom|lowest|low|worst)\s+(one|two|three|four|five|six|seven|eight|nine|ten|\d+)/i
- const rollRegex = () => /\b(roll|flip)\b/i
- 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(take|keep|drop|lose|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 = () => /\b(disadvantage|advantage)\b/i
- const percentRegex = () => /\b(percentile|percentiles|percent)\b/i
- const dicebotRegex = () => /\b(dicebot|dice bot)\b/i
- const multiplierRegex = () => /\bx(-?\d+)/i
- const helpRegex = () => /(dicebot|roll) help/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 "roll 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) {
- 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, 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.rerolls === 1) {
- str += ` with 1 reroll`
- } else if (this.rerolls > 1) {
- str += ` with ${this.rerolls} rerolls`
- }
- if (this.dropped && this.dropped.length) {
- str += ` dropping ${this.dropped.map(v => this.formatter && this.formatter.stringify ? this.formatter.stringify(v) : `[${v}]`).join(', ')}`
- }
- if (this.values.length > 1 || this.bonus) {
- str += ` = ${this.formatter && this.formatter.total ? this.formatter.total(this.values, this.bonus || 0, this) : this.value()}`
- }
- return str
- }
- }
- class BadRoll extends Roll {
- constructor(text) {
- super({text})
- }
- toString() {
- return this.text
- }
- }
- 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.multiplier === 0) return [ new BadRoll(`That was easy rolling ${this}`)]
- if (this.multiplier > 32) return [new BadRoll(`I can't keep track of ${this}`)]
- if (this.multiplier < 0) return [new BadRoll(`I finished before I started rolling ${this}`)]
- return Array(this.multiplier).fill().map(() => {
- 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.rng, this.sides), random(this.rng, this.sides)))
- } else if (this.modifier === 'disadvantage') {
- values.push(Math.min(random(this.rng, this.sides), random(this.rng, this.sides)))
- } else {
- values.push(random(this.rng, 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.rng, this.sides)
- foundAny = true
- rerolls++
- }
- }
- if (!foundAny) break
- }
-
- let dropped = []
- if (this.takeOrDrop) {
- let c = this.takeOrDrop.count
- if (this.takeOrDrop.takeDrop === 'drop') {
- c = Math.max(0, values.length - c)
- }
- if (this.takeOrDrop.highLow === 'highest') {
- values.sort((a,b) => b - a)
- } else {
- values.sort((a,b) => a - b)
- }
- dropped = values.splice(c)
- }
- return new Roll({
- values,
- dropped,
- bonus,
- rerolls,
- percent: this.percent,
- formatter: this.formatter
- })
- })
- }
- toString() {
- 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
- }
- 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`
- }
- }
- if (this.takeOrDrop) {
- str += ` and ${this.takeOrDrop.takeDrop} the ${this.takeOrDrop.highLow} ${this.takeOrDrop.count}`
- }
- if (this.multiplier !== 1) {
- str += ` x${this.multiplier}`
- }
- return str
- }
- }
- Dice.parse = (str, opts) => {
- str = str.toLowerCase()
- const rRegex = rollRegex()
- const roll = rRegex.exec(str)
- const rrRegex = rerollRegex()
- const rerolls = []
- const reroll = rrRegex.exec(str)
- const rerollNumbers = []
- const rtake = takeRegex()
- const take = rtake.exec(str)
- const mult = multiplierRegex().exec(str)
-
- let multiplier = 1
- if (mult) {
- multiplier = parseFloat(mult[1])
- }
- let todTakeDrop = ''
- let todHighLow = ''
- let todCount = 0
- if (take) {
- switch (take[1]) {
- case 'take':
- case 'keep':
- todTakeDrop = 'take'
- break
- case 'drop':
- case 'lose':
- todTakeDrop = 'drop'
- break
- }
- switch (take[2]) {
- case 'top':
- case 'highest':
- case 'high':
- case 'best':
- todHighLow = 'highest'
- break
- case 'bottom':
- case 'lowest':
- case 'low':
- case 'worst':
- todHighLow = 'lowest'
- break
- }
- switch (take[3]) {
- case 'one':
- todCount = 1
- break
- case 'two':
- todCount = 2
- break
- case 'three':
- todCount = 3
- break
- case 'four':
- todCount = 4
- break
- case 'five':
- todCount = 5
- break
- case 'six':
- todCount = 6
- break
- case 'seven':
- todCount = 7
- break
- case 'eight':
- todCount = 8
- break
- case 'nine':
- todCount = 9
- break
- case 'ten':
- todCount = 10
- break
- default:
- todCount = parseInt(take[3])
- }
- }
- if (reroll) {
- const nRegex = numberRegex()
- const strRerollNumbers = reroll[2]
- let broken = false
- while (!broken && (number = nRegex.exec(strRerollNumbers))) {
- switch (number[1]) {
- case 'drop':
- case 'keep':
- case 'take':
- case 'lose':
- broken = true
- break;
- 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 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({
- enabled: !!roll,
- count: 2,
- sides: 10,
- percent: true,
- multiplier,
- rng: opts && opts.rng
- }))
- }
- while (dice = regex.exec(str)) {
- ret.push(new Dice({
- enabled: !!roll,
- 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,
- percent,
- multiplier,
- takeOrDrop: (todTakeDrop && todHighLow && todCount)
- ? {
- takeDrop: todTakeDrop,
- highLow: todHighLow,
- count: todCount
- }
- : null,
- dicebot,
- rng: opts && opts.rng
- }))
- }
- return ret
- }
- Dice.chat = (chat, opts) => {
- if (helpRegex().test(chat)) {
- return `Tell me to roll dice by saying "Roll" followed by dice notation. For example, \`Roll a D20\`, or \`Roll 4d4+2\`. You can also \`Roll percentile\` to roll two D10's as 1-100, or \`Roll a D100\` (It's just as random). Also try \`Roll a D20 with advantage\`, \`Roll a D20 at disadvantage\`, or \`Roll 4d6 and reroll ones\`. You can roll a nice stat block with \`Roll 4d6, reroll ones, take the top 3 x6\`. If you mess up, you can edit your message without tainting the outcome.`
- } else {
- const dice = Dice.parse(chat, opts).filter(x => x.enabled)
- if (dice.length) {
- const diceStrings = dice.map(x => x.toString())
- if (diceStrings.length > 1) {
- diceStrings[diceStrings.length - 1] = `and ${diceStrings[diceStrings.length - 1]}`
- }
- const isCoin = dice.length === 1 && dice[0].sides === 2
- const rollingString = `${isCoin ? 'Flipping' : 'Rolling'} ${diceStrings.join(', ')}...`
- const rolls = dice.flatMap(die => die.roll())
- const rollsSplits = rolls.map(roll => roll.toString().split(' = '))
- const rollsStrings = rollsSplits.map(x => x[0])
- const rollsResults = rollsSplits.map(x => x[1]).filter(x => x)
- const results = rollsResults.length
- ? ' = ' + rollsResults.join(', ')
- : ''
-
- const bonusString = rolls
- .filter(x => x.formatter && x.formatter.bonusText)
- .map(x => x.formatter.bonusText(x))
- .filter(x => x)
- .map(x => '\n' + x)
- .join('')
- let rollSummary = rollsStrings.join(', ')
- if (rollSummary.length > 1000) rollSummary = '... '
- return `${rollingString} ${rollSummary}${results}.${bonusString}`
- }
- }
- }
- module.exports = {
- Dice,
- Roll,
- diceRegex,
- rerollRegex,
- advRegex
- }
|