From 110a574b8ab375e08008ec4cb514fa18daae4196 Mon Sep 17 00:00:00 2001 From: bertieb Date: Thu, 4 Feb 2021 17:22:19 +0000 Subject: [PATCH] Initial version, rolls 4 asphodice with rerolls --- dice.ts | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 70 +++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 dice.ts create mode 100644 tsconfig.json diff --git a/dice.ts b/dice.ts new file mode 100644 index 0000000..df6b02a --- /dev/null +++ b/dice.ts @@ -0,0 +1,185 @@ +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; + +} + +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)); + } + + 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.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)); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..45e2b14 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,70 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + "types": [ "node" ], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +}