roll-parser 3.0.0-beta.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -9
- package/MIGRATION.md +219 -0
- package/README.md +1026 -154
- 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 +15 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +18 -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 -17
- 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 +27 -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 +89 -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 +41 -75
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/evaluator.js +914 -0
- package/dist/evaluator/evaluator.js.map +1 -0
- 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 +57 -5
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -1
- package/dist/evaluator/modifiers/crit-threshold.js +42 -0
- package/dist/evaluator/modifiers/crit-threshold.js.map +1 -0
- package/dist/evaluator/modifiers/die-bound.d.ts +29 -0
- package/dist/evaluator/modifiers/die-bound.d.ts.map +1 -0
- package/dist/evaluator/modifiers/die-bound.js +17 -0
- package/dist/evaluator/modifiers/die-bound.js.map +1 -0
- package/dist/evaluator/modifiers/explode.d.ts +13 -5
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
- package/dist/evaluator/modifiers/explode.js +105 -0
- package/dist/evaluator/modifiers/explode.js.map +1 -0
- package/dist/evaluator/modifiers/flags.d.ts +47 -0
- package/dist/evaluator/modifiers/flags.d.ts.map +1 -0
- package/dist/evaluator/modifiers/flags.js +25 -0
- package/dist/evaluator/modifiers/flags.js.map +1 -0
- package/dist/evaluator/modifiers/keep-drop.d.ts +15 -29
- 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 +12 -4
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
- package/dist/evaluator/modifiers/reroll.js +68 -0
- package/dist/evaluator/modifiers/reroll.js.map +1 -0
- package/dist/evaluator/modifiers/sort.d.ts +5 -1
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -1
- 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 +6 -7
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
- package/dist/evaluator/modifiers/success-count.js +25 -0
- package/dist/evaluator/modifiers/success-count.js.map +1 -0
- package/dist/index.d.ts +33 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2560
- package/dist/index.js.map +1 -26
- package/dist/lexer/lexer.d.ts +50 -5
- 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 +31 -6
- 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 +324 -165
- 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 +118 -14
- 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/render.d.ts +95 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +227 -0
- package/dist/render.js.map +1 -0
- package/dist/rng/mock.d.ts +73 -12
- 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 +141 -9
- 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 +58 -28
- 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 -41
- package/dist/testing.js.map +1 -11
- package/dist/types.d.ts +349 -47
- 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 -34
- package/src/cli/args.ts +66 -10
- package/src/cli/format.ts +37 -26
- package/src/cli/index.ts +27 -84
- package/src/cli/main.ts +129 -0
- package/src/errors.ts +480 -27
- package/src/evaluator/die.ts +51 -0
- package/src/evaluator/env.ts +105 -0
- package/src/evaluator/evaluator.ts +693 -434
- package/src/evaluator/modifiers/crit-threshold.ts +96 -14
- package/src/evaluator/modifiers/die-bound.ts +48 -0
- package/src/evaluator/modifiers/explode.ts +70 -62
- package/src/evaluator/modifiers/flags.ts +78 -0
- package/src/evaluator/modifiers/keep-drop.ts +129 -127
- package/src/evaluator/modifiers/reroll.ts +44 -56
- package/src/evaluator/modifiers/sort.ts +21 -2
- package/src/evaluator/modifiers/success-count.ts +24 -12
- package/src/index.ts +56 -35
- package/src/lexer/lexer.ts +107 -34
- package/src/lexer/tokens.ts +31 -6
- package/src/parser/ast.ts +333 -346
- package/src/parser/guards.ts +248 -0
- package/src/parser/parser.ts +419 -242
- package/src/render.ts +392 -0
- package/src/rng/mock.ts +74 -13
- package/src/rng/seeded.ts +299 -64
- package/src/rng/types.ts +57 -0
- package/src/roll.ts +64 -47
- package/src/testing.ts +5 -9
- package/src/types.ts +353 -46
- package/src/version.ts +2 -0
- package/dist/cli.js +0 -2608
- package/dist/cli.js.map +0 -28
- package/dist/evaluator/index.d.ts +0 -8
- package/dist/evaluator/index.d.ts.map +0 -1
- package/dist/rng/index.d.ts +0 -8
- package/dist/rng/index.d.ts.map +0 -1
- package/src/evaluator/index.ts +0 -14
- package/src/rng/index.ts +0 -8
package/src/parser/ast.ts
CHANGED
|
@@ -13,92 +13,182 @@ import type { ComparePoint } from '../types.js';
|
|
|
13
13
|
* original notation string. The parser sets both on every node it produces;
|
|
14
14
|
* they are typed optional so hand-constructed ASTs (tests, programmatic
|
|
15
15
|
* consumers) remain valid without positions.
|
|
16
|
+
*
|
|
17
|
+
* `notation.slice(start, end)` recovers the source text of any parsed node,
|
|
18
|
+
* which is how the CLI underlines the failing sub-expression.
|
|
19
|
+
*
|
|
20
|
+
* @category AST
|
|
16
21
|
*/
|
|
17
22
|
export type NodeSpan = {
|
|
18
|
-
start?: number;
|
|
19
|
-
end?: number;
|
|
23
|
+
readonly start?: number;
|
|
24
|
+
readonly end?: number;
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
|
-
* Numeric literal node
|
|
28
|
+
* Numeric literal node — an integer or decimal written directly in the
|
|
29
|
+
* notation (`3`, `2.5`). Leaf node.
|
|
30
|
+
*
|
|
31
|
+
* Decimals are accepted and carried through arithmetic exactly as written;
|
|
32
|
+
* only dice counts and sides are required to resolve to integers.
|
|
33
|
+
*
|
|
34
|
+
* @category AST
|
|
24
35
|
*/
|
|
25
36
|
export type LiteralNode = NodeSpan & {
|
|
26
|
-
type: 'Literal';
|
|
27
|
-
value: number;
|
|
37
|
+
readonly type: 'Literal';
|
|
38
|
+
readonly value: number;
|
|
28
39
|
};
|
|
29
40
|
|
|
30
41
|
/**
|
|
31
|
-
* Dice roll node.
|
|
32
|
-
*
|
|
42
|
+
* Dice roll node (`NdX`, `dX`, `d%`).
|
|
43
|
+
*
|
|
44
|
+
* `count` and `sides` are full sub-expressions, not numbers, which is what
|
|
45
|
+
* makes computed dice like `(1+1)d(3*2)` and `(1d4)d6` expressible. Both are
|
|
46
|
+
* evaluated first and must resolve to integers; `d%` desugars to a `sides`
|
|
47
|
+
* literal of `100`, and an omitted count to a `count` literal of `1`.
|
|
48
|
+
*
|
|
49
|
+
* Dice rolled by the `count`/`sides` sub-expressions are meta-expression dice
|
|
50
|
+
* — they land in `RollResult.rolls` tagged `'meta'` and belong to no pool.
|
|
51
|
+
*
|
|
52
|
+
* @category AST
|
|
33
53
|
*/
|
|
34
54
|
export type DiceNode = NodeSpan & {
|
|
35
|
-
type: 'Dice';
|
|
36
|
-
count: ASTNode;
|
|
37
|
-
sides: ASTNode;
|
|
55
|
+
readonly type: 'Dice';
|
|
56
|
+
readonly count: ASTNode;
|
|
57
|
+
readonly sides: ASTNode;
|
|
38
58
|
};
|
|
39
59
|
|
|
40
60
|
/**
|
|
41
61
|
* Fate/Fudge dice node (`dF`).
|
|
42
62
|
* Each die produces a result in {-1, 0, +1}. No configurable sides.
|
|
63
|
+
*
|
|
64
|
+
* Fate dice carry `sides: 0` as a sentinel in their `DieResult`, so the
|
|
65
|
+
* default `critical`/`fumble` rule never fires — it is guarded on `sides > 1`,
|
|
66
|
+
* which also stops a `+1` face from reading as a fumbled 1. An explicit
|
|
67
|
+
* threshold still applies: `4dFcs>0` and `4dFcf=-1` set the flags, while the
|
|
68
|
+
* bare `cs`/`cf` forms are rejected at parse time.
|
|
69
|
+
*
|
|
70
|
+
* @category AST
|
|
43
71
|
*/
|
|
44
72
|
export type FateDiceNode = NodeSpan & {
|
|
45
|
-
type: 'FateDice';
|
|
46
|
-
count: ASTNode;
|
|
73
|
+
readonly type: 'FateDice';
|
|
74
|
+
readonly count: ASTNode;
|
|
47
75
|
};
|
|
48
76
|
|
|
49
77
|
/**
|
|
50
|
-
* Binary operation node.
|
|
78
|
+
* Binary operation node — the arithmetic backbone of an expression.
|
|
79
|
+
*
|
|
80
|
+
* | Operator | Meaning | Precedence | Associativity |
|
|
81
|
+
* |----------|---------|-----------:|---------------|
|
|
82
|
+
* | `+` `-` | add, subtract | 10 | left |
|
|
83
|
+
* | `*` `/` `%` | multiply, divide, modulo | 20 | left |
|
|
84
|
+
* | `**` | power (also spelled `^`) | 30 | right |
|
|
85
|
+
*
|
|
86
|
+
* Dice bind tighter than all of them (40), and postfix modifiers sit at 35,
|
|
87
|
+
* so `2d6+3` is `(2d6)+3` and `4d6kh3*2` is `(4d6kh3)*2`. The `vs` operator
|
|
88
|
+
* binds loosest of all (2) and has its own {@link VersusNode}.
|
|
89
|
+
*
|
|
90
|
+
* `/` and `%` throw `DIVISION_BY_ZERO` / `MODULO_BY_ZERO` on a zero right
|
|
91
|
+
* side; any operator producing a non-finite total throws `NON_FINITE_RESULT`.
|
|
92
|
+
* Division is not rounded — wrap it in `floor()` if you need an integer.
|
|
93
|
+
*
|
|
94
|
+
* @category AST
|
|
51
95
|
*/
|
|
52
96
|
export type BinaryOpNode = NodeSpan & {
|
|
53
|
-
type: 'BinaryOp';
|
|
54
|
-
operator: '+' | '-' | '*' | '/' | '%' | '**';
|
|
55
|
-
left: ASTNode;
|
|
56
|
-
right: ASTNode;
|
|
97
|
+
readonly type: 'BinaryOp';
|
|
98
|
+
readonly operator: '+' | '-' | '*' | '/' | '%' | '**';
|
|
99
|
+
readonly left: ASTNode;
|
|
100
|
+
readonly right: ASTNode;
|
|
57
101
|
};
|
|
58
102
|
|
|
59
103
|
/**
|
|
60
|
-
* Unary operation node.
|
|
104
|
+
* Unary operation node — prefix negation, the only unary operator.
|
|
105
|
+
*
|
|
106
|
+
* Binding power 25 sits between multiplication and power, so `-1d4` is
|
|
107
|
+
* `-(1d4)` (negate the roll) rather than `(-1)d4`, while `-2**2` is `-(2**2)`.
|
|
108
|
+
*
|
|
109
|
+
* @category AST
|
|
61
110
|
*/
|
|
62
111
|
export type UnaryOpNode = NodeSpan & {
|
|
63
|
-
type: 'UnaryOp';
|
|
64
|
-
operator: '-';
|
|
65
|
-
operand: ASTNode;
|
|
112
|
+
readonly type: 'UnaryOp';
|
|
113
|
+
readonly operator: '-';
|
|
114
|
+
readonly operand: ASTNode;
|
|
66
115
|
};
|
|
67
116
|
|
|
68
117
|
/**
|
|
69
|
-
* Keep/drop modifier node
|
|
118
|
+
* Keep/drop modifier node (`khN`, `klN`, `dhN`, `dlN`, and the `kN`
|
|
119
|
+
* shorthand for `khN`).
|
|
120
|
+
*
|
|
70
121
|
* Wraps a dice expression with keep highest/lowest or drop highest/lowest.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
* An omitted count defaults to 1, so `4d6kh` is `4d6kh1`. `count` is a full
|
|
123
|
+
* sub-expression, so `4d6kh(1d2)` is legal — and its dice are drawn *before*
|
|
124
|
+
* the pool.
|
|
125
|
+
*
|
|
126
|
+
* Chained modifiers (`4d6kh3dl1`) do not nest: the evaluator flattens the
|
|
127
|
+
* chain and applies each spec independently to the same pool, unioning the
|
|
128
|
+
* dropped sets, which is the Roll20 rule.
|
|
129
|
+
*
|
|
130
|
+
* @category AST
|
|
131
|
+
*/
|
|
132
|
+
export type KeepDropNode = NodeSpan & {
|
|
133
|
+
readonly type: 'KeepDrop';
|
|
134
|
+
readonly kind: 'keep' | 'drop';
|
|
135
|
+
readonly selector: 'highest' | 'lowest';
|
|
136
|
+
readonly count: ASTNode;
|
|
137
|
+
readonly target: ASTNode;
|
|
78
138
|
};
|
|
79
139
|
|
|
80
140
|
/**
|
|
81
141
|
* Exploding dice node (`!`, `!!`, `!p`, `!>Y`).
|
|
82
142
|
* Wraps a dice expression with a standard, compounding, or penetrating
|
|
83
143
|
* explosion. An absent `threshold` means "explode on the die's maximum face".
|
|
144
|
+
*
|
|
145
|
+
* Thresholds accept any comparator. In notation like `1d10!=10` (the
|
|
146
|
+
* Storyteller "10-again" rule) the `!` is the explode marker and `=10` the
|
|
147
|
+
* equality threshold — there is no `!=` comparator. Per-die explosion count
|
|
148
|
+
* is capped by `EvaluationOptions.maxExplodeIterations`.
|
|
149
|
+
*
|
|
150
|
+
* @category AST
|
|
84
151
|
*/
|
|
85
152
|
export type ExplodeNode = NodeSpan & {
|
|
86
|
-
type: 'Explode';
|
|
87
|
-
variant: 'standard' | 'compound' | 'penetrating';
|
|
88
|
-
threshold?: ComparePoint;
|
|
89
|
-
target: ASTNode;
|
|
153
|
+
readonly type: 'Explode';
|
|
154
|
+
readonly variant: 'standard' | 'compound' | 'penetrating';
|
|
155
|
+
readonly threshold?: ComparePoint;
|
|
156
|
+
readonly target: ASTNode;
|
|
90
157
|
};
|
|
91
158
|
|
|
92
159
|
/**
|
|
93
160
|
* Reroll node (`r<COND>`, `ro<COND>`).
|
|
94
161
|
* Re-rolls dice that match a comparison condition. `once: true` for `ro`
|
|
95
162
|
* keeps the second result regardless of match; `once: false` for `r`
|
|
96
|
-
* re-rolls recursively until the condition no longer matches
|
|
163
|
+
* re-rolls recursively until the condition no longer matches, bounded by
|
|
164
|
+
* `EvaluationOptions.maxRerollIterations`.
|
|
165
|
+
*
|
|
166
|
+
* @category AST
|
|
97
167
|
*/
|
|
98
168
|
export type RerollNode = NodeSpan & {
|
|
99
|
-
type: 'Reroll';
|
|
100
|
-
once: boolean;
|
|
101
|
-
condition: ComparePoint;
|
|
169
|
+
readonly type: 'Reroll';
|
|
170
|
+
readonly once: boolean;
|
|
171
|
+
readonly condition: ComparePoint;
|
|
172
|
+
readonly target: ASTNode;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Per-die clamp node (`minN`, `maxN`).
|
|
177
|
+
* `4d6min2` raises every die below 2 to 2; `4d6max5` lowers every die above
|
|
178
|
+
* 5 to 5. The clamp rewrites `DieResult.result` (preserving the raw face in
|
|
179
|
+
* `initialResult`) and re-sums the pool; `critical`/`fumble` keep reflecting
|
|
180
|
+
* the natural face. The bound is a full sub-expression (`4d6min(1d2)`),
|
|
181
|
+
* drawn *after* the pool like other threshold arguments.
|
|
182
|
+
*
|
|
183
|
+
* Chained bounds nest and apply left to right: `4d6min2max5` clamps into
|
|
184
|
+
* [2, 5].
|
|
185
|
+
*
|
|
186
|
+
* @category AST
|
|
187
|
+
*/
|
|
188
|
+
export type DieBoundNode = NodeSpan & {
|
|
189
|
+
type: 'DieBound';
|
|
190
|
+
bound: 'min' | 'max';
|
|
191
|
+
value: ASTNode;
|
|
102
192
|
target: ASTNode;
|
|
103
193
|
};
|
|
104
194
|
|
|
@@ -110,12 +200,14 @@ export type RerollNode = NodeSpan & {
|
|
|
110
200
|
* `SuccessCountNode` may not be wrapped by any postfix modifier, binary
|
|
111
201
|
* operator, unary operator, versus operand, or function argument. The
|
|
112
202
|
* `failThreshold` accepts any `CompareOp`; bare `fN` defaults to `operator: '='`.
|
|
203
|
+
*
|
|
204
|
+
* @category AST
|
|
113
205
|
*/
|
|
114
206
|
export type SuccessCountNode = NodeSpan & {
|
|
115
|
-
type: 'SuccessCount';
|
|
116
|
-
target: ASTNode;
|
|
117
|
-
threshold: ComparePoint;
|
|
118
|
-
failThreshold?: ComparePoint;
|
|
207
|
+
readonly type: 'SuccessCount';
|
|
208
|
+
readonly target: ASTNode;
|
|
209
|
+
readonly threshold: ComparePoint;
|
|
210
|
+
readonly failThreshold?: ComparePoint;
|
|
119
211
|
};
|
|
120
212
|
|
|
121
213
|
/**
|
|
@@ -127,11 +219,13 @@ export type SuccessCountNode = NodeSpan & {
|
|
|
127
219
|
* roll side. Lowest-precedence operator — chaining (`a vs b vs c`) is
|
|
128
220
|
* rejected at parse time; nesting via parens (`a vs (b vs c)`) is rejected
|
|
129
221
|
* at evaluation time.
|
|
222
|
+
*
|
|
223
|
+
* @category AST
|
|
130
224
|
*/
|
|
131
225
|
export type VersusNode = NodeSpan & {
|
|
132
|
-
type: 'Versus';
|
|
133
|
-
roll: ASTNode;
|
|
134
|
-
dc: ASTNode;
|
|
226
|
+
readonly type: 'Versus';
|
|
227
|
+
readonly roll: ASTNode;
|
|
228
|
+
readonly dc: ASTNode;
|
|
135
229
|
};
|
|
136
230
|
|
|
137
231
|
/**
|
|
@@ -141,11 +235,16 @@ export type VersusNode = NodeSpan & {
|
|
|
141
235
|
* variadic functions `max`, `min` (minimum 2 args). Arity is validated at
|
|
142
236
|
* parse time against a static table; by the time the evaluator sees a
|
|
143
237
|
* `FunctionCallNode`, `args.length` is guaranteed to match the function.
|
|
238
|
+
*
|
|
239
|
+
* `max`/`min` are variadic with no upper bound, so `max(1d20, 1d20, 1d20)`
|
|
240
|
+
* is a valid three-way advantage roll.
|
|
241
|
+
*
|
|
242
|
+
* @category AST
|
|
144
243
|
*/
|
|
145
244
|
export type FunctionCallNode = NodeSpan & {
|
|
146
|
-
type: 'FunctionCall';
|
|
147
|
-
name: string;
|
|
148
|
-
args: ASTNode[];
|
|
245
|
+
readonly type: 'FunctionCall';
|
|
246
|
+
readonly name: string;
|
|
247
|
+
readonly args: readonly ASTNode[];
|
|
149
248
|
};
|
|
150
249
|
|
|
151
250
|
/**
|
|
@@ -155,10 +254,12 @@ export type FunctionCallNode = NodeSpan & {
|
|
|
155
254
|
* `RollResult.expression` and `RollResult.rendered` round-trip through
|
|
156
255
|
* `parse` without losing precedence information. Semantically transparent:
|
|
157
256
|
* evaluation returns the inner expression's value unchanged.
|
|
257
|
+
*
|
|
258
|
+
* @category AST
|
|
158
259
|
*/
|
|
159
260
|
export type GroupedNode = NodeSpan & {
|
|
160
|
-
type: 'Grouped';
|
|
161
|
-
expression: ASTNode;
|
|
261
|
+
readonly type: 'Grouped';
|
|
262
|
+
readonly expression: ASTNode;
|
|
162
263
|
};
|
|
163
264
|
|
|
164
265
|
/**
|
|
@@ -169,10 +270,15 @@ export type GroupedNode = NodeSpan & {
|
|
|
169
270
|
* lexer preserves case in the `AT` token's `value`, distinct from other
|
|
170
271
|
* identifier tokens which lowercase. Leaf node — no LED, never wraps a
|
|
171
272
|
* sub-expression.
|
|
273
|
+
*
|
|
274
|
+
* A name missing from `context` throws `UNDEFINED_VARIABLE` unless
|
|
275
|
+
* `onMissingVariable: 'zero'` is set.
|
|
276
|
+
*
|
|
277
|
+
* @category AST
|
|
172
278
|
*/
|
|
173
279
|
export type VariableNode = NodeSpan & {
|
|
174
|
-
type: 'Variable';
|
|
175
|
-
name: string;
|
|
280
|
+
readonly type: 'Variable';
|
|
281
|
+
readonly name: string;
|
|
176
282
|
};
|
|
177
283
|
|
|
178
284
|
/**
|
|
@@ -183,10 +289,12 @@ export type VariableNode = NodeSpan & {
|
|
|
183
289
|
* with the sub-roll count. `expressions.length === 1` is a passthrough
|
|
184
290
|
* (flat-pool when wrapped by keep/drop); `expressions.length >= 2` treats
|
|
185
291
|
* each sub-roll's subtotal as a compound die for keep/drop selection.
|
|
292
|
+
*
|
|
293
|
+
* @category AST
|
|
186
294
|
*/
|
|
187
295
|
export type GroupNode = NodeSpan & {
|
|
188
|
-
type: 'Group';
|
|
189
|
-
expressions: ASTNode[];
|
|
296
|
+
readonly type: 'Group';
|
|
297
|
+
readonly expressions: readonly ASTNode[];
|
|
190
298
|
};
|
|
191
299
|
|
|
192
300
|
/**
|
|
@@ -197,41 +305,91 @@ export type GroupNode = NodeSpan & {
|
|
|
197
305
|
* `successes`/`failures`, or any die-level flag (`kept`/`dropped`/
|
|
198
306
|
* `critical`/`fumble`). Dropped dice retain their `dropped` flag and
|
|
199
307
|
* appear in sorted position alongside kept dice.
|
|
308
|
+
*
|
|
309
|
+
* `s` and `sa` both mean ascending; `sd` is descending.
|
|
310
|
+
*
|
|
311
|
+
* @category AST
|
|
200
312
|
*/
|
|
201
313
|
export type SortNode = NodeSpan & {
|
|
202
|
-
type: 'Sort';
|
|
203
|
-
order: 'ascending' | 'descending';
|
|
204
|
-
target: ASTNode;
|
|
314
|
+
readonly type: 'Sort';
|
|
315
|
+
readonly order: 'ascending' | 'descending';
|
|
316
|
+
readonly target: ASTNode;
|
|
205
317
|
};
|
|
206
318
|
|
|
207
319
|
/**
|
|
208
|
-
* Sentinel for bare `cs` / `cf` without a ComparePoint. Resolved
|
|
209
|
-
*
|
|
210
|
-
*
|
|
320
|
+
* Sentinel for bare `cs` / `cf` without a ComparePoint. Resolved at
|
|
321
|
+
* evaluation time against each die's own `sides` and its natural face
|
|
322
|
+
* (`initialResult ?? result`): critical when that face equals `sides`,
|
|
323
|
+
* fumble when it equals 1.
|
|
324
|
+
*
|
|
325
|
+
* @category AST
|
|
211
326
|
*/
|
|
212
327
|
export type CritThreshold = ComparePoint | 'default';
|
|
213
328
|
|
|
214
329
|
/**
|
|
215
330
|
* Critical threshold modifier node (`cs`, `cf`).
|
|
216
331
|
*
|
|
217
|
-
* Overrides the default `critical`/`fumble` flag logic for
|
|
218
|
-
*
|
|
332
|
+
* Overrides the default `critical`/`fumble` flag logic for `target`'s dice
|
|
333
|
+
* pool — including dice an enclosing `!` or `r` adds to it afterwards, which
|
|
334
|
+
* `target` never produced. Bare `cs`/`cf` uses the `'default'` sentinel
|
|
219
335
|
* (max face / 1). Custom thresholds accept any ComparePoint. Chaining
|
|
220
336
|
* collapses into a single node — `1d20cs=20cs=1cf>18` has two success
|
|
221
337
|
* and one fail threshold. Display-only: does not change `total`,
|
|
222
338
|
* explosion triggers, or success counting. `cs` and `cf` are independent
|
|
223
339
|
* overrides — a side with no explicit thresholds keeps the default rule
|
|
224
340
|
* (the evaluator substitutes the `'default'` sentinel at apply time).
|
|
341
|
+
*
|
|
342
|
+
* @category AST
|
|
225
343
|
*/
|
|
226
344
|
export type CritThresholdNode = NodeSpan & {
|
|
227
|
-
type: 'CritThreshold';
|
|
228
|
-
successThresholds: CritThreshold[];
|
|
229
|
-
failThresholds: CritThreshold[];
|
|
230
|
-
target: ASTNode;
|
|
345
|
+
readonly type: 'CritThreshold';
|
|
346
|
+
readonly successThresholds: readonly CritThreshold[];
|
|
347
|
+
readonly failThresholds: readonly CritThreshold[];
|
|
348
|
+
readonly target: ASTNode;
|
|
231
349
|
};
|
|
232
350
|
|
|
233
351
|
/**
|
|
234
|
-
*
|
|
352
|
+
* Discriminated union of all 17 AST node types — what {@link parse} returns
|
|
353
|
+
* and what {@link evaluate} consumes.
|
|
354
|
+
*
|
|
355
|
+
* Narrow it either by switching on `node.type` (PascalCase discriminants, as
|
|
356
|
+
* opposed to the camelCase ones on {@link RollPart}) or with the exported
|
|
357
|
+
* type guards: {@link isLiteral}, {@link isDice}, {@link isFateDice},
|
|
358
|
+
* {@link isBinaryOp}, {@link isUnaryOp}, {@link isKeepDrop},
|
|
359
|
+
* {@link isExplode}, {@link isReroll}, {@link isDieBound},
|
|
360
|
+
* {@link isSuccessCount}, {@link isVersus}, {@link isFunctionCall},
|
|
361
|
+
* {@link isGrouped}, {@link isVariable}, {@link isGroup}, {@link isSort},
|
|
362
|
+
* {@link isCritThreshold}.
|
|
363
|
+
*
|
|
364
|
+
* Nodes are plain data with no methods, so they are structurally clonable and
|
|
365
|
+
* safe to cache. Every parser-produced node carries a {@link NodeSpan}.
|
|
366
|
+
*
|
|
367
|
+
* @example A guard-based walker — count the dice an expression can roll
|
|
368
|
+
* ```typescript
|
|
369
|
+
* import {
|
|
370
|
+
* type ASTNode, isBinaryOp, isDice, isFateDice, isGroup, isGrouped,
|
|
371
|
+
* isFunctionCall, isLiteral, isUnaryOp, isVersus, parse,
|
|
372
|
+
* } from 'roll-parser';
|
|
373
|
+
*
|
|
374
|
+
* function countPools(node: ASTNode): number {
|
|
375
|
+
* if (isDice(node) || isFateDice(node)) return 1;
|
|
376
|
+
* if (isLiteral(node)) return 0;
|
|
377
|
+
* if (isBinaryOp(node)) return countPools(node.left) + countPools(node.right);
|
|
378
|
+
* if (isUnaryOp(node)) return countPools(node.operand);
|
|
379
|
+
* if (isGrouped(node)) return countPools(node.expression);
|
|
380
|
+
* if (isVersus(node)) return countPools(node.roll) + countPools(node.dc);
|
|
381
|
+
* if (isGroup(node)) return node.expressions.reduce((n, e) => n + countPools(e), 0);
|
|
382
|
+
* if (isFunctionCall(node)) return node.args.reduce((n, a) => n + countPools(a), 0);
|
|
383
|
+
* // Every remaining variant is a postfix modifier wrapping `target`.
|
|
384
|
+
* return 'target' in node ? countPools(node.target) : 0;
|
|
385
|
+
* }
|
|
386
|
+
*
|
|
387
|
+
* countPools(parse('2d6+3')); // 1
|
|
388
|
+
* countPools(parse('{2d20kh1+5, 3d8!}kh1')); // 2
|
|
389
|
+
* countPools(parse('1+2')); // 0
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @category AST
|
|
235
393
|
*/
|
|
236
394
|
export type ASTNode =
|
|
237
395
|
| LiteralNode
|
|
@@ -239,9 +397,10 @@ export type ASTNode =
|
|
|
239
397
|
| FateDiceNode
|
|
240
398
|
| BinaryOpNode
|
|
241
399
|
| UnaryOpNode
|
|
242
|
-
|
|
|
400
|
+
| KeepDropNode
|
|
243
401
|
| ExplodeNode
|
|
244
402
|
| RerollNode
|
|
403
|
+
| DieBoundNode
|
|
245
404
|
| SuccessCountNode
|
|
246
405
|
| VersusNode
|
|
247
406
|
| FunctionCallNode
|
|
@@ -252,377 +411,205 @@ export type ASTNode =
|
|
|
252
411
|
| CritThresholdNode;
|
|
253
412
|
|
|
254
413
|
/**
|
|
255
|
-
*
|
|
414
|
+
* Narrows an {@link ASTNode} to a numeric literal (`3`, `2.5`).
|
|
415
|
+
*
|
|
416
|
+
* @param node - Any AST node
|
|
417
|
+
* @returns `true` when `node` is a {@link LiteralNode}
|
|
418
|
+
*
|
|
419
|
+
* @category AST
|
|
256
420
|
*/
|
|
257
421
|
export function isLiteral(node: ASTNode): node is LiteralNode {
|
|
258
422
|
return node.type === 'Literal';
|
|
259
423
|
}
|
|
260
424
|
|
|
261
425
|
/**
|
|
262
|
-
*
|
|
426
|
+
* Narrows an {@link ASTNode} to a dice pool (`2d6`, `d%`, `(1d4)d6`).
|
|
427
|
+
*
|
|
428
|
+
* @param node - Any AST node
|
|
429
|
+
* @returns `true` when `node` is a {@link DiceNode}
|
|
430
|
+
*
|
|
431
|
+
* @category AST
|
|
263
432
|
*/
|
|
264
433
|
export function isDice(node: ASTNode): node is DiceNode {
|
|
265
434
|
return node.type === 'Dice';
|
|
266
435
|
}
|
|
267
436
|
|
|
268
437
|
/**
|
|
269
|
-
*
|
|
438
|
+
* Narrows an {@link ASTNode} to a Fate/Fudge pool (`4dF`).
|
|
439
|
+
*
|
|
440
|
+
* @param node - Any AST node
|
|
441
|
+
* @returns `true` when `node` is a {@link FateDiceNode}
|
|
442
|
+
*
|
|
443
|
+
* @category AST
|
|
270
444
|
*/
|
|
271
445
|
export function isFateDice(node: ASTNode): node is FateDiceNode {
|
|
272
446
|
return node.type === 'FateDice';
|
|
273
447
|
}
|
|
274
448
|
|
|
275
449
|
/**
|
|
276
|
-
*
|
|
450
|
+
* Narrows an {@link ASTNode} to an arithmetic operation (`+ - * / % **`).
|
|
451
|
+
*
|
|
452
|
+
* @param node - Any AST node
|
|
453
|
+
* @returns `true` when `node` is a {@link BinaryOpNode}
|
|
454
|
+
*
|
|
455
|
+
* @category AST
|
|
277
456
|
*/
|
|
278
457
|
export function isBinaryOp(node: ASTNode): node is BinaryOpNode {
|
|
279
458
|
return node.type === 'BinaryOp';
|
|
280
459
|
}
|
|
281
460
|
|
|
282
461
|
/**
|
|
283
|
-
*
|
|
462
|
+
* Narrows an {@link ASTNode} to a prefix negation (`-1d4`).
|
|
463
|
+
*
|
|
464
|
+
* @param node - Any AST node
|
|
465
|
+
* @returns `true` when `node` is a {@link UnaryOpNode}
|
|
466
|
+
*
|
|
467
|
+
* @category AST
|
|
284
468
|
*/
|
|
285
469
|
export function isUnaryOp(node: ASTNode): node is UnaryOpNode {
|
|
286
470
|
return node.type === 'UnaryOp';
|
|
287
471
|
}
|
|
288
472
|
|
|
289
473
|
/**
|
|
290
|
-
*
|
|
474
|
+
* Narrows an {@link ASTNode} to a keep/drop modifier (`kh`, `kl`, `dh`, `dl`).
|
|
475
|
+
*
|
|
476
|
+
* @param node - Any AST node
|
|
477
|
+
* @returns `true` when `node` is a {@link KeepDropNode}
|
|
478
|
+
*
|
|
479
|
+
* @category AST
|
|
291
480
|
*/
|
|
292
|
-
export function
|
|
293
|
-
return node.type === '
|
|
481
|
+
export function isKeepDrop(node: ASTNode): node is KeepDropNode {
|
|
482
|
+
return node.type === 'KeepDrop';
|
|
294
483
|
}
|
|
295
484
|
|
|
296
485
|
/**
|
|
297
|
-
*
|
|
486
|
+
* Narrows an {@link ASTNode} to an explosion (`!`, `!!`, `!p`).
|
|
487
|
+
*
|
|
488
|
+
* @param node - Any AST node
|
|
489
|
+
* @returns `true` when `node` is a {@link ExplodeNode}
|
|
490
|
+
*
|
|
491
|
+
* @category AST
|
|
298
492
|
*/
|
|
299
493
|
export function isExplode(node: ASTNode): node is ExplodeNode {
|
|
300
494
|
return node.type === 'Explode';
|
|
301
495
|
}
|
|
302
496
|
|
|
303
497
|
/**
|
|
304
|
-
*
|
|
498
|
+
* Narrows an {@link ASTNode} to a reroll (`r`, `ro`).
|
|
499
|
+
*
|
|
500
|
+
* @param node - Any AST node
|
|
501
|
+
* @returns `true` when `node` is a {@link RerollNode}
|
|
502
|
+
*
|
|
503
|
+
* @category AST
|
|
305
504
|
*/
|
|
306
505
|
export function isReroll(node: ASTNode): node is RerollNode {
|
|
307
506
|
return node.type === 'Reroll';
|
|
308
507
|
}
|
|
309
508
|
|
|
310
509
|
/**
|
|
311
|
-
*
|
|
510
|
+
* Narrows an {@link ASTNode} to a success count (`>=6`, with optional `f1`).
|
|
511
|
+
*
|
|
512
|
+
* @param node - Any AST node
|
|
513
|
+
* @returns `true` when `node` is a {@link SuccessCountNode}
|
|
514
|
+
*
|
|
515
|
+
* @category AST
|
|
312
516
|
*/
|
|
313
517
|
export function isSuccessCount(node: ASTNode): node is SuccessCountNode {
|
|
314
518
|
return node.type === 'SuccessCount';
|
|
315
519
|
}
|
|
316
520
|
|
|
317
521
|
/**
|
|
318
|
-
*
|
|
522
|
+
* Narrows an {@link ASTNode} to a PF2e degree-of-success check (`vs`).
|
|
523
|
+
*
|
|
524
|
+
* @param node - Any AST node
|
|
525
|
+
* @returns `true` when `node` is a {@link VersusNode}
|
|
526
|
+
*
|
|
527
|
+
* @category AST
|
|
319
528
|
*/
|
|
320
529
|
export function isVersus(node: ASTNode): node is VersusNode {
|
|
321
530
|
return node.type === 'Versus';
|
|
322
531
|
}
|
|
323
532
|
|
|
324
533
|
/**
|
|
325
|
-
*
|
|
534
|
+
* Narrows an {@link ASTNode} to a math function call (`floor`, `max`, …).
|
|
535
|
+
*
|
|
536
|
+
* @param node - Any AST node
|
|
537
|
+
* @returns `true` when `node` is a {@link FunctionCallNode}
|
|
538
|
+
*
|
|
539
|
+
* @category AST
|
|
326
540
|
*/
|
|
327
541
|
export function isFunctionCall(node: ASTNode): node is FunctionCallNode {
|
|
328
542
|
return node.type === 'FunctionCall';
|
|
329
543
|
}
|
|
330
544
|
|
|
331
545
|
/**
|
|
332
|
-
*
|
|
546
|
+
* Narrows an {@link ASTNode} to a parenthesized group (`(1d6+2)`).
|
|
547
|
+
*
|
|
548
|
+
* @param node - Any AST node
|
|
549
|
+
* @returns `true` when `node` is a {@link GroupedNode}
|
|
550
|
+
*
|
|
551
|
+
* @category AST
|
|
333
552
|
*/
|
|
334
553
|
export function isGrouped(node: ASTNode): node is GroupedNode {
|
|
335
554
|
return node.type === 'Grouped';
|
|
336
555
|
}
|
|
337
556
|
|
|
338
557
|
/**
|
|
339
|
-
*
|
|
558
|
+
* Narrows an {@link ASTNode} to a variable reference (`@str`).
|
|
559
|
+
*
|
|
560
|
+
* @param node - Any AST node
|
|
561
|
+
* @returns `true` when `node` is a {@link VariableNode}
|
|
562
|
+
*
|
|
563
|
+
* @category AST
|
|
340
564
|
*/
|
|
341
565
|
export function isVariable(node: ASTNode): node is VariableNode {
|
|
342
566
|
return node.type === 'Variable';
|
|
343
567
|
}
|
|
344
568
|
|
|
345
569
|
/**
|
|
346
|
-
*
|
|
570
|
+
* Narrows an {@link ASTNode} to a braced grouped roll (`{1d8, 1d10}`).
|
|
571
|
+
*
|
|
572
|
+
* @param node - Any AST node
|
|
573
|
+
* @returns `true` when `node` is a {@link GroupNode}
|
|
574
|
+
*
|
|
575
|
+
* @category AST
|
|
347
576
|
*/
|
|
348
577
|
export function isGroup(node: ASTNode): node is GroupNode {
|
|
349
578
|
return node.type === 'Group';
|
|
350
579
|
}
|
|
351
580
|
|
|
352
581
|
/**
|
|
353
|
-
*
|
|
582
|
+
* Narrows an {@link ASTNode} to a sort modifier (`s`, `sa`, `sd`).
|
|
583
|
+
*
|
|
584
|
+
* @param node - Any AST node
|
|
585
|
+
* @returns `true` when `node` is a {@link SortNode}
|
|
586
|
+
*
|
|
587
|
+
* @category AST
|
|
354
588
|
*/
|
|
355
589
|
export function isSort(node: ASTNode): node is SortNode {
|
|
356
590
|
return node.type === 'Sort';
|
|
357
591
|
}
|
|
358
592
|
|
|
359
593
|
/**
|
|
360
|
-
*
|
|
594
|
+
* Narrows an {@link ASTNode} to a crit-threshold override (`cs`, `cf`).
|
|
595
|
+
*
|
|
596
|
+
* @param node - Any AST node
|
|
597
|
+
* @returns `true` when `node` is a {@link CritThresholdNode}
|
|
598
|
+
*
|
|
599
|
+
* @category AST
|
|
361
600
|
*/
|
|
362
601
|
export function isCritThreshold(node: ASTNode): node is CritThresholdNode {
|
|
363
602
|
return node.type === 'CritThreshold';
|
|
364
603
|
}
|
|
365
604
|
|
|
366
605
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* `
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
export type TransparentWrapperKind = 'Grouped' | 'Modifier' | 'Sort' | 'CritThreshold';
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Walks `node` while its `.type` is in `kinds`, returning the first descendant
|
|
380
|
-
* that is not one of the listed wrappers. Each caller picks the subset that
|
|
381
|
-
* matches its rejection semantics — see `TransparentWrapperKind` for guidance.
|
|
382
|
-
*
|
|
383
|
-
* Used by parser reject helpers to look past wrappers when deciding whether
|
|
384
|
-
* an operand violates a rule (e.g., `Group` cannot be the target of `cs`/`cf`,
|
|
385
|
-
* even when wrapped in `Modifier` like `{1d6}kh1cs>5`).
|
|
386
|
-
*/
|
|
387
|
-
export function unwrapTransparent(
|
|
388
|
-
node: ASTNode,
|
|
389
|
-
kinds: readonly TransparentWrapperKind[],
|
|
390
|
-
): ASTNode {
|
|
391
|
-
let current = node;
|
|
392
|
-
while (true) {
|
|
393
|
-
switch (current.type) {
|
|
394
|
-
case 'Grouped':
|
|
395
|
-
if (!kinds.includes('Grouped')) return current;
|
|
396
|
-
current = current.expression;
|
|
397
|
-
break;
|
|
398
|
-
case 'Modifier':
|
|
399
|
-
if (!kinds.includes('Modifier')) return current;
|
|
400
|
-
current = current.target;
|
|
401
|
-
break;
|
|
402
|
-
case 'Sort':
|
|
403
|
-
if (!kinds.includes('Sort')) return current;
|
|
404
|
-
current = current.target;
|
|
405
|
-
break;
|
|
406
|
-
case 'CritThreshold':
|
|
407
|
-
if (!kinds.includes('CritThreshold')) return current;
|
|
408
|
-
current = current.target;
|
|
409
|
-
break;
|
|
410
|
-
default:
|
|
411
|
-
return current;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Returns `true` only when `node`'s direct result is a dice pool —
|
|
418
|
-
* `Dice`, `FateDice`, or a chained pool modifier (`Modifier` / `Explode` /
|
|
419
|
-
* `Reroll`). Does NOT recurse through arithmetic wrappers (`BinaryOp`,
|
|
420
|
-
* `UnaryOp`, `FunctionCall`), so `(1d6+5)` and `floor(1d6/2)` are rejected.
|
|
421
|
-
*
|
|
422
|
-
* Used by the parser to reject postfix pool-modifier targets (kh/kl/dh/dl,
|
|
423
|
-
* !/!!/!p, r/ro) that wrap a non-pool expression. Operating on the inner
|
|
424
|
-
* dice pool would silently drop the surrounding arithmetic.
|
|
425
|
-
*/
|
|
426
|
-
export function containsDicePool(node: ASTNode): boolean {
|
|
427
|
-
switch (node.type) {
|
|
428
|
-
case 'Dice':
|
|
429
|
-
case 'FateDice':
|
|
430
|
-
case 'Modifier':
|
|
431
|
-
case 'Explode':
|
|
432
|
-
case 'Reroll':
|
|
433
|
-
return true;
|
|
434
|
-
case 'Sort':
|
|
435
|
-
case 'CritThreshold':
|
|
436
|
-
return containsDicePool(node.target);
|
|
437
|
-
case 'Grouped':
|
|
438
|
-
return containsDicePool(node.expression);
|
|
439
|
-
case 'Group':
|
|
440
|
-
// ? Multi-sub-roll groups (`{a, b, c}kh1`) always accept: keep/drop
|
|
441
|
-
// operates on subtotals, which are "compound dice" by definition —
|
|
442
|
-
// even a literal-only `{3, 5, 7}kh1` is valid. Single-sub-roll
|
|
443
|
-
// groups are the user's explicit opt-in to flat-pool semantics, so
|
|
444
|
-
// we deep-walk through arithmetic that a raw `(1d6+5)kh1` would
|
|
445
|
-
// reject. This is the `{}` escape hatch per Stage 3 spec.
|
|
446
|
-
return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
|
|
447
|
-
default:
|
|
448
|
-
return false;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Deeper variant of `containsDicePool` that recurses through arithmetic and
|
|
454
|
-
* function wrappers. Used from the `Group` case above (ordinary parenthesized
|
|
455
|
-
* arithmetic `(1d6+5)kh1` must still reject, so the shallow `containsDicePool`
|
|
456
|
-
* handles those directly) and from the Sort parser guard (sort accepts
|
|
457
|
-
* `(1d6+2d8)s` per Stage 3 spec).
|
|
458
|
-
*/
|
|
459
|
-
export function deepContainsDicePool(node: ASTNode): boolean {
|
|
460
|
-
switch (node.type) {
|
|
461
|
-
case 'Dice':
|
|
462
|
-
case 'FateDice':
|
|
463
|
-
return true;
|
|
464
|
-
case 'BinaryOp':
|
|
465
|
-
return deepContainsDicePool(node.left) || deepContainsDicePool(node.right);
|
|
466
|
-
case 'UnaryOp':
|
|
467
|
-
return deepContainsDicePool(node.operand);
|
|
468
|
-
case 'Modifier':
|
|
469
|
-
case 'Explode':
|
|
470
|
-
case 'Reroll':
|
|
471
|
-
case 'SuccessCount':
|
|
472
|
-
case 'Sort':
|
|
473
|
-
case 'CritThreshold':
|
|
474
|
-
return deepContainsDicePool(node.target);
|
|
475
|
-
case 'Versus':
|
|
476
|
-
return deepContainsDicePool(node.roll) || deepContainsDicePool(node.dc);
|
|
477
|
-
case 'FunctionCall':
|
|
478
|
-
return node.args.some(deepContainsDicePool);
|
|
479
|
-
case 'Grouped':
|
|
480
|
-
return deepContainsDicePool(node.expression);
|
|
481
|
-
case 'Group':
|
|
482
|
-
return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
|
|
483
|
-
default:
|
|
484
|
-
return false;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Returns `true` if the pool this node resolves to is (or wraps) a `FateDice`
|
|
490
|
-
* pool. Walks through chained pool modifiers (`Modifier` / `Explode` /
|
|
491
|
-
* `Reroll`) but not arithmetic wrappers — callers should run
|
|
492
|
-
* `containsDicePool` first to reject those.
|
|
493
|
-
*
|
|
494
|
-
* Used by the parser to reject `!`, `!!`, `!p` applied to Fate pools
|
|
495
|
-
* (`4dF!`, `(4dF)kh2!`, etc.). Fate explosion semantics are undefined, so
|
|
496
|
-
* parse-time rejection is preferred over a silent evaluator no-op.
|
|
497
|
-
*
|
|
498
|
-
* Inside a `Group`, recursion uses `deepContainsFatePool` to mirror
|
|
499
|
-
* `containsDicePool`'s deep walk through the same case — otherwise
|
|
500
|
-
* `{4dF+1d6}cf` slips past the bare-Fate guard and the default fumble
|
|
501
|
-
* check (`result === 1`) flips `+1` faces into fumbles.
|
|
502
|
-
*/
|
|
503
|
-
export function containsFatePool(node: ASTNode): boolean {
|
|
504
|
-
switch (node.type) {
|
|
505
|
-
case 'FateDice':
|
|
506
|
-
return true;
|
|
507
|
-
case 'Modifier':
|
|
508
|
-
case 'Explode':
|
|
509
|
-
case 'Reroll':
|
|
510
|
-
case 'Sort':
|
|
511
|
-
case 'CritThreshold':
|
|
512
|
-
return containsFatePool(node.target);
|
|
513
|
-
case 'Grouped':
|
|
514
|
-
return containsFatePool(node.expression);
|
|
515
|
-
case 'Group':
|
|
516
|
-
return node.expressions.some(deepContainsFatePool);
|
|
517
|
-
default:
|
|
518
|
-
return false;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Deeper variant of `containsFatePool` that recurses through arithmetic and
|
|
524
|
-
* function wrappers. Mirrors `deepContainsDicePool`. Used from
|
|
525
|
-
* `containsFatePool`'s `Group` case so single-sub-roll groups containing
|
|
526
|
-
* arithmetic-wrapped Fate (`{4dF+1d6}cf`) still trip the bare-Fate guard.
|
|
527
|
-
*
|
|
528
|
-
* Outside a `Group`, ordinary parenthesized arithmetic (`(4dF+1d6)cf`) is
|
|
529
|
-
* already rejected upstream by shallow `containsDicePool`, so this helper
|
|
530
|
-
* intentionally stays Group-internal.
|
|
531
|
-
*/
|
|
532
|
-
export function deepContainsFatePool(node: ASTNode): boolean {
|
|
533
|
-
switch (node.type) {
|
|
534
|
-
case 'FateDice':
|
|
535
|
-
return true;
|
|
536
|
-
case 'BinaryOp':
|
|
537
|
-
return deepContainsFatePool(node.left) || deepContainsFatePool(node.right);
|
|
538
|
-
case 'UnaryOp':
|
|
539
|
-
return deepContainsFatePool(node.operand);
|
|
540
|
-
case 'Modifier':
|
|
541
|
-
case 'Explode':
|
|
542
|
-
case 'Reroll':
|
|
543
|
-
case 'SuccessCount':
|
|
544
|
-
case 'Sort':
|
|
545
|
-
case 'CritThreshold':
|
|
546
|
-
return deepContainsFatePool(node.target);
|
|
547
|
-
case 'Versus':
|
|
548
|
-
return deepContainsFatePool(node.roll) || deepContainsFatePool(node.dc);
|
|
549
|
-
case 'FunctionCall':
|
|
550
|
-
return node.args.some(deepContainsFatePool);
|
|
551
|
-
case 'Grouped':
|
|
552
|
-
return deepContainsFatePool(node.expression);
|
|
553
|
-
case 'Group':
|
|
554
|
-
return node.expressions.some(deepContainsFatePool);
|
|
555
|
-
default:
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Deep-walks a node to find any descendant `Group` with two or more
|
|
562
|
-
* sub-expressions. Used by `rejectGroupTarget`'s single-sub-roll
|
|
563
|
-
* passthrough so a multi-sub Group buried under arithmetic
|
|
564
|
-
* (`{{1d6,2d8}+0}cs>5`), function calls (`{abs({1d6,2d8})}cs>5`), or any
|
|
565
|
-
* other non-transparent wrapper still rejects with the same error code.
|
|
566
|
-
*
|
|
567
|
-
* Without this walk, the unwrap inside `rejectGroupTarget` only peels
|
|
568
|
-
* `Grouped`/`Modifier`/`Sort`/`CritThreshold` — a multi-sub Group cloaked
|
|
569
|
-
* in a `BinaryOp`/`UnaryOp`/`FunctionCall` revives issue #97.
|
|
570
|
-
*/
|
|
571
|
-
export function containsMultiSubGroup(node: ASTNode): boolean {
|
|
572
|
-
switch (node.type) {
|
|
573
|
-
case 'Group':
|
|
574
|
-
return node.expressions.length >= 2 || node.expressions.some(containsMultiSubGroup);
|
|
575
|
-
case 'BinaryOp':
|
|
576
|
-
return containsMultiSubGroup(node.left) || containsMultiSubGroup(node.right);
|
|
577
|
-
case 'UnaryOp':
|
|
578
|
-
return containsMultiSubGroup(node.operand);
|
|
579
|
-
case 'Modifier':
|
|
580
|
-
case 'Explode':
|
|
581
|
-
case 'Reroll':
|
|
582
|
-
case 'SuccessCount':
|
|
583
|
-
case 'Sort':
|
|
584
|
-
case 'CritThreshold':
|
|
585
|
-
return containsMultiSubGroup(node.target);
|
|
586
|
-
case 'Versus':
|
|
587
|
-
return containsMultiSubGroup(node.roll) || containsMultiSubGroup(node.dc);
|
|
588
|
-
case 'FunctionCall':
|
|
589
|
-
return node.args.some(containsMultiSubGroup);
|
|
590
|
-
case 'Grouped':
|
|
591
|
-
return containsMultiSubGroup(node.expression);
|
|
592
|
-
default:
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Deep-walks a node to find any descendant `Versus`. Used by
|
|
599
|
-
* `rejectVersusTarget`'s single-sub-roll Group passthrough so a buried
|
|
600
|
-
* Versus (`{1+(1d20 vs 15)}cs>18`, `{abs(1d20 vs 15)}cs>18`,
|
|
601
|
-
* `{-(1d20 vs 15)}kh1`) still rejects with `NESTED_VERSUS` instead of
|
|
602
|
-
* silently dropping `versusMetadata` at the modifier consumer site.
|
|
603
|
-
*/
|
|
604
|
-
export function containsVersus(node: ASTNode): boolean {
|
|
605
|
-
switch (node.type) {
|
|
606
|
-
case 'Versus':
|
|
607
|
-
return true;
|
|
608
|
-
case 'BinaryOp':
|
|
609
|
-
return containsVersus(node.left) || containsVersus(node.right);
|
|
610
|
-
case 'UnaryOp':
|
|
611
|
-
return containsVersus(node.operand);
|
|
612
|
-
case 'Modifier':
|
|
613
|
-
case 'Explode':
|
|
614
|
-
case 'Reroll':
|
|
615
|
-
case 'SuccessCount':
|
|
616
|
-
case 'Sort':
|
|
617
|
-
case 'CritThreshold':
|
|
618
|
-
return containsVersus(node.target);
|
|
619
|
-
case 'FunctionCall':
|
|
620
|
-
return node.args.some(containsVersus);
|
|
621
|
-
case 'Grouped':
|
|
622
|
-
return containsVersus(node.expression);
|
|
623
|
-
case 'Group':
|
|
624
|
-
return node.expressions.some(containsVersus);
|
|
625
|
-
default:
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
606
|
+
* Narrows an {@link ASTNode} to a per-die clamp (`min2`, `max5`).
|
|
607
|
+
*
|
|
608
|
+
* @param node - Any AST node
|
|
609
|
+
* @returns `true` when `node` is a {@link DieBoundNode}
|
|
610
|
+
*
|
|
611
|
+
* @category AST
|
|
612
|
+
*/
|
|
613
|
+
export function isDieBound(node: ASTNode): node is DieBoundNode {
|
|
614
|
+
return node.type === 'DieBound';
|
|
628
615
|
}
|