Browse Source

Move Asphodice class (etc) to own module

tags/v0.1.3
bertieb 3 years ago
parent
commit
c8e6b6bcf5
3 changed files with 382 additions and 380 deletions
  1. +379
    -0
      asphodice.ts
  2. +1
    -378
      dice.ts
  3. +2
    -2
      test/test-dice.spec.ts

+ 379
- 0
asphodice.ts View File

@@ -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
- 378
dice.ts View File

@@ -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 number: number = 4;


+ 2
- 2
test/test-dice.spec.ts View File

@@ -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 'mocha';



Loading…
Cancel
Save