@platonic-dice/core 2.2.2 → 3.0.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.
Files changed (66) hide show
  1. package/README.md +11 -2
  2. package/dist/analyseModTest.js +12 -10
  3. package/dist/analyseTest.js +43 -11
  4. package/dist/entities/DiceTestConditions.js +174 -0
  5. package/dist/entities/ModifiedTestConditions.js +30 -41
  6. package/dist/entities/RollModifier.js +15 -1
  7. package/dist/entities/TestConditions.js +26 -182
  8. package/dist/entities/TestConditionsArray.js +87 -0
  9. package/dist/entities/index.js +250 -39
  10. package/dist/index.js +42 -25
  11. package/dist/rollDiceMod.js +30 -17
  12. package/dist/rollDiceTest.js +68 -0
  13. package/dist/rollMod.js +3 -4
  14. package/dist/rollModTest.js +40 -13
  15. package/dist/rollTest.js +50 -15
  16. package/dist/utils/determineOutcome.js +35 -7
  17. package/dist/utils/getArrayEvaluator.js +48 -0
  18. package/dist/utils/getEvaluator.js +84 -0
  19. package/dist/utils/index.js +244 -16
  20. package/dist/utils/outcomeMapper.js +62 -15
  21. package/dist/utils/testRegistry.js +91 -0
  22. package/dist/utils/testValidators.js +190 -0
  23. package/package.json +12 -15
  24. package/dist/analyseModTest.d.ts +0 -126
  25. package/dist/analyseModTest.d.ts.map +0 -1
  26. package/dist/analyseTest.d.ts +0 -101
  27. package/dist/analyseTest.d.ts.map +0 -1
  28. package/dist/entities/DieType.d.ts +0 -35
  29. package/dist/entities/DieType.d.ts.map +0 -1
  30. package/dist/entities/ModifiedTestConditions.d.ts +0 -89
  31. package/dist/entities/ModifiedTestConditions.d.ts.map +0 -1
  32. package/dist/entities/Outcome.d.ts +0 -26
  33. package/dist/entities/Outcome.d.ts.map +0 -1
  34. package/dist/entities/RollModifier.d.ts +0 -115
  35. package/dist/entities/RollModifier.d.ts.map +0 -1
  36. package/dist/entities/RollType.d.ts +0 -24
  37. package/dist/entities/RollType.d.ts.map +0 -1
  38. package/dist/entities/TestConditions.d.ts +0 -97
  39. package/dist/entities/TestConditions.d.ts.map +0 -1
  40. package/dist/entities/TestType.d.ts +0 -28
  41. package/dist/entities/TestType.d.ts.map +0 -1
  42. package/dist/entities/index.d.ts +0 -19
  43. package/dist/entities/index.d.ts.map +0 -1
  44. package/dist/index.d.ts +0 -5
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/roll.d.ts +0 -66
  47. package/dist/roll.d.ts.map +0 -1
  48. package/dist/rollDice.d.ts +0 -48
  49. package/dist/rollDice.d.ts.map +0 -1
  50. package/dist/rollDiceMod.d.ts +0 -54
  51. package/dist/rollDiceMod.d.ts.map +0 -1
  52. package/dist/rollMod.d.ts +0 -52
  53. package/dist/rollMod.d.ts.map +0 -1
  54. package/dist/rollModTest.d.ts +0 -72
  55. package/dist/rollModTest.d.ts.map +0 -1
  56. package/dist/rollTest.d.ts +0 -59
  57. package/dist/rollTest.d.ts.map +0 -1
  58. package/dist/utils/determineOutcome.d.ts +0 -45
  59. package/dist/utils/determineOutcome.d.ts.map +0 -1
  60. package/dist/utils/generateResult.d.ts +0 -30
  61. package/dist/utils/generateResult.d.ts.map +0 -1
  62. package/dist/utils/index.d.ts +0 -8
  63. package/dist/utils/index.d.ts.map +0 -1
  64. package/dist/utils/outcomeMapper.d.ts +0 -29
  65. package/dist/utils/outcomeMapper.d.ts.map +0 -1
  66. package/dist-types.d.ts +0 -70
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ const rollDice = require("./rollDice.js");
13
13
  const roll = require("./roll.js");
14
14
  const rollMod = require("./rollMod.js");
