|
- 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;
- }
-
- export 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;
-
- }
-
- interface ItemCount {
- item: string | number | Outcomes;
- count: number;
- }
-
- declare let ItemCountSet: Array<ItemCount>;
-
-
- /**
- * Simple d10, implements `roll()`
- */
- class D10 implements Dice {
- sides: number = 10;
- type: string = "d10"
-
- 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;
- }
-
- [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] }
- *
- */
- }
-
- export 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)
- }
-
- /**
- * Determine and return outcome of roll
- *
- * @remarks
- *
- * Determines outcome as in "Success", "Failure", etc. Assumes that `resultDice` is
- * the 'final' result after any rerolls.
- *
- * @see {@link:countOutcomeBalance}
- */
- checkOutcome (resultDice: Array<number>): Outcomes{
- // Note: currently, one success = Success, regardless of number of failures
- // TODO: Critical failures, once decided with Mao
- if (this.allBelowCutOff(resultDice)) {
- return Outcomes.Fail;
- } else {
- return Outcomes.Success;
- }
- }
-
- /**
- * 'Cancel out' reroll dice
- *
- * @remarks
- *
- * Helper function.
- *
- * **In the context of deciding a reroll only**, the reroll dice can
- * 'cancel' each other out. The dice themselves remain intact for the
- * purposes of determining the final outcome.
- *
- * @returns Filtered dice
- */
- cancelRerollDice (resultDice: Array<number>): Array<number> {
- // Naive approach for now:
- // - count 10s and 1s
- // - remove all 10s and 1s from input array
- // - re-add 10s/1s after 'cancelling'
- let tens = 0;
- let ones = 0;
-
- // count
- for (let die of resultDice) {
- if (die === 10) {
- tens++;
- } else if (die === 1) {
- ones++;
- }
- }
-
- // remove any reroll dice
- let outputDice = resultDice.filter( die => (die != 1 && die != 10) );
-
- let balance = tens - ones;
-
- if (balance < 0) {
- // add balance of ones
- outputDice.push(...Array(Math.abs(balance)).fill(1));
- } else if (balance > 0) {
- // add balance of tens
- outputDice.push(...Array(Math.abs(balance)).fill(10));
- }
-
- return outputDice;
- }
-
- /**
- * Determines if a reroll is needed
- *
- * @remarks
- *
- * In some cases we don't reroll:
- * - reroll dice cancel each other out
- * - all are above cutoff (=> auto-success -- NB TODO crits)
- * - all below cutoff
- */
- rerollNeeded(resultDice: Array<number>): boolean{
- // 'Cancel out' matching re-roll dice
- let cancelledDice = this.cancelRerollDice(resultDice);
-
- // 1. if no re-rolls we can finish here
- if (!(cancelledDice.includes(this.passCrit) || resultDice.includes(this.failCrit))) {
- return false;
- }
-
- // count successes and fails
- let rerollGood = countOccurrencesOfNumber(cancelledDice, this.passCrit);
- let rerollBad = countOccurrencesOfNumber(cancelledDice, this.failCrit);
-
- // 2. only reroll if they don't cancel each other out
- if (rerollGood == rerollBad) {
- return false;
- }
-
- // 3. If all dice are above/below cutoff we don't need to reroll
- if (this.allAboveCutOff(cancelledDice) || this.allBelowCutOff(cancelledDice)) {
- return false;
- }
-
- // We should re-roll
- return true;
-
- }
-
- /**
- * 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));
- }
- // Reroll?
- if (this.rerollNeeded(results.dice)) {
- // Reminder: arr1 = arr2 is a copy by reference!
- let olddice = results.dice.slice();
- let rerollGood = countOccurrencesOfNumber(olddice, this.passCrit);
- let rerollBad = countOccurrencesOfNumber(olddice, this.failCrit);
- 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);
-
- // Finally, once we're done with rerolls etc, determine outcome
- results.outcome = this.checkOutcome(results.dice);
-
- return results;
- }
- }
|