roll-parser 3.0.0-alpha.0 → 3.0.0-beta.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 +91 -0
- package/README.md +166 -30
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/format.d.ts +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli.js +1110 -277
- package/dist/cli.js.map +28 -0
- package/dist/errors.d.ts +7 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/evaluator/evaluator.d.ts +63 -7
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/index.d.ts +2 -2
- package/dist/evaluator/index.d.ts.map +1 -1
- package/dist/evaluator/modifiers/compare.d.ts +1 -1
- package/dist/evaluator/modifiers/compare.d.ts.map +1 -1
- package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
- package/dist/evaluator/modifiers/explode.d.ts +8 -4
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
- package/dist/evaluator/modifiers/reroll.d.ts +4 -4
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
- package/dist/evaluator/modifiers/sort.d.ts +23 -0
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
- package/dist/evaluator/modifiers/success-count.d.ts +1 -1
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1112 -275
- package/dist/index.js.map +26 -0
- package/dist/lexer/lexer.d.ts +18 -3
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/tokens.d.ts +22 -2
- package/dist/lexer/tokens.d.ts.map +1 -1
- package/dist/parser/ast.d.ts +209 -24
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/parser.d.ts +48 -5
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/rng/index.d.ts +2 -2
- package/dist/rng/index.d.ts.map +1 -1
- package/dist/rng/mock.d.ts +1 -1
- package/dist/rng/mock.d.ts.map +1 -1
- package/dist/rng/seeded.d.ts +8 -1
- package/dist/rng/seeded.d.ts.map +1 -1
- package/dist/roll.d.ts +6 -2
- package/dist/roll.d.ts.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +11 -0
- package/dist/types.d.ts +143 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +22 -18
- package/src/cli/args.ts +2 -1
- package/src/cli/format.ts +8 -4
- package/src/cli/index.ts +22 -5
- package/src/errors.ts +15 -3
- package/src/evaluator/evaluator.ts +826 -111
- package/src/evaluator/index.ts +2 -2
- package/src/evaluator/modifiers/compare.ts +1 -1
- package/src/evaluator/modifiers/crit-threshold.ts +59 -0
- package/src/evaluator/modifiers/explode.ts +29 -25
- package/src/evaluator/modifiers/keep-drop.ts +1 -1
- package/src/evaluator/modifiers/reroll.ts +18 -25
- package/src/evaluator/modifiers/sort.ts +30 -0
- package/src/evaluator/modifiers/success-count.ts +2 -2
- package/src/index.ts +33 -15
- package/src/lexer/lexer.ts +101 -8
- package/src/lexer/tokens.ts +42 -2
- package/src/parser/ast.ts +397 -30
- package/src/parser/parser.ts +590 -67
- package/src/rng/index.ts +2 -2
- package/src/rng/mock.ts +1 -1
- package/src/rng/seeded.ts +31 -1
- package/src/roll.ts +14 -6
- package/src/testing.ts +1 -1
- package/src/types.ts +127 -2
- package/dist/index.mjs +0 -1724
- package/dist/testing.mjs +0 -39
package/src/parser/ast.ts
CHANGED
|
@@ -4,12 +4,25 @@
|
|
|
4
4
|
* @module parser/ast
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ComparePoint } from '../types';
|
|
7
|
+
import type { ComparePoint } from '../types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Source span carried by every AST node.
|
|
11
|
+
*
|
|
12
|
+
* `start` is inclusive, `end` exclusive, both in UTF-16 code units into the
|
|
13
|
+
* original notation string. The parser sets both on every node it produces;
|
|
14
|
+
* they are typed optional so hand-constructed ASTs (tests, programmatic
|
|
15
|
+
* consumers) remain valid without positions.
|
|
16
|
+
*/
|
|
17
|
+
export type NodeSpan = {
|
|
18
|
+
start?: number;
|
|
19
|
+
end?: number;
|
|
20
|
+
};
|
|
8
21
|
|
|
9
22
|
/**
|
|
10
23
|
* Numeric literal node.
|
|
11
24
|
*/
|
|
12
|
-
export type LiteralNode = {
|
|
25
|
+
export type LiteralNode = NodeSpan & {
|
|
13
26
|
type: 'Literal';
|
|
14
27
|
value: number;
|
|
15
28
|
};
|
|
@@ -18,7 +31,7 @@ export type LiteralNode = {
|
|
|
18
31
|
* Dice roll node.
|
|
19
32
|
* Count and sides can be expressions to support computed dice like (1+1)d(3*2).
|
|
20
33
|
*/
|
|
21
|
-
export type DiceNode = {
|
|
34
|
+
export type DiceNode = NodeSpan & {
|
|
22
35
|
type: 'Dice';
|
|
23
36
|
count: ASTNode;
|
|
24
37
|
sides: ASTNode;
|
|
@@ -28,7 +41,7 @@ export type DiceNode = {
|
|
|
28
41
|
* Fate/Fudge dice node (`dF`).
|
|
29
42
|
* Each die produces a result in {-1, 0, +1}. No configurable sides.
|
|
30
43
|
*/
|
|
31
|
-
export type FateDiceNode = {
|
|
44
|
+
export type FateDiceNode = NodeSpan & {
|
|
32
45
|
type: 'FateDice';
|
|
33
46
|
count: ASTNode;
|
|
34
47
|
};
|
|
@@ -36,7 +49,7 @@ export type FateDiceNode = {
|
|
|
36
49
|
/**
|
|
37
50
|
* Binary operation node.
|
|
38
51
|
*/
|
|
39
|
-
export type BinaryOpNode = {
|
|
52
|
+
export type BinaryOpNode = NodeSpan & {
|
|
40
53
|
type: 'BinaryOp';
|
|
41
54
|
operator: '+' | '-' | '*' | '/' | '%' | '**';
|
|
42
55
|
left: ASTNode;
|
|
@@ -46,7 +59,7 @@ export type BinaryOpNode = {
|
|
|
46
59
|
/**
|
|
47
60
|
* Unary operation node.
|
|
48
61
|
*/
|
|
49
|
-
export type UnaryOpNode = {
|
|
62
|
+
export type UnaryOpNode = NodeSpan & {
|
|
50
63
|
type: 'UnaryOp';
|
|
51
64
|
operator: '-';
|
|
52
65
|
operand: ASTNode;
|
|
@@ -56,7 +69,7 @@ export type UnaryOpNode = {
|
|
|
56
69
|
* Keep/drop modifier node.
|
|
57
70
|
* Wraps a dice expression with keep highest/lowest or drop highest/lowest.
|
|
58
71
|
*/
|
|
59
|
-
export type ModifierNode = {
|
|
72
|
+
export type ModifierNode = NodeSpan & {
|
|
60
73
|
type: 'Modifier';
|
|
61
74
|
modifier: 'keep' | 'drop';
|
|
62
75
|
selector: 'highest' | 'lowest';
|
|
@@ -69,7 +82,7 @@ export type ModifierNode = {
|
|
|
69
82
|
* Wraps a dice expression with a standard, compounding, or penetrating
|
|
70
83
|
* explosion. An absent `threshold` means "explode on the die's maximum face".
|
|
71
84
|
*/
|
|
72
|
-
export type ExplodeNode = {
|
|
85
|
+
export type ExplodeNode = NodeSpan & {
|
|
73
86
|
type: 'Explode';
|
|
74
87
|
variant: 'standard' | 'compound' | 'penetrating';
|
|
75
88
|
threshold?: ComparePoint;
|
|
@@ -82,7 +95,7 @@ export type ExplodeNode = {
|
|
|
82
95
|
* keeps the second result regardless of match; `once: false` for `r`
|
|
83
96
|
* re-rolls recursively until the condition no longer matches.
|
|
84
97
|
*/
|
|
85
|
-
export type RerollNode = {
|
|
98
|
+
export type RerollNode = NodeSpan & {
|
|
86
99
|
type: 'Reroll';
|
|
87
100
|
once: boolean;
|
|
88
101
|
condition: ComparePoint;
|
|
@@ -93,11 +106,12 @@ export type RerollNode = {
|
|
|
93
106
|
* Success counting node (`>=T`, `>T`, `<T`, `<=T`, `=T`, with optional `f=F`).
|
|
94
107
|
*
|
|
95
108
|
* Transforms a dice pool into a success count: each die meeting `threshold`
|
|
96
|
-
* adds +1, each die meeting `failThreshold` subtracts 1. Terminal —
|
|
97
|
-
*
|
|
98
|
-
* operator
|
|
109
|
+
* adds +1, each die meeting `failThreshold` subtracts 1. Terminal — a
|
|
110
|
+
* `SuccessCountNode` may not be wrapped by any postfix modifier, binary
|
|
111
|
+
* operator, unary operator, versus operand, or function argument. The
|
|
112
|
+
* `failThreshold` accepts any `CompareOp`; bare `fN` defaults to `operator: '='`.
|
|
99
113
|
*/
|
|
100
|
-
export type SuccessCountNode = {
|
|
114
|
+
export type SuccessCountNode = NodeSpan & {
|
|
101
115
|
type: 'SuccessCount';
|
|
102
116
|
target: ASTNode;
|
|
103
117
|
threshold: ComparePoint;
|
|
@@ -114,7 +128,7 @@ export type SuccessCountNode = {
|
|
|
114
128
|
* rejected at parse time; nesting via parens (`a vs (b vs c)`) is rejected
|
|
115
129
|
* at evaluation time.
|
|
116
130
|
*/
|
|
117
|
-
export type VersusNode = {
|
|
131
|
+
export type VersusNode = NodeSpan & {
|
|
118
132
|
type: 'Versus';
|
|
119
133
|
roll: ASTNode;
|
|
120
134
|
dc: ASTNode;
|
|
@@ -128,12 +142,94 @@ export type VersusNode = {
|
|
|
128
142
|
* parse time against a static table; by the time the evaluator sees a
|
|
129
143
|
* `FunctionCallNode`, `args.length` is guaranteed to match the function.
|
|
130
144
|
*/
|
|
131
|
-
export type FunctionCallNode = {
|
|
145
|
+
export type FunctionCallNode = NodeSpan & {
|
|
132
146
|
type: 'FunctionCall';
|
|
133
147
|
name: string;
|
|
134
148
|
args: ASTNode[];
|
|
135
149
|
};
|
|
136
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Parenthesized group node (`(<expr>)`).
|
|
153
|
+
*
|
|
154
|
+
* Preserves explicit grouping typed by the user so that
|
|
155
|
+
* `RollResult.expression` and `RollResult.rendered` round-trip through
|
|
156
|
+
* `parse` without losing precedence information. Semantically transparent:
|
|
157
|
+
* evaluation returns the inner expression's value unchanged.
|
|
158
|
+
*/
|
|
159
|
+
export type GroupedNode = NodeSpan & {
|
|
160
|
+
type: 'Grouped';
|
|
161
|
+
expression: ASTNode;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Variable reference node (`@name` or `@{name with spaces}`).
|
|
166
|
+
*
|
|
167
|
+
* Resolves to a numeric value from the evaluator's `context` map at
|
|
168
|
+
* evaluation time. Names are case-sensitive (`@StrMod` ≠ `@strmod`) — the
|
|
169
|
+
* lexer preserves case in the `AT` token's `value`, distinct from other
|
|
170
|
+
* identifier tokens which lowercase. Leaf node — no LED, never wraps a
|
|
171
|
+
* sub-expression.
|
|
172
|
+
*/
|
|
173
|
+
export type VariableNode = NodeSpan & {
|
|
174
|
+
type: 'Variable';
|
|
175
|
+
name: string;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Grouped-roll node (`{expr}`, `{expr1, expr2, ...}`).
|
|
180
|
+
*
|
|
181
|
+
* Distinct from `GroupedNode` (parenthesized wrapper) — a `GroupNode`
|
|
182
|
+
* collects one or more sub-expressions whose evaluation semantics change
|
|
183
|
+
* with the sub-roll count. `expressions.length === 1` is a passthrough
|
|
184
|
+
* (flat-pool when wrapped by keep/drop); `expressions.length >= 2` treats
|
|
185
|
+
* each sub-roll's subtotal as a compound die for keep/drop selection.
|
|
186
|
+
*/
|
|
187
|
+
export type GroupNode = NodeSpan & {
|
|
188
|
+
type: 'Group';
|
|
189
|
+
expressions: ASTNode[];
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Sort modifier node (`s`, `sa`, `sd`).
|
|
194
|
+
*
|
|
195
|
+
* Cosmetically reorders the dice produced by `target` in ascending or
|
|
196
|
+
* descending order. Purely visual — does not affect `total`,
|
|
197
|
+
* `successes`/`failures`, or any die-level flag (`kept`/`dropped`/
|
|
198
|
+
* `critical`/`fumble`). Dropped dice retain their `dropped` flag and
|
|
199
|
+
* appear in sorted position alongside kept dice.
|
|
200
|
+
*/
|
|
201
|
+
export type SortNode = NodeSpan & {
|
|
202
|
+
type: 'Sort';
|
|
203
|
+
order: 'ascending' | 'descending';
|
|
204
|
+
target: ASTNode;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Sentinel for bare `cs` / `cf` without a ComparePoint. Resolved to
|
|
209
|
+
* `result === sides` (for critical) or `result === 1` (for fumble) at
|
|
210
|
+
* evaluation time, using each die's own `sides`.
|
|
211
|
+
*/
|
|
212
|
+
export type CritThreshold = ComparePoint | 'default';
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Critical threshold modifier node (`cs`, `cf`).
|
|
216
|
+
*
|
|
217
|
+
* Overrides the default `critical`/`fumble` flag logic for the dice
|
|
218
|
+
* produced by `target`. Bare `cs`/`cf` uses the `'default'` sentinel
|
|
219
|
+
* (max face / 1). Custom thresholds accept any ComparePoint. Chaining
|
|
220
|
+
* collapses into a single node — `1d20cs=20cs=1cf>18` has two success
|
|
221
|
+
* and one fail threshold. Display-only: does not change `total`,
|
|
222
|
+
* explosion triggers, or success counting. `cs` and `cf` are independent
|
|
223
|
+
* overrides — a side with no explicit thresholds keeps the default rule
|
|
224
|
+
* (the evaluator substitutes the `'default'` sentinel at apply time).
|
|
225
|
+
*/
|
|
226
|
+
export type CritThresholdNode = NodeSpan & {
|
|
227
|
+
type: 'CritThreshold';
|
|
228
|
+
successThresholds: CritThreshold[];
|
|
229
|
+
failThresholds: CritThreshold[];
|
|
230
|
+
target: ASTNode;
|
|
231
|
+
};
|
|
232
|
+
|
|
137
233
|
/**
|
|
138
234
|
* Union type of all AST nodes.
|
|
139
235
|
*/
|
|
@@ -148,7 +244,12 @@ export type ASTNode =
|
|
|
148
244
|
| RerollNode
|
|
149
245
|
| SuccessCountNode
|
|
150
246
|
| VersusNode
|
|
151
|
-
| FunctionCallNode
|
|
247
|
+
| FunctionCallNode
|
|
248
|
+
| GroupedNode
|
|
249
|
+
| VariableNode
|
|
250
|
+
| GroupNode
|
|
251
|
+
| SortNode
|
|
252
|
+
| CritThresholdNode;
|
|
152
253
|
|
|
153
254
|
/**
|
|
154
255
|
* Type guard for LiteralNode.
|
|
@@ -228,34 +329,300 @@ export function isFunctionCall(node: ASTNode): node is FunctionCallNode {
|
|
|
228
329
|
}
|
|
229
330
|
|
|
230
331
|
/**
|
|
231
|
-
*
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
332
|
+
* Type guard for GroupedNode.
|
|
333
|
+
*/
|
|
334
|
+
export function isGrouped(node: ASTNode): node is GroupedNode {
|
|
335
|
+
return node.type === 'Grouped';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Type guard for VariableNode.
|
|
340
|
+
*/
|
|
341
|
+
export function isVariable(node: ASTNode): node is VariableNode {
|
|
342
|
+
return node.type === 'Variable';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Type guard for GroupNode.
|
|
347
|
+
*/
|
|
348
|
+
export function isGroup(node: ASTNode): node is GroupNode {
|
|
349
|
+
return node.type === 'Group';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Type guard for SortNode.
|
|
354
|
+
*/
|
|
355
|
+
export function isSort(node: ASTNode): node is SortNode {
|
|
356
|
+
return node.type === 'Sort';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Type guard for CritThresholdNode.
|
|
361
|
+
*/
|
|
362
|
+
export function isCritThreshold(node: ASTNode): node is CritThresholdNode {
|
|
363
|
+
return node.type === 'CritThreshold';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Wrapper kinds that `unwrapTransparent` can peel.
|
|
368
|
+
*
|
|
369
|
+
* "Transparent" is relative to the question being asked. `Modifier`/`Sort`/
|
|
370
|
+
* `CritThreshold` are transparent for "what is the underlying operand?" when
|
|
371
|
+
* deciding whether to reject a `Group` target — they preserve `containsDicePool`'s
|
|
372
|
+
* answer for whatever they wrap. They are NOT transparent for "is this a
|
|
373
|
+
* `SuccessCount`?" or "is this a `Versus`?", because the parsers that build
|
|
374
|
+
* those wrappers already reject `SuccessCount`/`Versus` operands upstream.
|
|
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.
|
|
236
421
|
*
|
|
237
|
-
* Used by the parser to reject
|
|
238
|
-
*
|
|
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.
|
|
239
425
|
*/
|
|
240
|
-
export function
|
|
426
|
+
export function containsDicePool(node: ASTNode): boolean {
|
|
241
427
|
switch (node.type) {
|
|
242
428
|
case 'Dice':
|
|
243
429
|
case 'FateDice':
|
|
430
|
+
case 'Modifier':
|
|
431
|
+
case 'Explode':
|
|
432
|
+
case 'Reroll':
|
|
244
433
|
return true;
|
|
245
|
-
case '
|
|
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:
|
|
246
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;
|
|
247
464
|
case 'BinaryOp':
|
|
248
|
-
return
|
|
465
|
+
return deepContainsDicePool(node.left) || deepContainsDicePool(node.right);
|
|
249
466
|
case 'UnaryOp':
|
|
250
|
-
return
|
|
467
|
+
return deepContainsDicePool(node.operand);
|
|
251
468
|
case 'Modifier':
|
|
252
469
|
case 'Explode':
|
|
253
470
|
case 'Reroll':
|
|
254
471
|
case 'SuccessCount':
|
|
255
|
-
|
|
472
|
+
case 'Sort':
|
|
473
|
+
case 'CritThreshold':
|
|
474
|
+
return deepContainsDicePool(node.target);
|
|
256
475
|
case 'Versus':
|
|
257
|
-
return
|
|
476
|
+
return deepContainsDicePool(node.roll) || deepContainsDicePool(node.dc);
|
|
258
477
|
case 'FunctionCall':
|
|
259
|
-
return node.args.some(
|
|
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;
|
|
260
627
|
}
|
|
261
628
|
}
|