roll-parser 3.0.0-alpha.0 → 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/CHANGELOG.md +189 -0
- package/MIGRATION.md +147 -0
- package/README.md +985 -43
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +81 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/format.d.ts +16 -4
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +17 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +36 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +83 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/errors.d.ts +332 -14
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +141 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluator/die.d.ts +26 -0
- package/dist/evaluator/die.d.ts.map +1 -0
- package/dist/evaluator/die.js +19 -0
- package/dist/evaluator/die.js.map +1 -0
- package/dist/evaluator/env.d.ts +58 -0
- package/dist/evaluator/env.d.ts.map +1 -0
- package/dist/evaluator/env.js +11 -0
- package/dist/evaluator/env.js.map +1 -0
- package/dist/evaluator/evaluator.d.ts +62 -40
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/evaluator.js +906 -0
- package/dist/evaluator/evaluator.js.map +1 -0
- package/dist/evaluator/modifiers/compare.d.ts +1 -1
- package/dist/evaluator/modifiers/compare.d.ts.map +1 -1
- package/dist/evaluator/modifiers/compare.js +15 -0
- package/dist/evaluator/modifiers/compare.js.map +1 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts +27 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
- package/dist/evaluator/modifiers/crit-threshold.js +23 -0
- package/dist/evaluator/modifiers/crit-threshold.js.map +1 -0
- package/dist/evaluator/modifiers/die-bound.d.ts +26 -0
- package/dist/evaluator/modifiers/die-bound.d.ts.map +1 -0
- package/dist/evaluator/modifiers/die-bound.js +14 -0
- package/dist/evaluator/modifiers/die-bound.js.map +1 -0
- package/dist/evaluator/modifiers/explode.d.ts +18 -6
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
- package/dist/evaluator/modifiers/explode.js +103 -0
- package/dist/evaluator/modifiers/explode.js.map +1 -0
- package/dist/evaluator/modifiers/flags.d.ts +37 -0
- package/dist/evaluator/modifiers/flags.d.ts.map +1 -0
- package/dist/evaluator/modifiers/flags.js +18 -0
- package/dist/evaluator/modifiers/flags.js.map +1 -0
- package/dist/evaluator/modifiers/keep-drop.d.ts +12 -28
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.js +82 -0
- package/dist/evaluator/modifiers/keep-drop.js.map +1 -0
- package/dist/evaluator/modifiers/reroll.d.ts +14 -6
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
- package/dist/evaluator/modifiers/reroll.js +62 -0
- package/dist/evaluator/modifiers/reroll.js.map +1 -0
- package/dist/evaluator/modifiers/sort.d.ts +27 -0
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
- package/dist/evaluator/modifiers/sort.js +13 -0
- package/dist/evaluator/modifiers/sort.js.map +1 -0
- package/dist/evaluator/modifiers/success-count.d.ts +2 -6
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
- package/dist/evaluator/modifiers/success-count.js +24 -0
- package/dist/evaluator/modifiers/success-count.js.map +1 -0
- package/dist/index.d.ts +35 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1723
- package/dist/index.js.map +1 -0
- package/dist/lexer/lexer.d.ts +68 -8
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/lexer.js +260 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lexer/tokens.d.ts +52 -7
- package/dist/lexer/tokens.d.ts.map +1 -1
- package/dist/lexer/tokens.js +42 -0
- package/dist/lexer/tokens.js.map +1 -0
- package/dist/parser/ast.d.ts +419 -80
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/ast.js +52 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/guards.d.ts +106 -0
- package/dist/parser/guards.d.ts.map +1 -0
- package/dist/parser/guards.js +121 -0
- package/dist/parser/guards.js.map +1 -0
- package/dist/parser/parser.d.ts +162 -15
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +751 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/rng/mock.d.ts +74 -13
- package/dist/rng/mock.d.ts.map +1 -1
- package/dist/rng/mock.js +30 -0
- package/dist/rng/mock.js.map +1 -0
- package/dist/rng/seeded.d.ts +149 -10
- package/dist/rng/seeded.d.ts.map +1 -1
- package/dist/rng/seeded.js +138 -0
- package/dist/rng/seeded.js.map +1 -0
- package/dist/rng/types.d.ts +57 -0
- package/dist/rng/types.d.ts.map +1 -1
- package/dist/rng/types.js +2 -0
- package/dist/rng/types.js.map +1 -0
- package/dist/roll.d.ts +59 -25
- package/dist/roll.d.ts.map +1 -1
- package/dist/roll.js +8 -0
- package/dist/roll.js.map +1 -0
- package/dist/testing.d.ts +5 -4
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -38
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +427 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +93 -40
- package/src/cli/args.ts +66 -9
- package/src/cli/format.ts +30 -7
- package/src/cli/index.ts +27 -67
- package/src/cli/main.ts +129 -0
- package/src/errors.ts +488 -23
- package/src/evaluator/die.ts +50 -0
- package/src/evaluator/env.ts +73 -0
- package/src/evaluator/evaluator.ts +1296 -349
- package/src/evaluator/modifiers/compare.ts +1 -1
- package/src/evaluator/modifiers/crit-threshold.ts +56 -0
- package/src/evaluator/modifiers/die-bound.ts +39 -0
- package/src/evaluator/modifiers/explode.ts +82 -76
- package/src/evaluator/modifiers/flags.ts +61 -0
- package/src/evaluator/modifiers/keep-drop.ts +124 -126
- package/src/evaluator/modifiers/reroll.ts +36 -64
- package/src/evaluator/modifiers/sort.ts +43 -0
- package/src/evaluator/modifiers/success-count.ts +6 -9
- package/src/index.ts +73 -34
- package/src/lexer/lexer.ts +201 -35
- package/src/lexer/tokens.ts +72 -7
- package/src/parser/ast.ts +453 -104
- package/src/parser/guards.ts +248 -0
- package/src/parser/parser.ts +835 -135
- package/src/rng/mock.ts +75 -14
- package/src/rng/seeded.ts +323 -58
- package/src/rng/types.ts +57 -0
- package/src/roll.ts +66 -41
- package/src/testing.ts +5 -9
- package/src/types.ts +416 -24
- package/src/version.ts +2 -0
- package/dist/cli.js +0 -1775
- package/dist/evaluator/index.d.ts +0 -8
- package/dist/evaluator/index.d.ts.map +0 -1
- package/dist/index.mjs +0 -1724
- package/dist/rng/index.d.ts +0 -8
- package/dist/rng/index.d.ts.map +0 -1
- package/dist/testing.mjs +0 -39
- package/src/evaluator/index.ts +0 -14
- package/src/rng/index.ts +0 -8
package/src/roll.ts
CHANGED
|
@@ -4,63 +4,88 @@
|
|
|
4
4
|
* @module roll
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { SeededRNG } from './rng/seeded';
|
|
7
|
+
import { evaluate } from './evaluator/evaluator.js';
|
|
8
|
+
import { parse } from './parser/parser.js';
|
|
9
|
+
import { SeededRNG } from './rng/seeded.js';
|
|
10
|
+
import type { RNG } from './rng/types.js';
|
|
11
|
+
import type { EvaluationOptions, RollResult } from './types.js';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
*
|
|
14
|
+
* Everything {@link roll} accepts on top of the shared {@link EvaluationOptions}:
|
|
15
|
+
* a randomness source, given either as a ready-made {@link RNG} or as a seed.
|
|
16
|
+
*
|
|
17
|
+
* @category Core
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { roll, SeededRNG } from 'roll-parser';
|
|
22
|
+
*
|
|
23
|
+
* roll('4d6', { seed: 'character-1' }); // reproducible
|
|
24
|
+
* roll('4d6', { rng: new SeededRNG(42) }); // rng wins over seed
|
|
25
|
+
* roll('1d20+@str', { context: { str: 4 } });
|
|
26
|
+
* ```
|
|
16
27
|
*/
|
|
17
|
-
export type RollOptions = {
|
|
18
|
-
/**
|
|
28
|
+
export type RollOptions = EvaluationOptions & {
|
|
29
|
+
/**
|
|
30
|
+
* Randomness source. Takes precedence over `seed` — when both are given,
|
|
31
|
+
* `seed` is ignored.
|
|
32
|
+
*/
|
|
19
33
|
rng?: RNG;
|
|
20
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Seed for a fresh `SeededRNG`. Equal seeds replay the same die sequence for
|
|
36
|
+
* the same notation. Ignored when `rng` is set.
|
|
37
|
+
*/
|
|
21
38
|
seed?: string | number;
|
|
22
|
-
/** Maximum total dice allowed per evaluation (default: 10,000) */
|
|
23
|
-
maxDice?: number;
|
|
24
|
-
/** Maximum explosion iterations allowed per die (default: 1,000) */
|
|
25
|
-
maxExplodeIterations?: number;
|
|
26
|
-
/** Maximum reroll iterations allowed per die (default: 1,000) */
|
|
27
|
-
maxRerollIterations?: number;
|
|
28
39
|
};
|
|
29
40
|
|
|
30
41
|
/**
|
|
31
|
-
* Parses and evaluates a dice notation string
|
|
42
|
+
* Parses and evaluates a dice notation string in one call — the main entry
|
|
43
|
+
* point of the library.
|
|
44
|
+
*
|
|
45
|
+
* Equivalent to `evaluate(parse(notation), rng, { notation })`. Each call
|
|
46
|
+
* builds a fresh `SeededRNG` unless `options.rng` is supplied, so reuse
|
|
47
|
+
* {@link parse} + {@link evaluate} directly when rolling the same notation in
|
|
48
|
+
* a loop.
|
|
32
49
|
*
|
|
33
|
-
* @param notation - Dice notation
|
|
34
|
-
* @param options -
|
|
35
|
-
* @returns Complete
|
|
50
|
+
* @param notation - Dice notation, e.g. `'2d6+3'` or `'4d6kh3'`
|
|
51
|
+
* @param options - RNG or seed, plus the shared {@link EvaluationOptions}
|
|
52
|
+
* @returns Complete {@link RollResult} with total, per-die results and the
|
|
53
|
+
* structured `parts` tree
|
|
54
|
+
* @throws {LexerError} On an invalid character
|
|
55
|
+
* @throws {ParseError} On invalid syntax
|
|
56
|
+
* @throws {EvaluatorError} On a limit breach or an impossible expression
|
|
57
|
+
* @throws {RollParserError} `INVALID_EVALUATION_LIMIT` when a supplied limit is
|
|
58
|
+
* not an integer in range — raised before any die is rolled
|
|
59
|
+
* @throws {RollParserError} `INVALID_NOTATION_TYPE` when `notation` is not a
|
|
60
|
+
* string, so `isRollParserError` still filters untrusted input completely
|
|
36
61
|
*
|
|
37
62
|
* @example
|
|
38
63
|
* ```typescript
|
|
64
|
+
* import { roll } from 'roll-parser';
|
|
65
|
+
*
|
|
39
66
|
* // Random roll
|
|
40
|
-
*
|
|
41
|
-
*
|
|
67
|
+
* roll('2d6+3').total; // 5..15
|
|
68
|
+
*
|
|
69
|
+
* // Seeded — same seed, same sequence
|
|
70
|
+
* roll('2d6+3', { seed: 'demo' }).rendered; // '2d6[1, 6] + 3 = 10'
|
|
71
|
+
* roll('2d6+3', { seed: 'demo' }).total; // 10
|
|
72
|
+
* ```
|
|
42
73
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
74
|
+
* @example Deterministic tests with the testing mock
|
|
75
|
+
* ```typescript
|
|
76
|
+
* import { roll } from 'roll-parser';
|
|
77
|
+
* import { createMockRng } from 'roll-parser/testing';
|
|
47
78
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* result.
|
|
79
|
+
* const result = roll('4d6kh3', { rng: createMockRng([3, 6, 2, 5]) });
|
|
80
|
+
* result.total; // 14
|
|
81
|
+
* result.rendered; // '4d6[3, 6, ~~2~~, 5] = 14'
|
|
51
82
|
* ```
|
|
83
|
+
*
|
|
84
|
+
* @category Core
|
|
52
85
|
*/
|
|
53
86
|
export function roll(notation: string, options: RollOptions = {}): RollResult {
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (options.maxDice != null) evalOptions.maxDice = options.maxDice;
|
|
59
|
-
if (options.maxExplodeIterations != null) {
|
|
60
|
-
evalOptions.maxExplodeIterations = options.maxExplodeIterations;
|
|
61
|
-
}
|
|
62
|
-
if (options.maxRerollIterations != null) {
|
|
63
|
-
evalOptions.maxRerollIterations = options.maxRerollIterations;
|
|
64
|
-
}
|
|
65
|
-
return evaluate(ast, rng, evalOptions);
|
|
87
|
+
// Explicitly-undefined limit keys forward harmlessly — `evaluate` nullish-checks each.
|
|
88
|
+
const { rng, seed, ...limits } = options;
|
|
89
|
+
|
|
90
|
+
return evaluate(parse(notation), rng ?? new SeededRNG(seed), { ...limits, notation });
|
|
66
91
|
}
|
package/src/testing.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test utilities for roll-parser consumers.
|
|
3
3
|
*
|
|
4
|
-
* Import from `roll-parser/testing` for deterministic dice testing.
|
|
4
|
+
* Import from `roll-parser/testing` for deterministic dice testing. The full
|
|
5
|
+
* TSDoc lives on the implementations in `./rng/mock.ts`; this entry point is
|
|
6
|
+
* a plain re-export barrel.
|
|
5
7
|
*
|
|
6
8
|
* @module testing
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
MockRNGExhaustedError as _MockRNGExhaustedError,
|
|
12
|
-
createMockRng as _createMockRng,
|
|
13
|
-
} from './rng/mock';
|
|
14
|
-
|
|
15
|
-
export const createMockRng = _createMockRng;
|
|
16
|
-
export const MockRNGExhaustedError = _MockRNGExhaustedError;
|
|
11
|
+
export { createMockRng, MockRNGExhaustedError } from './rng/mock.js';
|
|
12
|
+
export type { RNG } from './rng/types.js';
|
package/src/types.ts
CHANGED
|
@@ -4,33 +4,121 @@
|
|
|
4
4
|
* @module types
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ASTNode } from './parser/ast';
|
|
7
|
+
import type { ASTNode } from './parser/ast.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Comparison operator for compare points.
|
|
10
|
+
* Comparison operator for compare points. Spelled exactly as it appears in
|
|
11
|
+
* notation — `4d6!>=5` carries `'>='`, bare `f1` normalizes to `'='`.
|
|
12
|
+
*
|
|
13
|
+
* @category AST
|
|
11
14
|
*/
|
|
12
15
|
export type CompareOp = '>' | '>=' | '<' | '<=' | '=';
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
|
-
* A comparison threshold used by exploding dice, reroll,
|
|
18
|
+
* A comparison threshold used by exploding dice, reroll, success counting, and
|
|
19
|
+
* crit-threshold overrides.
|
|
20
|
+
*
|
|
21
|
+
* The value is an {@link ASTNode} to support computed thresholds
|
|
22
|
+
* (`1d6!>(1d2+3)`), matching the pattern used by `DiceNode.count` and
|
|
23
|
+
* `DiceNode.sides`. The evaluated counterpart is {@link ResolvedComparePoint}.
|
|
16
24
|
*
|
|
17
|
-
*
|
|
18
|
-
* matching the pattern used by DiceNode.count and DiceNode.sides.
|
|
25
|
+
* @category AST
|
|
19
26
|
*/
|
|
20
27
|
export type ComparePoint = {
|
|
21
|
-
operator: CompareOp;
|
|
22
|
-
value: ASTNode;
|
|
28
|
+
readonly operator: CompareOp;
|
|
29
|
+
readonly value: ASTNode;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A {@link ComparePoint} whose value has been evaluated to a number. Used
|
|
34
|
+
* throughout {@link RollPart}, where meta-expressions are already resolved —
|
|
35
|
+
* `1d6!>(1d2+3)` surfaces as `{ operator: '>', value: 5 }`.
|
|
36
|
+
*
|
|
37
|
+
* @category Results
|
|
38
|
+
*/
|
|
39
|
+
export type ResolvedComparePoint = {
|
|
40
|
+
readonly operator: CompareOp;
|
|
41
|
+
readonly value: number;
|
|
23
42
|
};
|
|
24
43
|
|
|
25
44
|
/**
|
|
26
|
-
*
|
|
45
|
+
* A resolved crit threshold — `'default'` means the per-die default rule
|
|
46
|
+
* (`result === sides` for critical, `result === 1` for fumble), which is what
|
|
47
|
+
* bare `cs` / `cf` produce.
|
|
48
|
+
*
|
|
49
|
+
* @category Results
|
|
50
|
+
*/
|
|
51
|
+
export type ResolvedCritThreshold = ResolvedComparePoint | 'default';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Tags attached to a {@link DieResult} by the evaluator. A die can carry more
|
|
55
|
+
* than one (an exploded die that was later dropped is `['dropped',
|
|
56
|
+
* 'exploded']`), and the set drives the markers in `RollResult.rendered`.
|
|
57
|
+
*
|
|
58
|
+
* | Tag | Meaning | Rendered as |
|
|
59
|
+
* |-----|---------|-------------|
|
|
60
|
+
* | `'kept'` | Counted toward the total. Every non-dropped die carries it. | plain |
|
|
61
|
+
* | `'dropped'` | Excluded from the total by `kh`/`kl`/`dh`/`dl` or group selection. | `~~n~~` |
|
|
62
|
+
* | `'exploded'` | Produced by, or the trigger of, an explosion (`!`, `!!`, `!p`). | plain |
|
|
63
|
+
* | `'rerolled'` | A discarded intermediate from `r` / `ro`, always paired with `'dropped'` — the replacement die carries no tag. | `~~n~~` (via `'dropped'`) |
|
|
64
|
+
* | `'min'` | Raised to a `minN` bound; `initialResult` keeps the raw face. | plain |
|
|
65
|
+
* | `'max'` | Lowered to a `maxN` bound; `initialResult` keeps the raw face. | plain |
|
|
66
|
+
* | `'success'` | Met a success-count threshold (`>=6`). | `**n**` |
|
|
67
|
+
* | `'failure'` | Met a failure threshold (`f1`). | `__n__` |
|
|
68
|
+
* | `'meta'` | Rolled by a meta-expression rather than by the visible pool. | not shown |
|
|
69
|
+
* | `'dc'` | The DC side of a `vs` comparison. Never part of the roll-side pool. | plain |
|
|
70
|
+
*
|
|
71
|
+
* `'meta'` is the one tag with no counterpart in the notation. Dice counts,
|
|
72
|
+
* sides, modifier counts and computed thresholds may themselves be dice
|
|
73
|
+
* (`(1d4)d6`, `4d6kh(1d2)`, `1d6!>(1d2+3)`). Those inner dice are not part of
|
|
74
|
+
* any pool, so they never appear in a {@link RollPart}; they are appended to
|
|
75
|
+
* `RollResult.rolls` tagged `'meta'` so an audit log can still show what the
|
|
76
|
+
* meta-expression rolled. Filter them out when summing or displaying a pool.
|
|
77
|
+
*
|
|
78
|
+
* `'dc'` marks the DC side of a `vs` comparison. Unlike `'meta'` these dice do
|
|
79
|
+
* render — `1d20[3] vs 2d10[5, 6]` shows both sides — but they are not part of
|
|
80
|
+
* the roll-side pool, so no modifier may sum, select, clamp, reroll, explode,
|
|
81
|
+
* or tally them. Filter them out when summing a pool, exactly as with `'meta'`.
|
|
82
|
+
*
|
|
83
|
+
* @category Results
|
|
27
84
|
*/
|
|
28
|
-
export type DieModifier =
|
|
85
|
+
export type DieModifier =
|
|
86
|
+
| 'dropped'
|
|
87
|
+
| 'kept'
|
|
88
|
+
| 'exploded'
|
|
89
|
+
| 'rerolled'
|
|
90
|
+
| 'min'
|
|
91
|
+
| 'max'
|
|
92
|
+
| 'success'
|
|
93
|
+
| 'failure'
|
|
94
|
+
| 'meta'
|
|
95
|
+
| 'dc';
|
|
29
96
|
|
|
30
97
|
/**
|
|
31
98
|
* PF2e Degree of Success. Produced by the `vs` operator when comparing a
|
|
32
99
|
* roll against a Difficulty Class. Ordering is significant — natural 20
|
|
33
100
|
* upgrades one step and natural 1 downgrades one step.
|
|
101
|
+
*
|
|
102
|
+
* Numeric by design: the enum members are plain numbers, so `--json` CLI
|
|
103
|
+
* output and `JSON.stringify` emit `0`–`3` and comparisons like
|
|
104
|
+
* `degree >= DegreeOfSuccess.Success` work.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* import { DegreeOfSuccess, roll } from 'roll-parser';
|
|
109
|
+
* import { createMockRng } from 'roll-parser/testing';
|
|
110
|
+
*
|
|
111
|
+
* const result = roll('1d20+7 vs 15', { rng: createMockRng([12]) });
|
|
112
|
+
* result.degree; // DegreeOfSuccess.Success (2)
|
|
113
|
+
* result.natural; // 12 — the raw d20 face, used for the ±1 step
|
|
114
|
+
* result.rendered; // '1d20[12] + 7 vs 15 = Success'
|
|
115
|
+
*
|
|
116
|
+
* if (result.degree != null && result.degree >= DegreeOfSuccess.Success) {
|
|
117
|
+
* // hit
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @category Results
|
|
34
122
|
*/
|
|
35
123
|
export enum DegreeOfSuccess {
|
|
36
124
|
CriticalFailure = 0,
|
|
@@ -40,7 +128,25 @@ export enum DegreeOfSuccess {
|
|
|
40
128
|
}
|
|
41
129
|
|
|
42
130
|
/**
|
|
43
|
-
*
|
|
131
|
+
* One physical die and everything the evaluator learned about it.
|
|
132
|
+
*
|
|
133
|
+
* The same object is shared between `RollResult.rolls` and the `rolls[]` of
|
|
134
|
+
* the {@link RollPart} that produced it — there is no deep clone, so mutating
|
|
135
|
+
* a die is visible through both views.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* import { roll } from 'roll-parser';
|
|
140
|
+
* import { createMockRng } from 'roll-parser/testing';
|
|
141
|
+
*
|
|
142
|
+
* const result = roll('4d6kh3', { rng: createMockRng([3, 6, 2, 5]) });
|
|
143
|
+
* result.rolls[1];
|
|
144
|
+
* // { sides: 6, result: 6, modifiers: ['kept'], critical: true, fumble: false }
|
|
145
|
+
* result.rolls[2];
|
|
146
|
+
* // { sides: 6, result: 2, modifiers: ['dropped'], critical: false, fumble: false }
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* @category Results
|
|
44
150
|
*/
|
|
45
151
|
export type DieResult = {
|
|
46
152
|
/**
|
|
@@ -51,6 +157,13 @@ export type DieResult = {
|
|
|
51
157
|
sides: number;
|
|
52
158
|
/** The rolled value */
|
|
53
159
|
result: number;
|
|
160
|
+
/**
|
|
161
|
+
* Raw first roll before any mutation (e.g., compound-explode accumulation).
|
|
162
|
+
* Only populated when `result` has been overwritten with a computed value.
|
|
163
|
+
* Consumers that need the original face (nat-20 / nat-1 detection) should
|
|
164
|
+
* read `initialResult ?? result`.
|
|
165
|
+
*/
|
|
166
|
+
initialResult?: number;
|
|
54
167
|
/** Modifiers applied to this die */
|
|
55
168
|
modifiers: DieModifier[];
|
|
56
169
|
/** True if rolled the maximum value (always false for Fate dice) */
|
|
@@ -60,9 +173,218 @@ export type DieResult = {
|
|
|
60
173
|
};
|
|
61
174
|
|
|
62
175
|
/**
|
|
63
|
-
*
|
|
176
|
+
* One entry inside a flattened keep/drop chain. Counts are resolved at
|
|
177
|
+
* evaluation time (meta-expressions like `kh(1d2)` become the rolled number).
|
|
178
|
+
*
|
|
179
|
+
* Chained keep/drop modifiers flatten into one list — `4d6kh3dl1` yields two
|
|
180
|
+
* specs against a single pool, each applied independently, with drop sets
|
|
181
|
+
* unioned (Roll20 semantics).
|
|
182
|
+
*
|
|
183
|
+
* @category Results
|
|
184
|
+
*/
|
|
185
|
+
export type KeepDropSpec = {
|
|
186
|
+
readonly kind: 'keep' | 'drop';
|
|
187
|
+
readonly selector: 'highest' | 'lowest';
|
|
188
|
+
readonly count: number;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Fields shared by every {@link RollPart} variant. `start`/`end` mirror the
|
|
193
|
+
* source span of the AST node the part was evaluated from — present whenever
|
|
194
|
+
* the AST came from `parse()`, absent on hand-built ASTs.
|
|
195
|
+
*
|
|
196
|
+
* Useful for code that walks the parts tree generically: any part can be
|
|
197
|
+
* narrowed to this shape without switching on `type` first.
|
|
198
|
+
*
|
|
199
|
+
* @category Results
|
|
64
200
|
*/
|
|
65
|
-
export type
|
|
201
|
+
export type RollPartBase = {
|
|
202
|
+
/** Sub-total this part contributed to its parent. */
|
|
203
|
+
total: number;
|
|
204
|
+
start?: number;
|
|
205
|
+
end?: number;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Structured breakdown of an evaluated expression, mirroring the AST 1:1 —
|
|
210
|
+
* every ASTNode produces exactly one RollPart. Discriminants are lowercase
|
|
211
|
+
* camelCase to distinguish evaluation-tree types from `ASTNode.type`
|
|
212
|
+
* (PascalCase) at a glance.
|
|
213
|
+
*
|
|
214
|
+
* Invariants:
|
|
215
|
+
* - `RollResult.parts.total === RollResult.total`.
|
|
216
|
+
* - `successCount.total === successes - failures`.
|
|
217
|
+
* - `literal.total === value` and `variable.total === value`.
|
|
218
|
+
* - Each part's `rolls[]` shares `DieResult` references with
|
|
219
|
+
* `RollResult.rolls[]`; both reflect post-evaluation state (explode
|
|
220
|
+
* accumulation, reroll flags, keep/drop flags). No deep clone.
|
|
221
|
+
*
|
|
222
|
+
* Meta-expression sub-trees (`4d6kh(1d2)`, `(1+1)d6` counts/sides, computed
|
|
223
|
+
* thresholds) are not surfaced as nested parts — their resolved numbers
|
|
224
|
+
* appear in the owning part, and their dice are inspectable in
|
|
225
|
+
* `RollResult.rolls` via the `'meta'` modifier tag.
|
|
226
|
+
*
|
|
227
|
+
* @example Walking the tree with an exhaustive switch
|
|
228
|
+
* ```typescript
|
|
229
|
+
* import type { RollPart } from 'roll-parser';
|
|
230
|
+
*
|
|
231
|
+
* function describe(part: RollPart): string {
|
|
232
|
+
* switch (part.type) {
|
|
233
|
+
* case 'literal':
|
|
234
|
+
* return String(part.value);
|
|
235
|
+
* case 'variable':
|
|
236
|
+
* return `@${part.name}`;
|
|
237
|
+
* case 'dice':
|
|
238
|
+
* return `${part.count}d${part.sides}[${part.rolls.map((d) => d.result).join(', ')}]`;
|
|
239
|
+
* case 'fateDice':
|
|
240
|
+
* return `${part.count}dF`;
|
|
241
|
+
* case 'grouped':
|
|
242
|
+
* return `(${describe(part.inner)})`;
|
|
243
|
+
* case 'binaryOp':
|
|
244
|
+
* return `${describe(part.left)} ${part.operator} ${describe(part.right)}`;
|
|
245
|
+
* case 'unaryOp':
|
|
246
|
+
* return `-${describe(part.operand)}`;
|
|
247
|
+
* case 'keepDrop':
|
|
248
|
+
* return `${describe(part.target)} [${part.specs.length} keep/drop]`;
|
|
249
|
+
* case 'explode':
|
|
250
|
+
* return `${describe(part.target)} (${part.variant} explode)`;
|
|
251
|
+
* case 'reroll':
|
|
252
|
+
* return `${describe(part.target)} (reroll${part.once ? ' once' : ''})`;
|
|
253
|
+
* case 'dieBound':
|
|
254
|
+
* return `${describe(part.target)} (${part.bound} ${part.value})`;
|
|
255
|
+
* case 'successCount':
|
|
256
|
+
* return `${describe(part.target)} => ${part.successes}-${part.failures}`;
|
|
257
|
+
* case 'versus':
|
|
258
|
+
* return `${describe(part.roll)} vs ${describe(part.dc)}`;
|
|
259
|
+
* case 'functionCall':
|
|
260
|
+
* return `${part.name}(${part.args.map(describe).join(', ')})`;
|
|
261
|
+
* case 'group':
|
|
262
|
+
* return `{${part.parts.map(describe).join(', ')}}`;
|
|
263
|
+
* case 'sort':
|
|
264
|
+
* return `${describe(part.target)} (${part.order})`;
|
|
265
|
+
* case 'critThreshold':
|
|
266
|
+
* return `${describe(part.target)} (crit override)`;
|
|
267
|
+
* }
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*
|
|
271
|
+
* @example Reading a concrete tree
|
|
272
|
+
* ```typescript
|
|
273
|
+
* import { roll } from 'roll-parser';
|
|
274
|
+
* import { createMockRng } from 'roll-parser/testing';
|
|
275
|
+
*
|
|
276
|
+
* const result = roll('4d6kh3 + 2', { rng: createMockRng([3, 6, 2, 5]) });
|
|
277
|
+
* result.parts.type; // 'binaryOp'
|
|
278
|
+
* result.parts.total; // 16 — always equal to result.total
|
|
279
|
+
* describe(result.parts); // '4d6[3, 6, 2, 5] [1 keep/drop] + 2'
|
|
280
|
+
* ```
|
|
281
|
+
*
|
|
282
|
+
* @category Results
|
|
283
|
+
*/
|
|
284
|
+
export type RollPart =
|
|
285
|
+
| (RollPartBase & { type: 'literal'; value: number })
|
|
286
|
+
| (RollPartBase & { type: 'variable'; name: string; value: number })
|
|
287
|
+
| (RollPartBase & { type: 'dice'; count: number; sides: number; rolls: DieResult[] })
|
|
288
|
+
| (RollPartBase & { type: 'fateDice'; count: number; rolls: DieResult[] })
|
|
289
|
+
| (RollPartBase & { type: 'grouped'; inner: RollPart })
|
|
290
|
+
| (RollPartBase & {
|
|
291
|
+
type: 'binaryOp';
|
|
292
|
+
operator: '+' | '-' | '*' | '/' | '%' | '**';
|
|
293
|
+
left: RollPart;
|
|
294
|
+
right: RollPart;
|
|
295
|
+
})
|
|
296
|
+
| (RollPartBase & { type: 'unaryOp'; operator: '-'; operand: RollPart })
|
|
297
|
+
| (RollPartBase & { type: 'keepDrop'; specs: KeepDropSpec[]; target: RollPart })
|
|
298
|
+
| (RollPartBase & {
|
|
299
|
+
type: 'explode';
|
|
300
|
+
variant: 'standard' | 'compound' | 'penetrating';
|
|
301
|
+
threshold?: ResolvedComparePoint;
|
|
302
|
+
target: RollPart;
|
|
303
|
+
})
|
|
304
|
+
| (RollPartBase & {
|
|
305
|
+
type: 'reroll';
|
|
306
|
+
once: boolean;
|
|
307
|
+
condition: ResolvedComparePoint;
|
|
308
|
+
target: RollPart;
|
|
309
|
+
})
|
|
310
|
+
| (RollPartBase & { type: 'dieBound'; bound: 'min' | 'max'; value: number; target: RollPart })
|
|
311
|
+
| (RollPartBase & {
|
|
312
|
+
type: 'successCount';
|
|
313
|
+
threshold: ResolvedComparePoint;
|
|
314
|
+
failThreshold?: ResolvedComparePoint;
|
|
315
|
+
target: RollPart;
|
|
316
|
+
successes: number;
|
|
317
|
+
failures: number;
|
|
318
|
+
})
|
|
319
|
+
| (RollPartBase & { type: 'versus'; roll: RollPart; dc: RollPart; degree: DegreeOfSuccess })
|
|
320
|
+
| (RollPartBase & { type: 'functionCall'; name: string; args: RollPart[] })
|
|
321
|
+
| (RollPartBase & { type: 'group'; parts: RollPart[]; keptIndices?: number[] })
|
|
322
|
+
| (RollPartBase & {
|
|
323
|
+
type: 'sort';
|
|
324
|
+
order: 'ascending' | 'descending';
|
|
325
|
+
/**
|
|
326
|
+
* Every die the target produced, in sorted order, sharing `DieResult`
|
|
327
|
+
* references with `target` — so flags set after the sort (`4d6s dl1`)
|
|
328
|
+
* show through both. Like `RollResult.rolls` it keeps `'meta'` dice,
|
|
329
|
+
* which `rendered` omits from the bracket.
|
|
330
|
+
*/
|
|
331
|
+
rolls: DieResult[];
|
|
332
|
+
target: RollPart;
|
|
333
|
+
})
|
|
334
|
+
| (RollPartBase & {
|
|
335
|
+
type: 'critThreshold';
|
|
336
|
+
successThresholds: ResolvedCritThreshold[];
|
|
337
|
+
failThresholds: ResolvedCritThreshold[];
|
|
338
|
+
target: RollPart;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The 17 discriminant strings of {@link RollPart}. Convenience alias for
|
|
343
|
+
* consumers writing exhaustive switches or part-type lookup tables.
|
|
344
|
+
*
|
|
345
|
+
* @category Results
|
|
346
|
+
*/
|
|
347
|
+
export type RollPartType = RollPart['type'];
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Complete roll result with all metadata — what {@link roll} and
|
|
351
|
+
* {@link evaluate} return.
|
|
352
|
+
*
|
|
353
|
+
* The top level is `Readonly` — a result describes one completed evaluation
|
|
354
|
+
* and is never re-targeted. The `rolls` array and the `parts` tree stay
|
|
355
|
+
* mutable so consumers can annotate or re-sort their own views.
|
|
356
|
+
*
|
|
357
|
+
* Fully JSON-serializable; this is exactly what the CLI's `--json` flag emits.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```typescript
|
|
361
|
+
* import { roll } from 'roll-parser';
|
|
362
|
+
* import { createMockRng } from 'roll-parser/testing';
|
|
363
|
+
*
|
|
364
|
+
* const result = roll('3d6', { rng: createMockRng([4, 2, 6]) });
|
|
365
|
+
*
|
|
366
|
+
* JSON.stringify(result);
|
|
367
|
+
* // {
|
|
368
|
+
* // "total": 12,
|
|
369
|
+
* // "notation": "3d6",
|
|
370
|
+
* // "expression": "3d6",
|
|
371
|
+
* // "rendered": "3d6[4, 2, 6] = 12",
|
|
372
|
+
* // "rolls": [
|
|
373
|
+
* // { "sides": 6, "result": 4, "modifiers": ["kept"], "critical": false, "fumble": false },
|
|
374
|
+
* // { "sides": 6, "result": 2, "modifiers": ["kept"], "critical": false, "fumble": false },
|
|
375
|
+
* // { "sides": 6, "result": 6, "modifiers": ["kept"], "critical": true, "fumble": false }
|
|
376
|
+
* // ],
|
|
377
|
+
* // "parts": {
|
|
378
|
+
* // "type": "dice", "count": 3, "sides": 6,
|
|
379
|
+
* // "rolls": [ ...the same three objects... ],
|
|
380
|
+
* // "total": 12, "start": 0, "end": 3
|
|
381
|
+
* // }
|
|
382
|
+
* // }
|
|
383
|
+
* ```
|
|
384
|
+
*
|
|
385
|
+
* @category Results
|
|
386
|
+
*/
|
|
387
|
+
export type RollResult = Readonly<{
|
|
66
388
|
/** Final computed total */
|
|
67
389
|
total: number;
|
|
68
390
|
/** Original input notation */
|
|
@@ -71,18 +393,28 @@ export type RollResult = {
|
|
|
71
393
|
expression: string;
|
|
72
394
|
/** Rendered result with individual rolls shown */
|
|
73
395
|
rendered: string;
|
|
74
|
-
/**
|
|
396
|
+
/**
|
|
397
|
+
* All individual die results, in evaluation order.
|
|
398
|
+
*
|
|
399
|
+
* Shares `DieResult` object references with the `rolls[]` arrays inside
|
|
400
|
+
* `parts` — no deep clone. Mutating a die here is visible through the part
|
|
401
|
+
* tree and vice versa; clone first if that matters.
|
|
402
|
+
*/
|
|
75
403
|
rolls: DieResult[];
|
|
404
|
+
/** Structured breakdown of the evaluated expression, mirroring the AST 1:1. */
|
|
405
|
+
parts: RollPart;
|
|
76
406
|
/**
|
|
77
407
|
* Number of dice tagged as success across the whole expression. Present
|
|
78
408
|
* only when a success-counting modifier was used. Independent of `total` —
|
|
79
|
-
* arithmetic on top of a success count (e.g. `5d6>=5
|
|
80
|
-
* but not `successes`.
|
|
409
|
+
* arithmetic on top of a success count (e.g. `{5d6>=5}+2`) affects `total`
|
|
410
|
+
* but not `successes`. Success counts are terminal, so the group braces are
|
|
411
|
+
* required: `5d6>=5 * 2` is an `INVALID_SUCCESS_COUNT_TARGET` parse error.
|
|
81
412
|
*/
|
|
82
413
|
successes?: number;
|
|
83
414
|
/**
|
|
84
415
|
* Number of dice tagged as failure across the whole expression. Present
|
|
85
|
-
*
|
|
416
|
+
* whenever a success-counting modifier was used — `0` when no failure
|
|
417
|
+
* threshold was given, since nothing can be tagged as a failure.
|
|
86
418
|
*/
|
|
87
419
|
failures?: number;
|
|
88
420
|
/**
|
|
@@ -95,18 +427,78 @@ export type RollResult = {
|
|
|
95
427
|
* exactly one kept d20 was rolled on the roll side of a `vs` expression.
|
|
96
428
|
*/
|
|
97
429
|
natural?: number;
|
|
98
|
-
}
|
|
430
|
+
}>;
|
|
99
431
|
|
|
100
432
|
/**
|
|
101
|
-
*
|
|
433
|
+
* Evaluation guardrails and variable resolution, shared by `EvaluateOptions`
|
|
434
|
+
* and `RollOptions`. Every field is optional — the defaults bound adversarial
|
|
435
|
+
* notation without capping any realistic expression.
|
|
436
|
+
*
|
|
437
|
+
* Tighten them when the notation comes from users you do not control; the
|
|
438
|
+
* caps are the difference between a typed `EvaluatorError` and a request that
|
|
439
|
+
* rolls ten million dice. Parse depth is capped separately and
|
|
440
|
+
* unconditionally by {@link MAX_PARSE_DEPTH}.
|
|
441
|
+
*
|
|
442
|
+
* The three numeric limits fail closed: omit one — or pass `undefined` or
|
|
443
|
+
* `null`, the no-options path a partial config produces — and it takes its
|
|
444
|
+
* default, but supply anything else that is not a safe integer in range — a
|
|
445
|
+
* string, `NaN`, `±Infinity`, a negative, a fraction — and evaluation throws
|
|
446
|
+
* `INVALID_EVALUATION_LIMIT` before rolling. `maxDice` accepts `>= 1`, the two
|
|
447
|
+
* iteration limits `>= 0`. No coercion: `maxDice: Number(input)` on
|
|
448
|
+
* unparseable input rejects rather than quietly reverting to the permissive
|
|
449
|
+
* default.
|
|
450
|
+
*
|
|
451
|
+
* @example Untrusted input
|
|
452
|
+
* ```typescript
|
|
453
|
+
* import { isRollParserError, roll } from 'roll-parser';
|
|
454
|
+
*
|
|
455
|
+
* const limits = {
|
|
456
|
+
* maxDice: 100,
|
|
457
|
+
* maxExplodeIterations: 20,
|
|
458
|
+
* maxRerollIterations: 20,
|
|
459
|
+
* };
|
|
460
|
+
*
|
|
461
|
+
* try {
|
|
462
|
+
* roll('99999d6', limits).total;
|
|
463
|
+
* } catch (error) {
|
|
464
|
+
* isRollParserError(error) && error.code; // 'DICE_LIMIT_EXCEEDED'
|
|
465
|
+
* }
|
|
466
|
+
* ```
|
|
467
|
+
*
|
|
468
|
+
* @example Variables
|
|
469
|
+
* ```typescript
|
|
470
|
+
* import { roll } from 'roll-parser';
|
|
471
|
+
*
|
|
472
|
+
* roll('1d20+@str', { context: { str: 4 }, seed: 'demo' }).total; // 5
|
|
473
|
+
* roll('1d20+@str', { onMissingVariable: 'zero', seed: 'demo' }).total; // 1
|
|
474
|
+
* ```
|
|
475
|
+
*
|
|
476
|
+
* @category Core
|
|
102
477
|
*/
|
|
103
|
-
export type
|
|
104
|
-
/**
|
|
105
|
-
notation?: string;
|
|
106
|
-
/** Maximum total dice allowed per evaluation (default: 10,000) */
|
|
478
|
+
export type EvaluationOptions = {
|
|
479
|
+
/** Maximum total dice allowed per evaluation; integer >= 1 (default: 10,000) */
|
|
107
480
|
maxDice?: number;
|
|
108
|
-
/** Maximum explosion iterations allowed per die (default: 1,000) */
|
|
481
|
+
/** Maximum explosion iterations allowed per die; integer >= 0 (default: 1,000) */
|
|
109
482
|
maxExplodeIterations?: number;
|
|
110
|
-
/** Maximum reroll iterations allowed per die (default: 1,000) */
|
|
483
|
+
/** Maximum reroll iterations allowed per die; integer >= 0 (default: 1,000) */
|
|
111
484
|
maxRerollIterations?: number;
|
|
485
|
+
/** Variable context for `@name` / `@{name}` references (default: empty) */
|
|
486
|
+
context?: Readonly<Record<string, number>>;
|
|
487
|
+
/** Behavior when a referenced variable is missing from context (default: 'throw') */
|
|
488
|
+
onMissingVariable?: 'throw' | 'zero';
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Options for {@link evaluate}: the shared {@link EvaluationOptions} plus the
|
|
493
|
+
* original notation, which `evaluate` cannot recover from an AST.
|
|
494
|
+
*
|
|
495
|
+
* @category Core
|
|
496
|
+
*/
|
|
497
|
+
export type EvaluateOptions = EvaluationOptions & {
|
|
498
|
+
/**
|
|
499
|
+
* Original notation string, echoed back as `RollResult.notation`. When
|
|
500
|
+
* omitted, falls back to the normalized `expression` reconstructed from the
|
|
501
|
+
* AST — {@link roll} always forwards the string the caller typed.
|
|
502
|
+
*/
|
|
503
|
+
notation?: string;
|
|
112
504
|
};
|
package/src/version.ts
ADDED