15
15
  const rollDiceMod = require("./rollDiceMod.js");
16
+ const rollDiceTest = require("./rollDiceTest.js");
16
17
  const rollTest = require("./rollTest.js");
17
18
  const rollModTest = require("./rollModTest.js");
18
19
  const analyseTest = require("./analyseTest.js");
@@ -22,30 +23,46 @@ const analyseModTest = require("./analyseModTest.js");
22
23
  const entities = require("./entities");
23
24
 
24
25
  /**
25
- * Combined exports for Node and TypeScript users.
26
- * @type {typeof import("./roll") &
27
- * typeof import("./rollDice") &
28
- * typeof import("./rollMod") &
29
- * typeof import("./rollDiceMod") &
30
- * typeof import("./rollTest") &
31
- * typeof import("./rollModTest") &
32
- * typeof import("./analyseTest") &
33
- * typeof import("./analyseModTest") &
34
- * typeof import("./entities") &
35
- * { default: any }}
26
+ * Attach named exports onto `exports` so consumers can access
27
+ * all helpers from the package root.
36
28
  */
37
- module.exports = {
38
- ...roll,
39
- ...rollDice,
40
- ...rollMod,
41
- ...rollDiceMod,
42
- ...entities,
43
- ...rollTest,
44
- ...rollModTest,
45
- ...analyseTest,
46
- ...analyseModTest,
47
- default: undefined, // placeholder; will be overwritten
48
- };
29
+ Object.assign(exports, roll);
30
+ Object.assign(exports, rollDice);
31
+ Object.assign(exports, rollMod);
32
+ Object.assign(exports, rollDiceMod);
33
+ Object.assign(exports, rollDiceTest);
34
+ Object.assign(exports, entities);
35
+ Object.assign(exports, rollTest);
36
+ Object.assign(exports, rollModTest);
37
+ Object.assign(exports, analyseTest);
38
+ Object.assign(exports, analyseModTest);
49
39
 
50
- // assign default at runtime
51
- module.exports.default = module.exports;
40
+ // provide a `default` export for compatibility
41
+ exports.default = exports;
42
+
43
+ // Re-export all named exports explicitly for compatibility
44
+ exports.DieType = entities.DieType;
45
+ exports.isValidDieType = entities.isValidDieType;
46
+ exports.Outcome = entities.Outcome;
47
+ exports.isValidOutcome = entities.isValidOutcome;
48
+ exports.RollType = entities.RollType;
49
+ exports.isValidRollType = entities.isValidRollType;
50
+ exports.TestType = entities.TestType;
51
+ exports.isValidTestType = entities.isValidTestType;
52
+ exports.RollModifier = entities.RollModifier;
53
+ exports.isValidRollModifier = entities.isValidRollModifier;
54
+ exports.normaliseRollModifier = entities.normaliseRollModifier;
55
+ exports.TestConditions = entities.TestConditions;
56
+ exports.areValidTestConditions = entities.areValidTestConditions;
57
+ exports.normaliseTestConditions = entities.normaliseTestConditions;
58
+ exports.TestConditionsArray = entities.TestConditionsArray;
59
+ exports.DiceTestConditions = entities.DiceTestConditions;
60
+ exports.ModifiedTestConditions = entities.ModifiedTestConditions;
61
+ exports.areValidModifiedTestConditions =
62
+ entities.areValidModifiedTestConditions;
63
+ exports.computeModifiedRange = entities.computeModifiedRange;
64
+ exports.rollTest = rollTest.rollTest;
65
+ exports.rollModTest = rollModTest.rollModTest;
66
+ exports.rollDiceTest = rollDiceTest.rollDiceTest;
67
+ exports.analyseTest = analyseTest.analyseTest;
68
+ exports.analyseModTest = analyseModTest.analyseModTest;
@@ -34,10 +34,7 @@ const rd = require("./rollDice.js");
34
34
  * @typedef {import("./entities/RollModifier").RollModifierFunction} RollModifierFunction
35
35
  * @typedef {import("./entities/RollModifier").RollModifierInstance} RollModifierInstance
36
36
  * @typedef {import("./entities/RollModifier").DiceModifier} DiceModifier
