@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.
- package/README.md +11 -2
- package/dist/analyseModTest.js +12 -10
- package/dist/analyseTest.js +43 -11
- package/dist/entities/DiceTestConditions.js +174 -0
- package/dist/entities/ModifiedTestConditions.js +30 -41
- package/dist/entities/RollModifier.js +15 -1
- package/dist/entities/TestConditions.js +26 -182
- package/dist/entities/TestConditionsArray.js +87 -0
- package/dist/entities/index.js +250 -39
- package/dist/index.js +42 -25
- package/dist/rollDiceMod.js +30 -17
- package/dist/rollDiceTest.js +68 -0
- package/dist/rollMod.js +3 -4
- package/dist/rollModTest.js +40 -13
- package/dist/rollTest.js +50 -15
- package/dist/utils/determineOutcome.js +35 -7
- package/dist/utils/getArrayEvaluator.js +48 -0
- package/dist/utils/getEvaluator.js +84 -0
- package/dist/utils/index.js +244 -16
- package/dist/utils/outcomeMapper.js +62 -15
- package/dist/utils/testRegistry.js +91 -0
- package/dist/utils/testValidators.js +190 -0
- package/package.json +12 -15
- package/dist/analyseModTest.d.ts +0 -126
- package/dist/analyseModTest.d.ts.map +0 -1
- package/dist/analyseTest.d.ts +0 -101
- package/dist/analyseTest.d.ts.map +0 -1
- package/dist/entities/DieType.d.ts +0 -35
- package/dist/entities/DieType.d.ts.map +0 -1
- package/dist/entities/ModifiedTestConditions.d.ts +0 -89
- package/dist/entities/ModifiedTestConditions.d.ts.map +0 -1
- package/dist/entities/Outcome.d.ts +0 -26
- package/dist/entities/Outcome.d.ts.map +0 -1
- package/dist/entities/RollModifier.d.ts +0 -115
- package/dist/entities/RollModifier.d.ts.map +0 -1
- package/dist/entities/RollType.d.ts +0 -24
- package/dist/entities/RollType.d.ts.map +0 -1
- package/dist/entities/TestConditions.d.ts +0 -97
- package/dist/entities/TestConditions.d.ts.map +0 -1
- package/dist/entities/TestType.d.ts +0 -28
- package/dist/entities/TestType.d.ts.map +0 -1
- package/dist/entities/index.d.ts +0 -19
- package/dist/entities/index.d.ts.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/roll.d.ts +0 -66
- package/dist/roll.d.ts.map +0 -1
- package/dist/rollDice.d.ts +0 -48
- package/dist/rollDice.d.ts.map +0 -1
- package/dist/rollDiceMod.d.ts +0 -54
- package/dist/rollDiceMod.d.ts.map +0 -1
- package/dist/rollMod.d.ts +0 -52
- package/dist/rollMod.d.ts.map +0 -1
- package/dist/rollModTest.d.ts +0 -72
- package/dist/rollModTest.d.ts.map +0 -1
- package/dist/rollTest.d.ts +0 -59
- package/dist/rollTest.d.ts.map +0 -1
- package/dist/utils/determineOutcome.d.ts +0 -45
- package/dist/utils/determineOutcome.d.ts.map +0 -1
- package/dist/utils/generateResult.d.ts +0 -30
- package/dist/utils/generateResult.d.ts.map +0 -1
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/outcomeMapper.d.ts +0 -29
- package/dist/utils/outcomeMapper.d.ts.map +0 -1
- 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
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
//
|
|
51
|
-
|
|
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;
|
package/dist/rollDiceMod.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
*
|
|
108
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
125
|
+
return result.modified.each.array;
|
|
117
126
|
case "net":
|
|
118
|
-
return
|
|
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
|
-
|
|
128
|
-
|
|
136
|
+
const rollDiceModArr =
|
|
137
|
+
/** @type {(dieType: DieTypeValue, modifier?: RollModifierLike, options?: { count?: number }) => number[]} */ (
|
|
138
|
+
alias("eachArray")
|
|
139
|
+
);
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
|
|
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").
|
|
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 {
|
|
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);
|
package/dist/rollModTest.js
CHANGED
|
@@ -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").
|
|
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 {
|
|
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
|
-
* @
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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|
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
72
|
+
// Inject dieType into the conditions object to satisfy validators
|
|
73
73
|
const fullConditions = { ...rest, dieType };
|
|
74
74
|
|
|
75
|
-
|
|
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 } =
|
|
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
|
|
90
|
-
* @param {
|
|
91
|
-
* @param {
|
|
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
|
+
};
|