dice.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. const diceRegex = () => /\b(\d*)[dD](\d+)\b(?:\s?([+-])\s?(\d+))?/g
  2. const rerollRegex = () => /\b(reroll|re-roll)\s+((?:(?:\S*|\d+s|\d+'s|\d+)\s*)+)/
  3. 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
  4. const advRegex = () => /(disadvantage|advantage)/i
  5. class Roll {
  6. constructor(roll) {
  7. if (roll) Object.assign(this, roll)
  8. }
  9. value() {
  10. return this.values.reduce((a, b) => a + b, this.bonus || 0)
  11. }
  12. toString() {
  13. let str = this.values.map(val => `[${val}]`).join(' + ')
  14. if (this.bonus < 0) {
  15. str += ` - ${Math.abs(this.bonus)}`
  16. } else if (this.bonus > 0) {
  17. str += ` + ${this.bonus}`
  18. }
  19. if (this.values.length > 1 || this.bonus) {
  20. str += ` = ${this.value()}`
  21. }
  22. if (this.rerolls === 1) {
  23. str += ` with 1 reroll`
  24. } else if (this.rerolls > 1) {
  25. str += ` with ${this.rerolls} rerolls`
  26. }
  27. return str
  28. }
  29. }
  30. class BadRoll extends Roll {
  31. constructor(text) {
  32. super({text})
  33. }
  34. toString() {
  35. return this.text
  36. }
  37. }
  38. const random = n => Math.floor(Math.random() * n) + 1
  39. class Dice {
  40. constructor(dice) {
  41. if (dice) Object.assign(this, dice)
  42. }
  43. roll() {
  44. if (this.count > 100) return new BadRoll(`I lost count rolling ${this}`)
  45. if (this.sides > 256) return new BadRoll(`The faces are too small to read on ${this}`)
  46. if (this.count === 0) return new BadRoll(`I finished before I started rolling ${this}`)
  47. if (this.sides === 0) return new BadRoll(`I lost my grip on ${this}`)
  48. if (this.sides === 1) return new BadRoll(`The mobius ${this} never stopped rolling`)
  49. if (this.bonusSign === '-' && this.bonus > this.sides) return new BadRoll(`It doesn't seem fair to roll ${this}`)
  50. let values = []
  51. for (let i = 0; i < this.count; i++) {
  52. if (this.modifier === 'advantage') {
  53. values.push(Math.max(random(this.sides), random(this.sides)))
  54. } else if (this.modifier === 'disadvantage') {
  55. values.push(Math.min(random(this.sides), random(this.sides)))
  56. } else {
  57. values.push(random(this.sides))
  58. }
  59. }
  60. let bonus = 0
  61. switch (this.bonusSign) {
  62. case '-': bonus = -this.bonus; break
  63. case '+': bonus = this.bonus; break
  64. }
  65. let rerolls = 0
  66. for (let i = 0; i < 10; i++) {
  67. let foundAny = false
  68. for (let d = 0; d < values.length; d++) {
  69. const v = values[d]
  70. if (this.rerollNumbers && this.rerollNumbers.includes(v)) {
  71. values[d] = random(this.sides)
  72. foundAny = true
  73. rerolls++
  74. }
  75. }
  76. if (!foundAny) break
  77. }
  78. return new Roll({
  79. values,
  80. bonus,
  81. rerolls
  82. })
  83. }
  84. toString() {
  85. let str = `${this.count}d${this.sides}`
  86. if (this.bonusSign) {
  87. str += this.bonusSign + this.bonus
  88. }
  89. if (this.modifier) {
  90. str += ` with ${this.modifier}`
  91. }
  92. if (this.rerollNumbers && this.rerollNumbers.length) {
  93. if (this.rerollNumbers.length === 1) {
  94. str += ` and reroll ${this.rerollNumbers[0]}'s`
  95. } else {
  96. str += ` and reroll ${this.rerollNumbers.slice(0, this.rerollNumbers.length - 1).join(`'s, `)}'s, and ${this.rerollNumbers[this.rerollNumbers.length - 1]}'s`
  97. }
  98. }
  99. return str
  100. }
  101. }
  102. Dice.parse = str => {
  103. const rrRegex = rerollRegex()
  104. const rerolls = []
  105. const reroll = rrRegex.exec(str)
  106. const rerollNumbers = []
  107. if (reroll) {
  108. const nRegex = numberRegex()
  109. while (number = nRegex.exec(str)) {
  110. switch (number[1]) {
  111. case 'ones':
  112. case 'one':
  113. rerollNumbers.push(1)
  114. break
  115. case 'twos':
  116. case 'two':
  117. rerollNumbers.push(2)
  118. break
  119. case 'threes':
  120. case 'three':
  121. rerollNumbers.push(3)
  122. break
  123. case 'fours':
  124. case 'four':
  125. rerollNumbers.push(4)
  126. break
  127. case 'fives':
  128. case 'five':
  129. rerollNumbers.push(5)
  130. break
  131. case 'sixes':
  132. case 'six':
  133. rerollNumbers.push(6)
  134. break
  135. case 'sevens':
  136. case 'seven':
  137. rerollNumbers.push(7)
  138. break
  139. case 'eights':
  140. case 'eight':
  141. rerollNumbers.push(8)
  142. break
  143. case 'nines':
  144. case 'nine':
  145. rerollNumbers.push(9)
  146. break
  147. case 'tens':
  148. case 'ten':
  149. rerollNumbers.push(10)
  150. break
  151. default:
  152. const n = parseFloat(number)
  153. if (!isNaN(n)) {
  154. rerollNumbers.push(n)
  155. }
  156. }
  157. }
  158. }
  159. const aRegex = advRegex()
  160. const ad = aRegex.exec(str)
  161. const modifier = ad ? (
  162. ad[1].toLowerCase()[0] === 'a' ? 'advantage' : 'disadvantage'
  163. ) : null
  164. const regex = diceRegex()
  165. const ret = []
  166. let dice
  167. while (dice = regex.exec(str)) {
  168. ret.push(new Dice({
  169. match: dice,
  170. count: dice[1] === '' ? 1 : parseFloat(dice[1]),
  171. sides: parseFloat(dice[2]),
  172. bonusSign: dice[3] || null,
  173. bonus: dice[4] === undefined ? null : parseFloat(dice[4]),
  174. rerollNumbers,
  175. modifier
  176. }))
  177. }
  178. return ret
  179. }
  180. Dice.chat = chat => {
  181. const dice = Dice.parse(chat)
  182. if (dice.length) {
  183. const diceStrings = dice.map(x => x.toString())
  184. if (diceStrings.length > 1) {
  185. diceStrings[diceStrings.length - 1] = `and ${diceStrings[diceStrings.length - 1]}`
  186. }
  187. const rollingString = `Rolling ${diceStrings.join(', ')}...`
  188. const rollsStrings = dice.map(die => die.roll().toString())
  189. return `${rollingString} ${rollsStrings.join(', ')}.`
  190. }
  191. }
  192. module.exports = {
  193. Dice,
  194. Roll,
  195. diceRegex,
  196. rerollRegex,
  197. advRegex
  198. }