Roll dice (eg Asphodice) and show outcomes https://rpg.bertieb.org/dice-roller/
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dice.ts 6.0 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. const util = require('util');
  2. function countOccurrencesOfNumber(inputArray: Array<number>, checkNumber: number): number {
  3. /*
  4. return inputArray.reduce(function(n: number, val: number) {
  5. return n + (val === checkNumber);
  6. }, 0);
  7. */
  8. return inputArray.filter(c => c === checkNumber).length;
  9. }
  10. function randIntMinOne(max: number): number {
  11. return (1 + Math.floor(Math.random() * Math.floor(max)));
  12. }
  13. interface Dice {
  14. readonly sides: number;
  15. readonly type: string;
  16. }
  17. enum Outcomes {
  18. Success = "Success",
  19. Fail = "Failure",
  20. CritSuccess = "Critical Success",
  21. CritFail = "Critical Failure",
  22. Other = "Something Else"
  23. }
  24. interface DiceResult {
  25. total: number;
  26. dice: Array<number>;
  27. olddice?: Array<number>;
  28. reroll?: boolean;
  29. outcome?: Outcomes;
  30. balance?: number;
  31. }
  32. class D10 implements Dice {
  33. sides: number = 10;
  34. type: string = "d10"
  35. constructor() {
  36. // this.sides = 10;
  37. }
  38. [util.inspect.custom](): string {
  39. return this.type;
  40. }
  41. roll(numberToRoll: number): DiceResult {
  42. let results: DiceResult = { total: 0, dice: [] };
  43. for (let i = 0; i < numberToRoll; i++) {
  44. results.dice.push(randIntMinOne(this.sides));
  45. }
  46. results.total = results.dice.reduce((acc: number, curr: number) => acc + curr);
  47. return results;
  48. }
  49. }
  50. class Asphodice extends D10 {
  51. readonly type: string = "asphodice";
  52. readonly passCrit: number = 10;
  53. readonly failCrit: number = 1;
  54. readonly successCutOff: number = 6; // this or larger
  55. rerollHighDice (rerollDice: Array<number>): Array<number> {
  56. // Re-roll the high dice (ie >= 6) for a chance of failure
  57. // happens on eg 1
  58. //
  59. // Basically we want to remove a die from the original result,
  60. // as long as that die isn't a 10, and is above the
  61. // cutoff. So we filter to get rerollCandidates, find the
  62. // highest value and get the index of that value in the
  63. // original results. We use that to splice() out (remove) that
  64. // value and push the rerolled dice onto the modified array
  65. //
  66. // Example:
  67. // rerollDice: [ 8, 1, 1, 4, 7, 10] --> filter (not 10, >= 6)
  68. // rerollCandidates : [8, 7] --> maxValue: 8
  69. // maxIndex: 0 (in original array)
  70. // rerollResult: 6
  71. // rerollOutcome: [ 1, 1, 4, 7, 10, 6] --> return
  72. let rerollCandidates: Array<number> = rerollDice.filter(die => (die < 10 && die >= this.successCutOff));
  73. let maxValue: number = Math.max(...rerollCandidates);
  74. let maxIndex = rerollDice.indexOf(maxValue);
  75. let rerollResult: number = randIntMinOne(this.sides);
  76. rerollDice.splice(maxIndex, 1);
  77. rerollDice.push(rerollResult);
  78. return rerollDice
  79. }
  80. rerollLowDice (rerollDice: Array<number>): Array<number> {
  81. // Re-roll the low dice (ie < 6) for a chance of success
  82. // happens on eg 10
  83. //
  84. // see this.rerollHighDice() for a full explanation
  85. let rerollCandidates: Array<number> = rerollDice.filter(die => (die > 1 && die < this.successCutOff));
  86. let minValue: number = Math.min(...rerollCandidates);
  87. let minIndex = rerollDice.indexOf(minValue);
  88. let rerollResult: number = randIntMinOne(this.sides);
  89. rerollDice.splice(minIndex, 1);
  90. rerollDice.push(rerollResult);
  91. return rerollDice
  92. }
  93. allAboveCutOff (checkDice: Array<number>): boolean {
  94. // if filtering those *below* cutoff results in empty set
  95. // then all must be above cutoff
  96. return ((checkDice.filter(die => die < this.successCutOff).length == 0));
  97. }
  98. allBelowCutOff (checkDice: Array<number>): boolean {
  99. // if filtering those *above or equal to* cutoff results in empty set
  100. // then all must be below cutoff
  101. return ((checkDice.filter(die => die >= this.successCutOff).length == 0));
  102. }
  103. countOutcomeBalance (resultDice: Array<number>): number{
  104. // Return a positive / negative number representing
  105. // the overall outcomes: each failure (eg < 6) is -1
  106. // while each success (eg >= 6) is +1
  107. // fun with ternary operators
  108. return resultDice.reduce(
  109. (acc: number, curr: number) =>
  110. { return (curr < this.successCutOff) ? acc - 1 : acc + 1 },
  111. 0);
  112. }
  113. roll (numToRoll: number): DiceResult {
  114. let results: DiceResult = { total: 0, dice: [] };
  115. // Initial roll
  116. for (let i = 0; i < numToRoll; i++) {
  117. results.dice.push(randIntMinOne(this.sides));
  118. }
  119. // Check for re-rolls
  120. // 1. if no re-rolls we can finish here
  121. if (!(results.dice.includes(this.passCrit) || results.dice.includes(this.failCrit))) {
  122. // results.total = results.dice.reduce((acc: number, curr: number) => acc + curr);
  123. results.reroll = false;
  124. } else {
  125. // count successes and fails
  126. let rerollGood = countOccurrencesOfNumber(results.dice, this.passCrit);
  127. let rerollBad = countOccurrencesOfNumber(results.dice, this.failCrit);
  128. // 2. only reroll if they don't cancel each other out
  129. if (rerollGood == rerollBad) {
  130. // console.log("Good = Bad, no need to reroll");
  131. results.reroll = false;
  132. // results.total = results.dice.reduce((acc: number, curr: number) => acc + curr);
  133. }
  134. // If all dice are above/below cutoff we don't need to reroll
  135. // 3a. Above
  136. else if (this.allAboveCutOff(results.dice)) {
  137. console.log("All above cutoff, auto-success", results.dice);
  138. results.reroll = false;
  139. }
  140. // 3b. Below
  141. else if (this.allBelowCutOff(results.dice)) {
  142. console.log("All below cutoff, auto-fail", results.dice);
  143. results.reroll = false;
  144. }
  145. // Reroll
  146. else {
  147. // Reminder: arr1 = arr2 is a copy by reference!
  148. let olddice = results.dice.slice();
  149. if (rerollGood > rerollBad) {
  150. // Re-roll low (<6) dice for chance of success
  151. results.dice = this.rerollLowDice(results.dice);
  152. } else {
  153. // Re-roll high (>=6) dice for chance of failure
  154. results.dice = this.rerollHighDice(results.dice);
  155. }
  156. results.olddice = olddice;
  157. results.reroll = true;
  158. }
  159. }
  160. results.balance = this.countOutcomeBalance(results.dice);
  161. results.total = results.dice.reduce((acc: number, curr: number) => acc + curr);
  162. return results;
  163. }
  164. }
  165. let asphodice: Asphodice = new Asphodice();
  166. let number: number = 4;
  167. for (let i = 0; i < 10; i++) {
  168. console.log("Rolling", number, asphodice);
  169. console.log(asphodice.roll(4));
  170. }