37
- */
38
-
39
- /**
40
- * @typedef {RollModifierInstance | RollModifierFunction | DiceModifier} rollDiceModModifier
37
+ * @typedef {import("./entities/RollModifier").RollModifierLike} RollModifierLike
41
38
  */
42
39
 
43
40
  /**
@@ -45,7 +42,7 @@ const rd = require("./rollDice.js");
45
42
  *
46
43
  * @function rollDiceMod
47
44
  * @param {DieTypeValue} dieType - The die type (e.g., `DieType.D6`).
48
- * @param {rollDiceModModifier} [modifier={}] - The modifier(s) to apply.
45
+ * @param {RollModifierLike} [modifier={}] - The modifier(s) to apply.
49
46
  * @param {{ count?: number }} [options={}] - Optional roll count (default: 1).
50
47
  * @returns {{
51
48
  * base: { array: number[], sum: number },
@@ -68,12 +65,16 @@ function rollDiceMod(dieType, modifier = {}, { count = 1 } = {}) {
68
65
  eachMod = normaliseRollModifier(undefined); // identity
69
66
  netMod = normaliseRollModifier(modifier);
70
67
  } else if (typeof modifier === "object" && modifier !== null) {
71
- const { each, net } = modifier;
68
+ let each, net;
69
+ if (modifier && typeof modifier === "object") {
70
+ each = modifier.each;
71
+ net = modifier.net;
72
+ }
72
73
  eachMod = normaliseRollModifier(each);
73
74
  netMod = normaliseRollModifier(net);
74
75
  } else {
75
76
  throw new TypeError(
76
- `Invalid modifier: ${modifier}. Must be a function, RollModifier, or object.`
77
+ `Invalid modifier: ${modifier}. Must be a function, RollModifier, or object.`,
77
78
  );
78
79
  }
79
80
 
@@ -103,19 +104,27 @@ function rollDiceMod(dieType, modifier = {}, { count = 1 } = {}) {
103
104
  /**
104
105
  * @private
105
106
  * Generates a simple accessor alias for `rollDiceMod`.
106
- *
107
- * @template {keyof { eachArray: number[]; net: number }} K
108
- * @param {K} key
107
+ * Returns either the `each.array` or the `net.value` depending on `key`.
108
+ * We keep the implementation untyped to avoid complex conditional JSDoc generics
109
+ * which cause TypeScript complaints; callers below provide explicit typings.
110
+ */
111
+ /**
112
+ * @param {'eachArray'|'net'} key
113
+ * @returns {(dieType: DieTypeValue, modifier?: RollModifierLike, options?: { count?: number }) => number|number[]}
109
114
  */
