roll-parser 2.3.2 → 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.
Files changed (109) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +175 -170
  3. package/dist/cli/args.d.ts +35 -0
  4. package/dist/cli/args.d.ts.map +1 -0
  5. package/dist/cli/format.d.ts +20 -0
  6. package/dist/cli/format.d.ts.map +1 -0
  7. package/dist/cli/index.d.ts +8 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli.js +2608 -0
  10. package/dist/cli.js.map +28 -0
  11. package/dist/errors.d.ts +42 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/evaluator/evaluator.d.ts +121 -0
  14. package/dist/evaluator/evaluator.d.ts.map +1 -0
  15. package/dist/evaluator/index.d.ts +8 -0
  16. package/dist/evaluator/index.d.ts.map +1 -0
  17. package/dist/evaluator/modifiers/compare.d.ts +15 -0
  18. package/dist/evaluator/modifiers/compare.d.ts.map +1 -0
  19. package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
  20. package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
  21. package/dist/evaluator/modifiers/explode.d.ts +48 -0
  22. package/dist/evaluator/modifiers/explode.d.ts.map +1 -0
  23. package/dist/evaluator/modifiers/keep-drop.d.ts +44 -0
  24. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -0
  25. package/dist/evaluator/modifiers/reroll.d.ts +33 -0
  26. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -0
  27. package/dist/evaluator/modifiers/sort.d.ts +23 -0
  28. package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
  29. package/dist/evaluator/modifiers/success-count.d.ts +28 -0
  30. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -0
  31. package/dist/index.d.ts +20 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +2560 -0
  34. package/dist/index.js.map +26 -0
  35. package/dist/lexer/lexer.d.ts +90 -0
  36. package/dist/lexer/lexer.d.ts.map +1 -0
  37. package/dist/lexer/tokens.d.ts +107 -0
  38. package/dist/lexer/tokens.d.ts.map +1 -0
  39. package/dist/parser/ast.d.ts +368 -0
  40. package/dist/parser/ast.d.ts.map +1 -0
  41. package/dist/parser/parser.d.ts +145 -0
  42. package/dist/parser/parser.d.ts.map +1 -0
  43. package/dist/rng/index.d.ts +8 -0
  44. package/dist/rng/index.d.ts.map +1 -0
  45. package/dist/rng/mock.d.ts +36 -0
  46. package/dist/rng/mock.d.ts.map +1 -0
  47. package/dist/rng/seeded.d.ts +40 -0
  48. package/dist/rng/seeded.d.ts.map +1 -0
  49. package/dist/rng/types.d.ts +24 -0
  50. package/dist/rng/types.d.ts.map +1 -0
  51. package/dist/roll.d.ts +51 -0
  52. package/dist/roll.d.ts.map +1 -0
  53. package/dist/testing.d.ts +11 -0
  54. package/dist/testing.d.ts.map +1 -0
  55. package/dist/testing.js +41 -0
  56. package/dist/testing.js.map +11 -0
  57. package/dist/types.d.ts +246 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/package.json +66 -43
  60. package/src/cli/args.ts +73 -0
  61. package/src/cli/format.ts +49 -0
  62. package/src/cli/index.ts +92 -0
  63. package/src/errors.ts +90 -0
  64. package/src/evaluator/evaluator.ts +1552 -0
  65. package/src/evaluator/index.ts +14 -0
  66. package/src/evaluator/modifiers/compare.ts +29 -0
  67. package/src/evaluator/modifiers/crit-threshold.ts +59 -0
  68. package/src/evaluator/modifiers/explode.ts +219 -0
  69. package/src/evaluator/modifiers/keep-drop.ts +151 -0
  70. package/src/evaluator/modifiers/reroll.ts +165 -0
  71. package/src/evaluator/modifiers/sort.ts +30 -0
  72. package/src/evaluator/modifiers/success-count.ts +59 -0
  73. package/src/index.ts +90 -0
  74. package/src/lexer/lexer.ts +374 -0
  75. package/src/lexer/tokens.ts +187 -0
  76. package/src/parser/ast.ts +628 -0
  77. package/src/parser/parser.ts +1239 -0
  78. package/src/rng/index.ts +8 -0
  79. package/src/rng/mock.ts +65 -0
  80. package/src/rng/seeded.ts +157 -0
  81. package/src/rng/types.ts +25 -0
  82. package/src/roll.ts +74 -0
  83. package/src/testing.ts +16 -0
  84. package/src/types.ts +237 -0
  85. package/bin/help.txt +0 -19
  86. package/bin/roll-parser.js +0 -23
  87. package/dist/roll-parser.js +0 -697
  88. package/dist/roll-parser.min.js +0 -1
  89. package/index.js +0 -24
  90. package/src/complex/parse.js +0 -37
  91. package/src/complex/parseAndRoll.js +0 -21
  92. package/src/complex/parseAndRollClassic.js +0 -20
  93. package/src/complex/parseAndRollSimple.js +0 -19
  94. package/src/complex/parseAndRollWod.js +0 -20
  95. package/src/complex/parseClassicRoll.js +0 -22
  96. package/src/complex/parseSimpleRoll.js +0 -21
  97. package/src/complex/parseWodRoll.js +0 -21
  98. package/src/converter.js +0 -45
  99. package/src/grammar.js +0 -14
  100. package/src/mapper.js +0 -50
  101. package/src/normalizer.js +0 -51
  102. package/src/object/Result.js +0 -23
  103. package/src/object/Roll.js +0 -26
  104. package/src/object/Type.js +0 -5
  105. package/src/object/WodRoll.js +0 -29
  106. package/src/parser.js +0 -43
  107. package/src/random.js +0 -22
  108. package/src/roller.js +0 -106
  109. package/src/stringifier.js +0 -33
