@@ -0,0 +1,379 @@ | |||||
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; | |||||
} | |||||
} | |||||
@@ -1,381 +1,4 @@ | |||||
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; | |||||
} | |||||
} | |||||
import { Asphodice } from "./asphodice"; | |||||
let asphodice: Asphodice = new Asphodice(); | let asphodice: Asphodice = new Asphodice(); | ||||
let number: number = 4; | let number: number = 4; | ||||
@@ -1,5 +1,5 @@ | |||||
import { Asphodice } from "../dice"; | |||||
import { Outcomes } from "../dice"; | |||||
import { Asphodice } from "../asphodice"; | |||||
import { Outcomes } from "../asphodice"; | |||||
import { expect } from 'chai'; | import { expect } from 'chai'; | ||||
import 'mocha'; | import 'mocha'; | ||||