@platonic-dice/core 2.0.2 → 2.1.0

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 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 { roll, rollDice, DieType, RollType } = require('@platonic-dice/core');
21
-
22
- console.log( roll(DieType.D20) );
23
- console.log( rollDice(DieType.D6, { count: 3 }) );
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 '@platonic-dice/core';
30
- console.log( roll(DieType.D20) );
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
+ };