package/dist/cli.js ADDED
@@ -0,0 +1,2608 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ var ROLL_PARSER_ERROR_CODES = [
5
+ "UNEXPECTED_CHARACTER",
6
+ "UNEXPECTED_IDENTIFIER",
7
+ "UNEXPECTED_TOKEN",
8
+ "UNEXPECTED_END",
9
+ "EXPECTED_TOKEN",
10
+ "INVALID_DICE_COUNT",
11
+ "INVALID_DICE_SIDES",
12
+ "DICE_LIMIT_EXCEEDED",
13
+ "DIVISION_BY_ZERO",
14
+ "MODULO_BY_ZERO",
15
+ "UNKNOWN_OPERATOR",
16
+ "UNKNOWN_NODE_TYPE",
17
+ "INVALID_MODIFIER_COUNT",
18
+ "INVALID_MODIFIER_TARGET",
19
+ "EXPLODE_LIMIT_EXCEEDED",
20
+ "INVALID_EXPLODE_TARGET",
21
+ "REROLL_LIMIT_EXCEEDED",
22
+ "INVALID_REROLL_TARGET",
23
+ "INVALID_SUCCESS_COUNT_TARGET",
24
+ "INVALID_SORT_TARGET",
25
+ "INVALID_CRIT_THRESHOLD_TARGET",
26
+ "INVALID_THRESHOLD",
27
+ "NESTED_VERSUS",
28
+ "INVALID_FUNCTION_ARITY",
29
+ "UNKNOWN_FUNCTION",
30
+ "UNDEFINED_VARIABLE",
31
+ "INVALID_VARIABLE_VALUE",
32
+ "AMBIGUOUS_DICE_CHAIN",
33
+ "MAX_DEPTH_EXCEEDED",
34
+ "NON_FINITE_RESULT"
35
+ ];
36
+
37
+ class RollParserError extends Error {
38
+ code;
39
+ constructor(message, code) {
40
+ super(message);
41
+ this.name = "RollParserError";
42
+ this.code = code;
43
+ }
44
+ }
45
+ var VALID_CODES = new Set(ROLL_PARSER_ERROR_CODES);
46
+ function isRollParserError(value) {
47
+ if (value instanceof RollParserError)
48
+ return true;
49
+ return value instanceof Error && "code" in value && typeof value.code === "string" && VALID_CODES.has(value.code);
50
+ }
51
+
52
+ // src/lexer/tokens.ts
53
+ var TokenType;
54
+ ((TokenType2) => {
55
+ TokenType2[TokenType2["NUMBER"] = 0] = "NUMBER";
56
+ TokenType2[TokenType2["DICE"] = 1] = "DICE";
57
+ TokenType2[TokenType2["DICE_PERCENT"] = 2] = "DICE_PERCENT";
58
+ TokenType2[TokenType2["DICE_FATE"] = 3] = "DICE_FATE";
59
+ TokenType2[TokenType2["PLUS"] = 4] = "PLUS";
60
+ TokenType2[TokenType2["MINUS"] = 5] = "MINUS";
61
+ TokenType2[TokenType2["MULTIPLY"] = 6] = "MULTIPLY";
62
+ TokenType2[TokenType2["DIVIDE"] = 7] = "DIVIDE";
63
+ TokenType2[TokenType2["MODULO"] = 8] = "MODULO";
64
+ TokenType2[TokenType2["POWER"] = 9] = "POWER";
65
+ TokenType2[TokenType2["GREATER"] = 10] = "GREATER";
66
+ TokenType2[TokenType2["GREATER_EQUAL"] = 11] = "GREATER_EQUAL";
67
+ TokenType2[TokenType2["LESS"] = 12] = "LESS";
68
+ TokenType2[TokenType2["LESS_EQUAL"] = 13] = "LESS_EQUAL";
69
+ TokenType2[TokenType2["EQUAL"] = 14] = "EQUAL";
70
+ TokenType2[TokenType2["LPAREN"] = 15] = "LPAREN";
71
+ TokenType2[TokenType2["RPAREN"] = 16] = "RPAREN";
72
+ TokenType2[TokenType2["COMMA"] = 17] = "COMMA";
73
+ TokenType2[TokenType2["KEEP_HIGH"] = 18] = "KEEP_HIGH";
74
+ TokenType2[TokenType2["KEEP_LOW"] = 19] = "KEEP_LOW";
75
+ TokenType2[TokenType2["DROP_HIGH"] = 20] = "DROP_HIGH";
76
+ TokenType2[TokenType2["DROP_LOW"] = 21] = "DROP_LOW";
77
+ TokenType2[TokenType2["EXPLODE"] = 22] = "EXPLODE";
78
+ TokenType2[TokenType2["EXPLODE_COMPOUND"] = 23] = "EXPLODE_COMPOUND";
79
+ TokenType2[TokenType2["EXPLODE_PENETRATING"] = 24] = "EXPLODE_PENETRATING";
80
+ TokenType2[TokenType2["REROLL"] = 25] = "REROLL";
81
+ TokenType2[TokenType2["REROLL_ONCE"] = 26] = "REROLL_ONCE";
82
+ TokenType2[TokenType2["FAIL"] = 27] = "FAIL";
83
+ TokenType2[TokenType2["FUNCTION"] = 28] = "FUNCTION";
84
+ TokenType2[TokenType2["VS"] = 29] = "VS";
85
+ TokenType2[TokenType2["LBRACE"] = 30] = "LBRACE";
86
+ TokenType2[TokenType2["RBRACE"] = 31] = "RBRACE";
87
+ TokenType2[TokenType2["AT"] = 32] = "AT";
88
+ TokenType2[TokenType2["SORT_ASC"] = 33] = "SORT_ASC";
89
+ TokenType2[TokenType2["SORT_DESC"] = 34] = "SORT_DESC";
90
+ TokenType2[TokenType2["CRIT_SUCCESS"] = 35] = "CRIT_SUCCESS";
91
+ TokenType2[TokenType2["CRIT_FAIL"] = 36] = "CRIT_FAIL";
92
+ TokenType2[TokenType2["EOF"] = 37] = "EOF";
93
+ })(TokenType ||= {});
94
+
95
+ // src/lexer/lexer.ts
96
+ class LexerError extends RollParserError {
97
+ position;
98
+ character;
99
+ constructor(message, code, position, character) {
100
+ super(`${message} at position ${position}: '${character}'`, code);
101
+ this.name = "LexerError";
102
+ this.position = position;
103
+ this.character = character;
104
+ }
105
+ }
106
+ var IDENTIFIER_KEYWORDS = {
107
+ kh: 18 /* KEEP_HIGH */,
108
+ kl: 19 /* KEEP_LOW */,
109
+ k: 18 /* KEEP_HIGH */,
110
+ dh: 20 /* DROP_HIGH */,
111
+ dl: 21 /* DROP_LOW */,
112
+ d: 1 /* DICE */,
113
+ r: 25 /* REROLL */,
114
+ ro: 26 /* REROLL_ONCE */,
115
+ f: 27 /* FAIL */,
116
+ vs: 29 /* VS */,
117
+ floor: 28 /* FUNCTION */,
118
+ ceil: 28 /* FUNCTION */,
119
+ round: 28 /* FUNCTION */,
120
+ abs: 28 /* FUNCTION */,
121
+ max: 28 /* FUNCTION */,
122
+ min: 28 /* FUNCTION */,
123
+ s: 33 /* SORT_ASC */,
124
+ sa: 33 /* SORT_ASC */,
125
+ sd: 34 /* SORT_DESC */,
126
+ cs: 35 /* CRIT_SUCCESS */,
127
+ cf: 36 /* CRIT_FAIL */
128
+ };
129
+ function buildIdentifierHint(identifier) {
130
+ for (let length = identifier.length - 1;length >= 1; length--) {
131
+ const prefix = identifier.slice(0, length);
132
+ if (IDENTIFIER_KEYWORDS[prefix] == null)
133
+ continue;
134
+ const rest = identifier.slice(length);
135
+ return ` (did you mean '${prefix}' followed by '${rest}'? separate modifiers with a count or space, e.g. '${prefix}1${rest}')`;
136
+ }
137
+ return "";
138
+ }
139
+
140
+ class Lexer {
141
+ pos = 0;
142
+ input;
143
+ constructor(input) {
144
+ this.input = input;
145
+ }
146
+ tokenize() {
147
+ const tokens = [];
148
+ while (true) {
149
+ const token = this.nextToken();
150
+ tokens.push(token);
151
+ if (token.type === 37 /* EOF */)
152
+ break;
153
+ }
154
+ return tokens;
155
+ }
156
+ nextToken() {
157
+ this.skipWhitespace();
158
+ if (this.isAtEnd()) {
159
+ return this.createToken(37 /* EOF */, "");
160
+ }
161
+ const startPos = this.pos;
162
+ const char = this.peek();
163
+ if (this.isDigit(char)) {
164
+ return this.scanNumber();
165
+ }
166
+ if (this.isAlpha(char)) {
167
+ return this.scanIdentifier();
168
+ }
169
+ if (char === "@") {
170
+ return this.scanAt();
171
+ }
172
+ this.advance();
173
+ switch (char) {
174
+ case "+":
175
+ return this.createTokenAt(4 /* PLUS */, char, startPos);
176
+ case "-":
177
+ return this.createTokenAt(5 /* MINUS */, char, startPos);
178
+ case "*":
179
+ if (this.match("*")) {
180
+ return this.createTokenAt(9 /* POWER */, "**", startPos);
181
+ }
182
+ return this.createTokenAt(6 /* MULTIPLY */, char, startPos);
183
+ case "/":
184
+ return this.createTokenAt(7 /* DIVIDE */, char, startPos);
185
+ case "%":
186
+ return this.createTokenAt(8 /* MODULO */, char, startPos);
187
+ case "^":
188
+ return this.createTokenAt(9 /* POWER */, char, startPos);
189
+ case "(":
190
+ return this.createTokenAt(15 /* LPAREN */, char, startPos);
191
+ case ")":
192
+ return this.createTokenAt(16 /* RPAREN */, char, startPos);
193
+ case ",":
194
+ return this.createTokenAt(17 /* COMMA */, char, startPos);
195
+ case "{":
196
+ return this.createTokenAt(30 /* LBRACE */, char, startPos);
197
+ case "}":
198
+ return this.createTokenAt(31 /* RBRACE */, char, startPos);
199
+ case ">":
200
+ if (this.match("=")) {
201
+ return this.createTokenAt(11 /* GREATER_EQUAL */, ">=", startPos);
202
+ }
203
+ return this.createTokenAt(10 /* GREATER */, char, startPos);
204
+ case "<":
205
+ if (this.match("=")) {
206
+ return this.createTokenAt(13 /* LESS_EQUAL */, "<=", startPos);
207
+ }
208
+ return this.createTokenAt(12 /* LESS */, char, startPos);
209
+ case "=":
210
+ return this.createTokenAt(14 /* EQUAL */, char, startPos);
211
+ case "!":
212
+ if (this.match("!")) {
213
+ return this.createTokenAt(23 /* EXPLODE_COMPOUND */, "!!", startPos);
214
+ }
215
+ if (!this.isAtEnd() && this.peek().toLowerCase() === "p") {
216
+ this.advance();
217
+ return this.createTokenAt(24 /* EXPLODE_PENETRATING */, "!p", startPos);
218
+ }
219
+ return this.createTokenAt(22 /* EXPLODE */, char, startPos);
220
+ default: {
221
+ const codePoint = this.input.codePointAt(startPos);
222
+ const display = codePoint == null ? char : String.fromCodePoint(codePoint);
223
+ throw new LexerError("Unexpected character", "UNEXPECTED_CHARACTER", startPos, display);
224
+ }
225
+ }
226
+ }
227
+ skipWhitespace() {
228
+ while (!this.isAtEnd() && this.isWhitespace(this.peek())) {
229
+ this.advance();
230
+ }
231
+ }
232
+ scanNumber() {
233
+ const startPos = this.pos;
234
+ let value = "";
235
+ while (!this.isAtEnd() && this.isDigit(this.peek())) {
236
+ value += this.advance();
237
+ }
238
+ if (!this.isAtEnd() && this.peek() === "." && this.isDigit(this.peekNext())) {
239
+ value += this.advance();
240
+ while (!this.isAtEnd() && this.isDigit(this.peek())) {
241
+ value += this.advance();
242
+ }
243
+ }
244
+ return this.createTokenAt(0 /* NUMBER */, value, startPos);
245
+ }
246
+ scanIdentifier() {
247
+ const startPos = this.pos;
248
+ const first = this.peek();
249
+ const second = this.peekNext();
250
+ if ((first === "d" || first === "D") && (second === "f" || second === "F")) {
251
+ this.advance();
252
+ this.advance();
253
+ return this.createTokenAt(3 /* DICE_FATE */, "df", startPos);
254
+ }
255
+ let value = "";
256
+ while (!this.isAtEnd() && this.isAlpha(this.peek())) {
257
+ value += this.advance();
258
+ }
259
+ const lower = value.toLowerCase();
260
+ if (lower === "d" && !this.isAtEnd() && this.peek() === "%") {
261
+ this.advance();
262
+ return this.createTokenAt(2 /* DICE_PERCENT */, "d%", startPos);
263
+ }
264
+ const tokenType = IDENTIFIER_KEYWORDS[lower];
265
+ if (tokenType != null) {
266
+ return this.createTokenAt(tokenType, lower, startPos);
267
+ }
268
+ throw new LexerError(`Unexpected identifier${buildIdentifierHint(lower)}`, "UNEXPECTED_IDENTIFIER", startPos, lower);
269
+ }
270
+ scanAt() {
271
+ const startPos = this.pos;
272
+ this.advance();
273
+ let name;
274
+ if (!this.isAtEnd() && this.peek() === "{") {
275
+ this.advance();
276
+ const nameStart = this.pos;
277
+ while (!this.isAtEnd() && this.peek() !== "}" && this.peek() !== `
278
+ `) {
279
+ this.advance();
280
+ }
281
+ if (this.isAtEnd() || this.peek() !== "}") {
282
+ throw new LexerError("Unterminated @{...} variable", "UNEXPECTED_CHARACTER", startPos, "@");
283
+ }
284
+ name = this.input.slice(nameStart, this.pos);
285
+ this.advance();
286
+ } else {
287
+ const nameStart = this.pos;
288
+ if (this.isAtEnd() || !this.isIdentifierStart(this.peek())) {
289
+ throw new LexerError("Empty @ variable name", "UNEXPECTED_CHARACTER", startPos, "@");
290
+ }
291
+ this.advance();
292
+ while (!this.isAtEnd() && this.isIdentifierPart(this.peek())) {
293
+ this.advance();
294
+ }
295
+ name = this.input.slice(nameStart, this.pos);
296
+ }
297
+ return this.createTokenAt(32 /* AT */, name, startPos);
298
+ }
299
+ peek() {
300
+ return this.input[this.pos] ?? "";
301
+ }
302
+ peekNext() {
303
+ return this.input[this.pos + 1] ?? "";
304
+ }
305
+ advance() {
306
+ return this.input[this.pos++] ?? "";
307
+ }
308
+ match(expected) {
309
+ if (this.isAtEnd())
310
+ return false;
311
+ if (this.input[this.pos] !== expected)
312
+ return false;
313
+ this.pos++;
314
+ return true;
315
+ }
316
+ isAtEnd() {
317
+ return this.pos >= this.input.length;
318
+ }
319
+ isDigit(char) {
320
+ return char >= "0" && char <= "9";
321
+ }
322
+ isAlpha(char) {
323
+ const c = char.toLowerCase();
324
+ return c >= "a" && c <= "z";
325
+ }
326
+ isIdentifierStart(char) {
327
+ return this.isAlpha(char) || char === "_";
328
+ }
329
+ isIdentifierPart(char) {
330
+ return this.isAlpha(char) || this.isDigit(char) || char === "_";
331
+ }
332
+ isWhitespace(char) {
333
+ return char === " " || char === "\t" || char === `
334
+ ` || char === "\r";
335
+ }
336
+ createToken(type, value) {
337
+ return { type, value, position: this.pos, end: this.pos };
338
+ }
339
+ createTokenAt(type, value, position) {
340
+ return { type, value, position, end: this.pos };
341
+ }
342
+ }
343
+ function lex(input) {
344
+ return new Lexer(input).tokenize();
345
+ }
346
+
347
+ // src/parser/ast.ts
348
+ function isModifier(node) {
349
+ return node.type === "Modifier";
350
+ }
351
+ function isSuccessCount(node) {
352
+ return node.type === "SuccessCount";
353
+ }
354
+ function isCritThreshold(node) {
355
+ return node.type === "CritThreshold";
356
+ }
357
+ function unwrapTransparent(node, kinds) {
358
+ let current = node;
359
+ while (true) {
360
+ switch (current.type) {
361
+ case "Grouped":
362
+ if (!kinds.includes("Grouped"))
363
+ return current;
364
+ current = current.expression;
365
+ break;
366
+ case "Modifier":
367
+ if (!kinds.includes("Modifier"))
368
+ return current;
369
+ current = current.target;
370
+ break;
371
+ case "Sort":
372
+ if (!kinds.includes("Sort"))
373
+ return current;
374
+ current = current.target;
375
+ break;
376
+ case "CritThreshold":
377
+ if (!kinds.includes("CritThreshold"))
378
+ return current;
379
+ current = current.target;
380
+ break;
381
+ default:
382
+ return current;
383
+ }
384
+ }
385
+ }
386
+ function containsDicePool(node) {
387
+ switch (node.type) {
388
+ case "Dice":
389
+ case "FateDice":
390
+ case "Modifier":
391
+ case "Explode":
392
+ case "Reroll":
393
+ return true;
394
+ case "Sort":
395
+ case "CritThreshold":
396
+ return containsDicePool(node.target);
397
+ case "Grouped":
398
+ return containsDicePool(node.expression);
399
+ case "Group":
400
+ return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
401
+ default:
402
+ return false;
403
+ }
404
+ }
405
+ function deepContainsDicePool(node) {
406
+ switch (node.type) {
407
+ case "Dice":
408
+ case "FateDice":
409
+ return true;
410
+ case "BinaryOp":
411
+ return deepContainsDicePool(node.left) || deepContainsDicePool(node.right);
412
+ case "UnaryOp":
413
+ return deepContainsDicePool(node.operand);
414
+ case "Modifier":
415
+ case "Explode":
416
+ case "Reroll":
417
+ case "SuccessCount":
418
+ case "Sort":
419
+ case "CritThreshold":
420
+ return deepContainsDicePool(node.target);
421
+ case "Versus":
422
+ return deepContainsDicePool(node.roll) || deepContainsDicePool(node.dc);
423
+ case "FunctionCall":
424
+ return node.args.some(deepContainsDicePool);
425
+ case "Grouped":
426
+ return deepContainsDicePool(node.expression);
427
+ case "Group":
428
+ return node.expressions.length >= 2 || node.expressions.some(deepContainsDicePool);
429
+ default:
430
+ return false;
431
+ }
432
+ }
433
+ function containsFatePool(node) {
434
+ switch (node.type) {
435
+ case "FateDice":
436
+ return true;
437
+ case "Modifier":
438
+ case "Explode":
439
+ case "Reroll":
440
+ case "Sort":
441
+ case "CritThreshold":
442
+ return containsFatePool(node.target);
443
+ case "Grouped":
444
+ return containsFatePool(node.expression);
445
+ case "Group":
446
+ return node.expressions.some(deepContainsFatePool);
447
+ default:
448
+ return false;
449
+ }
450
+ }
451
+ function deepContainsFatePool(node) {
452
+ switch (node.type) {
453
+ case "FateDice":
454
+ return true;
455
+ case "BinaryOp":
456
+ return deepContainsFatePool(node.left) || deepContainsFatePool(node.right);
457
+ case "UnaryOp":
458
+ return deepContainsFatePool(node.operand);
459
+ case "Modifier":
460
+ case "Explode":
461
+ case "Reroll":
462
+ case "SuccessCount":
463
+ case "Sort":
464
+ case "CritThreshold":
465
+ return deepContainsFatePool(node.target);
466
+ case "Versus":
467
+ return deepContainsFatePool(node.roll) || deepContainsFatePool(node.dc);
468
+ case "FunctionCall":
469
+ return node.args.some(deepContainsFatePool);
470
+ case "Grouped":
471
+ return deepContainsFatePool(node.expression);
472
+ case "Group":
473
+ return node.expressions.some(deepContainsFatePool);
474
+ default:
475
+ return false;
476
+ }
477
+ }
478
+ function containsMultiSubGroup(node) {
479
+ switch (node.type) {
480
+ case "Group":
481
+ return node.expressions.length >= 2 || node.expressions.some(containsMultiSubGroup);
482
+ case "BinaryOp":
483
+ return containsMultiSubGroup(node.left) || containsMultiSubGroup(node.right);
484
+ case "UnaryOp":
485
+ return containsMultiSubGroup(node.operand);
486
+ case "Modifier":
487
+ case "Explode":
488
+ case "Reroll":
489
+ case "SuccessCount":
490
+ case "Sort":
491
+ case "CritThreshold":
492
+ return containsMultiSubGroup(node.target);
493
+ case "Versus":
494
+ return containsMultiSubGroup(node.roll) || containsMultiSubGroup(node.dc);
495
+ case "FunctionCall":
496
+ return node.args.some(containsMultiSubGroup);
497
+ case "Grouped":
498
+ return containsMultiSubGroup(node.expression);
499
+ default:
500
+ return false;
501
+ }
502
+ }
503
+ function containsVersus(node) {
504
+ switch (node.type) {
505
+ case "Versus":
506
+ return true;
507
+ case "BinaryOp":
508
+ return containsVersus(node.left) || containsVersus(node.right);
509
+ case "UnaryOp":
510
+ return containsVersus(node.operand);
511
+ case "Modifier":
512
+ case "Explode":
513
+ case "Reroll":
514
+ case "SuccessCount":
515
+ case "Sort":
516
+ case "CritThreshold":
517
+ return containsVersus(node.target);
518
+ case "FunctionCall":
519
+ return node.args.some(containsVersus);
520
+ case "Grouped":
521
+ return containsVersus(node.expression);
522
+ case "Group":
523
+ return node.expressions.some(containsVersus);
524
+ default:
525
+ return false;
526
+ }
527
+ }
528
+
529
+ // src/parser/parser.ts
530
+ class ParseError extends RollParserError {
531
+ position;
532
+ token;
533
+ constructor(message, code, position, token) {
534
+ super(`${message} at position ${position}`, code);
535
+ this.name = "ParseError";
536
+ this.position = position;
537
+ this.token = token ?? undefined;
538
+ }
539
+ }
540
+ var BP = {
541
+ VS_LEFT: 2,
542
+ VS_RIGHT: 3,
543
+ COMPARE: 8,
544
+ ADD_LEFT: 10,
545
+ ADD_RIGHT: 11,
546
+ MUL_LEFT: 20,
547
+ MUL_RIGHT: 21,
548
+ UNARY: 25,
549
+ POW_LEFT: 31,
550
+ POW_RIGHT: 30,
551
+ MODIFIER: 35,
552
+ DICE_LEFT: 40,
553
+ DICE_RIGHT: 41
554
+ };
555
+ var MAX_PARSE_DEPTH = 128;
556
+ var TOKEN_DISPLAY = {
557
+ [15 /* LPAREN */]: `'('`,
558
+ [16 /* RPAREN */]: `')'`,
559
+ [30 /* LBRACE */]: `'{'`,
560
+ [31 /* RBRACE */]: `'}'`,
561
+ [17 /* COMMA */]: `','`
562
+ };
563
+ var FUNCTION_ARITY = {
564
+ floor: { min: 1, max: 1 },
565
+ ceil: { min: 1, max: 1 },
566
+ round: { min: 1, max: 1 },
567
+ abs: { min: 1, max: 1 },
568
+ max: { min: 2, max: Number.POSITIVE_INFINITY },
569
+ min: { min: 2, max: Number.POSITIVE_INFINITY }
570
+ };
571
+
572
+ class Parser {
573
+ tokens;
574
+ pos = 0;
575
+ depth = 0;
576
+ constructor(tokens) {
577
+ this.tokens = tokens;
578
+ }
579
+ parse() {
580
+ if (this.peek().type === 37 /* EOF */) {
581
+ throw new ParseError("Unexpected end of input", "UNEXPECTED_END", this.peek().position);
582
+ }
583
+ const ast = this.parseExpression(0);
584
+ if (this.peek().type !== 37 /* EOF */) {
585
+ const token = this.peek();
586
+ throw new ParseError(`Unexpected token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
587
+ }
588
+ return ast;
589
+ }
590
+ parseExpression(minBp) {
591
+ this.depth += 1;
592
+ if (this.depth > MAX_PARSE_DEPTH) {
593
+ throw new ParseError(`Expression nesting exceeds the maximum depth of ${MAX_PARSE_DEPTH}`, "MAX_DEPTH_EXCEEDED", this.peek().position, this.peek());
594
+ }
595
+ try {
596
+ let left = this.parseNud();
597
+ while (this.hasTokens()) {
598
+ const token = this.peek();
599
+ const leftBp = this.getLeftBp(token);
600
+ if (leftBp < minBp)
601
+ break;
602
+ this.advance();
603
+ left = this.parseLed(left, token);
604
+ }
605
+ return left;
606
+ } finally {
607
+ this.depth -= 1;
608
+ }
609
+ }
610
+ parseNud() {
611
+ const token = this.advance();
612
+ switch (token.type) {
613
+ case 0 /* NUMBER */:
614
+ return this.parseLiteral(token);
615
+ case 5 /* MINUS */:
616
+ return this.parseUnaryMinus(token);
617
+ case 1 /* DICE */:
618
+ return this.parsePrefixDice(token);
619
+ case 2 /* DICE_PERCENT */:
620
+ return this.parsePrefixDicePercent(token);
621
+ case 3 /* DICE_FATE */:
622
+ return this.parsePrefixFateDice(token);
623
+ case 15 /* LPAREN */:
624
+ return this.parseGrouped(token);
625
+ case 30 /* LBRACE */:
626
+ return this.parseGroup(token);
627
+ case 28 /* FUNCTION */:
628
+ return this.parseFunctionCall(token);
629
+ case 32 /* AT */:
630
+ return this.parseVariable(token);
631
+ case 37 /* EOF */:
632
+ throw new ParseError("Unexpected end of input", "UNEXPECTED_END", token.position);
633
+ default:
634
+ throw new ParseError(`Unexpected token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
635
+ }
636
+ }
637
+ parseLed(left, token) {
638
+ switch (token.type) {
639
+ case 1 /* DICE */:
640
+ return this.parseInfixDice(left, token);
641
+ case 2 /* DICE_PERCENT */:
642
+ return this.parseInfixDicePercent(left, token);
643
+ case 3 /* DICE_FATE */:
644
+ return this.parseInfixFateDice(left, token);
645
+ case 4 /* PLUS */:
646
+ case 5 /* MINUS */:
647
+ case 6 /* MULTIPLY */:
648
+ case 7 /* DIVIDE */:
649
+ case 8 /* MODULO */:
650
+ case 9 /* POWER */:
651
+ return this.parseBinaryOp(left, token);
652
+ case 18 /* KEEP_HIGH */:
653
+ case 19 /* KEEP_LOW */:
654
+ case 20 /* DROP_HIGH */:
655
+ case 21 /* DROP_LOW */:
656
+ return this.parseModifier(left, token);
657
+ case 22 /* EXPLODE */:
658
+ case 23 /* EXPLODE_COMPOUND */:
659
+ case 24 /* EXPLODE_PENETRATING */:
660
+ return this.parseExplode(left, token);
661
+ case 25 /* REROLL */:
662
+ case 26 /* REROLL_ONCE */:
663
+ return this.parseReroll(left, token);
664
+ case 33 /* SORT_ASC */:
665
+ case 34 /* SORT_DESC */:
666
+ return this.parseSort(left, token);
667
+ case 35 /* CRIT_SUCCESS */:
668
+ case 36 /* CRIT_FAIL */:
669
+ return this.parseCritThreshold(left, token);
670
+ case 10 /* GREATER */:
671
+ case 11 /* GREATER_EQUAL */:
672
+ case 12 /* LESS */:
673
+ case 13 /* LESS_EQUAL */:
674
+ case 14 /* EQUAL */:
675
+ return this.parseSuccessCount(left, token);
676
+ case 29 /* VS */:
677
+ return this.parseVersus(left, token);
678
+ default:
679
+ throw new ParseError(`Unexpected infix token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
680
+ }
681
+ }
682
+ static syntheticLiteral(value, token) {
683
+ return { type: "Literal", value, start: token.position, end: token.position };
684
+ }
685
+ parseLiteral(token) {
686
+ return {
687
+ type: "Literal",
688
+ value: Number.parseFloat(token.value),
689
+ start: token.position,
690
+ end: token.end
691
+ };
692
+ }
693
+ parseUnaryMinus(token) {
694
+ const operand = this.parseExpression(BP.UNARY);
695
+ this.rejectSuccessCountTarget(operand, token);
696
+ return {
697
+ type: "UnaryOp",
698
+ operator: "-",
699
+ operand,
700
+ start: token.position,
701
+ end: operand.end ?? token.end
702
+ };
703
+ }
704
+ parsePrefixDice(token) {
705
+ const sides = this.parseExpression(BP.DICE_RIGHT);
706
+ this.rejectSuccessCountTarget(sides, token);
707
+ this.rejectVersusTarget(sides, token);
708
+ return {
709
+ type: "Dice",
710
+ count: Parser.syntheticLiteral(1, token),
711
+ sides,
712
+ start: token.position,
713
+ end: sides.end ?? token.end
714
+ };
715
+ }
716
+ parseInfixDice(left, token) {
717
+ this.rejectSuccessCountTarget(left, token);
718
+ this.rejectVersusTarget(left, token);
719
+ this.rejectBareDiceChain(left, token);
720
+ const sides = this.parseExpression(BP.DICE_RIGHT);
721
+ this.rejectSuccessCountTarget(sides, token);
722
+ this.rejectVersusTarget(sides, token);
723
+ return {
724
+ type: "Dice",
725
+ count: left,
726
+ sides,
727
+ start: left.start ?? token.position,
728
+ end: sides.end ?? token.end
729
+ };
730
+ }
731
+ parsePrefixDicePercent(token) {
732
+ return {
733
+ type: "Dice",
734
+ count: Parser.syntheticLiteral(1, token),
735
+ sides: Parser.syntheticLiteral(100, token),
736
+ start: token.position,
737
+ end: token.end
738
+ };
739
+ }
740
+ parseInfixDicePercent(left, token) {
741
+ this.rejectSuccessCountTarget(left, token);
742
+ this.rejectVersusTarget(left, token);
743
+ this.rejectBareDiceChain(left, token);
744
+ return {
745
+ type: "Dice",
746
+ count: left,
747
+ sides: Parser.syntheticLiteral(100, token),
748
+ start: left.start ?? token.position,
749
+ end: token.end
750
+ };
751
+ }
752
+ parsePrefixFateDice(token) {
753
+ return {
754
+ type: "FateDice",
755
+ count: Parser.syntheticLiteral(1, token),
756
+ start: token.position,
757
+ end: token.end
758
+ };
759
+ }
760
+ parseInfixFateDice(left, token) {
761
+ this.rejectSuccessCountTarget(left, token);
762
+ this.rejectVersusTarget(left, token);
763
+ this.rejectBareDiceChain(left, token);
764
+ return {
765
+ type: "FateDice",
766
+ count: left,
767
+ start: left.start ?? token.position,
768
+ end: token.end
769
+ };
770
+ }
771
+ parseGrouped(token) {
772
+ const expression = this.parseExpression(0);
773
+ const close = this.expect(16 /* RPAREN */);
774
+ return { type: "Grouped", expression, start: token.position, end: close.end };
775
+ }
776
+ parseGroup(startToken) {
777
+ if (this.peek().type === 31 /* RBRACE */) {
778
+ throw new ParseError("Empty group", "UNEXPECTED_TOKEN", startToken.position, startToken);
779
+ }
780
+ const expressions = [this.parseExpression(0)];
781
+ while (this.peek().type === 17 /* COMMA */) {
782
+ this.advance();
783
+ expressions.push(this.parseExpression(0));
784
+ }
785
+ if (this.peek().type !== 31 /* RBRACE */) {
786
+ const unterminated = this.peek();
787
+ throw new ParseError(`Unterminated group: expected '}' or ','`, "EXPECTED_TOKEN", unterminated.position, unterminated);
788
+ }
789
+ const close = this.advance();
790
+ return { type: "Group", expressions, start: startToken.position, end: close.end };
791
+ }
792
+ parseVariable(token) {
793
+ return { type: "Variable", name: token.value, start: token.position, end: token.end };
794
+ }
795
+ parseFunctionCall(token) {
796
+ this.expect(15 /* LPAREN */);
797
+ const args = [];
798
+ if (this.peek().type !== 16 /* RPAREN */) {
799
+ const first = this.parseExpression(0);
800
+ this.rejectSuccessCountTarget(first, token);
801
+ args.push(first);
802
+ while (this.peek().type === 17 /* COMMA */) {
803
+ this.advance();
804
+ const next = this.parseExpression(0);
805
+ this.rejectSuccessCountTarget(next, token);
806
+ args.push(next);
807
+ }
808
+ }
809
+ const close = this.expect(16 /* RPAREN */);
810
+ const arity = FUNCTION_ARITY[token.value];
811
+ if (arity === undefined) {
812
+ throw new ParseError(`Unknown function '${token.value}'`, "UNKNOWN_FUNCTION", token.position, token);
813
+ }
814
+ if (args.length < arity.min || args.length > arity.max) {
815
+ const expected = arity.max === Number.POSITIVE_INFINITY ? `at least ${arity.min}` : arity.min === arity.max ? `${arity.min}` : `${arity.min}–${arity.max}`;
816
+ throw new ParseError(`Function '${token.value}' expects ${expected} argument${arity.min === 1 && arity.max === 1 ? "" : "s"}, got ${args.length}`, "INVALID_FUNCTION_ARITY", token.position, token);
817
+ }
818
+ return {
819
+ type: "FunctionCall",
820
+ name: token.value,
821
+ args,
822
+ start: token.position,
823
+ end: close.end
824
+ };
825
+ }
826
+ parseBinaryOp(left, token) {
827
+ this.rejectSuccessCountTarget(left, token);
828
+ const operator = this.getOperatorSymbol(token);
829
+ const rightBp = this.getRightBp(token);
830
+ const right = this.parseExpression(rightBp);
831
+ this.rejectSuccessCountTarget(right, token);
832
+ return {
833
+ type: "BinaryOp",
834
+ operator,
835
+ left,
836
+ right,
837
+ start: left.start ?? token.position,
838
+ end: right.end ?? token.end
839
+ };
840
+ }
841
+ rejectBareDiceChain(left, token) {
842
+ switch (left.type) {
843
+ case "Dice":
844
+ case "FateDice":
845
+ case "Explode":
846
+ case "Reroll":
847
+ case "Modifier":
848
+ case "Sort":
849
+ case "CritThreshold":
850
+ throw new ParseError(`Ambiguous dice chain: use 'dl'/'dh' to drop dice (4d6dl1) or parentheses for nested dice ((4d6)d1)`, "AMBIGUOUS_DICE_CHAIN", token.position, token);
851
+ default:
852
+ return;
853
+ }
854
+ }
855
+ rejectSuccessCountTarget(target, token) {
856
+ const node = unwrapTransparent(target, ["Grouped"]);
857
+ if (isSuccessCount(node)) {
858
+ throw new ParseError(`Cannot apply modifier after success counting`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
859
+ }
860
+ }
861
+ rejectGroupTarget(target, token, action, code, singleSubRollPasses = false) {
862
+ const node = unwrapTransparent(target, ["Grouped", "Modifier", "Sort", "CritThreshold"]);
863
+ if (node.type !== "Group")
864
+ return;
865
+ if (singleSubRollPasses && node.expressions.length === 1) {
866
+ const inner = node.expressions[0];
867
+ if (inner != null && containsMultiSubGroup(inner)) {
868
+ throw new ParseError(`Cannot ${action} a group`, code, token.position, token);
869
+ }
870
+ return;
871
+ }
872
+ throw new ParseError(`Cannot ${action} a group`, code, token.position, token);
873
+ }
874
+ rejectVersusTarget(target, token) {
875
+ const node = unwrapTransparent(target, ["Grouped"]);
876
+ if (node.type === "Versus") {
877
+ throw new ParseError(`Versus cannot be used as a meta-expression`, "NESTED_VERSUS", token.position, token);
878
+ }
879
+ if (node.type === "Group" && node.expressions.length === 1) {
880
+ const inner = node.expressions[0];
881
+ if (inner != null && containsVersus(inner)) {
882
+ throw new ParseError(`Versus cannot be used as a meta-expression`, "NESTED_VERSUS", token.position, token);
883
+ }
884
+ }
885
+ }
886
+ parseModifier(target, token) {
887
+ this.rejectSuccessCountTarget(target, token);
888
+ this.rejectVersusTarget(target, token);
889
+ if (!containsDicePool(target)) {
890
+ throw new ParseError(`Keep/drop modifiers require a dice pool target`, "INVALID_MODIFIER_TARGET", token.position, token);
891
+ }
892
+ const modifier = token.type === 18 /* KEEP_HIGH */ || token.type === 19 /* KEEP_LOW */ ? "keep" : "drop";
893
+ const selector = token.type === 18 /* KEEP_HIGH */ || token.type === 20 /* DROP_HIGH */ ? "highest" : "lowest";
894
+ const nextToken = this.peek().type;
895
+ const hasExplicitCount = nextToken === 0 /* NUMBER */ || nextToken === 15 /* LPAREN */ || nextToken === 32 /* AT */;
896
+ const count = hasExplicitCount ? this.parseExpression(BP.DICE_LEFT) : Parser.syntheticLiteral(1, token);
897
+ this.rejectSuccessCountTarget(count, token);
898
+ this.rejectVersusTarget(count, token);
899
+ return {
900
+ type: "Modifier",
901
+ modifier,
902
+ selector,
903
+ count,
904
+ target,
905
+ start: target.start ?? token.position,
906
+ end: hasExplicitCount ? count.end ?? token.end : token.end
907
+ };
908
+ }
909
+ parseExplode(target, token) {
910
+ this.rejectSuccessCountTarget(target, token);
911
+ this.rejectGroupTarget(target, token, "explode", "INVALID_EXPLODE_TARGET");
912
+ if (!containsDicePool(target)) {
913
+ throw new ParseError(`Explode modifier requires a dice pool target`, "INVALID_EXPLODE_TARGET", token.position, token);
914
+ }
915
+ if (containsFatePool(target)) {
916
+ throw new ParseError(`Fate dice cannot explode`, "INVALID_EXPLODE_TARGET", token.position, token);
917
+ }
918
+ if (target.type === "Explode") {
919
+ throw new ParseError(`Cannot chain explode modifiers`, "INVALID_EXPLODE_TARGET", token.position, token);
920
+ }
921
+ const variant = token.type === 22 /* EXPLODE */ ? "standard" : token.type === 23 /* EXPLODE_COMPOUND */ ? "compound" : "penetrating";
922
+ const node = {
923
+ type: "Explode",
924
+ variant,
925
+ target,
926
+ start: target.start ?? token.position,
927
+ end: token.end
928
+ };
929
+ if (this.isComparePointAhead()) {
930
+ node.threshold = this.parseComparePoint();
931
+ node.end = node.threshold.value.end ?? token.end;
932
+ }
933
+ return node;
934
+ }
935
+ parseReroll(target, token) {
936
+ this.rejectSuccessCountTarget(target, token);
937
+ this.rejectGroupTarget(target, token, "reroll", "INVALID_REROLL_TARGET");
938
+ if (!containsDicePool(target)) {
939
+ throw new ParseError(`Reroll modifier requires a dice pool target`, "INVALID_REROLL_TARGET", token.position, token);
940
+ }
941
+ if (!this.isComparePointAhead()) {
942
+ throw new ParseError(`Expected comparison operator after '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
943
+ }
944
+ const once = token.type === 26 /* REROLL_ONCE */;
945
+ const condition = this.parseComparePoint();
946
+ return {
947
+ type: "Reroll",
948
+ once,
949
+ condition,
950
+ target,
951
+ start: target.start ?? token.position,
952
+ end: condition.value.end ?? token.end
953
+ };
954
+ }
955
+ parseSort(target, token) {
956
+ this.rejectSuccessCountTarget(target, token);
957
+ this.rejectVersusTarget(target, token);
958
+ if (!deepContainsDicePool(target)) {
959
+ throw new ParseError(`Sort modifier requires a dice pool target`, "INVALID_SORT_TARGET", token.position, token);
960
+ }
961
+ const base = unwrapTransparent(target, ["Grouped", "Modifier", "Sort", "CritThreshold"]);
962
+ if (base.type === "Group" && base.expressions.length >= 2) {
963
+ throw new ParseError(`Sort modifier does not yet support multi-sub-roll groups`, "INVALID_SORT_TARGET", token.position, token);
964
+ }
965
+ const order = token.type === 33 /* SORT_ASC */ ? "ascending" : "descending";
966
+ return {
967
+ type: "Sort",
968
+ order,
969
+ target,
970
+ start: target.start ?? token.position,
971
+ end: token.end
972
+ };
973
+ }
974
+ parseCritThreshold(target, token) {
975
+ this.rejectSuccessCountTarget(target, token);
976
+ this.rejectVersusTarget(target, token);
977
+ this.rejectGroupTarget(target, token, "apply crit threshold to", "INVALID_CRIT_THRESHOLD_TARGET", true);
978
+ if (!containsDicePool(target)) {
979
+ throw new ParseError(`Crit threshold modifier requires a dice pool target`, "INVALID_CRIT_THRESHOLD_TARGET", token.position, token);
980
+ }
981
+ if (!this.isComparePointAhead() && containsFatePool(target)) {
982
+ throw new ParseError(`Bare cs/cf cannot apply to Fate dice`, "INVALID_CRIT_THRESHOLD_TARGET", token.position, token);
983
+ }
984
+ const threshold = this.isComparePointAhead() ? this.parseComparePoint() : "default";
985
+ const end = threshold === "default" ? token.end : threshold.value.end ?? token.end;
986
+ let chainTarget = target;
987
+ while (chainTarget.type === "Grouped") {
988
+ chainTarget = chainTarget.expression;
989
+ }
990
+ if (isCritThreshold(chainTarget)) {
991
+ if (token.type === 35 /* CRIT_SUCCESS */) {
992
+ chainTarget.successThresholds.push(threshold);
993
+ } else {
994
+ chainTarget.failThresholds.push(threshold);
995
+ }
996
+ chainTarget.end = end;
997
+ return chainTarget;
998
+ }
999
+ return {
1000
+ type: "CritThreshold",
1001
+ successThresholds: token.type === 35 /* CRIT_SUCCESS */ ? [threshold] : [],
1002
+ failThresholds: token.type === 36 /* CRIT_FAIL */ ? [threshold] : [],
1003
+ target,
1004
+ start: target.start ?? token.position,
1005
+ end
1006
+ };
1007
+ }
1008
+ parseSuccessCount(target, token) {
1009
+ this.rejectSuccessCountTarget(target, token);
1010
+ if (!containsDicePool(target)) {
1011
+ throw new ParseError(`Success counting requires a dice pool target`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
1012
+ }
1013
+ const operator = this.getCompareOp(token);
1014
+ const value = this.parseExpression(BP.DICE_LEFT);
1015
+ this.rejectSuccessCountTarget(value, token);
1016
+ this.rejectVersusTarget(value, token);
1017
+ const node = {
1018
+ type: "SuccessCount",
1019
+ target,
1020
+ threshold: { operator, value },
1021
+ start: target.start ?? token.position,
1022
+ end: value.end ?? token.end
1023
+ };
1024
+ if (this.peek().type === 27 /* FAIL */) {
1025
+ this.advance();
1026
+ if (this.isComparePointAhead()) {
1027
+ node.failThreshold = this.parseComparePoint();
1028
+ } else {
1029
+ const failValue = this.parseExpression(BP.DICE_LEFT);
1030
+ this.rejectSuccessCountTarget(failValue, token);
1031
+ this.rejectVersusTarget(failValue, token);
1032
+ node.failThreshold = { operator: "=", value: failValue };
1033
+ }
1034
+ node.end = node.failThreshold.value.end ?? node.end ?? token.end;
1035
+ }
1036
+ return node;
1037
+ }
1038
+ parseVersus(left, token) {
1039
+ this.rejectSuccessCountTarget(left, token);
1040
+ let leftChain = left;
1041
+ while (leftChain.type === "Grouped") {
1042
+ leftChain = leftChain.expression;
1043
+ }
1044
+ if (leftChain.type === "Versus") {
1045
+ throw new ParseError("Cannot chain versus operators", "NESTED_VERSUS", token.position, token);
1046
+ }
1047
+ const dc = this.parseExpression(BP.VS_RIGHT);
1048
+ this.rejectSuccessCountTarget(dc, token);
1049
+ return {
1050
+ type: "Versus",
1051
+ roll: left,
1052
+ dc,
1053
+ start: left.start ?? token.position,
1054
+ end: dc.end ?? token.end
1055
+ };
1056
+ }
1057
+ isComparePointAhead() {
1058
+ const type = this.peek().type;
1059
+ return type === 10 /* GREATER */ || type === 11 /* GREATER_EQUAL */ || type === 12 /* LESS */ || type === 13 /* LESS_EQUAL */ || type === 14 /* EQUAL */;
1060
+ }
1061
+ parseComparePoint() {
1062
+ const token = this.peek();
1063
+ const operator = this.getCompareOp(token);
1064
+ this.advance();
1065
+ const value = this.parseExpression(BP.DICE_LEFT);
1066
+ this.rejectSuccessCountTarget(value, token);
1067
+ this.rejectVersusTarget(value, token);
1068
+ return { operator, value };
1069
+ }
1070
+ getCompareOp(token) {
1071
+ switch (token.type) {
1072
+ case 10 /* GREATER */:
1073
+ return ">";
1074
+ case 11 /* GREATER_EQUAL */:
1075
+ return ">=";
1076
+ case 12 /* LESS */:
1077
+ return "<";
1078
+ case 13 /* LESS_EQUAL */:
1079
+ return "<=";
1080
+ case 14 /* EQUAL */:
1081
+ return "=";
1082
+ default:
1083
+ throw new ParseError(`Expected comparison operator but got '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
1084
+ }
1085
+ }
1086
+ getOperatorSymbol(token) {
1087
+ switch (token.type) {
1088
+ case 4 /* PLUS */:
1089
+ return "+";
1090
+ case 5 /* MINUS */:
1091
+ return "-";
1092
+ case 6 /* MULTIPLY */:
1093
+ return "*";
1094
+ case 7 /* DIVIDE */:
1095
+ return "/";
1096
+ case 8 /* MODULO */:
1097
+ return "%";
1098
+ case 9 /* POWER */:
1099
+ return "**";
1100
+ default:
1101
+ throw new ParseError(`Unknown operator '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
1102
+ }
1103
+ }
1104
+ getLeftBp(token) {
1105
+ switch (token.type) {
1106
+ case 29 /* VS */:
1107
+ return BP.VS_LEFT;
1108
+ case 4 /* PLUS */:
1109
+ case 5 /* MINUS */:
1110
+ return BP.ADD_LEFT;
1111
+ case 6 /* MULTIPLY */:
1112
+ case 7 /* DIVIDE */:
1113
+ case 8 /* MODULO */:
1114
+ return BP.MUL_LEFT;
1115
+ case 9 /* POWER */:
1116
+ return BP.POW_LEFT;
1117
+ case 1 /* DICE */:
1118
+ case 2 /* DICE_PERCENT */:
1119
+ case 3 /* DICE_FATE */:
1120
+ return BP.DICE_LEFT;
1121
+ case 18 /* KEEP_HIGH */:
1122
+ case 19 /* KEEP_LOW */:
1123
+ case 20 /* DROP_HIGH */:
1124
+ case 21 /* DROP_LOW */:
1125
+ case 22 /* EXPLODE */:
1126
+ case 23 /* EXPLODE_COMPOUND */:
1127
+ case 24 /* EXPLODE_PENETRATING */:
1128
+ case 25 /* REROLL */:
1129
+ case 26 /* REROLL_ONCE */:
1130
+ case 33 /* SORT_ASC */:
1131
+ case 34 /* SORT_DESC */:
1132
+ case 35 /* CRIT_SUCCESS */:
1133
+ case 36 /* CRIT_FAIL */:
1134
+ return BP.MODIFIER;
1135
+ case 10 /* GREATER */:
1136
+ case 11 /* GREATER_EQUAL */:
1137
+ case 12 /* LESS */:
1138
+ case 13 /* LESS_EQUAL */:
1139
+ case 14 /* EQUAL */:
1140
+ return BP.COMPARE;
1141
+ case 16 /* RPAREN */:
1142
+ case 37 /* EOF */:
1143
+ case 17 /* COMMA */:
1144
+ case 28 /* FUNCTION */:
1145
+ case 30 /* LBRACE */:
1146
+ case 31 /* RBRACE */:
1147
+ return -1;
1148
+ default:
1149
+ return 0;
1150
+ }
1151
+ }
1152
+ getRightBp(token) {
1153
+ switch (token.type) {
1154
+ case 29 /* VS */:
1155
+ return BP.VS_RIGHT;
1156
+ case 4 /* PLUS */:
1157
+ case 5 /* MINUS */:
1158
+ return BP.ADD_RIGHT;
1159
+ case 6 /* MULTIPLY */:
1160
+ case 7 /* DIVIDE */:
1161
+ case 8 /* MODULO */:
1162
+ return BP.MUL_RIGHT;
1163
+ case 9 /* POWER */:
1164
+ return BP.POW_RIGHT;
1165
+ case 1 /* DICE */:
1166
+ return BP.DICE_RIGHT;
1167
+ default:
1168
+ return 0;
1169
+ }
1170
+ }
1171
+ peek() {
1172
+ return this.tokens[this.pos] ?? { type: 37 /* EOF */, value: "", position: this.pos, end: this.pos };
1173
+ }
1174
+ advance() {
1175
+ const token = this.peek();
1176
+ this.pos++;
1177
+ return token;
1178
+ }
1179
+ expect(type) {
1180
+ const token = this.peek();
1181
+ if (token.type !== type) {
1182
+ const expected = TOKEN_DISPLAY[type] ?? TokenType[type];
1183
+ const got = token.type === 37 /* EOF */ ? "end of input" : `'${token.value}'`;
1184
+ throw new ParseError(`Expected ${expected} but got ${got}`, "EXPECTED_TOKEN", token.position, token);
1185
+ }
1186
+ return this.advance();
1187
+ }
1188
+ hasTokens() {
1189
+ return this.peek().type !== 37 /* EOF */;
1190
+ }
1191
+ }
1192
+
1193
+ // src/rng/seeded.ts
1194
+ class SeededRNG {
1195
+ s0;
1196
+ s1;
1197
+ s2;
1198
+ s3;
1199
+ constructor(seed) {
1200
+ this.s0 = 0;
1201
+ this.s1 = 0;
1202
+ this.s2 = 0;
1203
+ this.s3 = 0;
1204
+ this.initState(seed);
1205
+ for (let i = 0;i < 20; i++) {
1206
+ this.nextUint32();
1207
+ }
1208
+ }
1209
+ initState(seed) {
1210
+ const numSeed = seed == null ? (Date.now() ^ Math.random() * 4294967295) >>> 0 : typeof seed === "string" ? this.hashString(seed) : seed >>> 0;
1211
+ let s = numSeed;
1212
+ const state = [];
1213
+ for (let i = 0;i < 4; i++) {
1214
+ s = s + 2654435769 >>> 0;
1215
+ let z = s;
1216
+ z = Math.imul(z ^ z >>> 16, 2246822507) >>> 0;
1217
+ z = Math.imul(z ^ z >>> 13, 3266489909) >>> 0;
1218
+ state.push((z ^ z >>> 16) >>> 0);
1219
+ }
1220
+ this.s0 = state[0] ?? 0;
1221
+ this.s1 = state[1] ?? 0;
1222
+ this.s2 = state[2] ?? 0;
1223
+ this.s3 = state[3] ?? 0;
1224
+ if (this.s0 === 0 && this.s1 === 0 && this.s2 === 0 && this.s3 === 0) {
1225
+ this.s0 = 1;
1226
+ }
1227
+ }
1228
+ hashString(str) {
1229
+ let hash = 5381;
1230
+ for (let i = 0;i < str.length; i++) {
1231
+ hash = (hash << 5) + hash + str.charCodeAt(i) >>> 0;
1232
+ }
1233
+ return hash;
1234
+ }
1235
+ nextUint32() {
1236
+ let t = this.s3;
1237
+ const s = this.s0;
1238
+ this.s3 = this.s2;
1239
+ this.s2 = this.s1;
1240
+ this.s1 = s;
1241
+ t ^= t << 11;
1242
+ t ^= t >>> 8;
1243
+ this.s0 = (t ^ s ^ s >>> 19) >>> 0;
1244
+ return this.s0;
1245
+ }
1246
+ next() {
1247
+ return this.nextUint32() / 4294967296;
1248
+ }
1249
+ nextInt(min, max) {
1250
+ const lo = min > max ? max : min;
1251
+ const hi = min > max ? min : max;
1252
+ const range = hi - lo + 1;
1253
+ if (range <= 1) {
1254
+ return lo;
1255
+ }
1256
+ if (range > 4294967296) {
1257
+ return lo + this.nextBoundedWide(range);
1258
+ }
1259
+ const threshold = (4294967296 - range) % range;
1260
+ let value;
1261
+ do {
1262
+ value = this.nextUint32();
1263
+ } while (value < threshold);
1264
+ return lo + value % range;
1265
+ }
1266
+ nextBoundedWide(range) {
1267
+ const MAX_53 = 2 ** 53;
1268
+ if (range > MAX_53) {
1269
+ throw new RangeError(`nextInt range ${range} exceeds 2^53 and cannot be sampled exactly`);
1270
+ }
1271
+ const limit = Math.floor(MAX_53 / range) * range;
1272
+ let value;
1273
+ do {
1274
+ value = this.nextUint32() * 2097152 + (this.nextUint32() >>> 11);
1275
+ } while (value >= limit);
1276
+ return value % range;
1277
+ }
1278
+ }
1279
+
1280
+ // src/evaluator/modifiers/compare.ts
1281
+ function matchesCondition(result, operator, value) {
1282
+ switch (operator) {
1283
+ case ">":
1284
+ return result > value;
1285
+ case ">=":
1286
+ return result >= value;
1287
+ case "<":
1288
+ return result < value;
1289
+ case "<=":
1290
+ return result <= value;
1291
+ case "=":
1292
+ return result === value;
1293
+ }
1294
+ }
1295
+
1296
+ // src/evaluator/modifiers/crit-threshold.ts
1297
+ function applyCritThresholds(dice, successThresholds, failThresholds) {
1298
+ for (const die of dice) {
1299
+ if (die.modifiers.includes("meta"))
1300
+ continue;
1301
+ die.critical = successThresholds.some((t) => matchesCrit(t, die));
1302
+ die.fumble = failThresholds.some((t) => matchesFumble(t, die));
1303
+ }
1304
+ }
1305
+ function matchesCrit(threshold, die) {
1306
+ if (threshold === "default") {
1307
+ return die.result === die.sides && die.sides > 1;
1308
+ }
1309
+ return matchesCondition(die.result, threshold.operator, threshold.value);
1310
+ }
1311
+ function matchesFumble(threshold, die) {
1312
+ if (threshold === "default") {
1313
+ return die.result === 1 && die.sides > 1;
1314
+ }
1315
+ return matchesCondition(die.result, threshold.operator, threshold.value);
1316
+ }
1317
+
1318
+ // src/evaluator/modifiers/explode.ts
1319
+ var DEFAULT_MAX_EXPLODE_ITERATIONS = 1000;
1320
+ function buildShouldExplode(operator, thresholdValue) {
1321
+ if (operator == null || thresholdValue == null) {
1322
+ return (result, sides) => result === sides;
1323
+ }
1324
+ switch (operator) {
1325
+ case ">":
1326
+ return (result) => result > thresholdValue;
1327
+ case ">=":
1328
+ return (result) => result >= thresholdValue;
1329
+ case "<":
1330
+ return (result) => result < thresholdValue;
1331
+ case "<=":
1332
+ return (result) => result <= thresholdValue;
1333
+ case "=":
1334
+ return (result) => result === thresholdValue;
1335
+ }
1336
+ }
1337
+ function rollExplosion(sides, rng, env) {
1338
+ if (env.totalDiceRolled + 1 > env.maxDice) {
1339
+ throw new EvaluatorError(`Total dice count ${env.totalDiceRolled + 1} exceeds limit of ${env.maxDice}`, "DICE_LIMIT_EXCEEDED", "Explode");
1340
+ }
1341
+ env.totalDiceRolled += 1;
1342
+ return rng.nextInt(1, sides);
1343
+ }
1344
+ function explodeLimitError(maxIterations) {
1345
+ return new EvaluatorError(`Explode iteration limit of ${maxIterations} exceeded`, "EXPLODE_LIMIT_EXCEEDED", "Explode");
1346
+ }
1347
+ function canExplode(die) {
1348
+ if (die.modifiers.includes("dropped"))
1349
+ return false;
1350
+ if (die.sides < 1)
1351
+ return false;
1352
+ return true;
1353
+ }
1354
+ function applyStandardExplode(pool, shouldExplode, rng, env) {
1355
+ const result = [];
1356
+ for (const original of pool) {
1357
+ result.push(original);
1358
+ if (!canExplode(original))
1359
+ continue;
1360
+ const sides = original.sides;
1361
+ let last = original.result;
1362
+ let iterations = 0;
1363
+ while (shouldExplode(last, sides)) {
1364
+ if (iterations >= env.maxExplodeIterations) {
1365
+ throw explodeLimitError(env.maxExplodeIterations);
1366
+ }
1367
+ const next = rollExplosion(sides, rng, env);
1368
+ result.push({
1369
+ sides,
1370
+ result: next,
1371
+ modifiers: ["exploded", "kept"],
1372
+ critical: next === sides && sides > 1,
1373
+ fumble: next === 1 && sides > 1
1374
+ });
1375
+ last = next;
1376
+ iterations += 1;
1377
+ }
1378
+ }
1379
+ return result;
1380
+ }
1381
+ function applyCompoundExplode(pool, shouldExplode, rng, env) {
1382
+ for (const original of pool) {
1383
+ if (!canExplode(original))
1384
+ continue;
1385
+ const sides = original.sides;
1386
+ let accumulated = original.result;
1387
+ let last = original.result;
1388
+ let iterations = 0;
1389
+ let exploded = false;
1390
+ while (shouldExplode(last, sides)) {
1391
+ if (iterations >= env.maxExplodeIterations) {
1392
+ throw explodeLimitError(env.maxExplodeIterations);
1393
+ }
1394
+ const next = rollExplosion(sides, rng, env);
1395
+ accumulated += next;
1396
+ last = next;
1397
+ exploded = true;
1398
+ iterations += 1;
1399
+ }
1400
+ if (!exploded)
1401
+ continue;
1402
+ original.initialResult = original.result;
1403
+ original.result = accumulated;
1404
+ if (!original.modifiers.includes("exploded")) {
1405
+ original.modifiers = [...original.modifiers, "exploded"];
1406
+ }
1407
+ }
1408
+ return pool;
1409
+ }
1410
+ function applyPenetratingExplode(pool, shouldExplode, rng, env) {
1411
+ const result = [];
1412
+ for (const original of pool) {
1413
+ result.push(original);
1414
+ if (!canExplode(original))
1415
+ continue;
1416
+ const sides = original.sides;
1417
+ let lastRaw = original.result;
1418
+ let iterations = 0;
1419
+ while (shouldExplode(lastRaw, sides)) {
1420
+ if (iterations >= env.maxExplodeIterations) {
1421
+ throw explodeLimitError(env.maxExplodeIterations);
1422
+ }
1423
+ const raw = rollExplosion(sides, rng, env);
1424
+ const stored = raw - 1;
1425
+ result.push({
1426
+ sides,
1427
+ result: stored,
1428
+ modifiers: ["exploded", "kept"],
1429
+ critical: raw === sides && sides > 1,
1430
+ fumble: raw === 1 && sides > 1
1431
+ });
1432
+ lastRaw = raw;
1433
+ iterations += 1;
1434
+ }
1435
+ }
1436
+ return result;
1437
+ }
1438
+
1439
+ // src/evaluator/modifiers/keep-drop.ts
1440
+ function markAllKept(dice) {
1441
+ return dice.map((die) => {
1442
+ if (die.modifiers.includes("dropped"))
1443
+ return die;
1444
+ return {
1445
+ ...die,
1446
+ modifiers: die.modifiers.includes("kept") ? die.modifiers : [...die.modifiers, "kept"]
1447
+ };
1448
+ });
1449
+ }
1450
+ function eligibleIndexed(dice) {
1451
+ return dice.map((die, index) => ({ die, index })).filter(({ die }) => !die.modifiers.includes("dropped"));
1452
+ }
1453
+ function applySelection(dice, selectionIndices, selectionMarker) {
1454
+ const otherMarker = selectionMarker === "kept" ? "dropped" : "kept";
1455
+ return dice.map((die, index) => {
1456
+ if (die.modifiers.includes("dropped"))
1457
+ return die;
1458
+ const isSelected = selectionIndices.has(index);
1459
+ const marker = isSelected ? selectionMarker : otherMarker;
1460
+ return {
1461
+ ...die,
1462
+ modifiers: [...die.modifiers.filter((m) => m !== "kept" && m !== "dropped"), marker]
1463
+ };
1464
+ });
1465
+ }
1466
+ function markAllDropped(dice) {
1467
+ return dice.map((die) => {
1468
+ if (die.modifiers.includes("dropped"))
1469
+ return die;
1470
+ return {
1471
+ ...die,
1472
+ modifiers: [...die.modifiers.filter((m) => m !== "kept"), "dropped"]
1473
+ };
1474
+ });
1475
+ }
1476
+ function applyKeepHighest(dice, count) {
1477
+ const eligible = eligibleIndexed(dice);
1478
+ if (count >= eligible.length)
1479
+ return markAllKept(dice);
1480
+ if (count <= 0)
1481
+ return markAllDropped(dice);
1482
+ const sorted = [...eligible].sort((a, b) => b.die.result - a.die.result);
1483
+ const keptIndices = new Set(sorted.slice(0, count).map((item) => item.index));
1484
+ return applySelection(dice, keptIndices, "kept");
1485
+ }
1486
+ function applyKeepLowest(dice, count) {
1487
+ const eligible = eligibleIndexed(dice);
1488
+ if (count >= eligible.length)
1489
+ return markAllKept(dice);
1490
+ if (count <= 0)
1491
+ return markAllDropped(dice);
1492
+ const sorted = [...eligible].sort((a, b) => a.die.result - b.die.result);
1493
+ const keptIndices = new Set(sorted.slice(0, count).map((item) => item.index));
1494
+ return applySelection(dice, keptIndices, "kept");
1495
+ }
1496
+ function applyDropHighest(dice, count) {
1497
+ const eligible = eligibleIndexed(dice);
1498
+ if (count <= 0)
1499
+ return markAllKept(dice);
1500
+ if (count >= eligible.length)
1501
+ return markAllDropped(dice);
1502
+ const sorted = [...eligible].sort((a, b) => b.die.result - a.die.result);
1503
+ const droppedIndices = new Set(sorted.slice(0, count).map((item) => item.index));
1504
+ return applySelection(dice, droppedIndices, "dropped");
1505
+ }
1506
+ function applyDropLowest(dice, count) {
1507
+ const eligible = eligibleIndexed(dice);
1508
+ if (count <= 0)
1509
+ return markAllKept(dice);
1510
+ if (count >= eligible.length)
1511
+ return markAllDropped(dice);
1512
+ const sorted = [...eligible].sort((a, b) => a.die.result - b.die.result);
1513
+ const droppedIndices = new Set(sorted.slice(0, count).map((item) => item.index));
1514
+ return applySelection(dice, droppedIndices, "dropped");
1515
+ }
1516
+ function sumKeptDice(dice) {
1517
+ return dice.filter((die) => !die.modifiers.includes("dropped")).reduce((sum, die) => sum + die.result, 0);
1518
+ }
1519
+
1520
+ // src/evaluator/modifiers/reroll.ts
1521
+ var DEFAULT_MAX_REROLL_ITERATIONS = 1000;
1522
+ function rollReplacement(sides, rng, env) {
1523
+ if (env.totalDiceRolled + 1 > env.maxDice) {
1524
+ throw new EvaluatorError(`Total dice count ${env.totalDiceRolled + 1} exceeds limit of ${env.maxDice}`, "DICE_LIMIT_EXCEEDED", "Reroll");
1525
+ }
1526
+ env.totalDiceRolled += 1;
1527
+ if (sides === 0) {
1528
+ const result2 = rng.nextInt(-1, 1);
1529
+ return {
1530
+ sides: 0,
1531
+ result: result2,
1532
+ modifiers: [],
1533
+ critical: false,
1534
+ fumble: false
1535
+ };
1536
+ }
1537
+ const result = rng.nextInt(1, sides);
1538
+ return {
1539
+ sides,
1540
+ result,
1541
+ modifiers: [],
1542
+ critical: result === sides && sides > 1,
1543
+ fumble: result === 1 && sides > 1
1544
+ };
1545
+ }
1546
+ function rerollLimitError(maxIterations) {
1547
+ return new EvaluatorError(`Reroll iteration limit of ${maxIterations} exceeded`, "REROLL_LIMIT_EXCEEDED", "Reroll");
1548
+ }
1549
+ function canReroll(die) {
1550
+ return !die.modifiers.includes("dropped");
1551
+ }
1552
+ function stripSlotFlags(modifiers) {
1553
+ return modifiers.filter((m) => m !== "kept" && m !== "dropped" && m !== "rerolled");
1554
+ }
1555
+ function applyRecursiveReroll(pool, operator, value, rng, env) {
1556
+ const result = [];
1557
+ for (const original of pool) {
1558
+ if (!canReroll(original)) {
1559
+ result.push(original);
1560
+ continue;
1561
+ }
1562
+ let current = original;
1563
+ let iterations = 0;
1564
+ while (matchesCondition(current.result, operator, value)) {
1565
+ if (iterations >= env.maxRerollIterations) {
1566
+ throw rerollLimitError(env.maxRerollIterations);
1567
+ }
1568
+ current.modifiers = [...stripSlotFlags(current.modifiers), "rerolled", "dropped"];
1569
+ result.push(current);
1570
+ current = rollReplacement(current.sides, rng, env);
1571
+ iterations += 1;
1572
+ }
1573
+ current.modifiers = [...stripSlotFlags(current.modifiers), "kept"];
1574
+ result.push(current);
1575
+ }
1576
+ return result;
1577
+ }
1578
+ function applyRerollOnce(pool, operator, value, rng, env) {
1579
+ const result = [];
1580
+ for (const original of pool) {
1581
+ if (!canReroll(original)) {
1582
+ result.push(original);
1583
+ continue;
1584
+ }
1585
+ if (!matchesCondition(original.result, operator, value)) {
1586
+ original.modifiers = [...stripSlotFlags(original.modifiers), "kept"];
1587
+ result.push(original);
1588
+ continue;
1589
+ }
1590
+ original.modifiers = [...stripSlotFlags(original.modifiers), "rerolled", "dropped"];
1591
+ result.push(original);
1592
+ const replacement = rollReplacement(original.sides, rng, env);
1593
+ replacement.modifiers = [...stripSlotFlags(replacement.modifiers), "kept"];
1594
+ result.push(replacement);
1595
+ }
1596
+ return result;
1597
+ }
1598
+
1599
+ // src/evaluator/modifiers/sort.ts
1600
+ function sortDice(dice, order) {
1601
+ const cmp = order === "ascending" ? (a, b) => a.result - b.result : (a, b) => b.result - a.result;
1602
+ return [...dice].sort(cmp);
1603
+ }
1604
+
1605
+ // src/evaluator/modifiers/success-count.ts
1606
+ function countSuccesses(dice, threshold, failThreshold) {
1607
+ let successes = 0;
1608
+ let failures = 0;
1609
+ for (const die of dice) {
1610
+ if (die.modifiers.includes("dropped"))
1611
+ continue;
1612
+ if (matchesCondition(die.result, threshold.operator, threshold.value)) {
1613
+ die.modifiers.push("success");
1614
+ successes += 1;
1615
+ continue;
1616
+ }
1617
+ if (failThreshold != null && matchesCondition(die.result, failThreshold.operator, failThreshold.value)) {
1618
+ die.modifiers.push("failure");
1619
+ failures += 1;
1620
+ }
1621
+ }
1622
+ return { total: successes - failures, successes, failures };
1623
+ }
1624
+
1625
+ // src/evaluator/evaluator.ts
1626
+ class EvaluatorError extends RollParserError {
1627
+ nodeType;
1628
+ start;
1629
+ end;
1630
+ constructor(message, code, nodeType) {
1631
+ super(message, code);
1632
+ this.name = "EvaluatorError";
1633
+ this.nodeType = nodeType ?? undefined;
1634
+ this.start = undefined;
1635
+ this.end = undefined;
1636
+ }
1637
+ }
1638
+ var DEFAULT_MAX_DICE = 1e4;
1639
+ function partSpan(node) {
1640
+ if (node.start === undefined || node.end === undefined)
1641
+ return {};
1642
+ return { start: node.start, end: node.end };
1643
+ }
1644
+ function toPublicSpecs(specs) {
1645
+ return specs.map((spec) => ({ kind: spec.modifier, selector: spec.selector, count: spec.count }));
1646
+ }
1647
+ function createDieResult(sides, result) {
1648
+ return {
1649
+ sides,
1650
+ result,
1651
+ modifiers: [],
1652
+ critical: result === sides && sides > 1,
1653
+ fumble: result === 1 && sides > 1
1654
+ };
1655
+ }
1656
+ function createFateDieResult(result) {
1657
+ return {
1658
+ sides: 0,
1659
+ result,
1660
+ modifiers: [],
1661
+ critical: false,
1662
+ fumble: false
1663
+ };
1664
+ }
1665
+ function renderDice(dice) {
1666
+ const parts = dice.filter((die) => !die.modifiers.includes("meta")).map((die) => {
1667
+ if (die.modifiers.includes("dropped")) {
1668
+ return `~~${die.result}~~`;
1669
+ }
1670
+ if (die.modifiers.includes("success")) {
1671
+ return `**${die.result}**`;
1672
+ }
1673
+ if (die.modifiers.includes("failure")) {
1674
+ return `__${die.result}__`;
1675
+ }
1676
+ return String(die.result);
1677
+ });
1678
+ return `[${parts.join(", ")}]`;
1679
+ }
1680
+ function mergeMetaRolls(parent, source) {
1681
+ for (const die of source.rolls) {
1682
+ parent.rolls.push({
1683
+ ...die,
1684
+ modifiers: [
1685
+ ...die.modifiers.filter((m) => m !== "kept" && m !== "dropped" && m !== "success" && m !== "failure"),
1686
+ "meta",
1687
+ "dropped"
1688
+ ]
1689
+ });
1690
+ }
1691
+ }
1692
+ function evalNode(node, rng, ctx, env) {
1693
+ try {
1694
+ return evalNodeInner(node, rng, ctx, env);
1695
+ } catch (error) {
1696
+ if (error instanceof EvaluatorError && error.start === undefined && node.start !== undefined) {
1697
+ error.start = node.start;
1698
+ error.end = node.end;
1699
+ }
1700
+ throw error;
1701
+ }
1702
+ }
1703
+ function evalNodeInner(node, rng, ctx, env) {
1704
+ switch (node.type) {
1705
+ case "Literal":
1706
+ return evalLiteral(node, ctx);
1707
+ case "Dice":
1708
+ return evalDice(node, rng, ctx, env);
1709
+ case "FateDice":
1710
+ return evalFateDice(node, rng, ctx, env);
1711
+ case "BinaryOp":
1712
+ return evalBinaryOp(node, rng, ctx, env);
1713
+ case "UnaryOp":
1714
+ return evalUnaryOp(node, rng, ctx, env);
1715
+ case "Modifier":
1716
+ return evalModifier(node, rng, ctx, env);
1717
+ case "Explode":
1718
+ return evalExplode(node, rng, ctx, env);
1719
+ case "Reroll":
1720
+ return evalReroll(node, rng, ctx, env);
1721
+ case "SuccessCount":
1722
+ return evalSuccessCount(node, rng, ctx, env);
1723
+ case "Versus":
1724
+ return evalVersus(node, rng, ctx, env);
1725
+ case "FunctionCall":
1726
+ return evalFunctionCall(node, rng, ctx, env);
1727
+ case "Grouped":
1728
+ return evalGrouped(node, rng, ctx, env);
1729
+ case "Group":
1730
+ return evalGroup(node, rng, ctx, env);
1731
+ case "Sort":
1732
+ return evalSort(node, rng, ctx, env);
1733
+ case "CritThreshold":
1734
+ return evalCritThreshold(node, rng, ctx, env);
1735
+ case "Variable":
1736
+ return evalVariable(node, ctx, env);
1737
+ default: {
1738
+ const exhaustive = node;
1739
+ throw new EvaluatorError(`Unknown node type: ${exhaustive.type}`, "UNKNOWN_NODE_TYPE", exhaustive.type);
1740
+ }
1741
+ }
1742
+ }
1743
+ function evalLiteral(node, ctx) {
1744
+ const { value } = node;
1745
+ ctx.expressionParts.push(String(value));
1746
+ ctx.renderedParts.push(String(value));
1747
+ return { total: value, part: { type: "literal", value, total: value, ...partSpan(node) } };
1748
+ }
1749
+ function variableNeedsBraces(name) {
1750
+ return !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
1751
+ }
1752
+ function evalVariable(node, ctx, env) {
1753
+ const present = Object.hasOwn(env.context, node.name);
1754
+ if (!present) {
1755
+ if (env.onMissingVariable === "throw") {
1756
+ throw new EvaluatorError(`Undefined variable: ${node.name}`, "UNDEFINED_VARIABLE", "Variable");
1757
+ }
1758
+ const display2 = variableNeedsBraces(node.name) ? `@{${node.name}}` : `@${node.name}`;
1759
+ ctx.expressionParts.push("0");
1760
+ ctx.renderedParts.push(`${display2}[0]`);
1761
+ return {
1762
+ total: 0,
1763
+ part: { type: "variable", name: node.name, value: 0, total: 0, ...partSpan(node) }
1764
+ };
1765
+ }
1766
+ const value = env.context[node.name];
1767
+ if (!Number.isFinite(value)) {
1768
+ throw new EvaluatorError(`Invalid variable value: ${node.name} = ${value}`, "INVALID_VARIABLE_VALUE", "Variable");
1769
+ }
1770
+ const display = variableNeedsBraces(node.name) ? `@{${node.name}}` : `@${node.name}`;
1771
+ ctx.expressionParts.push(String(value));
1772
+ ctx.renderedParts.push(`${display}[${value}]`);
1773
+ return {
1774
+ total: value,
1775
+ part: { type: "variable", name: node.name, value, total: value, ...partSpan(node) }
1776
+ };
1777
+ }
1778
+ function evalDice(node, rng, ctx, env) {
1779
+ const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1780
+ const count = evalNode(node.count, rng, countCtx, env).total;
1781
+ mergeMetaRolls(ctx, countCtx);
1782
+ const sidesCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1783
+ const sides = evalNode(node.sides, rng, sidesCtx, env).total;
1784
+ mergeMetaRolls(ctx, sidesCtx);
1785
+ if (!Number.isInteger(count) || count < 0) {
1786
+ throw new EvaluatorError(`Invalid dice count: ${count}`, "INVALID_DICE_COUNT", "Dice");
1787
+ }
1788
+ if (!Number.isInteger(sides) || sides < 1) {
1789
+ throw new EvaluatorError(`Invalid dice sides: ${sides}`, "INVALID_DICE_SIDES", "Dice");
1790
+ }
1791
+ if (env.totalDiceRolled + count > env.maxDice) {
1792
+ throw new EvaluatorError(`Total dice count ${env.totalDiceRolled + count} exceeds limit of ${env.maxDice}`, "DICE_LIMIT_EXCEEDED", "Dice");
1793
+ }
1794
+ env.totalDiceRolled += count;
1795
+ const dice = [];
1796
+ for (let i = 0;i < count; i++) {
1797
+ const result = rng.nextInt(1, sides);
1798
+ dice.push(createDieResult(sides, result));
1799
+ }
1800
+ const markedDice = markAllKept(dice);
1801
+ ctx.rolls.push(...markedDice);
1802
+ const total = sumKeptDice(markedDice);
1803
+ const notation = `${count}d${sides}`;
1804
+ ctx.expressionParts.push(notation);
1805
+ ctx.renderedParts.push(`${notation}${renderDice(markedDice)}`);
1806
+ return {
1807
+ total,
1808
+ part: { type: "dice", count, sides, rolls: markedDice, total, ...partSpan(node) }
1809
+ };
1810
+ }
1811
+ function evalFateDice(node, rng, ctx, env) {
1812
+ const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1813
+ const count = evalNode(node.count, rng, countCtx, env).total;
1814
+ mergeMetaRolls(ctx, countCtx);
1815
+ if (!Number.isInteger(count) || count < 0) {
1816
+ throw new EvaluatorError(`Invalid dice count: ${count}`, "INVALID_DICE_COUNT", "FateDice");
1817
+ }
1818
+ if (env.totalDiceRolled + count > env.maxDice) {
1819
+ throw new EvaluatorError(`Total dice count ${env.totalDiceRolled + count} exceeds limit of ${env.maxDice}`, "DICE_LIMIT_EXCEEDED", "FateDice");
1820
+ }
1821
+ env.totalDiceRolled += count;
1822
+ const dice = [];
1823
+ for (let i = 0;i < count; i++) {
1824
+ const result = rng.nextInt(-1, 1);
1825
+ dice.push(createFateDieResult(result));
1826
+ }
1827
+ const markedDice = markAllKept(dice);
1828
+ ctx.rolls.push(...markedDice);
1829
+ const total = sumKeptDice(markedDice);
1830
+ const notation = `${count}dF`;
1831
+ ctx.expressionParts.push(notation);
1832
+ ctx.renderedParts.push(`${notation}${renderDice(markedDice)}`);
1833
+ return {
1834
+ total,
1835
+ part: { type: "fateDice", count, rolls: markedDice, total, ...partSpan(node) }
1836
+ };
1837
+ }
1838
+ function propagateMetadata(parent, metadata) {
1839
+ if (!metadata)
1840
+ return;
1841
+ if (parent.versusMetadata) {
1842
+ throw new EvaluatorError("Multiple versus operators in the same expression", "NESTED_VERSUS", "Versus");
1843
+ }
1844
+ parent.versusMetadata = metadata;
1845
+ }
1846
+ function mergeContext(parent, child) {
1847
+ parent.rolls.push(...child.rolls);
1848
+ propagateMetadata(parent, child.versusMetadata);
1849
+ }
1850
+ function evalBinaryOp(node, rng, ctx, env) {
1851
+ const leftCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1852
+ const rightCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1853
+ const left = evalNode(node.left, rng, leftCtx, env);
1854
+ const right = evalNode(node.right, rng, rightCtx, env);
1855
+ mergeContext(ctx, leftCtx);
1856
+ mergeContext(ctx, rightCtx);
1857
+ const leftExpr = leftCtx.expressionParts.join("");
1858
+ const rightExpr = rightCtx.expressionParts.join("");
1859
+ const leftRendered = leftCtx.renderedParts.join("");
1860
+ const rightRendered = rightCtx.renderedParts.join("");
1861
+ ctx.expressionParts.push(`${leftExpr} ${node.operator} ${rightExpr}`);
1862
+ ctx.renderedParts.push(`${leftRendered} ${node.operator} ${rightRendered}`);
1863
+ const total = applyBinaryOperator(node.operator, left.total, right.total);
1864
+ return {
1865
+ total,
1866
+ part: {
1867
+ type: "binaryOp",
1868
+ operator: node.operator,
1869
+ left: left.part,
1870
+ right: right.part,
1871
+ total,
1872
+ ...partSpan(node)
1873
+ }
1874
+ };
1875
+ }
1876
+ function applyBinaryOperator(operator, left, right) {
1877
+ switch (operator) {
1878
+ case "+":
1879
+ return left + right;
1880
+ case "-":
1881
+ return left - right;
1882
+ case "*":
1883
+ return left * right;
1884
+ case "/":
1885
+ if (right === 0) {
1886
+ throw new EvaluatorError("Division by zero", "DIVISION_BY_ZERO", "BinaryOp");
1887
+ }
1888
+ return left / right;
1889
+ case "%":
1890
+ if (right === 0) {
1891
+ throw new EvaluatorError("Modulo by zero", "MODULO_BY_ZERO", "BinaryOp");
1892
+ }
1893
+ return left % right;
1894
+ case "**":
1895
+ return left ** right;
1896
+ default: {
1897
+ const exhaustive = operator;
1898
+ throw new EvaluatorError(`Unknown operator: ${exhaustive}`, "UNKNOWN_OPERATOR", "BinaryOp");
1899
+ }
1900
+ }
1901
+ }
1902
+ function evalUnaryOp(node, rng, ctx, env) {
1903
+ const innerCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1904
+ const inner = evalNode(node.operand, rng, innerCtx, env);
1905
+ mergeContext(ctx, innerCtx);
1906
+ const innerExpr = innerCtx.expressionParts.join("");
1907
+ const innerRendered = innerCtx.renderedParts.join("");
1908
+ ctx.expressionParts.push(`-${innerExpr}`);
1909
+ ctx.renderedParts.push(`-${innerRendered}`);
1910
+ const total = -inner.total;
1911
+ return {
1912
+ total,
1913
+ part: { type: "unaryOp", operator: "-", operand: inner.part, total, ...partSpan(node) }
1914
+ };
1915
+ }
1916
+ function evalGrouped(node, rng, ctx, env) {
1917
+ const innerCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1918
+ const inner = evalNode(node.expression, rng, innerCtx, env);
1919
+ mergeContext(ctx, innerCtx);
1920
+ ctx.expressionParts.push(`(${innerCtx.expressionParts.join("")})`);
1921
+ ctx.renderedParts.push(`(${innerCtx.renderedParts.join("")})`);
1922
+ return {
1923
+ total: inner.total,
1924
+ part: { type: "grouped", inner: inner.part, total: inner.total, ...partSpan(node) }
1925
+ };
1926
+ }
1927
+ function evalGroup(node, rng, ctx, env) {
1928
+ const subExprs = [];
1929
+ const subRendered = [];
1930
+ const subParts = [];
1931
+ let total = 0;
1932
+ for (const expr of node.expressions) {
1933
+ const subCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1934
+ const sub = evalNode(expr, rng, subCtx, env);
1935
+ mergeContext(ctx, subCtx);
1936
+ subExprs.push(subCtx.expressionParts.join(""));
1937
+ subRendered.push(subCtx.renderedParts.join(""));
1938
+ subParts.push(sub.part);
1939
+ total += sub.total;
1940
+ }
1941
+ ctx.expressionParts.push(`{${subExprs.join(", ")}}`);
1942
+ ctx.renderedParts.push(`{${subRendered.join(", ")}}`);
1943
+ return { total, part: { type: "group", parts: subParts, total, ...partSpan(node) } };
1944
+ }
1945
+ function evalFunctionCall(node, rng, ctx, env) {
1946
+ const argCtxs = [];
1947
+ const argResults = [];
1948
+ for (const arg of node.args) {
1949
+ const argCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1950
+ argResults.push(evalNode(arg, rng, argCtx, env));
1951
+ argCtxs.push(argCtx);
1952
+ }
1953
+ for (const argCtx of argCtxs) {
1954
+ mergeContext(ctx, argCtx);
1955
+ }
1956
+ const argExprs = argCtxs.map((c) => c.expressionParts.join(""));
1957
+ const argRendereds = argCtxs.map((c) => c.renderedParts.join(""));
1958
+ ctx.expressionParts.push(`${node.name}(${argExprs.join(", ")})`);
1959
+ ctx.renderedParts.push(`${node.name}(${argRendereds.join(", ")})`);
1960
+ const total = applyFunction(node.name, argResults.map((r) => r.total));
1961
+ return {
1962
+ total,
1963
+ part: {
1964
+ type: "functionCall",
1965
+ name: node.name,
1966
+ args: argResults.map((r) => r.part),
1967
+ total,
1968
+ ...partSpan(node)
1969
+ }
1970
+ };
1971
+ }
1972
+ function applyFunction(name, values) {
1973
+ switch (name) {
1974
+ case "floor":
1975
+ return Math.floor(requireUnaryArg(name, values));
1976
+ case "ceil":
1977
+ return Math.ceil(requireUnaryArg(name, values));
1978
+ case "round":
1979
+ return Math.round(requireUnaryArg(name, values));
1980
+ case "abs":
1981
+ return Math.abs(requireUnaryArg(name, values));
1982
+ case "max":
1983
+ return Math.max(...values);
1984
+ case "min":
1985
+ return Math.min(...values);
1986
+ default:
1987
+ throw new EvaluatorError(`Unknown function: ${name}`, "UNKNOWN_FUNCTION", "FunctionCall");
1988
+ }
1989
+ }
1990
+ function requireUnaryArg(name, values) {
1991
+ const [x] = values;
1992
+ if (x === undefined) {
1993
+ throw new EvaluatorError(`Function '${name}' requires an argument`, "UNKNOWN_FUNCTION", "FunctionCall");
1994
+ }
1995
+ return x;
1996
+ }
1997
+ function flattenModifierChain(node, rng, ctx, env) {
1998
+ const specs = [];
1999
+ let current = node;
2000
+ while (isModifier(current)) {
2001
+ const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2002
+ const modCount = evalNode(current.count, rng, countCtx, env).total;
2003
+ mergeMetaRolls(ctx, countCtx);
2004
+ if (!Number.isInteger(modCount) || modCount < 0) {
2005
+ throw new EvaluatorError(`Invalid modifier count: ${modCount}`, "INVALID_MODIFIER_COUNT", "Modifier");
2006
+ }
2007
+ const code = current.modifier === "keep" ? current.selector === "highest" ? "kh" : "kl" : current.selector === "highest" ? "dh" : "dl";
2008
+ specs.push({ modifier: current.modifier, selector: current.selector, count: modCount, code });
2009
+ current = current.target;
2010
+ }
2011
+ specs.reverse();
2012
+ return { specs, baseTarget: current };
2013
+ }
2014
+ function applyModifierSpec(dice, spec) {
2015
+ if (spec.modifier === "keep") {
2016
+ return spec.selector === "highest" ? applyKeepHighest(dice, spec.count) : applyKeepLowest(dice, spec.count);
2017
+ }
2018
+ return spec.selector === "highest" ? applyDropHighest(dice, spec.count) : applyDropLowest(dice, spec.count);
2019
+ }
2020
+ function mergeDropSets(baseDice, specs) {
2021
+ const droppedIndices = new Set;
2022
+ for (const spec of specs) {
2023
+ const result = applyModifierSpec(baseDice, spec);
2024
+ for (let i = 0;i < result.length; i++) {
2025
+ if (result[i]?.modifiers.includes("dropped")) {
2026
+ droppedIndices.add(i);
2027
+ }
2028
+ }
2029
+ }
2030
+ baseDice.forEach((die, index) => {
2031
+ die.modifiers = droppedIndices.has(index) ? [...die.modifiers.filter((m) => m !== "kept" && m !== "dropped"), "dropped"] : [...die.modifiers.filter((m) => m !== "dropped" && m !== "kept"), "kept"];
2032
+ });
2033
+ return baseDice;
2034
+ }
2035
+ function formatExplodeCode(variant, threshold, thresholdValue) {
2036
+ const marker = variant === "standard" ? "!" : variant === "compound" ? "!!" : "!p";
2037
+ if (threshold == null || thresholdValue == null)
2038
+ return marker;
2039
+ return `${marker}${threshold.operator}${thresholdValue}`;
2040
+ }
2041
+ function evalExplode(node, rng, ctx, env) {
2042
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2043
+ const target = evalNode(node.target, rng, targetCtx, env);
2044
+ const targetExpr = targetCtx.expressionParts.join("");
2045
+ let thresholdValue;
2046
+ if (node.threshold != null) {
2047
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2048
+ thresholdValue = evalNode(node.threshold.value, rng, thresholdCtx, env).total;
2049
+ mergeMetaRolls(ctx, thresholdCtx);
2050
+ }
2051
+ const code = formatExplodeCode(node.variant, node.threshold, thresholdValue);
2052
+ const buildPart = (total2) => {
2053
+ const part = {
2054
+ type: "explode",
2055
+ variant: node.variant,
2056
+ target: target.part,
2057
+ total: total2,
2058
+ ...partSpan(node)
2059
+ };
2060
+ if (node.threshold != null && thresholdValue !== undefined) {
2061
+ part.threshold = { operator: node.threshold.operator, value: thresholdValue };
2062
+ }
2063
+ return part;
2064
+ };
2065
+ if (targetCtx.rolls.length === 0) {
2066
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2067
+ ctx.renderedParts.push(`${targetExpr}${code}`);
2068
+ return { total: target.total, part: buildPart(target.total) };
2069
+ }
2070
+ const shouldExplode = buildShouldExplode(node.threshold?.operator, thresholdValue);
2071
+ const expanded = node.variant === "standard" ? applyStandardExplode(targetCtx.rolls, shouldExplode, rng, env) : node.variant === "compound" ? applyCompoundExplode(targetCtx.rolls, shouldExplode, rng, env) : applyPenetratingExplode(targetCtx.rolls, shouldExplode, rng, env);
2072
+ ctx.rolls.push(...expanded);
2073
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2074
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(expanded)}`);
2075
+ const total = sumKeptDice(expanded);
2076
+ return { total, part: buildPart(total) };
2077
+ }
2078
+ function evalReroll(node, rng, ctx, env) {
2079
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2080
+ const target = evalNode(node.target, rng, targetCtx, env);
2081
+ const targetExpr = targetCtx.expressionParts.join("");
2082
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2083
+ const thresholdValue = evalNode(node.condition.value, rng, thresholdCtx, env).total;
2084
+ mergeMetaRolls(ctx, thresholdCtx);
2085
+ const code = `${node.once ? "ro" : "r"}${node.condition.operator}${thresholdValue}`;
2086
+ const condition = {
2087
+ operator: node.condition.operator,
2088
+ value: thresholdValue
2089
+ };
2090
+ if (targetCtx.rolls.length === 0) {
2091
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2092
+ ctx.renderedParts.push(`${targetExpr}${code}`);
2093
+ const total2 = sumKeptDice(targetCtx.rolls);
2094
+ return {
2095
+ total: total2,
2096
+ part: {
2097
+ type: "reroll",
2098
+ once: node.once,
2099
+ condition,
2100
+ target: target.part,
2101
+ total: total2,
2102
+ ...partSpan(node)
2103
+ }
2104
+ };
2105
+ }
2106
+ const pool = node.once ? applyRerollOnce(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env) : applyRecursiveReroll(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env);
2107
+ ctx.rolls.push(...pool);
2108
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2109
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(pool)}`);
2110
+ const total = sumKeptDice(pool);
2111
+ return {
2112
+ total,
2113
+ part: {
2114
+ type: "reroll",
2115
+ once: node.once,
2116
+ condition,
2117
+ target: target.part,
2118
+ total,
2119
+ ...partSpan(node)
2120
+ }
2121
+ };
2122
+ }
2123
+ function evalSort(node, rng, ctx, env) {
2124
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2125
+ const target = evalNode(node.target, rng, targetCtx, env);
2126
+ const sortedRolls = sortDice(targetCtx.rolls, node.order);
2127
+ ctx.rolls.push(...sortedRolls);
2128
+ propagateMetadata(ctx, targetCtx.versusMetadata);
2129
+ const code = node.order === "ascending" ? "s" : "sd";
2130
+ const targetExpr = targetCtx.expressionParts.join("");
2131
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2132
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(sortedRolls)}`);
2133
+ return {
2134
+ total: target.total,
2135
+ part: {
2136
+ type: "sort",
2137
+ order: node.order,
2138
+ target: target.part,
2139
+ total: target.total,
2140
+ ...partSpan(node)
2141
+ }
2142
+ };
2143
+ }
2144
+ function evalCritThreshold(node, rng, ctx, env) {
2145
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2146
+ const target = evalNode(node.target, rng, targetCtx, env);
2147
+ const successResolved = node.successThresholds.map((t) => resolveCritThreshold(t, rng, ctx, env));
2148
+ const failResolved = node.failThresholds.map((t) => resolveCritThreshold(t, rng, ctx, env));
2149
+ const successApplied = successResolved.length > 0 ? successResolved : ["default"];
2150
+ const failApplied = failResolved.length > 0 ? failResolved : ["default"];
2151
+ applyCritThresholds(targetCtx.rolls, successApplied, failApplied);
2152
+ ctx.rolls.push(...targetCtx.rolls);
2153
+ propagateMetadata(ctx, targetCtx.versusMetadata);
2154
+ const targetExpr = targetCtx.expressionParts.join("");
2155
+ const codes = [
2156
+ ...successResolved.map((t) => t === "default" ? "cs" : `cs${t.operator}${t.value}`),
2157
+ ...failResolved.map((t) => t === "default" ? "cf" : `cf${t.operator}${t.value}`)
2158
+ ].join("");
2159
+ ctx.expressionParts.push(`${targetExpr}${codes}`);
2160
+ ctx.renderedParts.push(`${targetExpr}${codes}${renderDice(targetCtx.rolls)}`);
2161
+ return {
2162
+ total: target.total,
2163
+ part: {
2164
+ type: "critThreshold",
2165
+ successThresholds: successResolved,
2166
+ failThresholds: failResolved,
2167
+ target: target.part,
2168
+ total: target.total,
2169
+ ...partSpan(node)
2170
+ }
2171
+ };
2172
+ }
2173
+ function resolveCritThreshold(threshold, rng, ctx, env) {
2174
+ if (threshold === "default")
2175
+ return "default";
2176
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2177
+ const resolved = evalNode(threshold.value, rng, thresholdCtx, env).total;
2178
+ mergeMetaRolls(ctx, thresholdCtx);
2179
+ if (!Number.isFinite(resolved)) {
2180
+ throw new EvaluatorError(`Invalid crit threshold: ${resolved}`, "INVALID_THRESHOLD", "CritThreshold");
2181
+ }
2182
+ return { operator: threshold.operator, value: resolved };
2183
+ }
2184
+ function evalModifier(node, rng, ctx, env) {
2185
+ const { specs, baseTarget } = flattenModifierChain(node, rng, ctx, env);
2186
+ if (baseTarget.type === "Group" && baseTarget.expressions.length >= 2) {
2187
+ return evalGroupModifier(node, baseTarget, specs, rng, ctx, env);
2188
+ }
2189
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2190
+ const target = evalNode(baseTarget, rng, targetCtx, env);
2191
+ const mergedDice = mergeDropSets(targetCtx.rolls, specs);
2192
+ ctx.rolls.push(...mergedDice);
2193
+ const total = sumKeptDice(mergedDice);
2194
+ const targetExpr = targetCtx.expressionParts.join("");
2195
+ const modifierCodes = specs.map((s) => `${s.code}${s.count}`).join("");
2196
+ ctx.expressionParts.push(`${targetExpr}${modifierCodes}`);
2197
+ ctx.renderedParts.push(`${targetExpr}${renderDice(mergedDice)}`);
2198
+ return {
2199
+ total,
2200
+ part: {
2201
+ type: "modifier",
2202
+ specs: toPublicSpecs(specs),
2203
+ target: target.part,
2204
+ total,
2205
+ ...partSpan(node)
2206
+ }
2207
+ };
2208
+ }
2209
+ function stripInnerMarkers(rendered) {
2210
+ return rendered.replace(/\*\*(-?\d+)\*\*/g, "$1").replace(/__(-?\d+)__/g, "$1").replace(/~~(-?\d+)~~/g, "$1");
2211
+ }
2212
+ function evalGroupModifier(node, group, specs, rng, ctx, env) {
2213
+ const subRolls = group.expressions.map((expr) => {
2214
+ const subCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2215
+ const sub = evalNode(expr, rng, subCtx, env);
2216
+ return {
2217
+ subtotal: sub.total,
2218
+ part: sub.part,
2219
+ rolls: subCtx.rolls,
2220
+ expr: subCtx.expressionParts.join(""),
2221
+ rendered: subCtx.renderedParts.join(""),
2222
+ versusMetadata: subCtx.versusMetadata
2223
+ };
2224
+ });
2225
+ const syntheticDice = subRolls.map((sub) => ({
2226
+ sides: 0,
2227
+ result: sub.subtotal,
2228
+ modifiers: [],
2229
+ critical: false,
2230
+ fumble: false
2231
+ }));
2232
+ const mergedSynthetic = mergeDropSets(syntheticDice, specs);
2233
+ const outerRendered = [];
2234
+ const keptIndices = [];
2235
+ let total = 0;
2236
+ for (let i = 0;i < subRolls.length; i++) {
2237
+ const sub = subRolls[i];
2238
+ const synth = mergedSynthetic[i];
2239
+ const isDropped = synth.modifiers.includes("dropped");
2240
+ if (isDropped) {
2241
+ for (const die of sub.rolls) {
2242
+ die.modifiers = [
2243
+ ...die.modifiers.filter((m) => m !== "kept" && m !== "dropped" && m !== "success" && m !== "failure"),
2244
+ "dropped"
2245
+ ];
2246
+ }
2247
+ ctx.rolls.push(...sub.rolls);
2248
+ outerRendered.push(`~~${stripInnerMarkers(sub.rendered)}~~`);
2249
+ } else {
2250
+ keptIndices.push(i);
2251
+ ctx.rolls.push(...sub.rolls);
2252
+ outerRendered.push(sub.rendered);
2253
+ total += sub.subtotal;
2254
+ }
2255
+ if (!isDropped) {
2256
+ propagateMetadata(ctx, sub.versusMetadata);
2257
+ }
2258
+ }
2259
+ const subExprStrs = subRolls.map((s) => s.expr);
2260
+ const modifierCodes = specs.map((s) => `${s.code}${s.count}`).join("");
2261
+ ctx.expressionParts.push(`{${subExprStrs.join(", ")}}${modifierCodes}`);
2262
+ ctx.renderedParts.push(`{${outerRendered.join(", ")}}`);
2263
+ const groupPart = {
2264
+ type: "group",
2265
+ parts: subRolls.map((s) => s.part),
2266
+ keptIndices,
2267
+ total,
2268
+ ...partSpan(group)
2269
+ };
2270
+ return {
2271
+ total,
2272
+ part: {
2273
+ type: "modifier",
2274
+ specs: toPublicSpecs(specs),
2275
+ target: groupPart,
2276
+ total,
2277
+ ...partSpan(node)
2278
+ }
2279
+ };
2280
+ }
2281
+ function resolveThreshold(value, rng, ctx, env, role) {
2282
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2283
+ const resolved = evalNode(value, rng, thresholdCtx, env).total;
2284
+ mergeMetaRolls(ctx, thresholdCtx);
2285
+ if (!Number.isFinite(resolved)) {
2286
+ throw new EvaluatorError(`Invalid ${role}: ${resolved}`, "INVALID_THRESHOLD", "SuccessCount");
2287
+ }
2288
+ return resolved;
2289
+ }
2290
+ function evalSuccessCount(node, rng, ctx, env) {
2291
+ env.hasSuccessCount = true;
2292
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2293
+ const target = evalNode(node.target, rng, targetCtx, env);
2294
+ const targetExpr = targetCtx.expressionParts.join("");
2295
+ const thresholdValue = resolveThreshold(node.threshold.value, rng, ctx, env, "threshold");
2296
+ const failValue = node.failThreshold != null ? resolveThreshold(node.failThreshold.value, rng, ctx, env, "fail threshold") : undefined;
2297
+ const code = `${node.threshold.operator}${thresholdValue}${failValue != null ? `f${failValue}` : ""}`;
2298
+ const buildPart = (total, successes, failures) => {
2299
+ const part = {
2300
+ type: "successCount",
2301
+ threshold: { operator: node.threshold.operator, value: thresholdValue },
2302
+ target: target.part,
2303
+ successes,
2304
+ failures,
2305
+ total,
2306
+ ...partSpan(node)
2307
+ };
2308
+ if (failValue != null && node.failThreshold != null) {
2309
+ part.failThreshold = { operator: node.failThreshold.operator, value: failValue };
2310
+ }
2311
+ return part;
2312
+ };
2313
+ if (targetCtx.rolls.length === 0) {
2314
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2315
+ ctx.renderedParts.push(`${targetExpr}${code}`);
2316
+ return { total: target.total, part: buildPart(target.total, 0, 0) };
2317
+ }
2318
+ const result = countSuccesses(targetCtx.rolls, { operator: node.threshold.operator, value: thresholdValue }, failValue != null && node.failThreshold != null ? { operator: node.failThreshold.operator, value: failValue } : undefined);
2319
+ ctx.rolls.push(...targetCtx.rolls);
2320
+ ctx.expressionParts.push(`${targetExpr}${code}`);
2321
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(targetCtx.rolls)}`);
2322
+ return {
2323
+ total: result.total,
2324
+ part: buildPart(result.total, result.successes, result.failures)
2325
+ };
2326
+ }
2327
+ function extractNatural(rolls) {
2328
+ const primaries = rolls.filter((d) => d.sides === 20 && !d.modifiers.includes("dropped") && !(d.modifiers.includes("exploded") && d.initialResult === undefined));
2329
+ if (primaries.length !== 1)
2330
+ return;
2331
+ const die = primaries[0];
2332
+ return die?.initialResult ?? die?.result;
2333
+ }
2334
+ function calculateDegree(total, dc, natural) {
2335
+ let degree;
2336
+ if (total >= dc + 10)
2337
+ degree = 3 /* CriticalSuccess */;
2338
+ else if (total >= dc)
2339
+ degree = 2 /* Success */;
2340
+ else if (total > dc - 10)
2341
+ degree = 1 /* Failure */;
2342
+ else
2343
+ degree = 0 /* CriticalFailure */;
2344
+ if (natural === 20 && degree < 3 /* CriticalSuccess */)
2345
+ degree++;
2346
+ if (natural === 1 && degree > 0 /* CriticalFailure */)
2347
+ degree--;
2348
+ return degree;
2349
+ }
2350
+ function degreeLabel(degree) {
2351
+ switch (degree) {
2352
+ case 0 /* CriticalFailure */:
2353
+ return "Critical Failure";
2354
+ case 1 /* Failure */:
2355
+ return "Failure";
2356
+ case 2 /* Success */:
2357
+ return "Success";
2358
+ case 3 /* CriticalSuccess */:
2359
+ return "Critical Success";
2360
+ }
2361
+ }
2362
+ function evalVersus(node, rng, ctx, env) {
2363
+ if (env.insideVersus) {
2364
+ throw new EvaluatorError("Cannot nest versus operators", "NESTED_VERSUS", "Versus");
2365
+ }
2366
+ env.insideVersus = true;
2367
+ try {
2368
+ const rollCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2369
+ const rollResult = evalNode(node.roll, rng, rollCtx, env);
2370
+ const natural = extractNatural(rollCtx.rolls);
2371
+ const dcCtx = { rolls: [], expressionParts: [], renderedParts: [] };
2372
+ const dcResult = evalNode(node.dc, rng, dcCtx, env);
2373
+ const degree = calculateDegree(rollResult.total, dcResult.total, natural);
2374
+ ctx.rolls.push(...rollCtx.rolls, ...dcCtx.rolls);
2375
+ const rollExpr = rollCtx.expressionParts.join("");
2376
+ const dcExpr = dcCtx.expressionParts.join("");
2377
+ const rollRendered = rollCtx.renderedParts.join("");
2378
+ const dcRendered = dcCtx.renderedParts.join("");
2379
+ ctx.expressionParts.push(`${rollExpr} vs ${dcExpr}`);
2380
+ ctx.renderedParts.push(`${rollRendered} vs ${dcRendered}`);
2381
+ ctx.versusMetadata = { degree, natural, dcTotal: dcResult.total };
2382
+ return {
2383
+ total: rollResult.total,
2384
+ part: {
2385
+ type: "versus",
2386
+ roll: rollResult.part,
2387
+ dc: dcResult.part,
2388
+ degree,
2389
+ total: rollResult.total,
2390
+ ...partSpan(node)
2391
+ }
2392
+ };
2393
+ } finally {
2394
+ env.insideVersus = false;
2395
+ }
2396
+ }
2397
+ function evaluate(ast, rng, options = {}) {
2398
+ const maxDice = options.maxDice != null && Number.isFinite(options.maxDice) && options.maxDice > 0 ? Math.floor(options.maxDice) : DEFAULT_MAX_DICE;
2399
+ const maxExplodeIterations = options.maxExplodeIterations != null && Number.isFinite(options.maxExplodeIterations) && options.maxExplodeIterations >= 0 ? Math.floor(options.maxExplodeIterations) : DEFAULT_MAX_EXPLODE_ITERATIONS;
2400
+ const maxRerollIterations = options.maxRerollIterations != null && Number.isFinite(options.maxRerollIterations) && options.maxRerollIterations >= 0 ? Math.floor(options.maxRerollIterations) : DEFAULT_MAX_REROLL_ITERATIONS;
2401
+ const context = options.context ?? {};
2402
+ const onMissingVariable = options.onMissingVariable ?? "throw";
2403
+ const env = {
2404
+ maxDice,
2405
+ maxExplodeIterations,
2406
+ maxRerollIterations,
2407
+ totalDiceRolled: 0,
2408
+ hasSuccessCount: false,
2409
+ insideVersus: false,
2410
+ context,
2411
+ onMissingVariable
2412
+ };
2413
+ const ctx = {
2414
+ rolls: [],
2415
+ expressionParts: [],
2416
+ renderedParts: []
2417
+ };
2418
+ const { total, part } = evalNode(ast, rng, ctx, env);
2419
+ if (!Number.isFinite(total)) {
2420
+ throw new EvaluatorError(`Result is not a finite number: ${total}`, "NON_FINITE_RESULT", ast.type);
2421
+ }
2422
+ const expression = ctx.expressionParts.join("");
2423
+ const trailing = ctx.versusMetadata ? degreeLabel(ctx.versusMetadata.degree) : String(total);
2424
+ const rendered = `${ctx.renderedParts.join("")} = ${trailing}`;
2425
+ const result = {
2426
+ total,
2427
+ notation: options.notation ?? expression,
2428
+ expression,
2429
+ rendered,
2430
+ rolls: ctx.rolls,
2431
+ parts: part
2432
+ };
2433
+ if (env.hasSuccessCount) {
2434
+ let successes = 0;
2435
+ let failures = 0;
2436
+ for (const die of ctx.rolls) {
2437
+ if (die.modifiers.includes("success"))
2438
+ successes += 1;
2439
+ else if (die.modifiers.includes("failure"))
2440
+ failures += 1;
2441
+ }
2442
+ result.successes = successes;
2443
+ result.failures = failures;
2444
+ }
2445
+ if (ctx.versusMetadata) {
2446
+ result.degree = ctx.versusMetadata.degree;
2447
+ if (ctx.versusMetadata.natural !== undefined) {
2448
+ result.natural = ctx.versusMetadata.natural;
2449
+ }
2450
+ }
2451
+ return result;
2452
+ }
2453
+
2454
+ // src/roll.ts
2455
+ function roll(notation, options = {}) {
2456
+ const rng = options.rng ?? new SeededRNG(options.seed);
2457
+ const tokens = lex(notation);
2458
+ const ast = new Parser(tokens).parse();
2459
+ const evalOptions = { notation };
2460
+ if (options.maxDice != null)
2461
+ evalOptions.maxDice = options.maxDice;
2462
+ if (options.maxExplodeIterations != null) {
2463
+ evalOptions.maxExplodeIterations = options.maxExplodeIterations;
2464
+ }
2465
+ if (options.maxRerollIterations != null) {
2466
+ evalOptions.maxRerollIterations = options.maxRerollIterations;
2467
+ }
2468
+ if (options.context != null)
2469
+ evalOptions.context = options.context;
2470
+ if (options.onMissingVariable != null) {
2471
+ evalOptions.onMissingVariable = options.onMissingVariable;
2472
+ }
2473
+ return evaluate(ast, rng, evalOptions);
2474
+ }
2475
+ // package.json
2476
+ var version = "3.0.0-beta.0";
2477
+
2478
+ // src/index.ts
2479
+ var VERSION = version;
2480
+
2481
+ // src/cli/args.ts
2482
+ function parseArgs(argv) {
2483
+ let verbose = false;
2484
+ let seed;
2485
+ let showHelp = false;
2486
+ let showVersion = false;
2487
+ const positional = [];
2488
+ for (let i = 0;i < argv.length; i++) {
2489
+ const arg = argv[i];
2490
+ if (arg === "--help" || arg === "-h") {
2491
+ showHelp = true;
2492
+ } else if (arg === "--version") {
2493
+ showVersion = true;
2494
+ } else if (arg === "--verbose" || arg === "-v") {
2495
+ verbose = true;
2496
+ } else if (arg === "--seed") {
2497
+ const next = argv[i + 1];
2498
+ if (next === undefined || next.startsWith("-") && !/^-\d/.test(next)) {
2499
+ return { ok: false, error: "Missing value for --seed" };
2500
+ }
2501
+ seed = next;
2502
+ i++;
2503
+ } else if (arg.startsWith("--seed=")) {
2504
+ const value = arg.slice("--seed=".length);
2505
+ if (value === "") {
2506
+ return { ok: false, error: "Missing value for --seed" };
2507
+ }
2508
+ seed = value;
2509
+ } else if (arg.startsWith("--")) {
2510
+ return { ok: false, error: `Unknown option: ${arg}` };
2511
+ } else if (arg.startsWith("-") && arg.length > 1 && !/^[\ddD(]/.test(arg.slice(1))) {
2512
+ return { ok: false, error: `Unknown option: ${arg}` };
2513
+ } else {
2514
+ positional.push(arg);
2515
+ }
2516
+ }
2517
+ const notation = positional.length > 0 ? positional.join(" ") : undefined;
2518
+ return { ok: true, args: { notation, verbose, seed, showHelp, showVersion } };
2519
+ }
2520
+
2521
+ // src/cli/format.ts
2522
+ function formatResult(result, verbose) {
2523
+ if (!verbose) {
2524
+ return String(result.total);
2525
+ }
2526
+ return formatRendered(result.rendered);
2527
+ }
2528
+ function formatRendered(rendered) {
2529
+ return rendered.replace(/\*\*(-?\d+)\*\*/g, "[$1]").replace(/__(-?\d+)__/g, "{$1}").replace(/~~([^~]+)~~/g, "($1)");
2530
+ }
2531
+
2532
+ // src/cli/index.ts
2533
+ var HELP_TEXT = `roll-parser v${VERSION}
2534
+
2535
+ Usage: roll-parser <notation> [options]
2536
+
2537
+ Options:
2538
+ -h, --help Show this help message
2539
+ --version Show version number
2540
+ -v, --verbose Show detailed roll breakdown
2541
+ --seed <value> Use seed for reproducible rolls
2542
+
2543
+ Examples:
2544
+ roll-parser 2d6+3
2545
+ roll-parser 4d6kh3 --verbose
2546
+ roll-parser 4d6dl1 --seed "character-str"
2547
+ `;
2548
+ function writeErrorContext(notation, error) {
2549
+ const position = "position" in error ? error.position : ("start" in error) ? error.start : undefined;
2550
+ if (typeof position !== "number" || !Number.isInteger(position))
2551
+ return;
2552
+ if (notation.includes(`
2553
+ `) || position < 0 || position > notation.length)
2554
+ return;
2555
+ process.stderr.write(` ${notation}
2556
+ `);
2557
+ process.stderr.write(` ${" ".repeat(position)}^
2558
+ `);
2559
+ }
2560
+ function main() {
2561
+ const parsed = parseArgs(process.argv.slice(2));
2562
+ if (!parsed.ok) {
2563
+ process.stderr.write(`Error: ${parsed.error}
2564
+ `);
2565
+ process.stderr.write(`Run "roll-parser --help" for usage.
2566
+ `);
2567
+ process.exitCode = 2;
2568
+ return;
2569
+ }
2570
+ const { args } = parsed;
2571
+ if (args.showHelp) {
2572
+ process.stdout.write(HELP_TEXT);
2573
+ return;
2574
+ }
2575
+ if (args.showVersion) {
2576
+ process.stdout.write(`${VERSION}
2577
+ `);
2578
+ return;
2579
+ }
2580
+ if (args.notation == null) {
2581
+ process.stderr.write(`Error: No dice notation provided.
2582
+ `);
2583
+ process.stderr.write(`Run "roll-parser --help" for usage.
2584
+ `);
2585
+ process.exitCode = 2;
2586
+ return;
2587
+ }
2588
+ try {
2589
+ const options = args.seed != null ? { seed: args.seed } : {};
2590
+ const result = roll(args.notation, options);
2591
+ const output = formatResult(result, args.verbose);
2592
+ process.stdout.write(`${output}
2593
+ `);
2594
+ } catch (error) {
2595
+ if (isRollParserError(error)) {
2596
+ process.stderr.write(`Error: ${error.message}
2597
+ `);
2598
+ writeErrorContext(args.notation, error);
2599
+ process.exitCode = 1;
2600
+ return;
2601
+ }
2602
+ throw error;
2603
+ }
2604
+ }
2605
+ main();
2606
+
2607
+ //# debugId=E5D058E5C568B60B64756E2164756E21
2608
+ //# sourceMappingURL=cli.js.map