const util = require('util'); function countOccurrencesOfNumber(inputArray: Array, checkNumber: number): number { /* return inputArray.reduce(function(n: number, val: number) { return n + (val === checkNumber); }, 0); */ return inputArray.filter(c => c === checkNumber).length; } 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; olddice?: Array; reroll?: boolean; outcome?: Outcomes; balance?: number; } class D10 implements Dice { sides: number = 10; type: string = "d10" constructor() { // this.sides = 10; } [util.inspect.custom](): string { return this.type; } 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 rerollHighDice (rerollDice: Array): Array { // 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 = 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 } rerollLowDice (rerollDice: Array): Array { // 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 = 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 } allAboveCutOff (checkDice: Array): 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)); } allBelowCutOff (checkDice: Array): 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)); } countOutcomeBalance (resultDice: Array): 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 return resultDice.reduce( (acc: number, curr: number) => { return (curr < this.successCutOff) ? acc - 1 : acc + 1 }, 0); } 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)); }