@platonic-dice/core 2.0.3 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -7
- package/dist/analyseModTest.d.ts +126 -0
- package/dist/analyseModTest.d.ts.map +1 -0
- package/dist/analyseModTest.js +181 -0
- package/dist/analyseTest.d.ts +101 -0
- package/dist/analyseTest.d.ts.map +1 -0
- package/dist/analyseTest.js +141 -0
- package/dist/entities/ModifiedTestConditions.d.ts +89 -0
- package/dist/entities/ModifiedTestConditions.d.ts.map +1 -0
- package/dist/entities/ModifiedTestConditions.js +265 -0
- package/dist/entities/index.d.ts +4 -1
- package/dist/entities/index.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/rollModTest.d.ts +72 -0
- package/dist/rollModTest.d.ts.map +1 -0
- package/dist/rollModTest.js +208 -0
- package/dist/rollTest.d.ts +21 -2
- package/dist/rollTest.d.ts.map +1 -1
- package/dist/rollTest.js +28 -6
- package/dist/utils/determineOutcome.d.ts.map +1 -1
- package/dist/utils/determineOutcome.js +26 -0
- package/dist/utils/index.d.ts +4 -1
- package/dist/utils/index.js +8 -0
- package/dist/utils/outcomeMapper.d.ts +29 -0
- package/dist/utils/outcomeMapper.d.ts.map +1 -0
- package/dist/utils/outcomeMapper.js +197 -0
- package/dist-types.d.ts +23 -1
- package/package.json +18 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Core JavaScript/TypeScript library providing dice-roll logic, modifiers, and test evaluation for tabletop RPGs.
|
|
4
4
|
|
|
5
|
-
This package contains the pure logic used by higher-level packages (for example `@platonic-dice/dice`). It exports rolling helpers, entities (die types, roll types, outcomes), and utility functions.
|
|
5
|
+
This package contains the pure logic used by higher-level packages (for example `@platonic-dice/dice`). It exports rolling helpers including `roll`, `rollMod`, `rollTest`, and `rollModTest` (combining modifiers with test evaluation), entities (die types, roll types, outcomes), and utility functions.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -17,17 +17,38 @@ npm install @platonic-dice/core
|
|
|
17
17
|
CommonJS:
|
|
18
18
|
|
|
19
19
|
```js
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const {
|
|
21
|
+
roll,
|
|
22
|
+
rollDice,
|
|
23
|
+
rollModTest,
|
|
24
|
+
DieType,
|
|
25
|
+
RollType,
|
|
26
|
+
} = require("@platonic-dice/core");
|
|
27
|
+
|
|
28
|
+
console.log(roll(DieType.D20));
|
|
29
|
+
console.log(rollDice(DieType.D6, { count: 3 }));
|
|
30
|
+
|
|
31
|
+
// New in 2.1.0: rollModTest combines modifiers with test evaluation
|
|
32
|
+
const result = rollModTest(DieType.D20, (n) => n + 5, {
|
|
33
|
+
testType: "skill",
|
|
34
|
+
target: 15,
|
|
35
|
+
});
|
|
36
|
+
console.log(
|
|
37
|
+
`Roll: ${result.base}, Modified: ${result.modified}, Outcome: ${result.outcome}`
|
|
38
|
+
);
|
|
24
39
|
```
|
|
25
40
|
|
|
26
41
|
ESM / TypeScript:
|
|
27
42
|
|
|
28
43
|
```ts
|
|
29
|
-
import { roll, DieType } from
|
|
30
|
-
console.log(
|
|
44
|
+
import { roll, rollModTest, DieType } from "@platonic-dice/core";
|
|
45
|
+
console.log(roll(DieType.D20));
|
|
46
|
+
|
|
47
|
+
// Combine modifiers with test evaluation
|
|
48
|
+
const result = rollModTest(DieType.D20, (n) => n + 5, {
|
|
49
|
+
testType: "at_least",
|
|
50
|
+
target: 15,
|
|
51
|
+
});
|
|
31
52
|
```
|
|
32
53
|
|
|
33
54
|
## Build & Test
|
|
@@ -48,6 +69,32 @@ cd packages/core
|
|
|
48
69
|
npm test
|
|
49
70
|
```
|
|
50
71
|
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
The `examples/` directory contains comprehensive examples for all major functions. Run them to see the library in action:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Run all core examples (roll, rollDice, rollMod, rollDiceMod, rollTest, rollModTest)
|
|
78
|
+
npm run examples
|
|
79
|
+
|
|
80
|
+
# Run all examples including advanced features and analysis functions
|
|
81
|
+
npm run examples:all
|
|
82
|
+
|
|
83
|
+
# Run individual example files
|
|
84
|
+
npm run examples:roll
|
|
85
|
+
npm run examples:rollDice
|
|
86
|
+
npm run examples:rollMod
|
|
87
|
+
npm run examples:rollDiceMod
|
|
88
|
+
npm run examples:rollTest
|
|
89
|
+
npm run examples:rollModTest
|
|
90
|
+
npm run examples:rollModTest:advanced
|
|
91
|
+
npm run examples:analyseTest
|
|
92
|
+
npm run examples:analyseModTest
|
|
93
|
+
npm run examples:entities
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Each example demonstrates practical usage patterns and outputs results to help you understand the API.
|
|
97
|
+
|
|
51
98
|
## Contributing
|
|
52
99
|
|
|
53
100
|
See the repository root `README.md` for contribution guidelines. Keep changes backwards-compatible where possible and include tests.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export type DieTypeValue = import("./entities/DieType").DieTypeValue;
|
|
2
|
+
export type OutcomeValue = import("./entities/Outcome").OutcomeValue;
|
|
3
|
+
export type RollModifierFunction = import("./entities/RollModifier").RollModifierFunction;
|
|
4
|
+
export type RollModifierInstance = import("./entities/RollModifier").RollModifierInstance;
|
|
5
|
+
export type TestTypeValue = import("./entities/TestType").TestTypeValue;
|
|
6
|
+
export type TestConditionsInstance = import("./entities/TestConditions").TestConditionsInstance;
|
|
7
|
+
export type ModifiedTestAnalysis = {
|
|
8
|
+
/**
|
|
9
|
+
* - Total number of possible die rolls
|
|
10
|
+
*/
|
|
11
|
+
totalPossibilities: number;
|
|
12
|
+
/**
|
|
13
|
+
* - Count of each outcome type
|
|
14
|
+
*/
|
|
15
|
+
outcomeCounts: {
|
|
16
|
+
[x: string]: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* - Probability (0-1) of each outcome
|
|
20
|
+
*/
|
|
21
|
+
outcomeProbabilities: {
|
|
22
|
+
[x: string]: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* - Map of base roll to outcome
|
|
26
|
+
*/
|
|
27
|
+
outcomesByRoll: {
|
|
28
|
+
[x: number]: import("./entities").OutcomeValue;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* - Map of base roll to modified value
|
|
32
|
+
*/
|
|
33
|
+
modifiedValuesByRoll: {
|
|
34
|
+
[x: number]: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* - Array of all possible base roll values
|
|
38
|
+
*/
|
|
39
|
+
rolls: number[];
|
|
40
|
+
/**
|
|
41
|
+
* - Base rolls grouped by their outcome
|
|
42
|
+
*/
|
|
43
|
+
rollsByOutcome: any;
|
|
44
|
+
/**
|
|
45
|
+
* - Range of modified values achievable
|
|
46
|
+
*/
|
|
47
|
+
modifiedRange: {
|
|
48
|
+
min: number;
|
|
49
|
+
max: number;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export type analyseModTestOptions = {
|
|
53
|
+
/**
|
|
54
|
+
* - If true, natural max/min rolls trigger
|
|
55
|
+
* critical outcomes. Defaults to true for Skill tests, false otherwise.
|
|
56
|
+
*/
|
|
57
|
+
useNaturalCrits?: boolean | undefined;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
|
|
61
|
+
* @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
|
|
62
|
+
* @typedef {import("./entities/RollModifier").RollModifierFunction} RollModifierFunction
|
|
63
|
+
* @typedef {import("./entities/RollModifier").RollModifierInstance} RollModifierInstance
|
|
64
|
+
* @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
|
|
65
|
+
* @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* @typedef {Object} ModifiedTestAnalysis
|
|
69
|
+
* @property {number} totalPossibilities - Total number of possible die rolls
|
|
70
|
+
* @property {Object.<string, number>} outcomeCounts - Count of each outcome type
|
|
71
|
+
* @property {Object.<string, number>} outcomeProbabilities - Probability (0-1) of each outcome
|
|
72
|
+
* @property {Object.<number, OutcomeValue>} outcomesByRoll - Map of base roll to outcome
|
|
73
|
+
* @property {Object.<number, number>} modifiedValuesByRoll - Map of base roll to modified value
|
|
74
|
+
* @property {number[]} rolls - Array of all possible base roll values
|
|
75
|
+
* @property {Object.<OutcomeValue, number[]>} rollsByOutcome - Base rolls grouped by their outcome
|
|
76
|
+
* @property {{ min: number, max: number }} modifiedRange - Range of modified values achievable
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* @typedef {Object} analyseModTestOptions
|
|
80
|
+
* @property {boolean} [useNaturalCrits] - If true, natural max/min rolls trigger
|
|
81
|
+
* critical outcomes. Defaults to true for Skill tests, false otherwise.
|
|
82
|
+
*/
|
|
83
|
+
/**
|
|
84
|
+
* analyses modified test conditions without performing an actual roll.
|
|
85
|
+
*
|
|
86
|
+
* @function analyseModTest
|
|
87
|
+
* @param {DieTypeValue} dieType - The type of die (e.g., `DieType.D20`).
|
|
88
|
+
* @param {RollModifierFunction|RollModifierInstance} modifier - The modifier to apply to the roll.
|
|
89
|
+
* @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
|
|
90
|
+
* Can be:
|
|
91
|
+
* - A `TestConditions` instance
|
|
92
|
+
* - A plain object `{ testType, ...conditions }`
|
|
93
|
+
* @param {analyseModTestOptions} [options={}] - Optional configuration
|
|
94
|
+
* @returns {ModifiedTestAnalysis} Detailed analysis of the modified test outcomes
|
|
95
|
+
* @throws {TypeError} If `dieType`, `modifier`, or `testConditions` are invalid.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // analyse a D20+5 skill check with DC 20
|
|
99
|
+
* const analysis = analyseModTest(
|
|
100
|
+
* DieType.D20,
|
|
101
|
+
* (n) => n + 5,
|
|
102
|
+
* {
|
|
103
|
+
* testType: TestType.Skill,
|
|
104
|
+
* target: 20,
|
|
105
|
+
* critical_success: 25,
|
|
106
|
+
* critical_failure: 6
|
|
107
|
+
* }
|
|
108
|
+
* );
|
|
109
|
+
*
|
|
110
|
+
* console.log(`Modified range: ${analysis.modifiedRange.min}-${analysis.modifiedRange.max}`);
|
|
111
|
+
* console.log(`Success rate: ${(analysis.outcomeProbabilities.success * 100).toFixed(1)}%`);
|
|
112
|
+
* console.log(`Need to roll: ${analysis.rollsByOutcome.success}`);
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* // See how modifier affects outcomes
|
|
116
|
+
* const noMod = analyseTest(DieType.D20, { testType: TestType.AtLeast, target: 15 });
|
|
117
|
+
* const withMod = analyseModTest(DieType.D20, n => n + 5, { testType: TestType.AtLeast, target: 15 });
|
|
118
|
+
*
|
|
119
|
+
* console.log(`Without modifier: ${(noMod.outcomeProbabilities.success * 100).toFixed(1)}%`);
|
|
120
|
+
* console.log(`With +5 modifier: ${(withMod.outcomeProbabilities.success * 100).toFixed(1)}%`);
|
|
121
|
+
*/
|
|
122
|
+
export function analyseModTest(dieType: DieTypeValue, modifier: RollModifierFunction | RollModifierInstance, testConditions: TestConditionsInstance | {
|
|
123
|
+
testType: TestTypeValue;
|
|
124
|
+
[key: string]: any;
|
|
125
|
+
}, options?: analyseModTestOptions): ModifiedTestAnalysis;
|
|
126
|
+
//# sourceMappingURL=analyseModTest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyseModTest.d.ts","sourceRoot":"","sources":["../src/analyseModTest.js"],"names":[],"mappings":"2BAyBa,OAAO,oBAAoB,EAAE,YAAY;2BACzC,OAAO,oBAAoB,EAAE,YAAY;mCACzC,OAAO,yBAAyB,EAAE,oBAAoB;mCACtD,OAAO,yBAAyB,EAAE,oBAAoB;4BACtD,OAAO,qBAAqB,EAAE,aAAa;qCAC3C,OAAO,2BAA2B,EAAE,sBAAsB;;;;;wBAKzD,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAKN,MAAM,EAAE;;;;;;;;mBAER;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;;;;;;;;;AAlB1C;;;;;;;GAOG;AAEH;;;;;;;;;;GAUG;AAEH;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wCAnCW,YAAY,YACZ,oBAAoB,GAAC,oBAAoB,kBACzC,sBAAsB,GAAC;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,YAItE,qBAAqB,GACnB,oBAAoB,CAkHhC"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @platonic-dice/core/src/analyseModTest
|
|
3
|
+
* @description
|
|
4
|
+
* analyses modified test conditions without performing an actual roll.
|
|
5
|
+
* Provides probability information and possible outcomes for a given test configuration
|
|
6
|
+
* with modifiers applied.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { analyseModTest, DieType, TestType } from "@platonic-dice/core";
|
|
10
|
+
*
|
|
11
|
+
* const analysis = analyseModTest(
|
|
12
|
+
* DieType.D20,
|
|
13
|
+
* (n) => n + 5,
|
|
14
|
+
* { testType: TestType.AtLeast, target: 20 }
|
|
15
|
+
* );
|
|
16
|
+
*
|
|
17
|
+
* console.log(analysis.successRate); // Probability with +5 modifier
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { normaliseRollModifier } = require("./entities");
|
|
21
|
+
const { ModifiedTestConditions } = require("./entities/ModifiedTestConditions");
|
|
22
|
+
const { createOutcomeMap } = require("./utils/outcomeMapper");
|
|
23
|
+
const { numSides } = require("./utils");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
|
|
27
|
+
* @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
|
|
28
|
+
* @typedef {import("./entities/RollModifier").RollModifierFunction} RollModifierFunction
|
|
29
|
+
* @typedef {import("./entities/RollModifier").RollModifierInstance} RollModifierInstance
|
|
30
|
+
* @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
|
|
31
|
+
* @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} ModifiedTestAnalysis
|
|
36
|
+
* @property {number} totalPossibilities - Total number of possible die rolls
|
|
37
|
+
* @property {Object.<string, number>} outcomeCounts - Count of each outcome type
|
|
38
|
+
* @property {Object.<string, number>} outcomeProbabilities - Probability (0-1) of each outcome
|
|
39
|
+
* @property {Object.<number, OutcomeValue>} outcomesByRoll - Map of base roll to outcome
|
|
40
|
+
* @property {Object.<number, number>} modifiedValuesByRoll - Map of base roll to modified value
|
|
41
|
+
* @property {number[]} rolls - Array of all possible base roll values
|
|
42
|
+
* @property {Object.<OutcomeValue, number[]>} rollsByOutcome - Base rolls grouped by their outcome
|
|
43
|
+
* @property {{ min: number, max: number }} modifiedRange - Range of modified values achievable
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} analyseModTestOptions
|
|
48
|
+
* @property {boolean} [useNaturalCrits] - If true, natural max/min rolls trigger
|
|
49
|
+
* critical outcomes. Defaults to true for Skill tests, false otherwise.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* analyses modified test conditions without performing an actual roll.
|
|
54
|
+
*
|
|
55
|
+
* @function analyseModTest
|
|
56
|
+
* @param {DieTypeValue} dieType - The type of die (e.g., `DieType.D20`).
|
|
57
|
+
* @param {RollModifierFunction|RollModifierInstance} modifier - The modifier to apply to the roll.
|
|
58
|
+
* @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
|
|
59
|
+
* Can be:
|
|
60
|
+
* - A `TestConditions` instance
|
|
61
|
+
* - A plain object `{ testType, ...conditions }`
|
|
62
|
+
* @param {analyseModTestOptions} [options={}] - Optional configuration
|
|
63
|
+
* @returns {ModifiedTestAnalysis} Detailed analysis of the modified test outcomes
|
|
64
|
+
* @throws {TypeError} If `dieType`, `modifier`, or `testConditions` are invalid.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // analyse a D20+5 skill check with DC 20
|
|
68
|
+
* const analysis = analyseModTest(
|
|
69
|
+
* DieType.D20,
|
|
70
|
+
* (n) => n + 5,
|
|
71
|
+
* {
|
|
72
|
+
* testType: TestType.Skill,
|
|
73
|
+
* target: 20,
|
|
74
|
+
* critical_success: 25,
|
|
75
|
+
* critical_failure: 6
|
|
76
|
+
* }
|
|
77
|
+
* );
|
|
78
|
+
*
|
|
79
|
+
* console.log(`Modified range: ${analysis.modifiedRange.min}-${analysis.modifiedRange.max}`);
|
|
80
|
+
* console.log(`Success rate: ${(analysis.outcomeProbabilities.success * 100).toFixed(1)}%`);
|
|
81
|
+
* console.log(`Need to roll: ${analysis.rollsByOutcome.success}`);
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // See how modifier affects outcomes
|
|
85
|
+
* const noMod = analyseTest(DieType.D20, { testType: TestType.AtLeast, target: 15 });
|
|
86
|
+
* const withMod = analyseModTest(DieType.D20, n => n + 5, { testType: TestType.AtLeast, target: 15 });
|
|
87
|
+
*
|
|
88
|
+
* console.log(`Without modifier: ${(noMod.outcomeProbabilities.success * 100).toFixed(1)}%`);
|
|
89
|
+
* console.log(`With +5 modifier: ${(withMod.outcomeProbabilities.success * 100).toFixed(1)}%`);
|
|
90
|
+
*/
|
|
91
|
+
function analyseModTest(dieType, modifier, testConditions, options = {}) {
|
|
92
|
+
if (!dieType) throw new TypeError("dieType is required.");
|
|
93
|
+
if (!modifier) throw new TypeError("modifier is required.");
|
|
94
|
+
if (!testConditions) throw new TypeError("testConditions is required.");
|
|
95
|
+
|
|
96
|
+
// Normalise the modifier (skip if already a RollModifier instance)
|
|
97
|
+
const { RollModifier } = require("./entities");
|
|
98
|
+
const mod =
|
|
99
|
+
modifier instanceof RollModifier
|
|
100
|
+
? modifier
|
|
101
|
+
: normaliseRollModifier(modifier);
|
|
102
|
+
|
|
103
|
+
// Create ModifiedTestConditions if input is a plain object
|
|
104
|
+
let conditionSet;
|
|
105
|
+
if (testConditions instanceof ModifiedTestConditions) {
|
|
106
|
+
conditionSet = testConditions;
|
|
107
|
+
} else {
|
|
108
|
+
// Plain object: { testType, ...rest }
|
|
109
|
+
const { testType, ...rest } = testConditions;
|
|
110
|
+
// @ts-ignore - rest is validated by ModifiedTestConditions constructor
|
|
111
|
+
conditionSet = new ModifiedTestConditions(testType, rest, dieType, mod);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create outcome map for all possible rolls (with modifier applied)
|
|
115
|
+
const outcomeMap = createOutcomeMap(
|
|
116
|
+
dieType,
|
|
117
|
+
conditionSet.testType,
|
|
118
|
+
// @ts-ignore - ModifiedTestConditions is compatible with TestConditions for outcome mapping
|
|
119
|
+
conditionSet,
|
|
120
|
+
mod, // include modifier
|
|
121
|
+
options.useNaturalCrits
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const sides = numSides(dieType);
|
|
125
|
+
const totalPossibilities = sides;
|
|
126
|
+
|
|
127
|
+
// Calculate modified values for each roll
|
|
128
|
+
/** @type {Object.<number, number>} */
|
|
129
|
+
const modifiedValuesByRoll = {};
|
|
130
|
+
for (let roll = 1; roll <= sides; roll++) {
|
|
131
|
+
modifiedValuesByRoll[roll] = mod.apply(roll);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Determine modified range
|
|
135
|
+
const modifiedValues = Object.values(modifiedValuesByRoll);
|
|
136
|
+
const modifiedRange = {
|
|
137
|
+
min: Math.min(...modifiedValues),
|
|
138
|
+
max: Math.max(...modifiedValues),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Count outcomes
|
|
142
|
+
/** @type {Object.<string, number>} */
|
|
143
|
+
const outcomeCounts = {};
|
|
144
|
+
/** @type {Object.<string, number[]>} */
|
|
145
|
+
const rollsByOutcome = {};
|
|
146
|
+
|
|
147
|
+
for (let roll = 1; roll <= sides; roll++) {
|
|
148
|
+
const outcome = outcomeMap[roll];
|
|
149
|
+
|
|
150
|
+
// Count outcomes
|
|
151
|
+
outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
|
|
152
|
+
|
|
153
|
+
// Group rolls by outcome
|
|
154
|
+
if (!rollsByOutcome[outcome]) {
|
|
155
|
+
rollsByOutcome[outcome] = [];
|
|
156
|
+
}
|
|
157
|
+
rollsByOutcome[outcome].push(roll);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Calculate probabilities
|
|
161
|
+
/** @type {Object.<string, number>} */
|
|
162
|
+
const outcomeProbabilities = {};
|
|
163
|
+
for (const [outcome, count] of Object.entries(outcomeCounts)) {
|
|
164
|
+
outcomeProbabilities[outcome] = count / totalPossibilities;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
totalPossibilities,
|
|
169
|
+
outcomeCounts,
|
|
170
|
+
outcomeProbabilities,
|
|
171
|
+
outcomesByRoll: outcomeMap,
|
|
172
|
+
modifiedValuesByRoll,
|
|
173
|
+
rolls: Array.from({ length: sides }, (_, i) => i + 1),
|
|
174
|
+
rollsByOutcome,
|
|
175
|
+
modifiedRange,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
analyseModTest,
|
|
181
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type DieTypeValue = import("./entities/DieType").DieTypeValue;
|
|
2
|
+
export type OutcomeValue = import("./entities/Outcome").OutcomeValue;
|
|
3
|
+
export type TestTypeValue = import("./entities/TestType").TestTypeValue;
|
|
4
|
+
export type TestConditionsInstance = import("./entities/TestConditions").TestConditionsInstance;
|
|
5
|
+
export type TestAnalysis = {
|
|
6
|
+
/**
|
|
7
|
+
* - Total number of possible die rolls
|
|
8
|
+
*/
|
|
9
|
+
totalPossibilities: number;
|
|
10
|
+
/**
|
|
11
|
+
* - Count of each outcome type
|
|
12
|
+
*/
|
|
13
|
+
outcomeCounts: {
|
|
14
|
+
[x: string]: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* - Probability (0-1) of each outcome
|
|
18
|
+
*/
|
|
19
|
+
outcomeProbabilities: {
|
|
20
|
+
[x: string]: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* - Map of roll value to outcome
|
|
24
|
+
*/
|
|
25
|
+
outcomesByRoll: {
|
|
26
|
+
[x: number]: import("./entities").OutcomeValue;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* - Array of all possible roll values
|
|
30
|
+
*/
|
|
31
|
+
rolls: number[];
|
|
32
|
+
/**
|
|
33
|
+
* - Rolls grouped by their outcome
|
|
34
|
+
*/
|
|
35
|
+
rollsByOutcome: any;
|
|
36
|
+
};
|
|
37
|
+
export type analyseTestOptions = {
|
|
38
|
+
/**
|
|
39
|
+
* - If true, natural max/min rolls trigger
|
|
40
|
+
* critical outcomes. Defaults to true for Skill tests, false otherwise.
|
|
41
|
+
*/
|
|
42
|
+
useNaturalCrits?: boolean | undefined;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
|
|
46
|
+
* @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
|
|
47
|
+
* @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
|
|
48
|
+
* @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} TestAnalysis
|
|
52
|
+
* @property {number} totalPossibilities - Total number of possible die rolls
|
|
53
|
+
* @property {Object.<string, number>} outcomeCounts - Count of each outcome type
|
|
54
|
+
* @property {Object.<string, number>} outcomeProbabilities - Probability (0-1) of each outcome
|
|
55
|
+
* @property {Object.<number, OutcomeValue>} outcomesByRoll - Map of roll value to outcome
|
|
56
|
+
* @property {number[]} rolls - Array of all possible roll values
|
|
57
|
+
* @property {Object.<OutcomeValue, number[]>} rollsByOutcome - Rolls grouped by their outcome
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} analyseTestOptions
|
|
61
|
+
* @property {boolean} [useNaturalCrits] - If true, natural max/min rolls trigger
|
|
62
|
+
* critical outcomes. Defaults to true for Skill tests, false otherwise.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* analyses test conditions without performing an actual roll.
|
|
66
|
+
*
|
|
67
|
+
* @function analyseTest
|
|
68
|
+
* @param {DieTypeValue} dieType - The type of die (e.g., `DieType.D20`).
|
|
69
|
+
* @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
|
|
70
|
+
* Can be:
|
|
71
|
+
* - A `TestConditions` instance.
|
|
72
|
+
* - A plain object `{ testType, ...conditions }`.
|
|
73
|
+
* @param {analyseTestOptions} [options={}] - Optional configuration for analysis
|
|
74
|
+
* @returns {TestAnalysis} Detailed analysis of the test outcomes
|
|
75
|
+
* @throws {TypeError} If `dieType` or `testConditions` are invalid.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // analyse a D20 skill check with DC 15
|
|
79
|
+
* const analysis = analyseTest(DieType.D20, {
|
|
80
|
+
* testType: TestType.Skill,
|
|
81
|
+
* target: 15,
|
|
82
|
+
* critical_success: 20,
|
|
83
|
+
* critical_failure: 1
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* console.log(`Success rate: ${(analysis.outcomeProbabilities.success * 100).toFixed(1)}%`);
|
|
87
|
+
* console.log(`Critical success on: ${analysis.rollsByOutcome.critical_success}`);
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // analyse without natural crits
|
|
91
|
+
* const analysis = analyseTest(
|
|
92
|
+
* DieType.D20,
|
|
93
|
+
* { testType: TestType.AtLeast, target: 15 },
|
|
94
|
+
* { useNaturalCrits: false }
|
|
95
|
+
* );
|
|
96
|
+
*/
|
|
97
|
+
export function analyseTest(dieType: DieTypeValue, testConditions: TestConditionsInstance | {
|
|
98
|
+
testType: TestTypeValue;
|
|
99
|
+
[key: string]: any;
|
|
100
|
+
}, options?: analyseTestOptions): TestAnalysis;
|
|
101
|
+
//# sourceMappingURL=analyseTest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyseTest.d.ts","sourceRoot":"","sources":["../src/analyseTest.js"],"names":[],"mappings":"2BA0Ba,OAAO,oBAAoB,EAAE,YAAY;2BACzC,OAAO,oBAAoB,EAAE,YAAY;4BACzC,OAAO,qBAAqB,EAAE,aAAa;qCAC3C,OAAO,2BAA2B,EAAE,sBAAsB;;;;;wBAKzD,MAAM;;;;;;;;;;;;;;;;;;;;;;WAIN,MAAM,EAAE;;;;;;;;;;;;;AAbtB;;;;;GAKG;AAEH;;;;;;;;GAQG;AAEH;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qCA7BW,YAAY,kBACZ,sBAAsB,GAAC;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,YAItE,kBAAkB,GAChB,YAAY,CA8ExB"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @platonic-dice/core/src/analyseTest
|
|
3
|
+
* @description
|
|
4
|
+
* analyses test conditions without performing an actual roll.
|
|
5
|
+
* Provides probability information and possible outcomes for a given test configuration.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { analyseTest, DieType, TestType } from "@platonic-dice/core";
|
|
9
|
+
*
|
|
10
|
+
* const analysis = analyseTest(DieType.D20, {
|
|
11
|
+
* testType: TestType.AtLeast,
|
|
12
|
+
* target: 15
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* console.log(analysis.totalPossibilities); // 20
|
|
16
|
+
* console.log(analysis.successCount); // 6
|
|
17
|
+
* console.log(analysis.successRate); // 0.3 (30%)
|
|
18
|
+
* console.log(analysis.outcomesByRoll); // { 1: "failure", 2: "failure", ... }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { DieType, TestType } = require("./entities");
|
|
22
|
+
const tc = require("./entities/TestConditions.js");
|
|
23
|
+
const { createOutcomeMap } = require("./utils/outcomeMapper");
|
|
24
|
+
const { numSides } = require("./utils");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
|
|
28
|
+
* @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
|
|
29
|
+
* @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
|
|
30
|
+
* @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} TestAnalysis
|
|
35
|
+
* @property {number} totalPossibilities - Total number of possible die rolls
|
|
36
|
+
* @property {Object.<string, number>} outcomeCounts - Count of each outcome type
|
|
37
|
+
* @property {Object.<string, number>} outcomeProbabilities - Probability (0-1) of each outcome
|
|
38
|
+
* @property {Object.<number, OutcomeValue>} outcomesByRoll - Map of roll value to outcome
|
|
39
|
+
* @property {number[]} rolls - Array of all possible roll values
|
|
40
|
+
* @property {Object.<OutcomeValue, number[]>} rollsByOutcome - Rolls grouped by their outcome
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} analyseTestOptions
|
|
45
|
+
* @property {boolean} [useNaturalCrits] - If true, natural max/min rolls trigger
|
|
46
|
+
* critical outcomes. Defaults to true for Skill tests, false otherwise.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* analyses test conditions without performing an actual roll.
|
|
51
|
+
*
|
|
52
|
+
* @function analyseTest
|
|
53
|
+
* @param {DieTypeValue} dieType - The type of die (e.g., `DieType.D20`).
|
|
54
|
+
* @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
|
|
55
|
+
* Can be:
|
|
56
|
+
* - A `TestConditions` instance.
|
|
57
|
+
* - A plain object `{ testType, ...conditions }`.
|
|
58
|
+
* @param {analyseTestOptions} [options={}] - Optional configuration for analysis
|
|
59
|
+
* @returns {TestAnalysis} Detailed analysis of the test outcomes
|
|
60
|
+
* @throws {TypeError} If `dieType` or `testConditions` are invalid.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // analyse a D20 skill check with DC 15
|
|
64
|
+
* const analysis = analyseTest(DieType.D20, {
|
|
65
|
+
* testType: TestType.Skill,
|
|
66
|
+
* target: 15,
|
|
67
|
+
* critical_success: 20,
|
|
68
|
+
* critical_failure: 1
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* console.log(`Success rate: ${(analysis.outcomeProbabilities.success * 100).toFixed(1)}%`);
|
|
72
|
+
* console.log(`Critical success on: ${analysis.rollsByOutcome.critical_success}`);
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // analyse without natural crits
|
|
76
|
+
* const analysis = analyseTest(
|
|
77
|
+
* DieType.D20,
|
|
78
|
+
* { testType: TestType.AtLeast, target: 15 },
|
|
79
|
+
* { useNaturalCrits: false }
|
|
80
|
+
* );
|
|
81
|
+
*/
|
|
82
|
+
function analyseTest(dieType, testConditions, options = {}) {
|
|
83
|
+
if (!dieType) throw new TypeError("dieType is required.");
|
|
84
|
+
|
|
85
|
+
// Normalise testConditions (skip if already a TestConditions instance)
|
|
86
|
+
const conditionSet =
|
|
87
|
+
testConditions instanceof tc.TestConditions
|
|
88
|
+
? testConditions
|
|
89
|
+
: tc.normaliseTestConditions(testConditions, dieType);
|
|
90
|
+
|
|
91
|
+
// Create outcome map for all possible rolls
|
|
92
|
+
const outcomeMap = createOutcomeMap(
|
|
93
|
+
dieType,
|
|
94
|
+
conditionSet.testType,
|
|
95
|
+
conditionSet,
|
|
96
|
+
null, // no modifier
|
|
97
|
+
options.useNaturalCrits
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const sides = numSides(dieType);
|
|
101
|
+
const totalPossibilities = sides;
|
|
102
|
+
|
|
103
|
+
// Count outcomes
|
|
104
|
+
/** @type {Object.<string, number>} */
|
|
105
|
+
const outcomeCounts = {};
|
|
106
|
+
/** @type {Object.<string, number[]>} */
|
|
107
|
+
const rollsByOutcome = {};
|
|
108
|
+
|
|
109
|
+
for (let roll = 1; roll <= sides; roll++) {
|
|
110
|
+
const outcome = outcomeMap[roll];
|
|
111
|
+
|
|
112
|
+
// Count outcomes
|
|
113
|
+
outcomeCounts[outcome] = (outcomeCounts[outcome] || 0) + 1;
|
|
114
|
+
|
|
115
|
+
// Group rolls by outcome
|
|
116
|
+
if (!rollsByOutcome[outcome]) {
|
|
117
|
+
rollsByOutcome[outcome] = [];
|
|
118
|
+
}
|
|
119
|
+
rollsByOutcome[outcome].push(roll);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Calculate probabilities
|
|
123
|
+
/** @type {Object.<string, number>} */
|
|
124
|
+
const outcomeProbabilities = {};
|
|
125
|
+
for (const [outcome, count] of Object.entries(outcomeCounts)) {
|
|
126
|
+
outcomeProbabilities[outcome] = count / totalPossibilities;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
totalPossibilities,
|
|
131
|
+
outcomeCounts,
|
|
132
|
+
outcomeProbabilities,
|
|
133
|
+
outcomesByRoll: outcomeMap,
|
|
134
|
+
rolls: Array.from({ length: sides }, (_, i) => i + 1),
|
|
135
|
+
rollsByOutcome,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
analyseTest,
|
|
141
|
+
};
|