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/parser/ast.ts
CHANGED
|
@@ -4,88 +4,188 @@
|
|
|
4
4
|
* @module parser/ast
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ComparePoint } from '../types';
|
|
7
|
+
import type { ComparePoint } from '../types.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
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
|
+
* `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
|
|
11
21
|
*/
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
export type NodeSpan = {
|
|
23
|
+
readonly start?: number;
|
|
24
|
+
readonly end?: number;
|
|
15
25
|
};
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
20
35
|
*/
|
|
21
|
-
export type
|
|
22
|
-
type: '
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
export type LiteralNode = NodeSpan & {
|
|
37
|
+
readonly type: 'Literal';
|
|
38
|
+
readonly value: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
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
|
|
53
|
+
*/
|
|
54
|
+
export type DiceNode = NodeSpan & {
|
|
55
|
+
readonly type: 'Dice';
|
|
56
|
+
readonly count: ASTNode;
|
|
57
|
+
readonly sides: ASTNode;
|
|
25
58
|
};
|
|
26
59
|
|
|
27
60
|
/**
|
|
28
61
|
* Fate/Fudge dice node (`dF`).
|
|
29
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`, and are
|
|
65
|
+
* never `critical` or `fumble` — there is no maximum face to hit.
|
|
66
|
+
*
|
|
67
|
+
* @category AST
|
|
30
68
|
*/
|
|
31
|
-
export type FateDiceNode = {
|
|
32
|
-
type: 'FateDice';
|
|
33
|
-
count: ASTNode;
|
|
69
|
+
export type FateDiceNode = NodeSpan & {
|
|
70
|
+
readonly type: 'FateDice';
|
|
71
|
+
readonly count: ASTNode;
|
|
34
72
|
};
|
|
35
73
|
|
|
36
74
|
/**
|
|
37
|
-
* Binary operation node.
|
|
75
|
+
* Binary operation node — the arithmetic backbone of an expression.
|
|
76
|
+
*
|
|
77
|
+
* | Operator | Meaning | Precedence | Associativity |
|
|
78
|
+
* |----------|---------|-----------:|---------------|
|
|
79
|
+
* | `+` `-` | add, subtract | 10 | left |
|
|
80
|
+
* | `*` `/` `%` | multiply, divide, modulo | 20 | left |
|
|
81
|
+
* | `**` | power (also spelled `^`) | 30 | right |
|
|
82
|
+
*
|
|
83
|
+
* Dice bind tighter than all of them (40), and postfix modifiers sit at 35,
|
|
84
|
+
* so `2d6+3` is `(2d6)+3` and `4d6kh3*2` is `(4d6kh3)*2`. The `vs` operator
|
|
85
|
+
* binds loosest of all (2) and has its own {@link VersusNode}.
|
|
86
|
+
*
|
|
87
|
+
* `/` and `%` throw `DIVISION_BY_ZERO` / `MODULO_BY_ZERO` on a zero right
|
|
88
|
+
* side; any operator producing a non-finite total throws `NON_FINITE_RESULT`.
|
|
89
|
+
* Division is not rounded — wrap it in `floor()` if you need an integer.
|
|
90
|
+
*
|
|
91
|
+
* @category AST
|
|
38
92
|
*/
|
|
39
|
-
export type BinaryOpNode = {
|
|
40
|
-
type: 'BinaryOp';
|
|
41
|
-
operator: '+' | '-' | '*' | '/' | '%' | '**';
|
|
42
|
-
left: ASTNode;
|
|
43
|
-
right: ASTNode;
|
|
93
|
+
export type BinaryOpNode = NodeSpan & {
|
|
94
|
+
readonly type: 'BinaryOp';
|
|
95
|
+
readonly operator: '+' | '-' | '*' | '/' | '%' | '**';
|
|
96
|
+
readonly left: ASTNode;
|
|
97
|
+
readonly right: ASTNode;
|
|
44
98
|
};
|
|
45
99
|
|
|
46
100
|
/**
|
|
47
|
-
* Unary operation node.
|
|
101
|
+
* Unary operation node — prefix negation, the only unary operator.
|
|
102
|
+
*
|
|
103
|
+
* Binding power 25 sits between multiplication and power, so `-1d4` is
|
|
104
|
+
* `-(1d4)` (negate the roll) rather than `(-1)d4`, while `-2**2` is `-(2**2)`.
|
|
105
|
+
*
|
|
106
|
+
* @category AST
|
|
48
107
|
*/
|
|
49
|
-
export type UnaryOpNode = {
|
|
50
|
-
type: 'UnaryOp';
|
|
51
|
-
operator: '-';
|
|
52
|
-
operand: ASTNode;
|
|
108
|
+
export type UnaryOpNode = NodeSpan & {
|
|
109
|
+
readonly type: 'UnaryOp';
|
|
110
|
+
readonly operator: '-';
|
|
111
|
+
readonly operand: ASTNode;
|
|
53
112
|
};
|
|
54
113
|
|
|
55
114
|
/**
|
|
56
|
-
* Keep/drop modifier node
|
|
115
|
+
* Keep/drop modifier node (`khN`, `klN`, `dhN`, `dlN`, and the `kN`
|
|
116
|
+
* shorthand for `khN`).
|
|
117
|
+
*
|
|
57
118
|
* Wraps a dice expression with keep highest/lowest or drop highest/lowest.
|
|
119
|
+
* An omitted count defaults to 1, so `4d6kh` is `4d6kh1`. `count` is a full
|
|
120
|
+
* sub-expression, so `4d6kh(1d2)` is legal — and its dice are drawn *before*
|
|
121
|
+
* the pool.
|
|
122
|
+
*
|
|
123
|
+
* Chained modifiers (`4d6kh3dl1`) do not nest: the evaluator flattens the
|
|
124
|
+
* chain and applies each spec independently to the same pool, unioning the
|
|
125
|
+
* dropped sets, which is the Roll20 rule.
|
|
126
|
+
*
|
|
127
|
+
* @category AST
|
|
58
128
|
*/
|
|
59
|
-
export type
|
|
60
|
-
type: '
|
|
61
|
-
|
|
62
|
-
selector: 'highest' | 'lowest';
|
|
63
|
-
count: ASTNode;
|
|
64
|
-
target: ASTNode;
|
|
129
|
+
export type KeepDropNode = NodeSpan & {
|
|
130
|
+
readonly type: 'KeepDrop';
|
|
131
|
+
readonly kind: 'keep' | 'drop';
|
|
132
|
+
readonly selector: 'highest' | 'lowest';
|
|
133
|
+
readonly count: ASTNode;
|
|
134
|
+
readonly target: ASTNode;
|
|
65
135
|
};
|
|
66
136
|
|
|
67
137
|
/**
|
|
68
138
|
* Exploding dice node (`!`, `!!`, `!p`, `!>Y`).
|
|
69
139
|
* Wraps a dice expression with a standard, compounding, or penetrating
|
|
70
140
|
* explosion. An absent `threshold` means "explode on the die's maximum face".
|
|
141
|
+
*
|
|
142
|
+
* Thresholds accept any comparator. In notation like `1d10!=10` (the
|
|
143
|
+
* Storyteller "10-again" rule) the `!` is the explode marker and `=10` the
|
|
144
|
+
* equality threshold — there is no `!=` comparator. Per-die explosion count
|
|
145
|
+
* is capped by `EvaluationOptions.maxExplodeIterations`.
|
|
146
|
+
*
|
|
147
|
+
* @category AST
|
|
71
148
|
*/
|
|
72
|
-
export type ExplodeNode = {
|
|
73
|
-
type: 'Explode';
|
|
74
|
-
variant: 'standard' | 'compound' | 'penetrating';
|
|
75
|
-
threshold?: ComparePoint;
|
|
76
|
-
target: ASTNode;
|
|
149
|
+
export type ExplodeNode = NodeSpan & {
|
|
150
|
+
readonly type: 'Explode';
|
|
151
|
+
readonly variant: 'standard' | 'compound' | 'penetrating';
|
|
152
|
+
readonly threshold?: ComparePoint;
|
|
153
|
+
readonly target: ASTNode;
|
|
77
154
|
};
|
|
78
155
|
|
|
79
156
|
/**
|
|
80
157
|
* Reroll node (`r<COND>`, `ro<COND>`).
|
|
81
158
|
* Re-rolls dice that match a comparison condition. `once: true` for `ro`
|
|
82
159
|
* keeps the second result regardless of match; `once: false` for `r`
|
|
83
|
-
* re-rolls recursively until the condition no longer matches
|
|
160
|
+
* re-rolls recursively until the condition no longer matches, bounded by
|
|
161
|
+
* `EvaluationOptions.maxRerollIterations`.
|
|
162
|
+
*
|
|
163
|
+
* @category AST
|
|
164
|
+
*/
|
|
165
|
+
export type RerollNode = NodeSpan & {
|
|
166
|
+
readonly type: 'Reroll';
|
|
167
|
+
readonly once: boolean;
|
|
168
|
+
readonly condition: ComparePoint;
|
|
169
|
+
readonly target: ASTNode;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Per-die clamp node (`minN`, `maxN`).
|
|
174
|
+
* `4d6min2` raises every die below 2 to 2; `4d6max5` lowers every die above
|
|
175
|
+
* 5 to 5. The clamp rewrites `DieResult.result` (preserving the raw face in
|
|
176
|
+
* `initialResult`) and re-sums the pool; `critical`/`fumble` keep reflecting
|
|
177
|
+
* the natural face. The bound is a full sub-expression (`4d6min(1d2)`),
|
|
178
|
+
* drawn *after* the pool like other threshold arguments.
|
|
179
|
+
*
|
|
180
|
+
* Chained bounds nest and apply left to right: `4d6min2max5` clamps into
|
|
181
|
+
* [2, 5].
|
|
182
|
+
*
|
|
183
|
+
* @category AST
|
|
84
184
|
*/
|
|
85
|
-
export type
|
|
86
|
-
type: '
|
|
87
|
-
|
|
88
|
-
|
|
185
|
+
export type DieBoundNode = NodeSpan & {
|
|
186
|
+
type: 'DieBound';
|
|
187
|
+
bound: 'min' | 'max';
|
|
188
|
+
value: ASTNode;
|
|
89
189
|
target: ASTNode;
|
|
90
190
|
};
|
|
91
191
|
|
|
@@ -93,15 +193,18 @@ export type RerollNode = {
|
|
|
93
193
|
* Success counting node (`>=T`, `>T`, `<T`, `<=T`, `=T`, with optional `f=F`).
|
|
94
194
|
*
|
|
95
195
|
* 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
|
|
196
|
+
* adds +1, each die meeting `failThreshold` subtracts 1. Terminal — a
|
|
197
|
+
* `SuccessCountNode` may not be wrapped by any postfix modifier, binary
|
|
198
|
+
* operator, unary operator, versus operand, or function argument. The
|
|
199
|
+
* `failThreshold` accepts any `CompareOp`; bare `fN` defaults to `operator: '='`.
|
|
200
|
+
*
|
|
201
|
+
* @category AST
|
|
99
202
|
*/
|
|
100
|
-
export type SuccessCountNode = {
|
|
101
|
-
type: 'SuccessCount';
|
|
102
|
-
target: ASTNode;
|
|
103
|
-
threshold: ComparePoint;
|
|
104
|
-
failThreshold?: ComparePoint;
|
|
203
|
+
export type SuccessCountNode = NodeSpan & {
|
|
204
|
+
readonly type: 'SuccessCount';
|
|
205
|
+
readonly target: ASTNode;
|
|
206
|
+
readonly threshold: ComparePoint;
|
|
207
|
+
readonly failThreshold?: ComparePoint;
|
|
105
208
|
};
|
|
106
209
|
|
|
107
210
|
/**
|
|
@@ -113,11 +216,13 @@ export type SuccessCountNode = {
|
|
|
113
216
|
* roll side. Lowest-precedence operator — chaining (`a vs b vs c`) is
|
|
114
217
|
* rejected at parse time; nesting via parens (`a vs (b vs c)`) is rejected
|
|
115
218
|
* at evaluation time.
|
|
219
|
+
*
|
|
220
|
+
* @category AST
|
|
116
221
|
*/
|
|
117
|
-
export type VersusNode = {
|
|
118
|
-
type: 'Versus';
|
|
119
|
-
roll: ASTNode;
|
|
120
|
-
dc: ASTNode;
|
|
222
|
+
export type VersusNode = NodeSpan & {
|
|
223
|
+
readonly type: 'Versus';
|
|
224
|
+
readonly roll: ASTNode;
|
|
225
|
+
readonly dc: ASTNode;
|
|
121
226
|
};
|
|
122
227
|
|
|
123
228
|
/**
|
|
@@ -127,15 +232,159 @@ export type VersusNode = {
|
|
|
127
232
|
* variadic functions `max`, `min` (minimum 2 args). Arity is validated at
|
|
128
233
|
* parse time against a static table; by the time the evaluator sees a
|
|
129
234
|
* `FunctionCallNode`, `args.length` is guaranteed to match the function.
|
|
235
|
+
*
|
|
236
|
+
* `max`/`min` are variadic with no upper bound, so `max(1d20, 1d20, 1d20)`
|
|
237
|
+
* is a valid three-way advantage roll.
|
|
238
|
+
*
|
|
239
|
+
* @category AST
|
|
240
|
+
*/
|
|
241
|
+
export type FunctionCallNode = NodeSpan & {
|
|
242
|
+
readonly type: 'FunctionCall';
|
|
243
|
+
readonly name: string;
|
|
244
|
+
readonly args: readonly ASTNode[];
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Parenthesized group node (`(<expr>)`).
|
|
249
|
+
*
|
|
250
|
+
* Preserves explicit grouping typed by the user so that
|
|
251
|
+
* `RollResult.expression` and `RollResult.rendered` round-trip through
|
|
252
|
+
* `parse` without losing precedence information. Semantically transparent:
|
|
253
|
+
* evaluation returns the inner expression's value unchanged.
|
|
254
|
+
*
|
|
255
|
+
* @category AST
|
|
256
|
+
*/
|
|
257
|
+
export type GroupedNode = NodeSpan & {
|
|
258
|
+
readonly type: 'Grouped';
|
|
259
|
+
readonly expression: ASTNode;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Variable reference node (`@name` or `@{name with spaces}`).
|
|
264
|
+
*
|
|
265
|
+
* Resolves to a numeric value from the evaluator's `context` map at
|
|
266
|
+
* evaluation time. Names are case-sensitive (`@StrMod` ≠ `@strmod`) — the
|
|
267
|
+
* lexer preserves case in the `AT` token's `value`, distinct from other
|
|
268
|
+
* identifier tokens which lowercase. Leaf node — no LED, never wraps a
|
|
269
|
+
* sub-expression.
|
|
270
|
+
*
|
|
271
|
+
* A name missing from `context` throws `UNDEFINED_VARIABLE` unless
|
|
272
|
+
* `onMissingVariable: 'zero'` is set.
|
|
273
|
+
*
|
|
274
|
+
* @category AST
|
|
275
|
+
*/
|
|
276
|
+
export type VariableNode = NodeSpan & {
|
|
277
|
+
readonly type: 'Variable';
|
|
278
|
+
readonly name: string;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Grouped-roll node (`{expr}`, `{expr1, expr2, ...}`).
|
|
283
|
+
*
|
|
284
|
+
* Distinct from `GroupedNode` (parenthesized wrapper) — a `GroupNode`
|
|
285
|
+
* collects one or more sub-expressions whose evaluation semantics change
|
|
286
|
+
* with the sub-roll count. `expressions.length === 1` is a passthrough
|
|
287
|
+
* (flat-pool when wrapped by keep/drop); `expressions.length >= 2` treats
|
|
288
|
+
* each sub-roll's subtotal as a compound die for keep/drop selection.
|
|
289
|
+
*
|
|
290
|
+
* @category AST
|
|
291
|
+
*/
|
|
292
|
+
export type GroupNode = NodeSpan & {
|
|
293
|
+
readonly type: 'Group';
|
|
294
|
+
readonly expressions: readonly ASTNode[];
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Sort modifier node (`s`, `sa`, `sd`).
|
|
299
|
+
*
|
|
300
|
+
* Cosmetically reorders the dice produced by `target` in ascending or
|
|
301
|
+
* descending order. Purely visual — does not affect `total`,
|
|
302
|
+
* `successes`/`failures`, or any die-level flag (`kept`/`dropped`/
|
|
303
|
+
* `critical`/`fumble`). Dropped dice retain their `dropped` flag and
|
|
304
|
+
* appear in sorted position alongside kept dice.
|
|
305
|
+
*
|
|
306
|
+
* `s` and `sa` both mean ascending; `sd` is descending.
|
|
307
|
+
*
|
|
308
|
+
* @category AST
|
|
309
|
+
*/
|
|
310
|
+
export type SortNode = NodeSpan & {
|
|
311
|
+
readonly type: 'Sort';
|
|
312
|
+
readonly order: 'ascending' | 'descending';
|
|
313
|
+
readonly target: ASTNode;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Sentinel for bare `cs` / `cf` without a ComparePoint. Resolved to
|
|
318
|
+
* `result === sides` (for critical) or `result === 1` (for fumble) at
|
|
319
|
+
* evaluation time, using each die's own `sides`.
|
|
320
|
+
*
|
|
321
|
+
* @category AST
|
|
130
322
|
*/
|
|
131
|
-
export type
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
323
|
+
export type CritThreshold = ComparePoint | 'default';
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Critical threshold modifier node (`cs`, `cf`).
|
|
327
|
+
*
|
|
328
|
+
* Overrides the default `critical`/`fumble` flag logic for the dice
|
|
329
|
+
* produced by `target`. Bare `cs`/`cf` uses the `'default'` sentinel
|
|
330
|
+
* (max face / 1). Custom thresholds accept any ComparePoint. Chaining
|
|
331
|
+
* collapses into a single node — `1d20cs=20cs=1cf>18` has two success
|
|
332
|
+
* and one fail threshold. Display-only: does not change `total`,
|
|
333
|
+
* explosion triggers, or success counting. `cs` and `cf` are independent
|
|
334
|
+
* overrides — a side with no explicit thresholds keeps the default rule
|
|
335
|
+
* (the evaluator substitutes the `'default'` sentinel at apply time).
|
|
336
|
+
*
|
|
337
|
+
* @category AST
|
|
338
|
+
*/
|
|
339
|
+
export type CritThresholdNode = NodeSpan & {
|
|
340
|
+
readonly type: 'CritThreshold';
|
|
341
|
+
readonly successThresholds: readonly CritThreshold[];
|
|
342
|
+
readonly failThresholds: readonly CritThreshold[];
|
|
343
|
+
readonly target: ASTNode;
|
|
135
344
|
};
|
|
136
345
|
|
|
137
346
|
/**
|
|
138
|
-
*
|
|
347
|
+
* Discriminated union of all 17 AST node types — what {@link parse} returns
|
|
348
|
+
* and what {@link evaluate} consumes.
|
|
349
|
+
*
|
|
350
|
+
* Narrow it either by switching on `node.type` (PascalCase discriminants, as
|
|
351
|
+
* opposed to the camelCase ones on {@link RollPart}) or with the exported
|
|
352
|
+
* type guards: {@link isLiteral}, {@link isDice}, {@link isFateDice},
|
|
353
|
+
* {@link isBinaryOp}, {@link isUnaryOp}, {@link isKeepDrop},
|
|
354
|
+
* {@link isExplode}, {@link isReroll}, {@link isDieBound},
|
|
355
|
+
* {@link isSuccessCount}, {@link isVersus}, {@link isFunctionCall},
|
|
356
|
+
* {@link isGrouped}, {@link isVariable}, {@link isGroup}, {@link isSort},
|
|
357
|
+
* {@link isCritThreshold}.
|
|
358
|
+
*
|
|
359
|
+
* Nodes are plain data with no methods, so they are structurally clonable and
|
|
360
|
+
* safe to cache. Every parser-produced node carries a {@link NodeSpan}.
|
|
361
|
+
*
|
|
362
|
+
* @example A guard-based walker — count the dice an expression can roll
|
|
363
|
+
* ```typescript
|
|
364
|
+
* import {
|
|
365
|
+
* type ASTNode, isBinaryOp, isDice, isFateDice, isGroup, isGrouped,
|
|
366
|
+
* isFunctionCall, isLiteral, isUnaryOp, isVersus, parse,
|
|
367
|
+
* } from 'roll-parser';
|
|
368
|
+
*
|
|
369
|
+
* function countPools(node: ASTNode): number {
|
|
370
|
+
* if (isDice(node) || isFateDice(node)) return 1;
|
|
371
|
+
* if (isLiteral(node)) return 0;
|
|
372
|
+
* if (isBinaryOp(node)) return countPools(node.left) + countPools(node.right);
|
|
373
|
+
* if (isUnaryOp(node)) return countPools(node.operand);
|
|
374
|
+
* if (isGrouped(node)) return countPools(node.expression);
|
|
375
|
+
* if (isVersus(node)) return countPools(node.roll) + countPools(node.dc);
|
|
376
|
+
* if (isGroup(node)) return node.expressions.reduce((n, e) => n + countPools(e), 0);
|
|
377
|
+
* if (isFunctionCall(node)) return node.args.reduce((n, a) => n + countPools(a), 0);
|
|
378
|
+
* // Every remaining variant is a postfix modifier wrapping `target`.
|
|
379
|
+
* return 'target' in node ? countPools(node.target) : 0;
|
|
380
|
+
* }
|
|
381
|
+
*
|
|
382
|
+
* countPools(parse('2d6+3')); // 1
|
|
383
|
+
* countPools(parse('{2d20kh1+5, 3d8!}kh1')); // 2
|
|
384
|
+
* countPools(parse('1+2')); // 0
|
|
385
|
+
* ```
|
|
386
|
+
*
|
|
387
|
+
* @category AST
|
|
139
388
|
*/
|
|
140
389
|
export type ASTNode =
|
|
141
390
|
| LiteralNode
|
|
@@ -143,119 +392,219 @@ export type ASTNode =
|
|
|
143
392
|
| FateDiceNode
|
|
144
393
|
| BinaryOpNode
|
|
145
394
|
| UnaryOpNode
|
|
146
|
-
|
|
|
395
|
+
| KeepDropNode
|
|
147
396
|
| ExplodeNode
|
|
148
397
|
| RerollNode
|
|
398
|
+
| DieBoundNode
|
|
149
399
|
| SuccessCountNode
|
|
150
400
|
| VersusNode
|
|
151
|
-
| FunctionCallNode
|
|
401
|
+
| FunctionCallNode
|
|
402
|
+
| GroupedNode
|
|
403
|
+
| VariableNode
|
|
404
|
+
| GroupNode
|
|
405
|
+
| SortNode
|
|
406
|
+
| CritThresholdNode;
|
|
152
407
|
|
|
153
408
|
/**
|
|
154
|
-
*
|
|
409
|
+
* Narrows an {@link ASTNode} to a numeric literal (`3`, `2.5`).
|
|
410
|
+
*
|
|
411
|
+
* @param node - Any AST node
|
|
412
|
+
* @returns `true` when `node` is a {@link LiteralNode}
|
|
413
|
+
*
|
|
414
|
+
* @category AST
|
|
155
415
|
*/
|
|
156
416
|
export function isLiteral(node: ASTNode): node is LiteralNode {
|
|
157
417
|
return node.type === 'Literal';
|
|
158
418
|
}
|
|
159
419
|
|
|
160
420
|
/**
|
|
161
|
-
*
|
|
421
|
+
* Narrows an {@link ASTNode} to a dice pool (`2d6`, `d%`, `(1d4)d6`).
|
|
422
|
+
*
|
|
423
|
+
* @param node - Any AST node
|
|
424
|
+
* @returns `true` when `node` is a {@link DiceNode}
|
|
425
|
+
*
|
|
426
|
+
* @category AST
|
|
162
427
|
*/
|
|
163
428
|
export function isDice(node: ASTNode): node is DiceNode {
|
|
164
429
|
return node.type === 'Dice';
|
|
165
430
|
}
|
|
166
431
|
|
|
167
432
|
/**
|
|
168
|
-
*
|
|
433
|
+
* Narrows an {@link ASTNode} to a Fate/Fudge pool (`4dF`).
|
|
434
|
+
*
|
|
435
|
+
* @param node - Any AST node
|
|
436
|
+
* @returns `true` when `node` is a {@link FateDiceNode}
|
|
437
|
+
*
|
|
438
|
+
* @category AST
|
|
169
439
|
*/
|
|
170
440
|
export function isFateDice(node: ASTNode): node is FateDiceNode {
|
|
171
441
|
return node.type === 'FateDice';
|
|
172
442
|
}
|
|
173
443
|
|
|
174
444
|
/**
|
|
175
|
-
*
|
|
445
|
+
* Narrows an {@link ASTNode} to an arithmetic operation (`+ - * / % **`).
|
|
446
|
+
*
|
|
447
|
+
* @param node - Any AST node
|
|
448
|
+
* @returns `true` when `node` is a {@link BinaryOpNode}
|
|
449
|
+
*
|
|
450
|
+
* @category AST
|
|
176
451
|
*/
|
|
177
452
|
export function isBinaryOp(node: ASTNode): node is BinaryOpNode {
|
|
178
453
|
return node.type === 'BinaryOp';
|
|
179
454
|
}
|
|
180
455
|
|
|
181
456
|
/**
|
|
182
|
-
*
|
|
457
|
+
* Narrows an {@link ASTNode} to a prefix negation (`-1d4`).
|
|
458
|
+
*
|
|
459
|
+
* @param node - Any AST node
|
|
460
|
+
* @returns `true` when `node` is a {@link UnaryOpNode}
|
|
461
|
+
*
|
|
462
|
+
* @category AST
|
|
183
463
|
*/
|
|
184
464
|
export function isUnaryOp(node: ASTNode): node is UnaryOpNode {
|
|
185
465
|
return node.type === 'UnaryOp';
|
|
186
466
|
}
|
|
187
467
|
|
|
188
468
|
/**
|
|
189
|
-
*
|
|
469
|
+
* Narrows an {@link ASTNode} to a keep/drop modifier (`kh`, `kl`, `dh`, `dl`).
|
|
470
|
+
*
|
|
471
|
+
* @param node - Any AST node
|
|
472
|
+
* @returns `true` when `node` is a {@link KeepDropNode}
|
|
473
|
+
*
|
|
474
|
+
* @category AST
|
|
190
475
|
*/
|
|
191
|
-
export function
|
|
192
|
-
return node.type === '
|
|
476
|
+
export function isKeepDrop(node: ASTNode): node is KeepDropNode {
|
|
477
|
+
return node.type === 'KeepDrop';
|
|
193
478
|
}
|
|
194
479
|
|
|
195
480
|
/**
|
|
196
|
-
*
|
|
481
|
+
* Narrows an {@link ASTNode} to an explosion (`!`, `!!`, `!p`).
|
|
482
|
+
*
|
|
483
|
+
* @param node - Any AST node
|
|
484
|
+
* @returns `true` when `node` is a {@link ExplodeNode}
|
|
485
|
+
*
|
|
486
|
+
* @category AST
|
|
197
487
|
*/
|
|
198
488
|
export function isExplode(node: ASTNode): node is ExplodeNode {
|
|
199
489
|
return node.type === 'Explode';
|
|
200
490
|
}
|
|
201
491
|
|
|
202
492
|
/**
|
|
203
|
-
*
|
|
493
|
+
* Narrows an {@link ASTNode} to a reroll (`r`, `ro`).
|
|
494
|
+
*
|
|
495
|
+
* @param node - Any AST node
|
|
496
|
+
* @returns `true` when `node` is a {@link RerollNode}
|
|
497
|
+
*
|
|
498
|
+
* @category AST
|
|
204
499
|
*/
|
|
205
500
|
export function isReroll(node: ASTNode): node is RerollNode {
|
|
206
501
|
return node.type === 'Reroll';
|
|
207
502
|
}
|
|
208
503
|
|
|
209
504
|
/**
|
|
210
|
-
*
|
|
505
|
+
* Narrows an {@link ASTNode} to a success count (`>=6`, with optional `f1`).
|
|
506
|
+
*
|
|
507
|
+
* @param node - Any AST node
|
|
508
|
+
* @returns `true` when `node` is a {@link SuccessCountNode}
|
|
509
|
+
*
|
|
510
|
+
* @category AST
|
|
211
511
|
*/
|
|
212
512
|
export function isSuccessCount(node: ASTNode): node is SuccessCountNode {
|
|
213
513
|
return node.type === 'SuccessCount';
|
|
214
514
|
}
|
|
215
515
|
|
|
216
516
|
/**
|
|
217
|
-
*
|
|
517
|
+
* Narrows an {@link ASTNode} to a PF2e degree-of-success check (`vs`).
|
|
518
|
+
*
|
|
519
|
+
* @param node - Any AST node
|
|
520
|
+
* @returns `true` when `node` is a {@link VersusNode}
|
|
521
|
+
*
|
|
522
|
+
* @category AST
|
|
218
523
|
*/
|
|
219
524
|
export function isVersus(node: ASTNode): node is VersusNode {
|
|
220
525
|
return node.type === 'Versus';
|
|
221
526
|
}
|
|
222
527
|
|
|
223
528
|
/**
|
|
224
|
-
*
|
|
529
|
+
* Narrows an {@link ASTNode} to a math function call (`floor`, `max`, …).
|
|
530
|
+
*
|
|
531
|
+
* @param node - Any AST node
|
|
532
|
+
* @returns `true` when `node` is a {@link FunctionCallNode}
|
|
533
|
+
*
|
|
534
|
+
* @category AST
|
|
225
535
|
*/
|
|
226
536
|
export function isFunctionCall(node: ASTNode): node is FunctionCallNode {
|
|
227
537
|
return node.type === 'FunctionCall';
|
|
228
538
|
}
|
|
229
539
|
|
|
230
540
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
541
|
+
* Narrows an {@link ASTNode} to a parenthesized group (`(1d6+2)`).
|
|
542
|
+
*
|
|
543
|
+
* @param node - Any AST node
|
|
544
|
+
* @returns `true` when `node` is a {@link GroupedNode}
|
|
545
|
+
*
|
|
546
|
+
* @category AST
|
|
547
|
+
*/
|
|
548
|
+
export function isGrouped(node: ASTNode): node is GroupedNode {
|
|
549
|
+
return node.type === 'Grouped';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Narrows an {@link ASTNode} to a variable reference (`@str`).
|
|
554
|
+
*
|
|
555
|
+
* @param node - Any AST node
|
|
556
|
+
* @returns `true` when `node` is a {@link VariableNode}
|
|
557
|
+
*
|
|
558
|
+
* @category AST
|
|
559
|
+
*/
|
|
560
|
+
export function isVariable(node: ASTNode): node is VariableNode {
|
|
561
|
+
return node.type === 'Variable';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Narrows an {@link ASTNode} to a braced grouped roll (`{1d8, 1d10}`).
|
|
566
|
+
*
|
|
567
|
+
* @param node - Any AST node
|
|
568
|
+
* @returns `true` when `node` is a {@link GroupNode}
|
|
569
|
+
*
|
|
570
|
+
* @category AST
|
|
571
|
+
*/
|
|
572
|
+
export function isGroup(node: ASTNode): node is GroupNode {
|
|
573
|
+
return node.type === 'Group';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Narrows an {@link ASTNode} to a sort modifier (`s`, `sa`, `sd`).
|
|
578
|
+
*
|
|
579
|
+
* @param node - Any AST node
|
|
580
|
+
* @returns `true` when `node` is a {@link SortNode}
|
|
581
|
+
*
|
|
582
|
+
* @category AST
|
|
583
|
+
*/
|
|
584
|
+
export function isSort(node: ASTNode): node is SortNode {
|
|
585
|
+
return node.type === 'Sort';
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Narrows an {@link ASTNode} to a crit-threshold override (`cs`, `cf`).
|
|
590
|
+
*
|
|
591
|
+
* @param node - Any AST node
|
|
592
|
+
* @returns `true` when `node` is a {@link CritThresholdNode}
|
|
593
|
+
*
|
|
594
|
+
* @category AST
|
|
595
|
+
*/
|
|
596
|
+
export function isCritThreshold(node: ASTNode): node is CritThresholdNode {
|
|
597
|
+
return node.type === 'CritThreshold';
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Narrows an {@link ASTNode} to a per-die clamp (`min2`, `max5`).
|
|
602
|
+
*
|
|
603
|
+
* @param node - Any AST node
|
|
604
|
+
* @returns `true` when `node` is a {@link DieBoundNode}
|
|
605
|
+
*
|
|
606
|
+
* @category AST
|
|
607
|
+
*/
|
|
608
|
+
export function isDieBound(node: ASTNode): node is DieBoundNode {
|
|
609
|
+
return node.type === 'DieBound';
|
|
261
610
|
}
|