110
115
  function alias(key) {
111
- /** @type {(...args: Parameters<typeof rollDiceMod>) => K extends 'eachArray' ? number[] : number} */
116
+ /**
117
+ * @param {DieTypeValue} dieType
118
+ * @param {RollModifierLike} [modifier]
119
+ * @param {{ count?: number }} [options]
120
+ */
112
121
  return (dieType, modifier = {}, options = {}) => {
113
122
  const result = rollDiceMod(dieType, modifier, options);
114
123
  switch (key) {
115
124
  case "eachArray":
116
- return /** @type {any} */ (result.modified.each.array);
125
+ return result.modified.each.array;
117
126
  case "net":
118
- return /** @type {any} */ (result.modified.net.value);
127
+ return result.modified.net.value;
119
128
  default:
120
129
  throw new TypeError(`Unknown alias key: ${key}`);
121
130
  }
@@ -124,11 +133,15 @@ function alias(key) {
124
133
 
125
134
  // --- Exports ---
126
135
 
127
- /** @type {(dieType: DieTypeValue, modifier?: rollDiceModModifier, options?: { count?: number }) => number[]} */
128
- const rollDiceModArr = alias("eachArray");
136
+ const rollDiceModArr =
137
+ /** @type {(dieType: DieTypeValue, modifier?: RollModifierLike, options?: { count?: number }) => number[]} */ (
138
+ alias("eachArray")
139
+ );
129
140
 
130
- /** @type {(dieType: DieTypeValue, modifier?: rollDiceModModifier, options?: { count?: number }) => number} */
131
- const rollDiceModNet = alias("net");
141
+ const rollDiceModNet =
142
+ /** @type {(dieType: DieTypeValue, modifier?: RollModifierLike, options?: { count?: number }) => number} */ (
143
+ alias("net")
144
+ );
132
145
 
133
146
  module.exports = {
134
147
  rollDiceMod,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @module @platonic-dice/core/src/rollDiceTest
3
+ * @description
4
+ * Rolls multiple dice and evaluates each die against a set of test conditions
5
+ * using `DiceTestConditions`.
6
+ */
7
+
8
+ const entities = require("./entities");
9
+ const { isValidDieType, DiceTestConditions, TestConditionsArray } = entities;
10
+ const { rollDice } = require("./rollDice.js");
11
+
12
+ /**
13
+ * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
14
+ */
15
+
16
+ /**
17
+ * Roll multiple dice and evaluate them against provided conditions.
18
+ *
19
+ * @param {DieTypeValue} dieType
20
+ * @param {import("./entities").DiceTestConditions|import("./entities/TestConditionsArray").TestConditionsArray|Array<import("./entities/TestConditions").TestConditionsLike>} conditions
21
+ * @param {{ count?: number, rules?: Array<{ type: "value_count"|"condition_count", value?: number, conditionIndex?: number, exact?: number, atLeast?: number, atMost?: number }>, useNaturalCrits?: boolean }} [options={}]
22
+ *
23
+ * @returns {{ base: { array: number[], sum: number }, result: Object }}
24
+ */
25
+ function rollDiceTest(
26
+ dieType,
27
+ conditions,
28
+ { count = 1, rules = [], useNaturalCrits = undefined } = {},
29
+ ) {
30
+ if (!isValidDieType(dieType))
31
+ throw new TypeError(`Invalid die type: ${dieType}`);
32
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 1) {
33
+ throw new TypeError("count must be a positive integer");
34
+ }
35
+
36
+ // Construct or validate provided DiceTestConditions
37
+ /** @type {import("./entities").DiceTestConditions} */
38
+ let dtc;
39
+ if (conditions instanceof DiceTestConditions) {
40
+ dtc = conditions;
41
+ if (dtc.count !== count)
42
+ throw new TypeError(
43
+ "Provided DiceTestConditions count does not match requested count",
44
+ );
45
+ } else if (
46
+ conditions instanceof TestConditionsArray ||
47
+ Array.isArray(conditions)
48
+ ) {
49
+ dtc = new DiceTestConditions({ count, conditions, rules, dieType });
50
+ } else {
51
+ throw new TypeError(
52
+ "conditions must be a DiceTestConditions instance, TestConditionsArray, or an array of TestConditions-like objects",
53
+ );
54
+ }
55
+
56
+ // Roll the dice
57
+ const base = rollDice(dieType, { count });
58
+
59
+ // Evaluate rolls using the DiceTestConditions evaluator
60
+ const evaluator = dtc.toEvaluator(undefined, useNaturalCrits);
61
+ const result = evaluator(base.array);
62
+
63
+ return { base, result };
64
+ }
65
+
66
+ module.exports = {
67
+ rollDiceTest,
68
+ };
package/dist/rollMod.js CHANGED
@@ -23,8 +23,7 @@ const r = require("./roll.js");
23
23
  /**
24
24
  * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
25
25
  * @typedef {import("./entities/RollType").RollTypeValue} RollTypeValue
26
- * @typedef {import("./entities/RollModifier").RollModifierFunction} RollModifierFunction
27
- * @typedef {import("./entities/RollModifier").RollModifierInstance} RollModifierInstance
26
+ * @typedef {import("./entities/RollModifier").RollModifierLike} RollModifierLike
28
27
  */
29
28
 
30
29
  /**
@@ -36,7 +35,7 @@ const r = require("./roll.js");
36
35
  *
37
36
  * @function rollMod
38
37
  * @param {DieTypeValue} dieType - The type of die to roll (e.g., `DieType.D20`).
39
- * @param {RollModifierFunction|RollModifierInstance} modifier - The modifier to apply.
38
+ * @param {RollModifierLike} modifier - The modifier to apply.
40
39
  * Can be either:
41
40
  * - A RollModifierFunction `(n: number) => number`
42
41
  * - A {@link RollModifier} instance
@@ -57,7 +56,7 @@ const r = require("./roll.js");
57
56
  * const result = rollMod(DieType.D10, (n) => Math.floor(n / 2), RollType.Advantage);
58
57
  */
59
58
  function rollMod(dieType, modifier, rollType = undefined) {
60
- const mod = normaliseRollModifier(modifier);
59
+ const mod = normaliseRollModifier(/** @type {any} */ (modifier));
61
60
 
62
61
  const base = r.roll(dieType, rollType);
63
62
  const modified = mod.apply(base);
@@ -32,13 +32,13 @@ const { ModifiedTestConditions } = require("./entities/ModifiedTestConditions");
32
32
  const r = require("./roll.js");
33
33
  const utils = require("./utils");
34
34
  const { createOutcomeMap } = require("./utils/outcomeMapper");
35
+ const { numSides } = require("./utils");
35
36
 
36
37
  /**
37
38
  * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
38
39
  * @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
39
40
  * @typedef {import("./entities/RollType").RollTypeValue} RollTypeValue
40
- * @typedef {import("./entities/RollModifier").RollModifierFunction} RollModifierFunction
41
- * @typedef {import("./entities/RollModifier").RollModifierInstance} RollModifierInstance
41
+ * @typedef {import("./entities/RollModifier").RollModifierLike} RollModifierLike
42
42
  * @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
43
43
  * @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
44
44
  */
@@ -70,11 +70,12 @@ function rankOutcome(outcome) {
70
70
  *
71
71
  * @function rollModTest
72
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.
73
+ * @param {RollModifierLike} modifier - The modifier to apply to the roll.
74
74
  * Can be either:
75
75
  * - A function `(n: number) => number`
76
76
  * - A {@link RollModifier} instance
77
- * @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
77
+ * @typedef {import("./entities/TestConditions").TestConditionsLike} TestConditionsLike
78
+ * @param {TestConditionsLike} testConditions
78
79
  * Can be:
79
80
  * - A `TestConditions` instance
80
81
  * - A plain object `{ testType, ...conditions }`
@@ -135,7 +136,7 @@ function rollModTest(
135
136
  const mod =
136
137
  modifier instanceof RollModifier
137
138
  ? modifier
138
- : normaliseRollModifier(modifier);
139
+ : normaliseRollModifier(/** @type {any} */ (modifier));
139
140
 
140
141
  // Create ModifiedTestConditions if input is a plain object
141
142
  let conditionSet;
@@ -149,14 +150,40 @@ function rollModTest(
149
150
  }
150
151
 
151
152
  // 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
- );
153
+ // Prefer registry evaluator if available; otherwise build outcome map.
154
+ /** @type {Record<number, OutcomeValue>|undefined} */
155
+ let outcomeMap;
156
+ try {
157
+ const { getRegistration } = require("./utils/testRegistry");
158
+ const reg = getRegistration(conditionSet.testType);
159
+ if (reg && typeof reg.buildEvaluator === "function") {
160
+ /** @type {import("./utils/testRegistry").Evaluator} */
161
+ const evaluator = reg.buildEvaluator(
162
+ dieType,
163
+ // @ts-ignore - ModifiedTestConditions is compatible with TestConditions for outcome mapping
164
+ conditionSet,
165
+ mod,
166
+ options.useNaturalCrits
167
+ );
168
+ outcomeMap = {};
169
+ const sides = numSides(dieType);
170
+ for (let roll = 1; roll <= sides; roll++)
171
+ outcomeMap[roll] = evaluator(roll);
172
+ }
173
+ } catch (err) {
174
+ // fall through to legacy logic
175
+ }
176
+
177
+ if (!outcomeMap) {
178
+ outcomeMap = createOutcomeMap(
179
+ dieType,
180
+ conditionSet.testType,
181
+ // @ts-ignore - ModifiedTestConditions is compatible with TestConditions for outcome mapping
182
+ conditionSet,
183
+ mod, // include modifier
184
+ options.useNaturalCrits
185
+ );
186
+ }
160
187
 
161
188
  // Handle advantage/disadvantage by comparing outcomes
162
189
  if (rollType) {
package/dist/rollTest.js CHANGED
@@ -25,6 +25,7 @@ const { DieType, TestType } = require("./entities");
25
25
  const tc = require("./entities/TestConditions.js");
26
26
  const r = require("./roll.js");
27
27
  const { createOutcomeMap } = require("./utils/outcomeMapper");
28
+ const { numSides } = require("./utils");
28
29
 
29
30
  /**
30
31
  * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
@@ -48,7 +49,7 @@ const { createOutcomeMap } = require("./utils/outcomeMapper");
48
49
  *
49
50
  * @function rollTest
50
51
  * @param {DieTypeValue} dieType - The type of die to roll (e.g., `DieType.D6`, `DieType.D20`).
51
- * @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
52
+ * @param {TestConditionsInstance|import("./entities/TestConditions").TestConditionsLike} testConditions
52
53
  * Can be:
53
54
  * - A `TestConditions` instance.
54
55
  * - A plain object `{ testType, ...conditions }`.
@@ -61,19 +62,56 @@ function rollTest(dieType, testConditions, rollType = undefined, options = {}) {
61
62
  if (!dieType) throw new TypeError("dieType is required.");
62
63
 
63
64
  // Normalise testConditions (skip if already a TestConditions instance)
64
- const conditionSet =
65
- testConditions instanceof tc.TestConditions
66
- ? testConditions
67
- : tc.normaliseTestConditions(testConditions, dieType);
65
+ let conditionSet;
66
+ if (testConditions instanceof tc.TestConditions) {
67
+ conditionSet = testConditions;
68
+ } else {
69
+ // Plain object: validate early for clearer errors, then normalise.
70
+ // We still call `normaliseTestConditions` so tests and any instrumentation
71
+ // that spy on it will observe the delegation (and the constructor remains
72
+ // the final authority for detailed RangeErrors).
73
+ const { testType, ...rest } = testConditions;
74
+ const fullConditions = { ...rest, dieType };
75
+ const validators = require("./utils/testValidators");
76
+ const { isValidTestType } = require("./entities/TestType");
68
77
 
69
- // Create outcome map for all possible rolls
70
- const outcomeMap = createOutcomeMap(
78
+ if (!isValidTestType(testType)) {
79
+ // Call normaliser so callers/spies see the delegation, then throw
80
+ // a consistent TypeError for unsupported test types.
81
+ try {
82
+ tc.normaliseTestConditions(testConditions, dieType);
83
+ } catch (err) {
84
+ // ignore original error; prefer consistent message
85
+ }
86
+ throw new TypeError(`Invalid test type: ${testType}`);
87
+ }
88
+
89
+ if (!validators.areValidTestConditions(fullConditions, testType)) {
90
+ // Call the normaliser to preserve existing call-sites/tests that expect
91
+ // the delegation; then fail fast with a standardised message.
92
+ try {
93
+ tc.normaliseTestConditions(testConditions, dieType);
94
+ } catch (err) {
95
+ // swallow the deeper error in favor of a clearer TypeError below
96
+ }
97
+ throw new TypeError("Invalid test conditions shape.");
98
+ }
99
+
100
+ conditionSet = tc.normaliseTestConditions(testConditions, dieType);
101
+ }
102
+
103
+ // Use centralised evaluator helper (registry or fallback)
104
+ const { getEvaluator } = require("./utils/getEvaluator");
105
+ const evaluator = getEvaluator(
71
106
  dieType,
72
- conditionSet.testType,
73
107
  conditionSet,
74
- null, // no modifier
75
- options.useNaturalCrits
108
+ undefined,
109
+ options.useNaturalCrits,
76
110
  );
111
+ const sides = numSides(dieType);
112
+ /** @type {Record<number, OutcomeValue>} */
113
+ const outcomeMap = {};
114
+ for (let b = 1; b <= sides; b++) outcomeMap[b] = evaluator(b);
77
115
 
78
116
  // Perform the roll
79
117
  const base = r.roll(dieType, rollType);
@@ -115,8 +153,5 @@ for (const [dieKey, dieValue] of Object.entries(DieType)) {
115
153
  }
116
154
  }
117
155
 
118
- // Export all generated aliases
119
- module.exports = {
120
- rollTest,
121
- ...aliases,
122
- };
156
+ // Export all generated aliases as named exports so tsc emits named declarations
157
+ Object.assign(exports, { rollTest, ...aliases });
@@ -18,7 +18,7 @@ function getEntities() {
18
18
 
19
19
  /**
20
20
  * @private
21
- * @typedef {{ testType: TestTypeValue, dieType: DieTypeValue } & Conditions} TestConditionsLike
21
+ * @typedef {import("../entities/TestConditions").TestConditionsLike & { dieType: DieTypeValue }} TestConditionsLike
22
22
  */
23
23
 
24
24
  /**
@@ -69,14 +69,42 @@ function determineOutcome(value, testConditions) {
69
69
  testConditions
70
70
  );
71
71
 
72
- // Inject dieType into the conditions object to satisfy TestConditions
72
+ // Inject dieType into the conditions object to satisfy validators
73
73
  const fullConditions = { ...rest, dieType };
74
74
 
75
- testConditions = new TestConditions(testType, fullConditions, dieType);
75
+ // Fast-path validation using shared validators to provide clearer,
76
+ // centralised error messages for plain-object inputs before attempting
77
+ // construction of a TestConditions instance.
78
+ const validators = require("../utils/testValidators");
79
+ const { isValidTestType } = require("../entities/TestType");
80
+
81
+ if (!isValidTestType(testType)) {
82
+ // Mirror the constructor's TypeError for unsupported test types.
83
+ throw new TypeError(`Invalid test type: ${testType}`);
84
+ }
85
+
86
+ if (!validators.areValidTestConditions(fullConditions, testType)) {
87
+ // Fail fast with a clear TypeError for invalid shapes. The validator
88
+ // returns false for malformed conditions (range errors, missing keys,
89
+ // etc.). Consumers constructing TestConditions directly will still
90
+ // receive more specific RangeError messages from the constructor;
91
+ // here we centralise failure for plain-object inputs.
92
+ throw new TypeError("Invalid test conditions shape.");
93
+ }
94
+
95
+ // At this point the plain object is well-formed; normalise via the
96
+ // constructor to obtain a proper TestConditions instance.
97
+ testConditions = new TestConditions(
98
+ testType,
99
+ /** @type {any} */ (fullConditions),
100
+ dieType
101
+ );
76
102
  }
77
103
 
78
104
  /** @type {TestConditionsInstance} */
79
- const { testType, conditions } = testConditions;
105
+ const { testType, conditions } = /** @type {TestConditionsInstance} */ (
106
+ testConditions
107
+ );
80
108
 
81
109
  return evaluateOutcome(value, testType, conditions, Outcome, TestType);
82
110
  }
@@ -86,9 +114,9 @@ function determineOutcome(value, testConditions) {
86
114
  * @private
87
115
  * @param {number} value
88
116
  * @param {TestTypeValue} testType
89
- * @param {Conditions | any} conditions
90
- * @param {any} Outcome
91
- * @param {any} TestType
117
+ * @param {Conditions} conditions
118
+ * @param {typeof import("../entities/Outcome").Outcome} Outcome
119
+ * @param {typeof import("../entities/TestType").TestType} TestType
92
120
  * @returns {OutcomeValue}
93
121
  */
94
122
  function evaluateOutcome(value, testType, conditions, Outcome, TestType) {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @module @platonic-dice/core/src/utils/getArrayEvaluator
3
+ * @description
4
+ * Builds an evaluator function for a `TestConditionsArray` that maps a single
5
+ * numeric input to an array of outcomes (one per contained TestConditions).
6
+ */
7
+
8
+ const { getEvaluator } = require("./getEvaluator");
9
+
10
+ /**
11
+ * @typedef {import("../entities/TestConditions").TestConditionsInstance} TestConditionsInstance
12
+ * @typedef {import("../entities/TestConditions").TestConditionsLike} TestConditionsLike
13
+ * @typedef {import("../entities/TestConditionsArray").TestConditionsArrayInstance} TestConditionsArrayInstance
14
+ */
15
+
16
+ /**
17
+ * Create an evaluator for a TestConditionsArray instance.
18
+ *
19
+ * @param {TestConditionsArrayInstance} tcArray - The TestConditionsArray instance
20
+ * @param {import("../entities/RollModifier").RollModifierInstance} [modifier]
21
+ * @param {boolean} [useNaturalCrits]
22
+ * @returns {(value: number) => string[]} Function mapping numeric value -> array of Outcome values
23
+ */
24
+ function getArrayEvaluator(
25
+ tcArray,
26
+ modifier = undefined,
27
+ useNaturalCrits = undefined,
28
+ ) {
29
+ if (!tcArray) throw new TypeError("tcArray is required");
30
+
31
+ if (typeof tcArray.toArray !== "function") {
32
+ throw new TypeError("tcArray must be a TestConditionsArray instance");
33
+ }
34
+
35
+ const conditions = tcArray.toArray();
36
+
37
+ // Build per-entry evaluators using existing getEvaluator (reuses createOutcomeMap cache)
38
+ const perEntryEvaluators = conditions.map((tc) =>
39
+ getEvaluator(tc.dieType, tc, modifier, useNaturalCrits),
40
+ );
41
+
42
+ return /** @param {number} value */ (value) =>
43
+ perEntryEvaluators.map((fn) => fn(value));
44
+ }
45
+
46
+ module.exports = {
47
+ getArrayEvaluator,
48
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @module @platonic-dice/core/src/utils/getEvaluator
3
+ * @description
4
+ * Helper to obtain a per-base evaluator for a given die + conditions.
5
+ *
6
+ * It first consults the `testRegistry` for a `buildEvaluator`. If none is
7
+ * registered, it falls back to building an outcome map via
8
+ * `createOutcomeMap` and returns a function that indexes into that map.
9
+ */
10
+
11
+ const { createOutcomeMap } = require("./outcomeMapper");
12
+ const { numSides } = require("./generateResult");
13
+
14
+ /**
15
+ * @typedef {import("../entities/DieType").DieTypeValue} DieTypeValue
16
+ * @typedef {import("../entities/TestType").TestTypeValue} TestTypeValue
17
+ * @typedef {import("../entities/Outcome").OutcomeValue} OutcomeValue
18
+ * @typedef {import("../entities/TestConditions").TestConditionsInstance} TestConditionsInstance
19
+ * @typedef {(base: number) => OutcomeValue} Evaluator
20
+ */
21
+
22
+ /**
23
+ * Get an evaluator function mapping base roll -> OutcomeValue.
24
+ *
25
+ * @typedef {import("../entities/TestConditions").TestConditionsLike} TestConditionsLike
26
+ * @param {DieTypeValue} dieType
27
+ * @param {TestConditionsLike} testConditions
28
+ * @param {import("../entities/RollModifier").RollModifierInstance} [modifier]
29
+ * @param {boolean} [useNaturalCrits]
30
+ * @returns {Evaluator}
31
+ */
32
+ function getEvaluator(
33
+ dieType,
34
+ testConditions,
35
+ modifier = undefined,
36
+ useNaturalCrits = undefined,
37
+ ) {
38
+ if (!testConditions || !testConditions.testType) {
39
+ throw new TypeError(
40
+ "testConditions must include a 'testType' field or be a TestConditions instance",
41
+ );
42
+ }
43
+
44
+ const { getRegistration } = require("./testRegistry");
45
+
46
+ const testType = testConditions.testType;
47
+ const reg = getRegistration(testType);
48
+ if (reg && typeof reg.buildEvaluator === "function") {
49
+ return reg.buildEvaluator(
50
+ dieType,
51
+ testConditions,
52
+ modifier,
53
+ useNaturalCrits,
54
+ );
55
+ }
56
+
57
+ // Fallback: build an outcome map and return a simple indexer
58
+ // Ensure we pass a TestConditions instance into createOutcomeMap to match
59
+ // its runtime/typing contract. Normalise plain objects when necessary.
60
+ const {
61
+ TestConditions,
62
+ normaliseTestConditions,
63
+ } = require("../entities/TestConditions");
64
+ let tcInstance = testConditions;
65
+ if (!(testConditions instanceof TestConditions)) {
66
+ // Runtime normalization: `normaliseTestConditions` will validate and return a
67
+ // `TestConditions` instance when given a plain object.
68
+ tcInstance = normaliseTestConditions(testConditions, dieType);
69
+ }
70
+
71
+ const outcomeMap = createOutcomeMap(
72
+ dieType,
73
+ testType,
74
+ // `tcInstance` is a validated TestConditions instance at runtime
75
+ tcInstance,
76
+ modifier,
77
+ useNaturalCrits,
78
+ );
79
+ return /** @param {number} base */ (base) => outcomeMap[base];
80
+ }
81
+
82
+ module.exports = {
83
+ getEvaluator,
84
+ };