@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.
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @module @platonic-dice/core/src/rollModTest
3
+ * @description
4
+ * Rolls a die with a modifier and evaluates the modified result against test conditions.
5
+ * Combines the logic of {@link rollMod} and {@link rollTest}.
6
+ *
7
+ * @example
8
+ * import { rollModTest, TestType, DieType } from "@platonic-dice/core";
9
+ *
10
+ * // Roll a D20 with +5 bonus, check if result ≥ 15
11
+ * const result = rollModTest(
12
+ * DieType.D20,
13
+ * (n) => n + 5,
14
+ * { testType: TestType.AtLeast, target: 15 }
15
+ * );
16
+ * console.log(result.base); // e.g., 12
17
+ * console.log(result.modified); // e.g., 17
18
+ * console.log(result.outcome); // e.g., "success"
19
+ *
20
+ * @example
21
+ * // Skill check with advantage and modifier
22
+ * const result = rollModTest(
23
+ * DieType.D20,
24
+ * new RollModifier((n) => n + 3),
25
+ * { testType: TestType.Skill, target: 12, critical_success: 20, critical_failure: 1 },
26
+ * RollType.Advantage
27
+ * );
28
+ */
29
+
30
+ const { normaliseRollModifier } = require("./entities");
31
+ const { ModifiedTestConditions } = require("./entities/ModifiedTestConditions");
32
+ const r = require("./roll.js");
33
+ const utils = require("./utils");
34
+ const { createOutcomeMap } = require("./utils/outcomeMapper");
35
+
36
+ /**
37
+ * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
38
+ * @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
39
+ * @typedef {import("./entities/RollType").RollTypeValue} RollTypeValue
40
+ * @typedef {import("./entities/RollModifier").RollModifierFunction} RollModifierFunction
41
+ * @typedef {import("./entities/RollModifier").RollModifierInstance} RollModifierInstance
42
+ * @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
43
+ * @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
44
+ */
45
+
46
+ /**
47
+ * Helper to rank outcomes for comparison.
48
+ * @private
49
+ * @param {OutcomeValue} outcome
50
+ * @returns {number}
51
+ */
52
+ function rankOutcome(outcome) {
53
+ const { Outcome } = require("./entities");
54
+ switch (outcome) {
55
+ case Outcome.CriticalFailure:
56
+ return 0;
57
+ case Outcome.Failure:
58
+ return 1;
59
+ case Outcome.Success:
60
+ return 2;
61
+ case Outcome.CriticalSuccess:
62
+ return 3;
63
+ default:
64
+ return 1; // Default to failure rank
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Rolls a die with a modifier and evaluates the modified result against test conditions.
70
+ *
71
+ * @function rollModTest
72
+ * @param {DieTypeValue} dieType - The type of die to roll (e.g., `DieType.D20`).
73
+ * @param {RollModifierFunction|RollModifierInstance} modifier - The modifier to apply to the roll.
74
+ * Can be either:
75
+ * - A function `(n: number) => number`
76
+ * - A {@link RollModifier} instance
77
+ * @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
78
+ * Can be:
79
+ * - A `TestConditions` instance
80
+ * - A plain object `{ testType, ...conditions }`
81
+ * @param {RollTypeValue} [rollType=undefined] - Optional roll mode (`RollType.Advantage` or `RollType.Disadvantage`).
82
+ * @param {Object} [options={}] - Optional configuration.
83
+ * @param {boolean} [options.useNaturalCrits] - If true, natural max/min rolls on the die trigger
84
+ * critical success/failure (for Skill tests) or success/failure (for other test types).
85
+ * If undefined, defaults to true for Skill test type and false for all others.
86
+ * @returns {{ base: number, modified: number, outcome: OutcomeValue }}
87
+ * - `base`: The raw die roll
88
+ * - `modified`: The roll after applying the modifier
89
+ * - `outcome`: The success/failure result based on test conditions
90
+ * @throws {TypeError} If `dieType`, `modifier`, or `testConditions` are invalid.
91
+ *
92
+ * @example
93
+ * const result = rollModTest(
94
+ * DieType.D20,
95
+ * (n) => n + 2,
96
+ * { testType: TestType.AtLeast, target: 15 }
97
+ * );
98
+ * console.log(result); // { base: 14, modified: 16, outcome: "success" }
99
+ *
100
+ * @example
101
+ * // With natural crits enabled (TTRPG style)
102
+ * const result = rollModTest(
103
+ * DieType.D20,
104
+ * (n) => n + 5,
105
+ * { testType: TestType.Skill, target: 15, critical_success: 25, critical_failure: 2 },
106
+ * undefined,
107
+ * { useNaturalCrits: true }
108
+ * );
109
+ * // If base roll is 20, outcome is always "critical_success"
110
+ * // If base roll is 1, outcome is always "critical_failure"
111
+ *
112
+ * @example
113
+ * // With advantage - compares outcomes, not just base rolls
114
+ * const result = rollModTest(
115
+ * DieType.D20,
116
+ * (n) => n + 3,
117
+ * { testType: TestType.Skill, target: 12, critical_success: 20, critical_failure: 1 },
118
+ * RollType.Advantage
119
+ * );
120
+ * // Rolls twice, returns the result with the better outcome
121
+ */
122
+ function rollModTest(
123
+ dieType,
124
+ modifier,
125
+ testConditions,
126
+ rollType = undefined,
127
+ options = {}
128
+ ) {
129
+ if (!dieType) throw new TypeError("dieType is required.");
130
+ if (!modifier) throw new TypeError("modifier is required.");
131
+ if (!testConditions) throw new TypeError("testConditions is required.");
132
+
133
+ // Normalise the modifier (skip if already a RollModifier instance)
134
+ const { RollModifier } = require("./entities");
135
+ const mod =
136
+ modifier instanceof RollModifier
137
+ ? modifier
138
+ : normaliseRollModifier(modifier);
139
+
140
+ // Create ModifiedTestConditions if input is a plain object
141
+ let conditionSet;
142
+ if (testConditions instanceof ModifiedTestConditions) {
143
+ conditionSet = testConditions;
144
+ } else {
145
+ // Plain object: { testType, ...conditions }
146
+ const { testType, ...rest } = testConditions;
147
+ // @ts-ignore - rest is validated by ModifiedTestConditions constructor
148
+ conditionSet = new ModifiedTestConditions(testType, rest, dieType, mod);
149
+ }
150
+
151
+ // Create outcome map for all possible rolls (with modifier applied)
152
+ const outcomeMap = createOutcomeMap(
153
+ dieType,
154
+ conditionSet.testType,
155
+ // @ts-ignore - ModifiedTestConditions is compatible with TestConditions for outcome mapping
156
+ conditionSet,
157
+ mod, // include modifier
158
+ options.useNaturalCrits
159
+ );
160
+
161
+ // Handle advantage/disadvantage by comparing outcomes
162
+ if (rollType) {
163
+ const { RollType } = require("./entities");
164
+
165
+ // Roll twice
166
+ const roll1 = utils.generateResult(dieType);
167
+ const roll2 = utils.generateResult(dieType);
168
+
169
+ // Look up outcomes from pre-computed map
170
+ const outcome1 = outcomeMap[roll1];
171
+ const outcome2 = outcomeMap[roll2];
172
+
173
+ // Build result objects
174
+ const result1 = {
175
+ base: roll1,
176
+ modified: mod.apply(roll1),
177
+ outcome: outcome1,
178
+ };
179
+ const result2 = {
180
+ base: roll2,
181
+ modified: mod.apply(roll2),
182
+ outcome: outcome2,
183
+ };
184
+
185
+ // Compare outcomes: higher rank is better
186
+ const rank1 = rankOutcome(outcome1);
187
+ const rank2 = rankOutcome(outcome2);
188
+
189
+ if (rollType === RollType.Advantage) {
190
+ // Return the result with better outcome (higher rank)
191
+ return rank1 >= rank2 ? result1 : result2;
192
+ } else {
193
+ // Disadvantage: return the result with worse outcome (lower rank)
194
+ return rank1 <= rank2 ? result1 : result2;
195
+ }
196
+ }
197
+
198
+ // Normal roll (no advantage/disadvantage)
199
+ const base = r.roll(dieType);
200
+ const outcome = outcomeMap[base];
201
+ const modified = mod.apply(base);
202
+
203
+ return { base, modified, outcome };
204
+ }
205
+
206
+ module.exports = {
207
+ rollModTest,
208
+ };
@@ -1,5 +1,5 @@
1
1
  declare namespace _exports {
2
- export { DieTypeValue, OutcomeValue, RollTypeValue, TestTypeValue, TestConditionsInstance };
2
+ export { DieTypeValue, OutcomeValue, RollTypeValue, TestTypeValue, TestConditionsInstance, RollTestOptions };
3
3
  }
4
4
  declare namespace _exports {
5
5
  export { rollTest };
@@ -10,6 +10,16 @@ type OutcomeValue = import("./entities/Outcome").OutcomeValue;
10
10
  type RollTypeValue = import("./entities/RollType").RollTypeValue;
11
11
  type TestTypeValue = import("./entities/TestType").TestTypeValue;
12
12
  type TestConditionsInstance = import("./entities/TestConditions").TestConditionsInstance;
13
+ type RollTestOptions = {
14
+ /**
15
+ * - If true, rolling the die's maximum value
16
+ * triggers CriticalSuccess (for Skill tests) or Success (for AtLeast/AtMost tests),
17
+ * and rolling 1 triggers CriticalFailure (for Skill tests) or Failure (for AtLeast)
18
+ * or Success (for AtMost). If undefined, defaults to true for TestType.Skill
19
+ * and false for all other test types.
20
+ */
21
+ useNaturalCrits?: boolean | undefined;
22
+ };
13
23
  /**
14
24
  * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
15
25
  * @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
@@ -17,6 +27,14 @@ type TestConditionsInstance = import("./entities/TestConditions").TestConditions
17
27
  * @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
18
28
  * @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
19
29
  */
30
+ /**
31
+ * @typedef {Object} RollTestOptions
32
+ * @property {boolean} [useNaturalCrits] - If true, rolling the die's maximum value
33
+ * triggers CriticalSuccess (for Skill tests) or Success (for AtLeast/AtMost tests),
34
+ * and rolling 1 triggers CriticalFailure (for Skill tests) or Failure (for AtLeast)
35
+ * or Success (for AtMost). If undefined, defaults to true for TestType.Skill
36
+ * and false for all other test types.
37
+ */
20
38
  /**
21
39
  * Rolls a die and evaluates it against specified test conditions.
22
40
  *
@@ -27,13 +45,14 @@ type TestConditionsInstance = import("./entities/TestConditions").TestConditions
27
45
  * - A `TestConditions` instance.
28
46
  * - A plain object `{ testType, ...conditions }`.
29
47
  * @param {RollTypeValue} [rollType=undefined] - Optional roll mode (`RollType.Advantage` or `RollType.Disadvantage`).
48
+ * @param {RollTestOptions} [options={}] - Optional configuration for natural crits
30
49
  * @returns {{ base: number, outcome: OutcomeValue }} The raw roll and its evaluated outcome.
31
50
  * @throws {TypeError} If `dieType` or `testConditions` are invalid.
32
51
  */
33
52
  declare function rollTest(dieType: DieTypeValue, testConditions: TestConditionsInstance | {
34
53
  testType: TestTypeValue;
35
54
  [key: string]: any;
36
- }, rollType?: RollTypeValue): {
55
+ }, rollType?: RollTypeValue, options?: RollTestOptions): {
37
56
  base: number;
38
57
  outcome: OutcomeValue;
39
58
  };
@@ -1 +1 @@
1
- {"version":3,"file":"rollTest.d.ts","sourceRoot":"","sources":["../src/rollTest.js"],"names":[],"mappings":";;;;;;;oBA6Ba,OAAO,oBAAoB,EAAE,YAAY;oBACzC,OAAO,oBAAoB,EAAE,YAAY;qBACzC,OAAO,qBAAqB,EAAE,aAAa;qBAC3C,OAAO,qBAAqB,EAAE,aAAa;8BAC3C,OAAO,2BAA2B,EAAE,sBAAsB;AALvE;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AACH,mCATW,YAAY,kBACZ,sBAAsB,GAAC;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,aAItE,aAAa,GACX;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CAgBnD"}
1
+ {"version":3,"file":"rollTest.d.ts","sourceRoot":"","sources":["../src/rollTest.js"],"names":[],"mappings":";;;;;;;oBA6Ba,OAAO,oBAAoB,EAAE,YAAY;oBACzC,OAAO,oBAAoB,EAAE,YAAY;qBACzC,OAAO,qBAAqB,EAAE,aAAa;qBAC3C,OAAO,qBAAqB,EAAE,aAAa;8BAC3C,OAAO,2BAA2B,EAAE,sBAAsB;;;;;;;;;;;AALvE;;;;;;GAMG;AAEH;;;;;;;GAOG;AAEH;;;;;;;;;;;;;GAaG;AACH,mCAVW,YAAY,kBACZ,sBAAsB,GAAC;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,aAItE,aAAa,YACb,eAAe,GACb;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CA4BnD"}
package/dist/rollTest.js CHANGED
@@ -24,7 +24,7 @@
24
24
  const { DieType, TestType } = require("./entities");
25
25
  const tc = require("./entities/TestConditions.js");
26
26
  const r = require("./roll.js");
27
- const utils = require("./utils");
27
+ const { createOutcomeMap } = require("./utils/outcomeMapper");
28
28
 
29
29
  /**
30
30
  * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
@@ -34,6 +34,15 @@ const utils = require("./utils");
34
34
  * @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
35
35
  */
36
36
 
37
+ /**
38
+ * @typedef {Object} RollTestOptions
39
+ * @property {boolean} [useNaturalCrits] - If true, rolling the die's maximum value
40
+ * triggers CriticalSuccess (for Skill tests) or Success (for AtLeast/AtMost tests),
41
+ * and rolling 1 triggers CriticalFailure (for Skill tests) or Failure (for AtLeast)
42
+ * or Success (for AtMost). If undefined, defaults to true for TestType.Skill
43
+ * and false for all other test types.
44
+ */
45
+
37
46
  /**
38
47
  * Rolls a die and evaluates it against specified test conditions.
39
48
  *
@@ -44,20 +53,33 @@ const utils = require("./utils");
44
53
  * - A `TestConditions` instance.
45
54
  * - A plain object `{ testType, ...conditions }`.
46
55
  * @param {RollTypeValue} [rollType=undefined] - Optional roll mode (`RollType.Advantage` or `RollType.Disadvantage`).
56
+ * @param {RollTestOptions} [options={}] - Optional configuration for natural crits
47
57
  * @returns {{ base: number, outcome: OutcomeValue }} The raw roll and its evaluated outcome.
48
58
  * @throws {TypeError} If `dieType` or `testConditions` are invalid.
49
59
  */
50
- function rollTest(dieType, testConditions, rollType = undefined) {
60
+ function rollTest(dieType, testConditions, rollType = undefined, options = {}) {
51
61
  if (!dieType) throw new TypeError("dieType is required.");
52
62
 
53
- // Normalise testConditions
54
- const conditionSet = tc.normaliseTestConditions(testConditions, dieType);
63
+ // Normalise testConditions (skip if already a TestConditions instance)
64
+ const conditionSet =
65
+ testConditions instanceof tc.TestConditions
66
+ ? testConditions
67
+ : tc.normaliseTestConditions(testConditions, dieType);
68
+
69
+ // Create outcome map for all possible rolls
70
+ const outcomeMap = createOutcomeMap(
71
+ dieType,
72
+ conditionSet.testType,
73
+ conditionSet,
74
+ null, // no modifier
75
+ options.useNaturalCrits
76
+ );
55
77
 
56
78
  // Perform the roll
57
79
  const base = r.roll(dieType, rollType);
58
80
 
59
- // Determine the outcome using the normalized conditions
60
- const outcome = utils.determineOutcome(base, conditionSet);
81
+ // Look up outcome from pre-computed map
82
+ const outcome = outcomeMap[base];
61
83
 
62
84
  return { base, outcome };
63
85
  }
@@ -1 +1 @@
1
- {"version":3,"file":"determineOutcome.d.ts","sourceRoot":"","sources":["../../src/utils/determineOutcome.js"],"names":[],"mappings":"2BAWa,OAAO,qBAAqB,EAAE,YAAY;qCAC1C,OAAO,4BAA4B,EAAE,sBAAsB;yBAC3D,OAAO,4BAA4B,EAAE,UAAU;4BAC/C,OAAO,sBAAsB,EAAE,aAAa;2BAC5C,OAAO,qBAAqB,EAAE,YAAY;iCAK1C;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAAG,UAAU;AAV5E;;;;;;GAMG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wCAlBW,MAAM,kBACN,sBAAsB,GAAC,kBAAkB,GACvC,YAAY,CAgFxB"}
1
+ {"version":3,"file":"determineOutcome.d.ts","sourceRoot":"","sources":["../../src/utils/determineOutcome.js"],"names":[],"mappings":"2BAWa,OAAO,qBAAqB,EAAE,YAAY;qCAC1C,OAAO,4BAA4B,EAAE,sBAAsB;yBAC3D,OAAO,4BAA4B,EAAE,UAAU;4BAC/C,OAAO,sBAAsB,EAAE,aAAa;2BAC5C,OAAO,qBAAqB,EAAE,YAAY;iCAK1C;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAAG,UAAU;AAV5E;;;;;;GAMG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wCAlBW,MAAM,kBACN,sBAAsB,GAAC,kBAAkB,GACvC,YAAY,CAmDxB"}
@@ -46,11 +46,23 @@ function getEntities() {
46
46
  */
47
47
  function determineOutcome(value, testConditions) {
48
48
  const { Outcome, TestType, TestConditions } = getEntities();
49
+ const {
50
+ ModifiedTestConditions,
51
+ } = require("../entities/ModifiedTestConditions");
49
52
 
50
53
  if (typeof value !== "number" || Number.isNaN(value)) {
51
54
  throw new TypeError("value must be a valid number.");
52
55
  }
53
56
 
57
+ // Handle ModifiedTestConditions instances - no need to re-validate
58
+ if (testConditions instanceof ModifiedTestConditions) {
59
+ /** @type {TestConditionsInstance} */
60
+ // @ts-ignore - ModifiedTestConditions has compatible structure for outcome evaluation
61
+ const { testType, conditions } = testConditions;
62
+ // Use the switch statement below to determine outcome
63
+ return evaluateOutcome(value, testType, conditions, Outcome, TestType);
64
+ }
65
+
54
66
  // Normalise plain object input into a TestConditions instance
55
67
  if (!(testConditions instanceof TestConditions)) {
56
68
  const { testType, dieType, ...rest } = /** @type {TestConditionsLike} */ (
@@ -66,6 +78,20 @@ function determineOutcome(value, testConditions) {
66
78
  /** @type {TestConditionsInstance} */
67
79
  const { testType, conditions } = testConditions;
68
80
 
81
+ return evaluateOutcome(value, testType, conditions, Outcome, TestType);
82
+ }
83
+
84
+ /**
85
+ * Core evaluation logic extracted for reuse.
86
+ * @private
87
+ * @param {number} value
88
+ * @param {TestTypeValue} testType
89
+ * @param {Conditions | any} conditions
90
+ * @param {any} Outcome
91
+ * @param {any} TestType
92
+ * @returns {OutcomeValue}
93
+ */
94
+ function evaluateOutcome(value, testType, conditions, Outcome, TestType) {
69
95
  switch (testType) {
70
96
  case TestType.AtLeast:
71
97
  case TestType.AtMost:
@@ -1,5 +1,8 @@
1
1
  import { determineOutcome } from "./determineOutcome.js";
2
2
  import { generateResult } from "./generateResult.js";
3
3
  import { numSides } from "./generateResult.js";
4
- export { determineOutcome, generateResult, numSides };
4
+ import { createOutcomeMap } from "./outcomeMapper.js";
5
+ import { clearOutcomeMapCache } from "./outcomeMapper.js";
6
+ import { getOutcomeMapCacheSize } from "./outcomeMapper.js";
7
+ export { determineOutcome, generateResult, numSides, createOutcomeMap, clearOutcomeMapCache, getOutcomeMapCacheSize };
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -18,9 +18,17 @@
18
18
  */
19
19
  const { determineOutcome } = require("./determineOutcome.js");
20
20
  const { generateResult, numSides } = require("./generateResult.js");
21
+ const {
22
+ createOutcomeMap,
23
+ clearOutcomeMapCache,
24
+ getOutcomeMapCacheSize,
25
+ } = require("./outcomeMapper.js");
21
26
 
22
27
  module.exports = {
23
28
  determineOutcome,
24
29
  generateResult,
25
30
  numSides,
31
+ createOutcomeMap,
32
+ clearOutcomeMapCache,
33
+ getOutcomeMapCacheSize,
26
34
  };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Creates an outcome map for all possible base rolls given the configuration.
3
+ * Uses memoization cache for performance.
4
+ *
5
+ * @param {import("../entities/DieType").DieTypeValue} dieType - The type of die
6
+ * @param {import("../entities/TestType").TestTypeValue} testType - The type of test being performed
7
+ * @param {import("../entities/TestConditions").TestConditionsInstance} testConditions - The test conditions
8
+ * @param {import("../entities/RollModifier").RollModifierInstance|null} modifier - Optional modifier to apply
9
+ * @param {boolean|null} useNaturalCrits - Whether to use natural crits (null = auto-determine)
10
+ * @returns {Object.<number, import("../entities/Outcome").OutcomeValue>} Map of baseRoll -> outcome
11
+ */
12
+ export function createOutcomeMap(dieType: import("../entities/DieType").DieTypeValue, testType: import("../entities/TestType").TestTypeValue, testConditions: import("../entities/TestConditions").TestConditionsInstance, modifier?: import("../entities/RollModifier").RollModifierInstance | null, useNaturalCrits?: boolean | null): {
13
+ [x: number]: import("../entities").OutcomeValue;
14
+ };
15
+ /**
16
+ * Clears the outcome map cache.
17
+ * Useful for testing or memory management.
18
+ *
19
+ * @function clearOutcomeMapCache
20
+ */
21
+ export function clearOutcomeMapCache(): void;
22
+ /**
23
+ * Gets the current size of the outcome map cache.
24
+ *
25
+ * @function getOutcomeMapCacheSize
26
+ * @returns {number}
27
+ */
28
+ export function getOutcomeMapCacheSize(): number;
29
+ //# sourceMappingURL=outcomeMapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcomeMapper.d.ts","sourceRoot":"","sources":["../../src/utils/outcomeMapper.js"],"names":[],"mappings":"AAsGA;;;;;;;;;;GAUG;AACH,0CAPW,OAAO,qBAAqB,EAAE,YAAY,YAC1C,OAAO,sBAAsB,EAAE,aAAa,kBAC5C,OAAO,4BAA4B,EAAE,sBAAsB,aAC3D,OAAO,0BAA0B,EAAE,oBAAoB,GAAC,IAAI,oBAC5D,OAAO,GAAC,IAAI;;EA4DtB;AAED;;;;;GAKG;AACH,6CAEC;AAED;;;;;GAKG;AACH,0CAFa,MAAM,CAIlB"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * @module @platonic-dice/core/src/utils/outcomeMapper
3
+ * @description
4
+ * Creates outcome maps for die rolls, handling natural crits and modifiers.
5
+ * This provides a centralized way to determine all possible outcomes for a
6
+ * given die configuration.
7
+ *
8
+ * Includes memoization cache for performance optimization.
9
+ */
10
+
11
+ const { determineOutcome } = require("./determineOutcome");
12
+ const { numSides } = require("./generateResult");
13
+
14
+ /**
15
+ * Lazy-loaded entities to avoid circular dependencies.
16
+ * @private
17
+ */
18
+ function getEntities() {
19
+ return require("../entities");
20
+ }
21
+
22
+ /**
23
+ * Memoization cache for outcome maps.
24
+ * @private
25
+ * @type {Map<string, Object.<number, import("../entities/Outcome").OutcomeValue>>}
26
+ */
27
+ const outcomeMapCache = new Map();
28
+
29
+ /**
30
+ * Applies natural crit logic based on test type.
31
+ * Only applies to Skill, AtLeast, and AtMost test types.
32
+ *
33
+ * @private
34
+ * @param {import("../entities/Outcome").OutcomeValue} currentOutcome - The outcome before natural crit override
35
+ * @param {boolean} isNaturalMax - Whether this is the maximum die value
36
+ * @param {boolean} isNaturalMin - Whether this is the minimum die value (1)
37
+ * @param {import("../entities/TestType").TestTypeValue} testType - The type of test
38
+ * @returns {import("../entities/Outcome").OutcomeValue} The potentially overridden outcome
39
+ */
40
+ function applyNaturalCritOverride(
41
+ currentOutcome,
42
+ isNaturalMax,
43
+ isNaturalMin,
44
+ testType
45
+ ) {
46
+ const { TestType, Outcome } = getEntities();
47
+
48
+ // Skill tests: natural crits become Critical outcomes
49
+ if (testType === TestType.Skill) {
50
+ if (isNaturalMax) return Outcome.CriticalSuccess;
51
+ if (isNaturalMin) return Outcome.CriticalFailure;
52
+ return currentOutcome;
53
+ }
54
+
55
+ // At-Most tests: natural max is BAD (failure), natural min is GOOD (success)
56
+ if (testType === TestType.AtMost) {
57
+ if (isNaturalMax) return Outcome.Failure;
58
+ if (isNaturalMin) return Outcome.Success;
59
+ return currentOutcome;
60
+ }
61
+
62
+ // At-Least tests: natural max is GOOD (success), natural min is BAD (failure)
63
+ if (testType === TestType.AtLeast) {
64
+ if (isNaturalMax) return Outcome.Success;
65
+ if (isNaturalMin) return Outcome.Failure;
66
+ return currentOutcome;
67
+ }
68
+
69
+ // Other test types (Exact, Within, InList): natural crits don't apply
70
+ return currentOutcome;
71
+ }
72
+
73
+ /**
74
+ * Creates a cache key for outcome map memoization.
75
+ *
76
+ * @private
77
+ * @param {import("../entities/DieType").DieTypeValue} dieType
78
+ * @param {import("../entities/TestType").TestTypeValue} testType
79
+ * @param {import("../entities/TestConditions").TestConditionsInstance} testConditions
80
+ * @param {import("../entities/RollModifier").RollModifierInstance|null} modifier
81
+ * @param {boolean} useNaturalCrits
82
+ * @returns {string}
83
+ */
84
+ function createCacheKey(
85
+ dieType,
86
+ testType,
87
+ testConditions,
88
+ modifier,
89
+ useNaturalCrits
90
+ ) {
91
+ // Serialize the conditions object for hashing
92
+ const conditionsKey = JSON.stringify({
93
+ testType: testConditions.testType,
94
+ conditions: testConditions.conditions,
95
+ });
96
+
97
+ // Create modifier key (use function toString for consistent hashing)
98
+ const modifierKey = modifier ? modifier.fn.toString() : "none";
99
+
100
+ return `${dieType}|${testType}|${conditionsKey}|${modifierKey}|${useNaturalCrits}`;
101
+ }
102
+
103
+ /**
104
+ * Creates an outcome map for all possible base rolls given the configuration.
105
+ * Uses memoization cache for performance.
106
+ *
107
+ * @param {import("../entities/DieType").DieTypeValue} dieType - The type of die
108
+ * @param {import("../entities/TestType").TestTypeValue} testType - The type of test being performed
109
+ * @param {import("../entities/TestConditions").TestConditionsInstance} testConditions - The test conditions
110
+ * @param {import("../entities/RollModifier").RollModifierInstance|null} modifier - Optional modifier to apply
111
+ * @param {boolean|null} useNaturalCrits - Whether to use natural crits (null = auto-determine)
112
+ * @returns {Object.<number, import("../entities/Outcome").OutcomeValue>} Map of baseRoll -> outcome
113
+ */
114
+ function createOutcomeMap(
115
+ dieType,
116
+ testType,
117
+ testConditions,
118
+ modifier = null,
119
+ useNaturalCrits = null
120
+ ) {
121
+ const { TestType } = getEntities();
122
+
123
+ // Auto-determine useNaturalCrits if not specified
124
+ // Default: true for Skill tests, false for all others
125
+ const shouldUseNaturalCrits = useNaturalCrits ?? testType === TestType.Skill;
126
+
127
+ // Check cache first
128
+ const cacheKey = createCacheKey(
129
+ dieType,
130
+ testType,
131
+ testConditions,
132
+ modifier,
133
+ shouldUseNaturalCrits
134
+ );
135
+ const cached = outcomeMapCache.get(cacheKey);
136
+ if (cached) {
137
+ return cached;
138
+ }
139
+
140
+ const sides = numSides(dieType);
141
+ /** @type {Object.<number, import("../entities/Outcome").OutcomeValue>} */
142
+ const outcomeMap = {};
143
+
144
+ for (let baseRoll = 1; baseRoll <= sides; baseRoll++) {
145
+ // Apply modifier if present
146
+ const value = modifier ? modifier.apply(baseRoll) : baseRoll;
147
+
148
+ // Determine base outcome from test evaluation
149
+ let outcome = determineOutcome(value, testConditions);
150
+
151
+ // Apply natural crit overrides if enabled
152
+ if (shouldUseNaturalCrits) {
153
+ const isNaturalMax = baseRoll === sides;
154
+ const isNaturalMin = baseRoll === 1;
155
+
156
+ outcome = applyNaturalCritOverride(
157
+ outcome,
158
+ isNaturalMax,
159
+ isNaturalMin,
160
+ testType
161
+ );
162
+ }
163
+
164
+ outcomeMap[baseRoll] = outcome;
165
+ }
166
+
167
+ // Store in cache before returning
168
+ outcomeMapCache.set(cacheKey, outcomeMap);
169
+
170
+ return outcomeMap;
171
+ }
172
+
173
+ /**
174
+ * Clears the outcome map cache.
175
+ * Useful for testing or memory management.
176
+ *
177
+ * @function clearOutcomeMapCache
178
+ */
179
+ function clearOutcomeMapCache() {
180
+ outcomeMapCache.clear();
181
+ }
182
+
183
+ /**
184
+ * Gets the current size of the outcome map cache.
185
+ *
186
+ * @function getOutcomeMapCacheSize
187
+ * @returns {number}
188
+ */
189
+ function getOutcomeMapCacheSize() {
190
+ return outcomeMapCache.size;
191
+ }
192
+
193
+ module.exports = {
194
+ createOutcomeMap,
195
+ clearOutcomeMapCache,
196
+ getOutcomeMapCacheSize,
197
+ };
package/dist-types.d.ts CHANGED
@@ -22,8 +22,11 @@ export * from "./dist/rollMod";
22
22
  export * from "./dist/rollDice";
23
23
  export * from "./dist/rollDiceMod";
24
24
  export * from "./dist/rollTest";
25
+ export * from "./dist/rollModTest";
26
+ export * from "./dist/analyseTest";
27
+ export * from "./dist/analyseModTest";
25
28
 
26
- // The `rollMod`/`rollTest` d.ts files are emitted using a CommonJS `export =`
29
+ // The `rollMod`/`rollTest`/`rollModTest` d.ts files are emitted using a CommonJS `export =`
27
30
  // shape which doesn't always expose named exports for consumers under some
28
31
  // moduleResolution strategies. Provide explicit ESM-style type declarations
29
32
  // here so TypeScript imports like `import { rollMod } from '@platonic-dice/core'`
@@ -46,3 +49,22 @@ export function rollTest(
46
49
  },
47
50
  rollType?: import("./dist/entities/RollType").RollTypeValue
48
51
  ): { base: number; outcome: import("./dist/entities/Outcome").OutcomeValue };
52
+
53
+ export function rollModTest(
54
+ dieType: import("./dist/entities/DieType").DieTypeValue,
55
+ modifier:
56
+ | import("./dist/entities/RollModifier").RollModifierFunction
57
+ | import("./dist/entities/RollModifier").RollModifierInstance,
58
+ testConditions:
59
+ | import("./dist/entities/TestConditions").TestConditionsInstance
60
+ | {
61
+ testType: import("./dist/entities/TestType").TestTypeValue;
62
+ [k: string]: any;
63
+ },
64
+ rollType?: import("./dist/entities/RollType").RollTypeValue,
65
+ options?: { useNaturalCrits?: boolean }
66
+ ): {
67
+ base: number;
68
+ modified: number;
69
+ outcome: import("./dist/entities/Outcome").OutcomeValue;
70
+ };