|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- const util = require('util');
-
- /**
- * Counts the occurrences of a number in an array of numbers
- *
- * @param inputArray - array of numbers
- * @param checkNumber - number to count occurrences of
- * @returns number of occurrences
- *
- */
- function countOccurrencesOfNumber(inputArray: Array<number>, checkNumber: number): number {
- return inputArray.filter(c => c === checkNumber).length;
- }
-
- /**
- * Generate a random integer between *1* and `max` (inclusive)
- *
- * @param max - upper bound of random integer to generate
- * @returns random integer
- */
- function randIntMinOne(max: number): number {
- return (1 + Math.floor(Math.random() * Math.floor(max)));
- }
-
- interface Dice {
- readonly sides: number;
- readonly type: string;
- }
-
- enum Outcomes {
- Success = "Success",
- Fail = "Failure",
- CritSuccess = "Critical Success",
- CritFail = "Critical Failure",
- Other = "Something Else"
- }
-
- interface DiceResult {
- total: number;
- dice: Array<number>;
- olddice?: Array<number>;
- reroll?: boolean;
- outcome?: Outcomes;
- balance?: number;
-
- }
- /**
- * Simple d10, implements `roll()`
- */
- class D10 implements Dice {
- sides: number = 10;
- type: string = "d10"
-
- constructor() {
- }
-
- [util.inspect.custom](): string {
- return this.type;
- }
-
- /**
- * Roll a specified number of dice
- *
- * @param numberToRoll - integer number of dice to roll
- * @returns DiceResult: .total and .dice (individual dice)
- *
- * @example Roll 4d10
- *
- * ```
- * D10.roll(4)
- * { total: 16, dice [ 3, 5, 6, 2] }
- *
- */
- roll(numberToRoll: number): DiceResult {
- let results: DiceResult = { total: 0, dice: [] };
- for (let i = 0; i < numberToRoll; i++) {
- results.dice.push(randIntMinOne(this.sides));
- }
-
- results.total = results.dice.reduce((acc: number, curr: number) => acc + curr);
-
- return results;
- }
- }
-
- class Asphodice extends D10 {
- readonly type: string = "asphodice";
- readonly passCrit: number = 10;
- readonly failCrit: number = 1;
- readonly successCutOff: number = 6; // this or larger
-
- /**
- * Re-roll the high dice (ie >= 6) for a chance of failure
- * happens on eg 1
- *
- * @remarks
- * Basically we want to remove a die from the original result,
- * as long as that die isn't a 10, and is above the
- * cutoff. So we filter to get rerollCandidates, find the
- * highest value and get the index of that value in the
- * original results. We use that to splice() out (remove) that
- * value and push the rerolled dice onto the modified array
- *
- * @param rerollDice
- * @returns rerollOutcome
- *
- * @example Re-roll High Dice on 1
- * ```
- * rerollHighDice([8, 1, 1, 4, 7, 10]);
- * [ 1, 1, 4, 7, 10, 6]
- * ```
- *
- * ## Explanation
- *
- * - first filter dice to be not 10 and >= 6 (see {@link Asphodice.successCutOff})
- * - rerollCandidates are [8, 7]
- * - max([8, 7] = 8
- * - indexOf(8) = 0
- * - rerollResult = 6 (see {@link randIntMinOne()})
- * - `splice()` replaced dice out of array = [ 1, 1, 4, 7, 10 ]
- * - `push()` new dice = [ 1, 1, 4, 7, 10, 6]
- */
- rerollHighDice (rerollDice: Array<number>): Array<number> {
- let rerollCandidates: Array<number> = rerollDice.filter(die => (die < 10 && die >= this.successCutOff));
- let maxValue: number = Math.max(...rerollCandidates);
- let maxIndex = rerollDice.indexOf(maxValue);
- let rerollResult: number = randIntMinOne(this.sides);
-
- rerollDice.splice(maxIndex, 1);
- rerollDice.push(rerollResult);
- return rerollDice
- }
-
- /**
- * Re-roll low dice (ie < 6) for chance of success (ie getting >= 6)
- *
- * @remarks
- *
- * Generally happens on a 10.
- *
- * @see {@link Asphodice.rerollHighDice} for a fuller explanation
- * for what process in opposite direction
- */
- rerollLowDice (rerollDice: Array<number>): Array<number> {
- let rerollCandidates: Array<number> = rerollDice.filter(die => (die > 1 && die < this.successCutOff));
- let minValue: number = Math.min(...rerollCandidates);
- let minIndex = rerollDice.indexOf(minValue);
- let rerollResult: number = randIntMinOne(this.sides);
-
- rerollDice.splice(minIndex, 1);
- rerollDice.push(rerollResult);
- return rerollDice
- }
-
- /**
- * Used to determine if all dice are above Asphodice.successCutOff
- *
- * @param checkDice - Array of Dice to test
- *
- * @see {@link Asphodice.roll} where it is used to determine if
- * a re-roll can be skipped
- *
- * @see {@link allBelowCutOff} also.
- */
- allAboveCutOff (checkDice: Array<number>): boolean {
- // if filtering those *below* cutoff results in empty set
- // then all must be above cutoff
- return ((checkDice.filter(die => die < this.successCutOff).length == 0));
- }
-
- /**
- * Used to determine if all dice are below Asphodice.successCutOff
- *
- * @param checkDice - Array of Dice to test
- *
- * @see {@link Asphodice.roll} where it is used to determine if
- * a re-roll can be skipped
- *
- * @see {@link allAboveCutOff} also.
- */
- allBelowCutOff (checkDice: Array<number>): boolean {
- // if filtering those *above or equal to* cutoff results in empty set
- // then all must be below cutoff
- return ((checkDice.filter(die => die >= this.successCutOff).length == 0));
- }
-
- /**
- * Determine balance of outcomes - ie successes minus failures
- *
- * @remarks
- *
- * Success = 1, failure = -1
- *
- * Particularly for Asphodice, we aren't terribly concerned with the
- * numeric sum total of the dice values, we are more concerned with
- * the overall outcome (see {@link Outcomes}) and for narrative purposes
- * perhaps the balance of outcomes; for example, 1 success may be
- * narrated differently than 4 successes.
- *
- * **Special note**: This ignores crits! **End special note**
- *
- * TODO: Special consideration of a single die roll (?)
- *
- * @param resultDice - the final dice after any re-rolls
- * @returns Single positive/negative number with balance of roll outcomes
- *
- * @example Count Outcomes of 5 Dice
- * ```
- * countOutcomeBalance([ 7, 4, 4, 9, 1 ])
- * -1
- * ```
- *
- * - 7 and 9 are above successCutOff → +2
- * - 4, 4, 1 are below successCutOff → -3
- */
- countOutcomeBalance (resultDice: Array<number>): number{
- // fun with ternary operators
- return resultDice.reduce(
- (acc: number, curr: number) =>
- { return (curr < this.successCutOff) ? acc - 1 : acc + 1 },
- 0);
- // PS also good practice to supply initialValue (0)
- }
-
- /**
- * Roll an Asphodie or Asphodice
- *
- * @param numToRoll
- * @returns a {@link DiceResult} with additional properties:
- * - reroll: boolean - whether we rerolled
- * - olddice: Array - original roll
- * - dice: Array - final outcome
- * - balance: number - +/- of outcomes (successes/failures)
- */
- roll (numToRoll: number): DiceResult {
- let results: DiceResult = { total: 0, dice: [] };
-
- // Initial roll
- for (let i = 0; i < numToRoll; i++) {
- results.dice.push(randIntMinOne(this.sides));
- }
-
- // Check for re-rolls
- // 1. if no re-rolls we can finish here
- if (!(results.dice.includes(this.passCrit) || results.dice.includes(this.failCrit))) {
- // results.total = results.dice.reduce((acc: number, curr: number) => acc + curr);
- results.reroll = false;
- } else {
- // count successes and fails
- let rerollGood = countOccurrencesOfNumber(results.dice, this.passCrit);
- let rerollBad = countOccurrencesOfNumber(results.dice, this.failCrit);
-
- // 2. only reroll if they don't cancel each other out
- if (rerollGood == rerollBad) {
- // console.log("Good = Bad, no need to reroll");
- results.reroll = false;
- // results.total = results.dice.reduce((acc: number, curr: number) => acc + curr);
- }
- // If all dice are above/below cutoff we don't need to reroll
- // 3a. Above
- else if (this.allAboveCutOff(results.dice)) {
- console.log("All above cutoff, auto-success", results.dice);
- results.reroll = false;
- }
- // 3b. Below
- else if (this.allBelowCutOff(results.dice)) {
- console.log("All below cutoff, auto-fail", results.dice);
- results.reroll = false;
- }
- // Reroll
- else {
- // Reminder: arr1 = arr2 is a copy by reference!
- let olddice = results.dice.slice();
- if (rerollGood > rerollBad) {
- // Re-roll low (<6) dice for chance of success
- results.dice = this.rerollLowDice(results.dice);
- } else {
- // Re-roll high (>=6) dice for chance of failure
- results.dice = this.rerollHighDice(results.dice);
- }
- results.olddice = olddice;
- results.reroll = true;
- }
- }
-
- results.balance = this.countOutcomeBalance(results.dice);
- results.total = results.dice.reduce((acc: number, curr: number) => acc + curr);
- return results;
- }
- }
-
- let asphodice: Asphodice = new Asphodice();
- let number: number = 4;
- for (let i = 0; i < 10; i++) {
- console.log("Rolling", number, asphodice);
- console.log(asphodice.roll(4));
- }
|