roll-parser 3.0.0-beta.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 +107 -9
- package/MIGRATION.md +147 -0
- package/README.md +960 -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 +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 -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 +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 +41 -75
- 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.js +15 -0
- package/dist/evaluator/modifiers/compare.js.map +1 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts +0 -1
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -1
- 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 +13 -5
- 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 +12 -4
- 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 +4 -0
- 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 +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 +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 +314 -160
- 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/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 +306 -44
- 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 +83 -34
- package/src/cli/args.ts +66 -10
- package/src/cli/format.ts +22 -3
- 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 +50 -0
- package/src/evaluator/env.ts +73 -0
- package/src/evaluator/evaluator.ts +653 -421
- package/src/evaluator/modifiers/crit-threshold.ts +3 -6
- package/src/evaluator/modifiers/die-bound.ts +39 -0
- package/src/evaluator/modifiers/explode.ts +60 -58
- package/src/evaluator/modifiers/flags.ts +61 -0
- package/src/evaluator/modifiers/keep-drop.ts +124 -126
- package/src/evaluator/modifiers/reroll.ts +28 -49
- package/src/evaluator/modifiers/sort.ts +14 -1
- package/src/evaluator/modifiers/success-count.ts +5 -8
- 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 +323 -341
- package/src/parser/guards.ts +248 -0
- package/src/parser/parser.ts +419 -242
- 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 +310 -43
- 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
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser-internal AST guards.
|
|
3
|
+
*
|
|
4
|
+
* These helpers answer structural questions the parser's reject rules ask
|
|
5
|
+
* ("does this operand resolve to a dice pool?", "is a Versus buried in
|
|
6
|
+
* here?"). They are implementation detail of `parser.ts` — the public
|
|
7
|
+
* `isXxx` type guards stay in `ast.ts`.
|
|
8
|
+
*
|
|
9
|
+
* @module parser/guards
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ASTNode } from './ast.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Peels nested `Grouped` wrappers, returning the first descendant that is not
|
|
16
|
+
* one. The narrow unwrap for reject helpers whose forbidden node cannot live
|
|
17
|
+
* inside `KeepDrop`/`Sort`/`CritThreshold` — those parsers already reject it
|
|
18
|
+
* upstream, so peeling parentheses is all that is left to see through.
|
|
19
|
+
*/
|
|
20
|
+
export function unwrapGrouped(node: ASTNode): ASTNode {
|
|
21
|
+
let current = node;
|
|
22
|
+
while (current.type === 'Grouped') {
|
|
23
|
+
current = current.expression;
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Peels every transparent wrapper — `Grouped`, `KeepDrop`, `Sort`,
|
|
30
|
+
* `CritThreshold`, `DieBound` — returning the first descendant that is none of
|
|
31
|
+
* them.
|
|
32
|
+
*
|
|
33
|
+
* "Transparent" is relative to the question being asked. These wrappers
|
|
34
|
+
* preserve `containsDicePool`'s answer for whatever they wrap, so they are
|
|
35
|
+
* transparent for "what is the underlying operand?" when deciding whether to
|
|
36
|
+
* reject a `Group` target (e.g., `Group` cannot be the target of `cs`/`cf`,
|
|
37
|
+
* even when wrapped in `KeepDrop` like `{1d6}kh1cs>5`). They are NOT
|
|
38
|
+
* transparent for "is this a `SuccessCount`?" or "is this a `Versus`?" —
|
|
39
|
+
* those questions use `unwrapGrouped`.
|
|
40
|
+
*/
|
|
41
|
+
export function unwrapAllTransparent(node: ASTNode): ASTNode {
|
|
42
|
+
let current = node;
|
|
43
|
+
while (true) {
|
|
44
|
+
switch (current.type) {
|
|
45
|
+
case 'Grouped':
|
|
46
|
+
current = current.expression;
|
|
47
|
+
break;
|
|
48
|
+
case 'KeepDrop':
|
|
49
|
+
case 'Sort':
|
|
50
|
+
case 'CritThreshold':
|
|
51
|
+
case 'DieBound':
|
|
52
|
+
current = current.target;
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns `true` when `node` or any descendant satisfies `isHit`. Recurses
|
|
62
|
+
* directly through the entire node vocabulary: arithmetic operands,
|
|
63
|
+
* modifier-chain targets, `Versus` sides, function arguments, and group
|
|
64
|
+
* sub-expressions. This is the shared driver behind the four deep walkers
|
|
65
|
+
* below. The shallow walkers (`containsDicePool`, `containsFatePool`) stay
|
|
66
|
+
* hand-written because their rejection semantics deliberately stop at
|
|
67
|
+
* arithmetic boundaries.
|
|
68
|
+
*/
|
|
69
|
+
function someDescendant(node: ASTNode, isHit: (node: ASTNode) => boolean): boolean {
|
|
70
|
+
if (isHit(node)) return true;
|
|
71
|
+
|
|
72
|
+
switch (node.type) {
|
|
73
|
+
case 'BinaryOp':
|
|
74
|
+
return someDescendant(node.left, isHit) || someDescendant(node.right, isHit);
|
|
75
|
+
case 'UnaryOp':
|
|
76
|
+
return someDescendant(node.operand, isHit);
|
|
77
|
+
case 'KeepDrop':
|
|
78
|
+
case 'Explode':
|
|
79
|
+
case 'Reroll':
|
|
80
|
+
case 'DieBound':
|
|
81
|
+
case 'SuccessCount':
|
|
82
|
+
case 'Sort':
|
|
83
|
+
case 'CritThreshold':
|
|
84
|
+
return someDescendant(node.target, isHit);
|
|
85
|
+
case 'Versus':
|
|
86
|
+
return someDescendant(node.roll, isHit) || someDescendant(node.dc, isHit);
|
|
87
|
+
case 'FunctionCall': {
|
|
88
|
+
for (const arg of node.args) {
|
|
89
|
+
if (someDescendant(arg, isHit)) return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
case 'Grouped':
|
|
94
|
+
return someDescendant(node.expression, isHit);
|
|
95
|
+
case 'Group': {
|
|
96
|
+
for (const expression of node.expressions) {
|
|
97
|
+
if (someDescendant(expression, isHit)) return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Hoisted `someDescendant` predicates — the deep walkers run on nearly every
|
|
107
|
+
// LED parse, so rebuilding these closures per call is measurable churn.
|
|
108
|
+
const isDicePoolHit = (current: ASTNode): boolean =>
|
|
109
|
+
current.type === 'Dice' ||
|
|
110
|
+
current.type === 'FateDice' ||
|
|
111
|
+
(current.type === 'Group' && current.expressions.length >= 2);
|
|
112
|
+
|
|
113
|
+
const isFateDiceHit = (current: ASTNode): boolean => current.type === 'FateDice';
|
|
114
|
+
|
|
115
|
+
const isMultiSubGroupHit = (current: ASTNode): boolean =>
|
|
116
|
+
current.type === 'Group' && current.expressions.length >= 2;
|
|
117
|
+
|
|
118
|
+
const isVersusHit = (current: ASTNode): boolean => current.type === 'Versus';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns `true` only when `node`'s direct result is a dice pool —
|
|
122
|
+
* `Dice`, `FateDice`, or a chained pool modifier (`KeepDrop` / `Explode` /
|
|
123
|
+
* `Reroll`). Does NOT recurse through arithmetic wrappers (`BinaryOp`,
|
|
124
|
+
* `UnaryOp`, `FunctionCall`), so `(1d6+5)` and `floor(1d6/2)` are rejected.
|
|
125
|
+
*
|
|
126
|
+
* Used by the parser to reject postfix pool-modifier targets (kh/kl/dh/dl,
|
|
127
|
+
* !/!!/!p, r/ro) that wrap a non-pool expression. Operating on the inner
|
|
128
|
+
* dice pool would silently drop the surrounding arithmetic.
|
|
129
|
+
*/
|
|
130
|
+
export function containsDicePool(node: ASTNode): boolean {
|
|
131
|
+
switch (node.type) {
|
|
132
|
+
case 'Dice':
|
|
133
|
+
case 'FateDice':
|
|
134
|
+
case 'KeepDrop':
|
|
135
|
+
case 'Explode':
|
|
136
|
+
case 'Reroll':
|
|
137
|
+
case 'DieBound':
|
|
138
|
+
return true;
|
|
139
|
+
case 'Sort':
|
|
140
|
+
case 'CritThreshold':
|
|
141
|
+
return containsDicePool(node.target);
|
|
142
|
+
case 'Grouped':
|
|
143
|
+
return containsDicePool(node.expression);
|
|
144
|
+
case 'Group':
|
|
145
|
+
// Multi-sub-roll groups (`{a, b, c}kh1`) always accept: keep/drop operates
|
|
146
|
+
// on subtotals, which are compound dice by definition — even a
|
|
147
|
+
// literal-only `{3, 5, 7}kh1` is valid. A single-sub-roll group is the
|
|
148
|
+
// user's explicit opt-in to flat-pool semantics, so it deep-walks
|
|
149
|
+
// arithmetic that a raw `(1d6+5)kh1` rejects — the Stage 3 `{}` escape
|
|
150
|
+
// hatch.
|
|
151
|
+
return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
|
|
152
|
+
default:
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Deeper variant of `containsDicePool` that recurses through arithmetic and
|
|
159
|
+
* function wrappers. Used from the `Group` case above (ordinary parenthesized
|
|
160
|
+
* arithmetic `(1d6+5)kh1` must still reject, so the shallow `containsDicePool`
|
|
161
|
+
* handles those directly) and from the Sort parser guard (sort accepts
|
|
162
|
+
* `(1d6+2d8)s` per Stage 3 spec).
|
|
163
|
+
*
|
|
164
|
+
* A multi-sub-roll `Group` counts as a pool in its own right (compound dice),
|
|
165
|
+
* matching `containsDicePool`'s Group rule.
|
|
166
|
+
*/
|
|
167
|
+
export function deepContainsDicePool(node: ASTNode): boolean {
|
|
168
|
+
return someDescendant(node, isDicePoolHit);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Returns `true` if the pool this node resolves to is (or wraps) a `FateDice`
|
|
173
|
+
* pool. Walks through chained pool modifiers (`KeepDrop` / `Explode` /
|
|
174
|
+
* `Reroll`) but not arithmetic wrappers — callers should run
|
|
175
|
+
* `containsDicePool` first to reject those.
|
|
176
|
+
*
|
|
177
|
+
* Used by the parser to reject `!`, `!!`, `!p` applied to Fate pools
|
|
178
|
+
* (`4dF!`, `(4dF)kh2!`, etc.). Fate explosion semantics are undefined, so
|
|
179
|
+
* parse-time rejection is preferred over a silent evaluator no-op.
|
|
180
|
+
*
|
|
181
|
+
* Inside a `Group`, recursion uses `deepContainsFatePool` to mirror
|
|
182
|
+
* `containsDicePool`'s deep walk through the same case — otherwise
|
|
183
|
+
* `{4dF+1d6}cf` slips past the bare-Fate guard and the default fumble
|
|
184
|
+
* check (`result === 1`) flips `+1` faces into fumbles.
|
|
185
|
+
*/
|
|
186
|
+
export function containsFatePool(node: ASTNode): boolean {
|
|
187
|
+
switch (node.type) {
|
|
188
|
+
case 'FateDice':
|
|
189
|
+
return true;
|
|
190
|
+
case 'KeepDrop':
|
|
191
|
+
case 'Explode':
|
|
192
|
+
case 'Reroll':
|
|
193
|
+
case 'DieBound':
|
|
194
|
+
case 'Sort':
|
|
195
|
+
case 'CritThreshold':
|
|
196
|
+
return containsFatePool(node.target);
|
|
197
|
+
case 'Grouped':
|
|
198
|
+
return containsFatePool(node.expression);
|
|
199
|
+
case 'Group':
|
|
200
|
+
return node.expressions.some(deepContainsFatePool);
|
|
201
|
+
default:
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Deeper variant of `containsFatePool` that recurses through arithmetic and
|
|
208
|
+
* function wrappers. Mirrors `deepContainsDicePool`. Used from
|
|
209
|
+
* `containsFatePool`'s `Group` case so single-sub-roll groups containing
|
|
210
|
+
* arithmetic-wrapped Fate (`{4dF+1d6}cf`) still trip the bare-Fate guard.
|
|
211
|
+
*
|
|
212
|
+
* Outside a `Group`, ordinary parenthesized arithmetic (`(4dF+1d6)cf`) is
|
|
213
|
+
* already rejected upstream by shallow `containsDicePool`, so this helper
|
|
214
|
+
* intentionally stays Group-internal.
|
|
215
|
+
*/
|
|
216
|
+
export function deepContainsFatePool(node: ASTNode): boolean {
|
|
217
|
+
return someDescendant(node, isFateDiceHit);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Deep-walks a node to find any descendant `Group` with two or more
|
|
222
|
+
* sub-expressions. Used by `rejectGroupTarget`'s single-sub-roll
|
|
223
|
+
* passthrough so a multi-sub Group buried under arithmetic
|
|
224
|
+
* (`{{1d6,2d8}+0}cs>5`), function calls (`{abs({1d6,2d8})}cs>5`), or any
|
|
225
|
+
* other non-transparent wrapper still rejects with the same error code.
|
|
226
|
+
*
|
|
227
|
+
* Without this walk, the unwrap inside `rejectGroupTarget` only peels
|
|
228
|
+
* `Grouped`/`KeepDrop`/`Sort`/`CritThreshold` — a multi-sub Group cloaked
|
|
229
|
+
* in a `BinaryOp`/`UnaryOp`/`FunctionCall` reaches the evaluator, where it
|
|
230
|
+
* flags crits on dice belonging to dropped sub-rolls.
|
|
231
|
+
*/
|
|
232
|
+
export function containsMultiSubGroup(node: ASTNode): boolean {
|
|
233
|
+
return someDescendant(node, isMultiSubGroupHit);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Deep-walks a node to find any descendant `Versus`. A buried Versus
|
|
238
|
+
* (`floor(1d20 vs 15)`, `(1d20 vs 15)+0`, `{1+(1d20 vs 15)}`) must reject with
|
|
239
|
+
* `NESTED_VERSUS` rather than silently drop `versusMetadata` at the consumer.
|
|
240
|
+
*
|
|
241
|
+
* Two callers, both on that hole: `rejectVersusMetaOperand` guards every meta
|
|
242
|
+
* operand — dice count and sides, keep/drop count, thresholds, die bounds — and
|
|
243
|
+
* `rejectVersusTarget` uses it for its single-sub-roll Group passthrough.
|
|
244
|
+
* `evalMetaOperand` runs the same walk as the hand-built-AST backstop.
|
|
245
|
+
*/
|
|
246
|
+
export function containsVersus(node: ASTNode): boolean {
|
|
247
|
+
return someDescendant(node, isVersusHit);
|
|
248
|
+
}
|