dice.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. const rollRegex = () => /\b(roll|flip)\b/i
  2. const diceRegex = () => /\b(\d*)[dD](\d+)\b(?:\s?([+-])\s?(\d+))?/g
  3. const rerollRegex = () => /\b(reroll|re-roll)\s+((?:(?:\S*|\d+s|\d+'s|\d+)\s*)+)/
  4. 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
  5. const advRegex = () => /\b(disadvantage|advantage)\b/i
  6. const percentRegex = () => /\b(percentile|percentiles|percent)\b/i
  7. const dicebotRegex = () => /\b(dicebot|dice bot)\b/i
  8. const unicoder = require('./unicoder')
  9. const d6 = {
  10. stringify: n => unicoder.codesets.dice[n],
  11. total: (ns, bonus) => ns.reduce((a, b) => a + b, bonus).toString()
  12. }
  13. const coin = {
  14. 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') : ''}`,
  15. total: ns => {
  16. const heads = ns.filter(x => x === 1).length
  17. const tails = ns.filter(x => x === 2).length
  18. if (heads === 1 && tails === 0) return 'Heads'
  19. if (heads === 0 && tails === 1) return 'Tails'
  20. if (heads === 0 && tails > 0) return `${tails} Tails and no Heads`
  21. if (heads > 0 && tails === 0) return `${heads} Heads and no Tails`
  22. if (heads > 0 && tails > 0) return `${heads} Heads and ${tails} Tails`
  23. return 'The coin disappears'
  24. }
  25. }
  26. const d10 = {
  27. stringify: (n, i, total, { percent = false } = {}) => {
  28. if (total === 2 && percent) {
  29. if (i === 0) {
  30. return n === 10 ? '00' : `${n}0`
  31. } else {
  32. return n === 10 ? '0' : n.toString()
  33. }
  34. } else {
  35. return unicoder.codesets.negRound[n === 10 ? '0' : n.toString()]
  36. }
  37. },
  38. total: (ns, bonus, { percent = false } = {}) => {
  39. if (ns.length === 2 & percent) {
  40. if (ns[0] === 0 && ns[1] === 0) return '100'
  41. return ((ns[0] === 10 ? 0 : ns[0]) * 10 + (ns[1] % 10)).toString()
  42. }
  43. return ns.reduce((a, b) => a + b, bonus).toString()
  44. },
  45. bonusText: (x) => {
  46. const {percent = false, values = []} = x || {}
  47. if (values.length === 2 && !percent) return '_Did you mean "roll percentile"?_'
  48. }
  49. }
  50. const d20 = {
  51. stringify: n => unicoder.codesets.negRound[n],
  52. total: (ns, bonus) => ns.reduce((a, b) => a + b, bonus).toString()
  53. }
  54. const ascii = {
  55. stringify: n => n.toString(),
  56. total: (ns, bonus) => ns.reduce((a, b) => a + b, bonus).toString()
  57. }
  58. class Roll {
  59. constructor(roll) {
  60. if (roll) Object.assign(this, roll)
  61. }
  62. value() {
  63. return this.values.reduce((a, b) => a + b, this.bonus || 0)
  64. }
  65. toString() {
  66. let str = this.values.map((val, i) => this.formatter && this.formatter.stringify ? this.formatter.stringify(val, i, this.values.length, this) : `[${val}]`).join(' + ')
  67. if (this.bonus < 0) {
  68. str += ` - ${Math.abs(this.bonus)}`
  69. } else if (this.bonus > 0) {
  70. str += ` + ${this.bonus}`
  71. }
  72. if (this.values.length > 1 || this.bonus) {
  73. str += ` = ${this.formatter && this.formatter.total ? this.formatter.total(this.values, this.bonus || 0, this) : this.value()}`
  74. }
  75. if (this.rerolls === 1) {
  76. str += ` with 1 reroll`
  77. } else if (this.rerolls > 1) {
  78. str += ` with ${this.rerolls} rerolls`
  79. }
  80. return str
  81. }
  82. }
  83. class BadRoll extends Roll {
  84. constructor(text) {
  85. super({text})
  86. }
  87. toString() {
  88. return this.text
  89. }
  90. }
  91. const random = (rng, n) => Math.floor(rng() * n) + 1
  92. class Dice {
  93. constructor(dice) {
  94. if (dice) Object.assign(this, dice)
  95. if (!this.formatter) {
  96. if (this.sides === 2) this.formatter = coin
  97. else if (this.sides === 6) this.formatter = d6
  98. else if (this.sides === 10) this.formatter = d10
  99. else if (this.sides <= 20) this.formatter = d20
  100. else this.formatter = ascii
  101. }
  102. if (!this.rng) this.rng = () => Math.random()
  103. }
  104. roll() {
  105. if (this.count > 100) return new BadRoll(`I lost count rolling ${this}`)
  106. if (this.sides > 256) return new BadRoll(`The faces are too small to read on ${this}`)
  107. if (this.count === 0) return new BadRoll(`I finished before I started rolling ${this}`)
  108. if (this.sides === 0) return new BadRoll(`I lost my grip on ${this}`)
  109. if (this.sides === 1) return new BadRoll(`The mobius ${this} never stopped rolling`)
  110. if (this.bonusSign === '-' && this.bonus > this.sides) return new BadRoll(`It doesn't seem fair to roll ${this}`)
  111. let values = []
  112. for (let i = 0; i < this.count; i++) {
  113. if (this.modifier === 'advantage') {
  114. values.push(Math.max(random(this.rng, this.sides), random(this.rng, this.sides)))
  115. } else if (this.modifier === 'disadvantage') {
  116. values.push(Math.min(random(this.rng, this.sides), random(this.rng, this.sides)))
  117. } else {
  118. values.push(random(this.rng, this.sides))
  119. }
  120. }
  121. let bonus = 0
  122. switch (this.bonusSign) {
  123. case '-': bonus = -this.bonus; break
  124. case '+': bonus = this.bonus; break
  125. }
  126. let rerolls = 0
  127. for (let i = 0; i < 10; i++) {
  128. let foundAny = false
  129. for (let d = 0; d < values.length; d++) {
  130. const v = values[d]
  131. if (this.rerollNumbers && this.rerollNumbers.includes(v)) {
  132. values[d] = random(this.rng, this.sides)
  133. foundAny = true
  134. rerolls++
  135. }
  136. }
  137. if (!foundAny) break
  138. }
  139. return new Roll({
  140. values,
  141. bonus,
  142. rerolls,
  143. percent: this.percent,
  144. formatter: this.formatter
  145. })
  146. }
  147. toString() {
  148. let str =
  149. this.sides === 2
  150. ? (
  151. this.count === 1 ? 'a coin' : `${this.count} coins`
  152. )
  153. : (this.count === 2 && this.percent)
  154. ? 'percentile dice'
  155. :`${this.count}d${this.sides}`
  156. if (this.bonusSign) {
  157. str += this.bonusSign + this.bonus
  158. }
  159. if (this.modifier) {
  160. str += ` with ${this.modifier}`
  161. }
  162. if (this.rerollNumbers && this.rerollNumbers.length) {
  163. if (this.rerollNumbers.length === 1) {
  164. str += ` and reroll ${this.rerollNumbers[0]}'s`
  165. } else {
  166. str += ` and reroll ${this.rerollNumbers.slice(0, this.rerollNumbers.length - 1).join(`'s, `)}'s, and ${this.rerollNumbers[this.rerollNumbers.length - 1]}'s`
  167. }
  168. }
  169. return str
  170. }
  171. }
  172. Dice.parse = (str, opts) => {
  173. const rRegex = rollRegex()
  174. const roll = rRegex.exec(str)
  175. const rrRegex = rerollRegex()
  176. const rerolls = []
  177. const reroll = rrRegex.exec(str)
  178. const rerollNumbers = []
  179. if (reroll) {
  180. const nRegex = numberRegex()
  181. while (number = nRegex.exec(str)) {
  182. switch (number[1]) {
  183. case 'ones':
  184. case 'one':
  185. rerollNumbers.push(1)
  186. break
  187. case 'twos':
  188. case 'two':
  189. rerollNumbers.push(2)
  190. break
  191. case 'threes':
  192. case 'three':
  193. rerollNumbers.push(3)
  194. break
  195. case 'fours':
  196. case 'four':
  197. rerollNumbers.push(4)
  198. break
  199. case 'fives':
  200. case 'five':
  201. rerollNumbers.push(5)
  202. break
  203. case 'sixes':
  204. case 'six':
  205. rerollNumbers.push(6)
  206. break
  207. case 'sevens':
  208. case 'seven':
  209. rerollNumbers.push(7)
  210. break
  211. case 'eights':
  212. case 'eight':
  213. rerollNumbers.push(8)
  214. break
  215. case 'nines':
  216. case 'nine':
  217. rerollNumbers.push(9)
  218. break
  219. case 'tens':
  220. case 'ten':
  221. rerollNumbers.push(10)
  222. break
  223. default:
  224. const n = parseFloat(number)
  225. if (!isNaN(n)) {
  226. rerollNumbers.push(n)
  227. }
  228. }
  229. }
  230. }
  231. const aRegex = advRegex()
  232. const ad = aRegex.exec(str)
  233. const dicebot = dicebotRegex().test(str)
  234. const modifier = ad ? (
  235. ad[1].toLowerCase()[0] === 'a' ? 'advantage' : 'disadvantage'
  236. ) : null
  237. const percent = percentRegex().test(str)
  238. const regex = diceRegex()
  239. const ret = []
  240. let dice
  241. if (percent) {
  242. ret.push(new Dice({
  243. enabled: !!roll,
  244. count: 2,
  245. sides: 10,
  246. percent: true,
  247. rng: opts && opts.rng
  248. }))
  249. }
  250. while (dice = regex.exec(str)) {
  251. ret.push(new Dice({
  252. enabled: !!roll,
  253. match: dice,
  254. count: dice[1] === '' ? 1 : parseFloat(dice[1]),
  255. sides: parseFloat(dice[2]),
  256. bonusSign: dice[3] || null,
  257. bonus: dice[4] === undefined ? null : parseFloat(dice[4]),
  258. rerollNumbers,
  259. modifier,
  260. percent,
  261. dicebot,
  262. rng: opts && opts.rng
  263. }))
  264. }
  265. return ret
  266. }
  267. Dice.chat = (chat, opts) => {
  268. const dice = Dice.parse(chat, opts).filter(x => x.enabled)
  269. if (dice.length) {
  270. const diceStrings = dice.map(x => x.toString())
  271. if (diceStrings.length > 1) {
  272. diceStrings[diceStrings.length - 1] = `and ${diceStrings[diceStrings.length - 1]}`
  273. }
  274. const isCoin = dice.length === 1 && dice[0].sides === 2
  275. const rollingString = `${isCoin ? 'Flipping' : 'Rolling'} ${diceStrings.join(', ')}...`
  276. const rolls = dice.map(die => die.roll())
  277. const rollsStrings = rolls.map(roll => roll.toString())
  278. const bonusString = rolls
  279. .filter(x => x.formatter && x.formatter.bonusText)
  280. .map(x => x.formatter.bonusText(x))
  281. .filter(x => x)
  282. .map(x => '\n' + x)
  283. .join('')
  284. return `${rollingString} ${rollsStrings.join(', ')}.${bonusString}`
  285. }
  286. }
  287. module.exports = {
  288. Dice,
  289. Roll,
  290. diceRegex,
  291. rerollRegex,
  292. advRegex
  293. }