|
@@ -1,14 +1,23 @@ |
|
|
const util = require('util'); |
|
|
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 { |
|
|
function countOccurrencesOfNumber(inputArray: Array<number>, checkNumber: number): number { |
|
|
/* |
|
|
|
|
|
return inputArray.reduce(function(n: number, val: number) { |
|
|
|
|
|
return n + (val === checkNumber); |
|
|
|
|
|
}, 0); |
|
|
|
|
|
*/ |
|
|
|
|
|
return inputArray.filter(c => c === checkNumber).length; |
|
|
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 { |
|
|
function randIntMinOne(max: number): number { |
|
|
return (1 + Math.floor(Math.random() * Math.floor(max))); |
|
|
return (1 + Math.floor(Math.random() * Math.floor(max))); |
|
|
} |
|
|
} |
|
@@ -35,19 +44,33 @@ interface DiceResult { |
|
|
balance?: number; |
|
|
balance?: number; |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Simple d10, implements `roll()` |
|
|
|
|
|
*/ |
|
|
class D10 implements Dice { |
|
|
class D10 implements Dice { |
|
|
sides: number = 10; |
|
|
sides: number = 10; |
|
|
type: string = "d10" |
|
|
type: string = "d10" |
|
|
|
|
|
|
|
|
constructor() { |
|
|
constructor() { |
|
|
// this.sides = 10; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
[util.inspect.custom](): string { |
|
|
[util.inspect.custom](): string { |
|
|
return this.type; |
|
|
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 { |
|
|
roll(numberToRoll: number): DiceResult { |
|
|
let results: DiceResult = { total: 0, dice: [] }; |
|
|
let results: DiceResult = { total: 0, dice: [] }; |
|
|
for (let i = 0; i < numberToRoll; i++) { |
|
|
for (let i = 0; i < numberToRoll; i++) { |
|
@@ -66,24 +89,38 @@ class Asphodice extends D10 { |
|
|
readonly failCrit: number = 1; |
|
|
readonly failCrit: number = 1; |
|
|
readonly successCutOff: number = 6; // this or larger |
|
|
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> { |
|
|
rerollHighDice (rerollDice: Array<number>): Array<number> { |
|
|
// Re-roll the high dice (ie >= 6) for a chance of failure |
|
|
|
|
|
// happens on eg 1 |
|
|
|
|
|
// |
|
|
|
|
|
// 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 |
|
|
|
|
|
// |
|
|
|
|
|
// Example: |
|
|
|
|
|
// rerollDice: [ 8, 1, 1, 4, 7, 10] --> filter (not 10, >= 6) |
|
|
|
|
|
// rerollCandidates : [8, 7] --> maxValue: 8 |
|
|
|
|
|
// maxIndex: 0 (in original array) |
|
|
|
|
|
// rerollResult: 6 |
|
|
|
|
|
// rerollOutcome: [ 1, 1, 4, 7, 10, 6] --> return |
|
|
|
|
|
|
|
|
|
|
|
let rerollCandidates: Array<number> = rerollDice.filter(die => (die < 10 && die >= this.successCutOff)); |
|
|
let rerollCandidates: Array<number> = rerollDice.filter(die => (die < 10 && die >= this.successCutOff)); |
|
|
let maxValue: number = Math.max(...rerollCandidates); |
|
|
let maxValue: number = Math.max(...rerollCandidates); |
|
|
let maxIndex = rerollDice.indexOf(maxValue); |
|
|
let maxIndex = rerollDice.indexOf(maxValue); |
|
@@ -94,12 +131,17 @@ class Asphodice extends D10 { |
|
|
return rerollDice |
|
|
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> { |
|
|
rerollLowDice (rerollDice: Array<number>): Array<number> { |
|
|
// Re-roll the low dice (ie < 6) for a chance of success |
|
|
|
|
|
// happens on eg 10 |
|
|
|
|
|
// |
|
|
|
|
|
// see this.rerollHighDice() for a full explanation |
|
|
|
|
|
|
|
|
|
|
|
let rerollCandidates: Array<number> = rerollDice.filter(die => (die > 1 && die < this.successCutOff)); |
|
|
let rerollCandidates: Array<number> = rerollDice.filter(die => (die > 1 && die < this.successCutOff)); |
|
|
let minValue: number = Math.min(...rerollCandidates); |
|
|
let minValue: number = Math.min(...rerollCandidates); |
|
|
let minIndex = rerollDice.indexOf(minValue); |
|
|
let minIndex = rerollDice.indexOf(minValue); |
|
@@ -110,30 +152,86 @@ class Asphodice extends D10 { |
|
|
return rerollDice |
|
|
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 { |
|
|
allAboveCutOff (checkDice: Array<number>): boolean { |
|
|
// if filtering those *below* cutoff results in empty set |
|
|
// if filtering those *below* cutoff results in empty set |
|
|
// then all must be above cutoff |
|
|
// then all must be above cutoff |
|
|
return ((checkDice.filter(die => die < this.successCutOff).length == 0)); |
|
|
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 { |
|
|
allBelowCutOff (checkDice: Array<number>): boolean { |
|
|
// if filtering those *above or equal to* cutoff results in empty set |
|
|
// if filtering those *above or equal to* cutoff results in empty set |
|
|
// then all must be below cutoff |
|
|
// then all must be below cutoff |
|
|
return ((checkDice.filter(die => die >= this.successCutOff).length == 0)); |
|
|
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{ |
|
|
countOutcomeBalance (resultDice: Array<number>): number{ |
|
|
// Return a positive / negative number representing |
|
|
|
|
|
// the overall outcomes: each failure (eg < 6) is -1 |
|
|
|
|
|
// while each success (eg >= 6) is +1 |
|
|
|
|
|
|
|
|
|
|
|
// fun with ternary operators |
|
|
// fun with ternary operators |
|
|
return resultDice.reduce( |
|
|
return resultDice.reduce( |
|
|
(acc: number, curr: number) => |
|
|
(acc: number, curr: number) => |
|
|
{ return (curr < this.successCutOff) ? acc - 1 : acc + 1 }, |
|
|
{ return (curr < this.successCutOff) ? acc - 1 : acc + 1 }, |
|
|
0); |
|
|
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 { |
|
|
roll (numToRoll: number): DiceResult { |
|
|
let results: DiceResult = { total: 0, dice: [] }; |
|
|
let results: DiceResult = { total: 0, dice: [] }; |
|
|
|
|
|
|
|
|