roll-parser 2.3.2 → 3.0.0-alpha.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 (101) hide show
  1. package/README.md +47 -178
  2. package/dist/cli/args.d.ts +35 -0
  3. package/dist/cli/args.d.ts.map +1 -0
  4. package/dist/cli/format.d.ts +20 -0
  5. package/dist/cli/format.d.ts.map +1 -0
  6. package/dist/cli/index.d.ts +8 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli.js +1775 -0
  9. package/dist/errors.d.ts +39 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/evaluator/evaluator.d.ts +65 -0
  12. package/dist/evaluator/evaluator.d.ts.map +1 -0
  13. package/dist/evaluator/index.d.ts +8 -0
  14. package/dist/evaluator/index.d.ts.map +1 -0
  15. package/dist/evaluator/modifiers/compare.d.ts +15 -0
  16. package/dist/evaluator/modifiers/compare.d.ts.map +1 -0
  17. package/dist/evaluator/modifiers/explode.d.ts +44 -0
  18. package/dist/evaluator/modifiers/explode.d.ts.map +1 -0
  19. package/dist/evaluator/modifiers/keep-drop.d.ts +44 -0
  20. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -0
  21. package/dist/evaluator/modifiers/reroll.d.ts +33 -0
  22. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -0
  23. package/dist/evaluator/modifiers/success-count.d.ts +28 -0
  24. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -0
  25. package/dist/index.d.ts +20 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +1723 -0
  28. package/dist/index.mjs +1724 -0
  29. package/dist/lexer/lexer.d.ts +75 -0
  30. package/dist/lexer/lexer.d.ts.map +1 -0
  31. package/dist/lexer/tokens.d.ts +87 -0
  32. package/dist/lexer/tokens.d.ts.map +1 -0
  33. package/dist/parser/ast.d.ts +183 -0
  34. package/dist/parser/ast.d.ts.map +1 -0
  35. package/dist/parser/parser.d.ts +102 -0
  36. package/dist/parser/parser.d.ts.map +1 -0
  37. package/dist/rng/index.d.ts +8 -0
  38. package/dist/rng/index.d.ts.map +1 -0
  39. package/dist/rng/mock.d.ts +36 -0
  40. package/dist/rng/mock.d.ts.map +1 -0
  41. package/dist/rng/seeded.d.ts +33 -0
  42. package/dist/rng/seeded.d.ts.map +1 -0
  43. package/dist/rng/types.d.ts +24 -0
  44. package/dist/rng/types.d.ts.map +1 -0
  45. package/dist/roll.d.ts +47 -0
  46. package/dist/roll.d.ts.map +1 -0
  47. package/dist/testing.d.ts +11 -0
  48. package/dist/testing.d.ts.map +1 -0
  49. package/dist/testing.js +38 -0
  50. package/dist/testing.mjs +39 -0
  51. package/dist/types.d.ts +105 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/package.json +62 -43
  54. package/src/cli/args.ts +72 -0
  55. package/src/cli/format.ts +45 -0
  56. package/src/cli/index.ts +75 -0
  57. package/src/errors.ts +78 -0
  58. package/src/evaluator/evaluator.ts +837 -0
  59. package/src/evaluator/index.ts +14 -0
  60. package/src/evaluator/modifiers/compare.ts +29 -0
  61. package/src/evaluator/modifiers/explode.ts +215 -0
  62. package/src/evaluator/modifiers/keep-drop.ts +151 -0
  63. package/src/evaluator/modifiers/reroll.ts +172 -0
  64. package/src/evaluator/modifiers/success-count.ts +59 -0
  65. package/src/index.ts +72 -0
  66. package/src/lexer/lexer.ts +281 -0
  67. package/src/lexer/tokens.ts +147 -0
  68. package/src/parser/ast.ts +261 -0
  69. package/src/parser/parser.ts +716 -0
  70. package/src/rng/index.ts +8 -0
  71. package/src/rng/mock.ts +65 -0
  72. package/src/rng/seeded.ts +127 -0
  73. package/src/rng/types.ts +25 -0
  74. package/src/roll.ts +66 -0
  75. package/src/testing.ts +16 -0
  76. package/src/types.ts +112 -0
  77. package/bin/help.txt +0 -19
  78. package/bin/roll-parser.js +0 -23
  79. package/dist/roll-parser.js +0 -697
  80. package/dist/roll-parser.min.js +0 -1
  81. package/index.js +0 -24
  82. package/src/complex/parse.js +0 -37
  83. package/src/complex/parseAndRoll.js +0 -21
  84. package/src/complex/parseAndRollClassic.js +0 -20
  85. package/src/complex/parseAndRollSimple.js +0 -19
  86. package/src/complex/parseAndRollWod.js +0 -20
  87. package/src/complex/parseClassicRoll.js +0 -22
  88. package/src/complex/parseSimpleRoll.js +0 -21
  89. package/src/complex/parseWodRoll.js +0 -21
  90. package/src/converter.js +0 -45
  91. package/src/grammar.js +0 -14
  92. package/src/mapper.js +0 -50
  93. package/src/normalizer.js +0 -51
  94. package/src/object/Result.js +0 -23
  95. package/src/object/Roll.js +0 -26
  96. package/src/object/Type.js +0 -5
  97. package/src/object/WodRoll.js +0 -29
  98. package/src/parser.js +0 -43
  99. package/src/random.js +0 -22
  100. package/src/roller.js +0 -106
  101. package/src/stringifier.js +0 -33
package/dist/index.js ADDED
@@ -0,0 +1,1723 @@
1
+ // src/errors.ts
2
+ var ROLL_PARSER_ERROR_CODES = [
3
+ "UNEXPECTED_CHARACTER",
4
+ "UNEXPECTED_IDENTIFIER",
5
+ "UNEXPECTED_TOKEN",
6
+ "UNEXPECTED_END",
7
+ "EXPECTED_TOKEN",
8
+ "INVALID_DICE_COUNT",
9
+ "INVALID_DICE_SIDES",
10
+ "DICE_LIMIT_EXCEEDED",
11
+ "DIVISION_BY_ZERO",
12
+ "MODULO_BY_ZERO",
13
+ "UNKNOWN_OPERATOR",
14
+ "UNKNOWN_NODE_TYPE",
15
+ "INVALID_MODIFIER_COUNT",
16
+ "EXPLODE_LIMIT_EXCEEDED",
17
+ "INVALID_EXPLODE_TARGET",
18
+ "REROLL_LIMIT_EXCEEDED",
19
+ "INVALID_SUCCESS_COUNT_TARGET",
20
+ "INVALID_THRESHOLD",
21
+ "NESTED_VERSUS",
22
+ "INVALID_FUNCTION_ARITY",
23
+ "UNKNOWN_FUNCTION"
24
+ ];
25
+
26
+ class RollParserError extends Error {
27
+ code;
28
+ constructor(message, code) {
29
+ super(message);
30
+ this.name = "RollParserError";
31
+ this.code = code;
32
+ }
33
+ }
34
+ var VALID_CODES = new Set(ROLL_PARSER_ERROR_CODES);
35
+ function isRollParserError(value) {
36
+ if (value instanceof RollParserError)
37
+ return true;
38
+ return value instanceof Error && "code" in value && typeof value.code === "string" && VALID_CODES.has(value.code);
39
+ }
40
+ // src/lexer/tokens.ts
41
+ var TokenType;
42
+ ((TokenType2) => {
43
+ TokenType2[TokenType2["NUMBER"] = 0] = "NUMBER";
44
+ TokenType2[TokenType2["DICE"] = 1] = "DICE";
45
+ TokenType2[TokenType2["DICE_PERCENT"] = 2] = "DICE_PERCENT";
46
+ TokenType2[TokenType2["DICE_FATE"] = 3] = "DICE_FATE";
47
+ TokenType2[TokenType2["PLUS"] = 4] = "PLUS";
48
+ TokenType2[TokenType2["MINUS"] = 5] = "MINUS";
49
+ TokenType2[TokenType2["MULTIPLY"] = 6] = "MULTIPLY";
50
+ TokenType2[TokenType2["DIVIDE"] = 7] = "DIVIDE";
51
+ TokenType2[TokenType2["MODULO"] = 8] = "MODULO";
52
+ TokenType2[TokenType2["POWER"] = 9] = "POWER";
53
+ TokenType2[TokenType2["GREATER"] = 10] = "GREATER";
54
+ TokenType2[TokenType2["GREATER_EQUAL"] = 11] = "GREATER_EQUAL";
55
+ TokenType2[TokenType2["LESS"] = 12] = "LESS";
56
+ TokenType2[TokenType2["LESS_EQUAL"] = 13] = "LESS_EQUAL";
57
+ TokenType2[TokenType2["EQUAL"] = 14] = "EQUAL";
58
+ TokenType2[TokenType2["LPAREN"] = 15] = "LPAREN";
59
+ TokenType2[TokenType2["RPAREN"] = 16] = "RPAREN";
60
+ TokenType2[TokenType2["COMMA"] = 17] = "COMMA";
61
+ TokenType2[TokenType2["KEEP_HIGH"] = 18] = "KEEP_HIGH";
62
+ TokenType2[TokenType2["KEEP_LOW"] = 19] = "KEEP_LOW";
63
+ TokenType2[TokenType2["DROP_HIGH"] = 20] = "DROP_HIGH";
64
+ TokenType2[TokenType2["DROP_LOW"] = 21] = "DROP_LOW";
65
+ TokenType2[TokenType2["EXPLODE"] = 22] = "EXPLODE";
66
+ TokenType2[TokenType2["EXPLODE_COMPOUND"] = 23] = "EXPLODE_COMPOUND";
67
+ TokenType2[TokenType2["EXPLODE_PENETRATING"] = 24] = "EXPLODE_PENETRATING";
68
+ TokenType2[TokenType2["REROLL"] = 25] = "REROLL";
69
+ TokenType2[TokenType2["REROLL_ONCE"] = 26] = "REROLL_ONCE";
70
+ TokenType2[TokenType2["FAIL"] = 27] = "FAIL";
71
+ TokenType2[TokenType2["FUNCTION"] = 28] = "FUNCTION";
72
+ TokenType2[TokenType2["VS"] = 29] = "VS";
73
+ TokenType2[TokenType2["EOF"] = 30] = "EOF";
74
+ })(TokenType ||= {});
75
+
76
+ // src/lexer/lexer.ts
77
+ class LexerError extends RollParserError {
78
+ position;
79
+ character;
80
+ constructor(message, code, position, character) {
81
+ super(`${message} at position ${position}: '${character}'`, code);
82
+ this.name = "LexerError";
83
+ this.position = position;
84
+ this.character = character;
85
+ }
86
+ }
87
+ var IDENTIFIER_KEYWORDS = {
88
+ kh: 18 /* KEEP_HIGH */,
89
+ kl: 19 /* KEEP_LOW */,
90
+ k: 18 /* KEEP_HIGH */,
91
+ dh: 20 /* DROP_HIGH */,
92
+ dl: 21 /* DROP_LOW */,
93
+ d: 1 /* DICE */,
94
+ r: 25 /* REROLL */,
95
+ ro: 26 /* REROLL_ONCE */,
96
+ f: 27 /* FAIL */,
97
+ vs: 29 /* VS */,
98
+ floor: 28 /* FUNCTION */,
99
+ ceil: 28 /* FUNCTION */,
100
+ round: 28 /* FUNCTION */,
101
+ abs: 28 /* FUNCTION */,
102
+ max: 28 /* FUNCTION */,
103
+ min: 28 /* FUNCTION */
104
+ };
105
+
106
+ class Lexer {
107
+ pos = 0;
108
+ input;
109
+ constructor(input) {
110
+ this.input = input;
111
+ }
112
+ tokenize() {
113
+ const tokens = [];
114
+ while (true) {
115
+ const token = this.nextToken();
116
+ tokens.push(token);
117
+ if (token.type === 30 /* EOF */)
118
+ break;
119
+ }
120
+ return tokens;
121
+ }
122
+ nextToken() {
123
+ this.skipWhitespace();
124
+ if (this.isAtEnd()) {
125
+ return this.createToken(30 /* EOF */, "");
126
+ }
127
+ const startPos = this.pos;
128
+ const char = this.peek();
129
+ if (this.isDigit(char)) {
130
+ return this.scanNumber();
131
+ }
132
+ if (this.isAlpha(char)) {
133
+ return this.scanIdentifier();
134
+ }
135
+ this.advance();
136
+ switch (char) {
137
+ case "+":
138
+ return this.createTokenAt(4 /* PLUS */, char, startPos);
139
+ case "-":
140
+ return this.createTokenAt(5 /* MINUS */, char, startPos);
141
+ case "*":
142
+ if (this.match("*")) {
143
+ return this.createTokenAt(9 /* POWER */, "**", startPos);
144
+ }
145
+ return this.createTokenAt(6 /* MULTIPLY */, char, startPos);
146
+ case "/":
147
+ return this.createTokenAt(7 /* DIVIDE */, char, startPos);
148
+ case "%":
149
+ return this.createTokenAt(8 /* MODULO */, char, startPos);
150
+ case "^":
151
+ return this.createTokenAt(9 /* POWER */, char, startPos);
152
+ case "(":
153
+ return this.createTokenAt(15 /* LPAREN */, char, startPos);
154
+ case ")":
155
+ return this.createTokenAt(16 /* RPAREN */, char, startPos);
156
+ case ",":
157
+ return this.createTokenAt(17 /* COMMA */, char, startPos);
158
+ case ">":
159
+ if (this.match("=")) {
160
+ return this.createTokenAt(11 /* GREATER_EQUAL */, ">=", startPos);
161
+ }
162
+ return this.createTokenAt(10 /* GREATER */, char, startPos);
163
+ case "<":
164
+ if (this.match("=")) {
165
+ return this.createTokenAt(13 /* LESS_EQUAL */, "<=", startPos);
166
+ }
167
+ return this.createTokenAt(12 /* LESS */, char, startPos);
168
+ case "=":
169
+ return this.createTokenAt(14 /* EQUAL */, char, startPos);
170
+ case "!":
171
+ if (this.match("!")) {
172
+ return this.createTokenAt(23 /* EXPLODE_COMPOUND */, "!!", startPos);
173
+ }
174
+ if (!this.isAtEnd() && this.peek().toLowerCase() === "p") {
175
+ this.advance();
176
+ return this.createTokenAt(24 /* EXPLODE_PENETRATING */, "!p", startPos);
177
+ }
178
+ return this.createTokenAt(22 /* EXPLODE */, char, startPos);
179
+ default:
180
+ throw new LexerError("Unexpected character", "UNEXPECTED_CHARACTER", startPos, char);
181
+ }
182
+ }
183
+ skipWhitespace() {
184
+ while (!this.isAtEnd() && this.isWhitespace(this.peek())) {
185
+ this.advance();
186
+ }
187
+ }
188
+ scanNumber() {
189
+ const startPos = this.pos;
190
+ let value = "";
191
+ while (!this.isAtEnd() && this.isDigit(this.peek())) {
192
+ value += this.advance();
193
+ }
194
+ if (!this.isAtEnd() && this.peek() === "." && this.isDigit(this.peekNext())) {
195
+ value += this.advance();
196
+ while (!this.isAtEnd() && this.isDigit(this.peek())) {
197
+ value += this.advance();
198
+ }
199
+ }
200
+ return this.createTokenAt(0 /* NUMBER */, value, startPos);
201
+ }
202
+ scanIdentifier() {
203
+ const startPos = this.pos;
204
+ const first = this.peek();
205
+ const second = this.peekNext();
206
+ if ((first === "d" || first === "D") && (second === "f" || second === "F")) {
207
+ this.advance();
208
+ this.advance();
209
+ return this.createTokenAt(3 /* DICE_FATE */, "df", startPos);
210
+ }
211
+ let value = "";
212
+ while (!this.isAtEnd() && this.isAlpha(this.peek())) {
213
+ value += this.advance();
214
+ }
215
+ const lower = value.toLowerCase();
216
+ if (lower === "d" && !this.isAtEnd() && this.peek() === "%") {
217
+ this.advance();
218
+ return this.createTokenAt(2 /* DICE_PERCENT */, "d%", startPos);
219
+ }
220
+ const tokenType = IDENTIFIER_KEYWORDS[lower];
221
+ if (tokenType != null) {
222
+ return this.createTokenAt(tokenType, lower, startPos);
223
+ }
224
+ throw new LexerError("Unexpected identifier", "UNEXPECTED_IDENTIFIER", startPos, lower);
225
+ }
226
+ peek() {
227
+ return this.input[this.pos] ?? "";
228
+ }
229
+ peekNext() {
230
+ return this.input[this.pos + 1] ?? "";
231
+ }
232
+ advance() {
233
+ return this.input[this.pos++] ?? "";
234
+ }
235
+ match(expected) {
236
+ if (this.isAtEnd())
237
+ return false;
238
+ if (this.input[this.pos] !== expected)
239
+ return false;
240
+ this.pos++;
241
+ return true;
242
+ }
243
+ isAtEnd() {
244
+ return this.pos >= this.input.length;
245
+ }
246
+ isDigit(char) {
247
+ return char >= "0" && char <= "9";
248
+ }
249
+ isAlpha(char) {
250
+ const c = char.toLowerCase();
251
+ return c >= "a" && c <= "z";
252
+ }
253
+ isWhitespace(char) {
254
+ return char === " " || char === "\t" || char === `
255
+ ` || char === "\r";
256
+ }
257
+ createToken(type, value) {
258
+ return { type, value, position: this.pos };
259
+ }
260
+ createTokenAt(type, value, position) {
261
+ return { type, value, position };
262
+ }
263
+ }
264
+ function lex(input) {
265
+ return new Lexer(input).tokenize();
266
+ }
267
+ // src/parser/ast.ts
268
+ function isLiteral(node) {
269
+ return node.type === "Literal";
270
+ }
271
+ function isDice(node) {
272
+ return node.type === "Dice";
273
+ }
274
+ function isFateDice(node) {
275
+ return node.type === "FateDice";
276
+ }
277
+ function isBinaryOp(node) {
278
+ return node.type === "BinaryOp";
279
+ }
280
+ function isUnaryOp(node) {
281
+ return node.type === "UnaryOp";
282
+ }
283
+ function isModifier(node) {
284
+ return node.type === "Modifier";
285
+ }
286
+ function isExplode(node) {
287
+ return node.type === "Explode";
288
+ }
289
+ function isReroll(node) {
290
+ return node.type === "Reroll";
291
+ }
292
+ function isSuccessCount(node) {
293
+ return node.type === "SuccessCount";
294
+ }
295
+ function isVersus(node) {
296
+ return node.type === "Versus";
297
+ }
298
+ function isFunctionCall(node) {
299
+ return node.type === "FunctionCall";
300
+ }
301
+ function containsDice(node) {
302
+ switch (node.type) {
303
+ case "Dice":
304
+ case "FateDice":
305
+ return true;
306
+ case "Literal":
307
+ return false;
308
+ case "BinaryOp":
309
+ return containsDice(node.left) || containsDice(node.right);
310
+ case "UnaryOp":
311
+ return containsDice(node.operand);
312
+ case "Modifier":
313
+ case "Explode":
314
+ case "Reroll":
315
+ case "SuccessCount":
316
+ return containsDice(node.target);
317
+ case "Versus":
318
+ return containsDice(node.roll) || containsDice(node.dc);
319
+ case "FunctionCall":
320
+ return node.args.some(containsDice);
321
+ }
322
+ }
323
+
324
+ // src/parser/parser.ts
325
+ class ParseError extends RollParserError {
326
+ position;
327
+ token;
328
+ constructor(message, code, position, token) {
329
+ super(`${message} at position ${position}`, code);
330
+ this.name = "ParseError";
331
+ this.position = position;
332
+ this.token = token ?? undefined;
333
+ }
334
+ }
335
+ var BP = {
336
+ VS_LEFT: 2,
337
+ VS_RIGHT: 3,
338
+ ADD_LEFT: 10,
339
+ ADD_RIGHT: 11,
340
+ MUL_LEFT: 20,
341
+ MUL_RIGHT: 21,
342
+ UNARY: 25,
343
+ POW_LEFT: 31,
344
+ POW_RIGHT: 30,
345
+ MODIFIER: 35,
346
+ DICE_LEFT: 40,
347
+ DICE_RIGHT: 41
348
+ };
349
+ var FUNCTION_ARITY = {
350
+ floor: { min: 1, max: 1 },
351
+ ceil: { min: 1, max: 1 },
352
+ round: { min: 1, max: 1 },
353
+ abs: { min: 1, max: 1 },
354
+ max: { min: 2, max: Number.POSITIVE_INFINITY },
355
+ min: { min: 2, max: Number.POSITIVE_INFINITY }
356
+ };
357
+
358
+ class Parser {
359
+ tokens;
360
+ pos = 0;
361
+ constructor(tokens) {
362
+ this.tokens = tokens;
363
+ }
364
+ parse() {
365
+ if (this.peek().type === 30 /* EOF */) {
366
+ throw new ParseError("Unexpected end of input", "UNEXPECTED_END", this.peek().position);
367
+ }
368
+ const ast = this.parseExpression(0);
369
+ if (this.peek().type !== 30 /* EOF */) {
370
+ const token = this.peek();
371
+ throw new ParseError(`Unexpected token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
372
+ }
373
+ return ast;
374
+ }
375
+ parseExpression(minBp) {
376
+ let left = this.parseNud();
377
+ while (this.hasTokens()) {
378
+ const token = this.peek();
379
+ const leftBp = this.getLeftBp(token);
380
+ if (leftBp < minBp)
381
+ break;
382
+ this.advance();
383
+ left = this.parseLed(left, token);
384
+ }
385
+ return left;
386
+ }
387
+ parseNud() {
388
+ const token = this.advance();
389
+ switch (token.type) {
390
+ case 0 /* NUMBER */:
391
+ return this.parseLiteral(token);
392
+ case 5 /* MINUS */:
393
+ return this.parseUnaryMinus();
394
+ case 1 /* DICE */:
395
+ return this.parsePrefixDice();
396
+ case 2 /* DICE_PERCENT */:
397
+ return this.parsePrefixDicePercent();
398
+ case 3 /* DICE_FATE */:
399
+ return this.parsePrefixFateDice();
400
+ case 15 /* LPAREN */:
401
+ return this.parseGrouped();
402
+ case 28 /* FUNCTION */:
403
+ return this.parseFunctionCall(token);
404
+ case 30 /* EOF */:
405
+ throw new ParseError("Unexpected end of input", "UNEXPECTED_END", token.position);
406
+ default:
407
+ throw new ParseError(`Unexpected token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
408
+ }
409
+ }
410
+ parseLed(left, token) {
411
+ switch (token.type) {
412
+ case 1 /* DICE */:
413
+ return this.parseInfixDice(left);
414
+ case 2 /* DICE_PERCENT */:
415
+ return this.parseInfixDicePercent(left);
416
+ case 3 /* DICE_FATE */:
417
+ return this.parseInfixFateDice(left);
418
+ case 4 /* PLUS */:
419
+ case 5 /* MINUS */:
420
+ case 6 /* MULTIPLY */:
421
+ case 7 /* DIVIDE */:
422
+ case 8 /* MODULO */:
423
+ case 9 /* POWER */:
424
+ return this.parseBinaryOp(left, token);
425
+ case 18 /* KEEP_HIGH */:
426
+ case 19 /* KEEP_LOW */:
427
+ case 20 /* DROP_HIGH */:
428
+ case 21 /* DROP_LOW */:
429
+ return this.parseModifier(left, token);
430
+ case 22 /* EXPLODE */:
431
+ case 23 /* EXPLODE_COMPOUND */:
432
+ case 24 /* EXPLODE_PENETRATING */:
433
+ return this.parseExplode(left, token);
434
+ case 25 /* REROLL */:
435
+ case 26 /* REROLL_ONCE */:
436
+ return this.parseReroll(left, token);
437
+ case 10 /* GREATER */:
438
+ case 11 /* GREATER_EQUAL */:
439
+ case 12 /* LESS */:
440
+ case 13 /* LESS_EQUAL */:
441
+ case 14 /* EQUAL */:
442
+ return this.parseSuccessCount(left, token);
443
+ case 29 /* VS */:
444
+ return this.parseVersus(left, token);
445
+ default:
446
+ throw new ParseError(`Unexpected infix token '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
447
+ }
448
+ }
449
+ parseLiteral(token) {
450
+ return {
451
+ type: "Literal",
452
+ value: Number.parseFloat(token.value)
453
+ };
454
+ }
455
+ parseUnaryMinus() {
456
+ const operand = this.parseExpression(BP.UNARY);
457
+ return {
458
+ type: "UnaryOp",
459
+ operator: "-",
460
+ operand
461
+ };
462
+ }
463
+ parsePrefixDice() {
464
+ const sides = this.parseExpression(BP.DICE_RIGHT);
465
+ return {
466
+ type: "Dice",
467
+ count: { type: "Literal", value: 1 },
468
+ sides
469
+ };
470
+ }
471
+ parseInfixDice(left) {
472
+ const sides = this.parseExpression(BP.DICE_RIGHT);
473
+ return {
474
+ type: "Dice",
475
+ count: left,
476
+ sides
477
+ };
478
+ }
479
+ parsePrefixDicePercent() {
480
+ return {
481
+ type: "Dice",
482
+ count: { type: "Literal", value: 1 },
483
+ sides: { type: "Literal", value: 100 }
484
+ };
485
+ }
486
+ parseInfixDicePercent(left) {
487
+ return {
488
+ type: "Dice",
489
+ count: left,
490
+ sides: { type: "Literal", value: 100 }
491
+ };
492
+ }
493
+ parsePrefixFateDice() {
494
+ return {
495
+ type: "FateDice",
496
+ count: { type: "Literal", value: 1 }
497
+ };
498
+ }
499
+ parseInfixFateDice(left) {
500
+ return {
501
+ type: "FateDice",
502
+ count: left
503
+ };
504
+ }
505
+ parseGrouped() {
506
+ const expr = this.parseExpression(0);
507
+ this.expect(16 /* RPAREN */);
508
+ return expr;
509
+ }
510
+ parseFunctionCall(token) {
511
+ this.expect(15 /* LPAREN */);
512
+ const args = [];
513
+ if (this.peek().type !== 16 /* RPAREN */) {
514
+ args.push(this.parseExpression(0));
515
+ while (this.peek().type === 17 /* COMMA */) {
516
+ this.advance();
517
+ args.push(this.parseExpression(0));
518
+ }
519
+ }
520
+ this.expect(16 /* RPAREN */);
521
+ const arity = FUNCTION_ARITY[token.value];
522
+ if (arity === undefined) {
523
+ throw new ParseError(`Unknown function '${token.value}'`, "UNKNOWN_FUNCTION", token.position, token);
524
+ }
525
+ if (args.length < arity.min || args.length > arity.max) {
526
+ const expected = arity.max === Number.POSITIVE_INFINITY ? `at least ${arity.min}` : arity.min === arity.max ? `${arity.min}` : `${arity.min}–${arity.max}`;
527
+ 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);
528
+ }
529
+ return { type: "FunctionCall", name: token.value, args };
530
+ }
531
+ parseBinaryOp(left, token) {
532
+ const operator = this.getOperatorSymbol(token);
533
+ const rightBp = this.getRightBp(token);
534
+ const right = this.parseExpression(rightBp);
535
+ return {
536
+ type: "BinaryOp",
537
+ operator,
538
+ left,
539
+ right
540
+ };
541
+ }
542
+ rejectSuccessCountTarget(target, token) {
543
+ if (isSuccessCount(target)) {
544
+ throw new ParseError(`Cannot apply modifier after success counting`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
545
+ }
546
+ }
547
+ parseModifier(target, token) {
548
+ this.rejectSuccessCountTarget(target, token);
549
+ const modifier = token.type === 18 /* KEEP_HIGH */ || token.type === 19 /* KEEP_LOW */ ? "keep" : "drop";
550
+ const selector = token.type === 18 /* KEEP_HIGH */ || token.type === 20 /* DROP_HIGH */ ? "highest" : "lowest";
551
+ const nextToken = this.peek().type;
552
+ const count = nextToken === 0 /* NUMBER */ || nextToken === 15 /* LPAREN */ ? this.parseExpression(BP.DICE_LEFT) : { type: "Literal", value: 1 };
553
+ return {
554
+ type: "Modifier",
555
+ modifier,
556
+ selector,
557
+ count,
558
+ target
559
+ };
560
+ }
561
+ parseExplode(target, token) {
562
+ this.rejectSuccessCountTarget(target, token);
563
+ if (target.type === "Explode") {
564
+ throw new ParseError(`Cannot chain explode modifiers`, "INVALID_EXPLODE_TARGET", token.position, token);
565
+ }
566
+ const variant = token.type === 22 /* EXPLODE */ ? "standard" : token.type === 23 /* EXPLODE_COMPOUND */ ? "compound" : "penetrating";
567
+ const node = { type: "Explode", variant, target };
568
+ if (this.isComparePointAhead()) {
569
+ node.threshold = this.parseComparePoint();
570
+ }
571
+ return node;
572
+ }
573
+ parseReroll(target, token) {
574
+ this.rejectSuccessCountTarget(target, token);
575
+ if (!this.isComparePointAhead()) {
576
+ throw new ParseError(`Expected comparison operator after '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
577
+ }
578
+ const once = token.type === 26 /* REROLL_ONCE */;
579
+ const condition = this.parseComparePoint();
580
+ return { type: "Reroll", once, condition, target };
581
+ }
582
+ parseSuccessCount(target, token) {
583
+ this.rejectSuccessCountTarget(target, token);
584
+ if (!containsDice(target)) {
585
+ throw new ParseError(`Success counting requires a dice expression`, "INVALID_SUCCESS_COUNT_TARGET", token.position, token);
586
+ }
587
+ const operator = this.getCompareOp(token);
588
+ const value = this.parseExpression(BP.DICE_LEFT);
589
+ const node = {
590
+ type: "SuccessCount",
591
+ target,
592
+ threshold: { operator, value }
593
+ };
594
+ if (this.peek().type === 27 /* FAIL */) {
595
+ this.advance();
596
+ const failValue = this.parseExpression(BP.DICE_LEFT);
597
+ node.failThreshold = { operator: "=", value: failValue };
598
+ }
599
+ return node;
600
+ }
601
+ parseVersus(left, token) {
602
+ if (left.type === "Versus") {
603
+ throw new ParseError("Cannot chain versus operators", "NESTED_VERSUS", token.position, token);
604
+ }
605
+ const dc = this.parseExpression(BP.VS_RIGHT);
606
+ return { type: "Versus", roll: left, dc };
607
+ }
608
+ isComparePointAhead() {
609
+ const type = this.peek().type;
610
+ return type === 10 /* GREATER */ || type === 11 /* GREATER_EQUAL */ || type === 12 /* LESS */ || type === 13 /* LESS_EQUAL */ || type === 14 /* EQUAL */;
611
+ }
612
+ parseComparePoint() {
613
+ const token = this.peek();
614
+ const operator = this.getCompareOp(token);
615
+ this.advance();
616
+ const value = this.parseExpression(BP.DICE_LEFT);
617
+ return { operator, value };
618
+ }
619
+ getCompareOp(token) {
620
+ switch (token.type) {
621
+ case 10 /* GREATER */:
622
+ return ">";
623
+ case 11 /* GREATER_EQUAL */:
624
+ return ">=";
625
+ case 12 /* LESS */:
626
+ return "<";
627
+ case 13 /* LESS_EQUAL */:
628
+ return "<=";
629
+ case 14 /* EQUAL */:
630
+ return "=";
631
+ default:
632
+ throw new ParseError(`Expected comparison operator but got '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
633
+ }
634
+ }
635
+ getOperatorSymbol(token) {
636
+ switch (token.type) {
637
+ case 4 /* PLUS */:
638
+ return "+";
639
+ case 5 /* MINUS */:
640
+ return "-";
641
+ case 6 /* MULTIPLY */:
642
+ return "*";
643
+ case 7 /* DIVIDE */:
644
+ return "/";
645
+ case 8 /* MODULO */:
646
+ return "%";
647
+ case 9 /* POWER */:
648
+ return "**";
649
+ default:
650
+ throw new ParseError(`Unknown operator '${token.value}'`, "UNEXPECTED_TOKEN", token.position, token);
651
+ }
652
+ }
653
+ getLeftBp(token) {
654
+ switch (token.type) {
655
+ case 29 /* VS */:
656
+ return BP.VS_LEFT;
657
+ case 4 /* PLUS */:
658
+ case 5 /* MINUS */:
659
+ return BP.ADD_LEFT;
660
+ case 6 /* MULTIPLY */:
661
+ case 7 /* DIVIDE */:
662
+ case 8 /* MODULO */:
663
+ return BP.MUL_LEFT;
664
+ case 9 /* POWER */:
665
+ return BP.POW_LEFT;
666
+ case 1 /* DICE */:
667
+ case 2 /* DICE_PERCENT */:
668
+ case 3 /* DICE_FATE */:
669
+ return BP.DICE_LEFT;
670
+ case 18 /* KEEP_HIGH */:
671
+ case 19 /* KEEP_LOW */:
672
+ case 20 /* DROP_HIGH */:
673
+ case 21 /* DROP_LOW */:
674
+ case 22 /* EXPLODE */:
675
+ case 23 /* EXPLODE_COMPOUND */:
676
+ case 24 /* EXPLODE_PENETRATING */:
677
+ case 25 /* REROLL */:
678
+ case 26 /* REROLL_ONCE */:
679
+ case 10 /* GREATER */:
680
+ case 11 /* GREATER_EQUAL */:
681
+ case 12 /* LESS */:
682
+ case 13 /* LESS_EQUAL */:
683
+ case 14 /* EQUAL */:
684
+ return BP.MODIFIER;
685
+ case 16 /* RPAREN */:
686
+ case 30 /* EOF */:
687
+ case 17 /* COMMA */:
688
+ case 28 /* FUNCTION */:
689
+ return -1;
690
+ default:
691
+ return 0;
692
+ }
693
+ }
694
+ getRightBp(token) {
695
+ switch (token.type) {
696
+ case 29 /* VS */:
697
+ return BP.VS_RIGHT;
698
+ case 4 /* PLUS */:
699
+ case 5 /* MINUS */:
700
+ return BP.ADD_RIGHT;
701
+ case 6 /* MULTIPLY */:
702
+ case 7 /* DIVIDE */:
703
+ case 8 /* MODULO */:
704
+ return BP.MUL_RIGHT;
705
+ case 9 /* POWER */:
706
+ return BP.POW_RIGHT;
707
+ case 1 /* DICE */:
708
+ return BP.DICE_RIGHT;
709
+ default:
710
+ return 0;
711
+ }
712
+ }
713
+ peek() {
714
+ return this.tokens[this.pos] ?? { type: 30 /* EOF */, value: "", position: this.pos };
715
+ }
716
+ advance() {
717
+ const token = this.peek();
718
+ this.pos++;
719
+ return token;
720
+ }
721
+ expect(type) {
722
+ const token = this.peek();
723
+ if (token.type !== type) {
724
+ const expected = TokenType[type];
725
+ throw new ParseError(`Expected ${expected} but got '${token.value}'`, "EXPECTED_TOKEN", token.position, token);
726
+ }
727
+ return this.advance();
728
+ }
729
+ hasTokens() {
730
+ return this.peek().type !== 30 /* EOF */;
731
+ }
732
+ }
733
+ function parse(notation) {
734
+ const tokens = lex(notation);
735
+ const parser = new Parser(tokens);
736
+ return parser.parse();
737
+ }
738
+ // src/rng/seeded.ts
739
+ class SeededRNG {
740
+ s0;
741
+ s1;
742
+ s2;
743
+ s3;
744
+ constructor(seed) {
745
+ this.s0 = 0;
746
+ this.s1 = 0;
747
+ this.s2 = 0;
748
+ this.s3 = 0;
749
+ this.initState(seed);
750
+ for (let i = 0;i < 20; i++) {
751
+ this.nextUint32();
752
+ }
753
+ }
754
+ initState(seed) {
755
+ const numSeed = seed == null ? (Date.now() ^ Math.random() * 4294967295) >>> 0 : typeof seed === "string" ? this.hashString(seed) : seed >>> 0;
756
+ let s = numSeed;
757
+ const state = [];
758
+ for (let i = 0;i < 4; i++) {
759
+ s = s + 2654435769 >>> 0;
760
+ let z = s;
761
+ z = Math.imul(z ^ z >>> 16, 2246822507) >>> 0;
762
+ z = Math.imul(z ^ z >>> 13, 3266489909) >>> 0;
763
+ state.push((z ^ z >>> 16) >>> 0);
764
+ }
765
+ this.s0 = state[0] ?? 0;
766
+ this.s1 = state[1] ?? 0;
767
+ this.s2 = state[2] ?? 0;
768
+ this.s3 = state[3] ?? 0;
769
+ if (this.s0 === 0 && this.s1 === 0 && this.s2 === 0 && this.s3 === 0) {
770
+ this.s0 = 1;
771
+ }
772
+ }
773
+ hashString(str) {
774
+ let hash = 5381;
775
+ for (let i = 0;i < str.length; i++) {
776
+ hash = (hash << 5) + hash + str.charCodeAt(i) >>> 0;
777
+ }
778
+ return hash;
779
+ }
780
+ nextUint32() {
781
+ let t = this.s3;
782
+ const s = this.s0;
783
+ this.s3 = this.s2;
784
+ this.s2 = this.s1;
785
+ this.s1 = s;
786
+ t ^= t << 11;
787
+ t ^= t >>> 8;
788
+ this.s0 = (t ^ s ^ s >>> 19) >>> 0;
789
+ return this.s0;
790
+ }
791
+ next() {
792
+ return this.nextUint32() / 4294967296;
793
+ }
794
+ nextInt(min, max) {
795
+ const lo = min > max ? max : min;
796
+ const hi = min > max ? min : max;
797
+ const range = hi - lo + 1;
798
+ if (range <= 1) {
799
+ return lo;
800
+ }
801
+ const threshold = (4294967296 - range) % range;
802
+ let value;
803
+ do {
804
+ value = this.nextUint32();
805
+ } while (value < threshold);
806
+ return lo + value % range;
807
+ }
808
+ }
809
+ // src/types.ts
810
+ var DegreeOfSuccess;
811
+ ((DegreeOfSuccess2) => {
812
+ DegreeOfSuccess2[DegreeOfSuccess2["CriticalFailure"] = 0] = "CriticalFailure";
813
+ DegreeOfSuccess2[DegreeOfSuccess2["Failure"] = 1] = "Failure";
814
+ DegreeOfSuccess2[DegreeOfSuccess2["Success"] = 2] = "Success";
815
+ DegreeOfSuccess2[DegreeOfSuccess2["CriticalSuccess"] = 3] = "CriticalSuccess";
816
+ })(DegreeOfSuccess ||= {});
817
+
818
+ // src/evaluator/modifiers/explode.ts
819
+ var DEFAULT_MAX_EXPLODE_ITERATIONS = 1000;
820
+ function buildShouldExplode(operator, thresholdValue) {
821
+ if (operator == null || thresholdValue == null) {
822
+ return (result, sides) => result === sides;
823
+ }
824
+ switch (operator) {
825
+ case ">":
826
+ return (result) => result > thresholdValue;
827
+ case ">=":
828
+ return (result) => result >= thresholdValue;
829
+ case "<":
830
+ return (result) => result < thresholdValue;
831
+ case "<=":
832
+ return (result) => result <= thresholdValue;
833
+ case "=":
834
+ return (result) => result === thresholdValue;
835
+ }
836
+ }
837
+ function rollExplosion(sides, rng, env) {
838
+ if (env.totalDiceRolled + 1 > env.maxDice) {
839
+ throw new EvaluatorError(`Total dice count ${env.totalDiceRolled + 1} exceeds limit of ${env.maxDice}`, "DICE_LIMIT_EXCEEDED", "Explode");
840
+ }
841
+ env.totalDiceRolled += 1;
842
+ return rng.nextInt(1, sides);
843
+ }
844
+ function explodeLimitError(maxIterations) {
845
+ return new EvaluatorError(`Explode iteration limit of ${maxIterations} exceeded`, "EXPLODE_LIMIT_EXCEEDED", "Explode");
846
+ }
847
+ function canExplode(die) {
848
+ if (die.modifiers.includes("dropped"))
849
+ return false;
850
+ if (die.sides < 1)
851
+ return false;
852
+ return true;
853
+ }
854
+ function applyStandardExplode(pool, shouldExplode, rng, env) {
855
+ const result = [];
856
+ for (const original of pool) {
857
+ result.push(original);
858
+ if (!canExplode(original))
859
+ continue;
860
+ const sides = original.sides;
861
+ let last = original.result;
862
+ let iterations = 0;
863
+ while (shouldExplode(last, sides)) {
864
+ if (iterations >= env.maxExplodeIterations) {
865
+ throw explodeLimitError(env.maxExplodeIterations);
866
+ }
867
+ const next = rollExplosion(sides, rng, env);
868
+ result.push({
869
+ sides,
870
+ result: next,
871
+ modifiers: ["exploded", "kept"],
872
+ critical: next === sides && sides > 1,
873
+ fumble: next === 1
874
+ });
875
+ last = next;
876
+ iterations += 1;
877
+ }
878
+ }
879
+ return result;
880
+ }
881
+ function applyCompoundExplode(pool, shouldExplode, rng, env) {
882
+ return pool.map((original) => {
883
+ if (!canExplode(original))
884
+ return original;
885
+ const sides = original.sides;
886
+ let accumulated = original.result;
887
+ let last = original.result;
888
+ let iterations = 0;
889
+ let exploded = false;
890
+ while (shouldExplode(last, sides)) {
891
+ if (iterations >= env.maxExplodeIterations) {
892
+ throw explodeLimitError(env.maxExplodeIterations);
893
+ }
894
+ const next = rollExplosion(sides, rng, env);
895
+ accumulated += next;
896
+ last = next;
897
+ exploded = true;
898
+ iterations += 1;
899
+ }
900
+ if (!exploded)
901
+ return original;
902
+ return {
903
+ sides,
904
+ result: accumulated,
905
+ modifiers: original.modifiers.includes("exploded") ? original.modifiers : [...original.modifiers, "exploded"],
906
+ critical: original.critical,
907
+ fumble: original.fumble
908
+ };
909
+ });
910
+ }
911
+ function applyPenetratingExplode(pool, shouldExplode, rng, env) {
912
+ const result = [];
913
+ for (const original of pool) {
914
+ result.push(original);
915
+ if (!canExplode(original))
916
+ continue;
917
+ const sides = original.sides;
918
+ let lastRaw = original.result;
919
+ let iterations = 0;
920
+ while (shouldExplode(lastRaw, sides)) {
921
+ if (iterations >= env.maxExplodeIterations) {
922
+ throw explodeLimitError(env.maxExplodeIterations);
923
+ }
924
+ const raw = rollExplosion(sides, rng, env);
925
+ const stored = raw - 1;
926
+ result.push({
927
+ sides,
928
+ result: stored,
929
+ modifiers: ["exploded", "kept"],
930
+ critical: raw === sides && sides > 1,
931
+ fumble: raw === 1
932
+ });
933
+ lastRaw = raw;
934
+ iterations += 1;
935
+ }
936
+ }
937
+ return result;
938
+ }
939
+
940
+ // src/evaluator/modifiers/keep-drop.ts
941
+ function markAllKept(dice) {
942
+ return dice.map((die) => {
943
+ if (die.modifiers.includes("dropped"))
944
+ return die;
945
+ return {
946
+ ...die,
947
+ modifiers: die.modifiers.includes("kept") ? die.modifiers : [...die.modifiers, "kept"]
948
+ };
949
+ });
950
+ }
951
+ function eligibleIndexed(dice) {
952
+ return dice.map((die, index) => ({ die, index })).filter(({ die }) => !die.modifiers.includes("dropped"));
953
+ }
954
+ function applySelection(dice, selectionIndices, selectionMarker) {
955
+ const otherMarker = selectionMarker === "kept" ? "dropped" : "kept";
956
+ return dice.map((die, index) => {
957
+ if (die.modifiers.includes("dropped"))
958
+ return die;
959
+ const isSelected = selectionIndices.has(index);
960
+ const marker = isSelected ? selectionMarker : otherMarker;
961
+ return {
962
+ ...die,
963
+ modifiers: [...die.modifiers.filter((m) => m !== "kept" && m !== "dropped"), marker]
964
+ };
965
+ });
966
+ }
967
+ function markAllDropped(dice) {
968
+ return dice.map((die) => {
969
+ if (die.modifiers.includes("dropped"))
970
+ return die;
971
+ return {
972
+ ...die,
973
+ modifiers: [...die.modifiers.filter((m) => m !== "kept"), "dropped"]
974
+ };
975
+ });
976
+ }
977
+ function applyKeepHighest(dice, count) {
978
+ const eligible = eligibleIndexed(dice);
979
+ if (count >= eligible.length)
980
+ return markAllKept(dice);
981
+ if (count <= 0)
982
+ return markAllDropped(dice);
983
+ const sorted = [...eligible].sort((a, b) => b.die.result - a.die.result);
984
+ const keptIndices = new Set(sorted.slice(0, count).map((item) => item.index));
985
+ return applySelection(dice, keptIndices, "kept");
986
+ }
987
+ function applyKeepLowest(dice, count) {
988
+ const eligible = eligibleIndexed(dice);
989
+ if (count >= eligible.length)
990
+ return markAllKept(dice);
991
+ if (count <= 0)
992
+ return markAllDropped(dice);
993
+ const sorted = [...eligible].sort((a, b) => a.die.result - b.die.result);
994
+ const keptIndices = new Set(sorted.slice(0, count).map((item) => item.index));
995
+ return applySelection(dice, keptIndices, "kept");
996
+ }
997
+ function applyDropHighest(dice, count) {
998
+ const eligible = eligibleIndexed(dice);
999
+ if (count <= 0)
1000
+ return markAllKept(dice);
1001
+ if (count >= eligible.length)
1002
+ return markAllDropped(dice);
1003
+ const sorted = [...eligible].sort((a, b) => b.die.result - a.die.result);
1004
+ const droppedIndices = new Set(sorted.slice(0, count).map((item) => item.index));
1005
+ return applySelection(dice, droppedIndices, "dropped");
1006
+ }
1007
+ function applyDropLowest(dice, count) {
1008
+ const eligible = eligibleIndexed(dice);
1009
+ if (count <= 0)
1010
+ return markAllKept(dice);
1011
+ if (count >= eligible.length)
1012
+ return markAllDropped(dice);
1013
+ const sorted = [...eligible].sort((a, b) => a.die.result - b.die.result);
1014
+ const droppedIndices = new Set(sorted.slice(0, count).map((item) => item.index));
1015
+ return applySelection(dice, droppedIndices, "dropped");
1016
+ }
1017
+ function sumKeptDice(dice) {
1018
+ return dice.filter((die) => !die.modifiers.includes("dropped")).reduce((sum, die) => sum + die.result, 0);
1019
+ }
1020
+
1021
+ // src/evaluator/modifiers/compare.ts
1022
+ function matchesCondition(result, operator, value) {
1023
+ switch (operator) {
1024
+ case ">":
1025
+ return result > value;
1026
+ case ">=":
1027
+ return result >= value;
1028
+ case "<":
1029
+ return result < value;
1030
+ case "<=":
1031
+ return result <= value;
1032
+ case "=":
1033
+ return result === value;
1034
+ }
1035
+ }
1036
+
1037
+ // src/evaluator/modifiers/reroll.ts
1038
+ var DEFAULT_MAX_REROLL_ITERATIONS = 1000;
1039
+ function rollReplacement(sides, rng, env) {
1040
+ if (env.totalDiceRolled + 1 > env.maxDice) {
1041
+ throw new EvaluatorError(`Total dice count ${env.totalDiceRolled + 1} exceeds limit of ${env.maxDice}`, "DICE_LIMIT_EXCEEDED", "Reroll");
1042
+ }
1043
+ env.totalDiceRolled += 1;
1044
+ if (sides === 0) {
1045
+ const result2 = rng.nextInt(-1, 1);
1046
+ return {
1047
+ sides: 0,
1048
+ result: result2,
1049
+ modifiers: [],
1050
+ critical: false,
1051
+ fumble: false
1052
+ };
1053
+ }
1054
+ const result = rng.nextInt(1, sides);
1055
+ return {
1056
+ sides,
1057
+ result,
1058
+ modifiers: [],
1059
+ critical: result === sides && sides > 1,
1060
+ fumble: result === 1
1061
+ };
1062
+ }
1063
+ function rerollLimitError(maxIterations) {
1064
+ return new EvaluatorError(`Reroll iteration limit of ${maxIterations} exceeded`, "REROLL_LIMIT_EXCEEDED", "Reroll");
1065
+ }
1066
+ function canReroll(die) {
1067
+ return !die.modifiers.includes("dropped");
1068
+ }
1069
+ function stripSlotFlags(modifiers) {
1070
+ return modifiers.filter((m) => m !== "kept" && m !== "dropped" && m !== "rerolled");
1071
+ }
1072
+ function applyRecursiveReroll(pool, operator, value, rng, env) {
1073
+ const result = [];
1074
+ for (const original of pool) {
1075
+ if (!canReroll(original)) {
1076
+ result.push(original);
1077
+ continue;
1078
+ }
1079
+ let current = original;
1080
+ let iterations = 0;
1081
+ while (matchesCondition(current.result, operator, value)) {
1082
+ if (iterations >= env.maxRerollIterations) {
1083
+ throw rerollLimitError(env.maxRerollIterations);
1084
+ }
1085
+ result.push({
1086
+ ...current,
1087
+ modifiers: [...stripSlotFlags(current.modifiers), "rerolled", "dropped"]
1088
+ });
1089
+ current = rollReplacement(current.sides, rng, env);
1090
+ iterations += 1;
1091
+ }
1092
+ result.push({
1093
+ ...current,
1094
+ modifiers: [...stripSlotFlags(current.modifiers), "kept"]
1095
+ });
1096
+ }
1097
+ return result;
1098
+ }
1099
+ function applyRerollOnce(pool, operator, value, rng, env) {
1100
+ const result = [];
1101
+ for (const original of pool) {
1102
+ if (!canReroll(original)) {
1103
+ result.push(original);
1104
+ continue;
1105
+ }
1106
+ if (!matchesCondition(original.result, operator, value)) {
1107
+ result.push({
1108
+ ...original,
1109
+ modifiers: [...stripSlotFlags(original.modifiers), "kept"]
1110
+ });
1111
+ continue;
1112
+ }
1113
+ result.push({
1114
+ ...original,
1115
+ modifiers: [...stripSlotFlags(original.modifiers), "rerolled", "dropped"]
1116
+ });
1117
+ const replacement = rollReplacement(original.sides, rng, env);
1118
+ result.push({
1119
+ ...replacement,
1120
+ modifiers: [...stripSlotFlags(replacement.modifiers), "kept"]
1121
+ });
1122
+ }
1123
+ return result;
1124
+ }
1125
+
1126
+ // src/evaluator/modifiers/success-count.ts
1127
+ function countSuccesses(dice, threshold, failThreshold) {
1128
+ let successes = 0;
1129
+ let failures = 0;
1130
+ for (const die of dice) {
1131
+ if (die.modifiers.includes("dropped"))
1132
+ continue;
1133
+ if (matchesCondition(die.result, threshold.operator, threshold.value)) {
1134
+ die.modifiers.push("success");
1135
+ successes += 1;
1136
+ continue;
1137
+ }
1138
+ if (failThreshold != null && matchesCondition(die.result, failThreshold.operator, failThreshold.value)) {
1139
+ die.modifiers.push("failure");
1140
+ failures += 1;
1141
+ }
1142
+ }
1143
+ return { total: successes - failures, successes, failures };
1144
+ }
1145
+
1146
+ // src/evaluator/evaluator.ts
1147
+ class EvaluatorError extends RollParserError {
1148
+ nodeType;
1149
+ constructor(message, code, nodeType) {
1150
+ super(message, code);
1151
+ this.name = "EvaluatorError";
1152
+ this.nodeType = nodeType ?? undefined;
1153
+ }
1154
+ }
1155
+ var DEFAULT_MAX_DICE = 1e4;
1156
+ function createDieResult(sides, result) {
1157
+ return {
1158
+ sides,
1159
+ result,
1160
+ modifiers: [],
1161
+ critical: result === sides && sides > 1,
1162
+ fumble: result === 1
1163
+ };
1164
+ }
1165
+ function createFateDieResult(result) {
1166
+ return {
1167
+ sides: 0,
1168
+ result,
1169
+ modifiers: [],
1170
+ critical: false,
1171
+ fumble: false
1172
+ };
1173
+ }
1174
+ function renderDice(dice) {
1175
+ const parts = dice.map((die) => {
1176
+ if (die.modifiers.includes("dropped")) {
1177
+ return `~~${die.result}~~`;
1178
+ }
1179
+ if (die.modifiers.includes("success")) {
1180
+ return `**${die.result}**`;
1181
+ }
1182
+ if (die.modifiers.includes("failure")) {
1183
+ return `__${die.result}__`;
1184
+ }
1185
+ return String(die.result);
1186
+ });
1187
+ return `[${parts.join(", ")}]`;
1188
+ }
1189
+ function evalNode(node, rng, ctx, env) {
1190
+ switch (node.type) {
1191
+ case "Literal":
1192
+ return evalLiteral(node.value, ctx);
1193
+ case "Dice":
1194
+ return evalDice(node, rng, ctx, env);
1195
+ case "FateDice":
1196
+ return evalFateDice(node, rng, ctx, env);
1197
+ case "BinaryOp":
1198
+ return evalBinaryOp(node, rng, ctx, env);
1199
+ case "UnaryOp":
1200
+ return evalUnaryOp(node, rng, ctx, env);
1201
+ case "Modifier":
1202
+ return evalModifier(node, rng, ctx, env);
1203
+ case "Explode":
1204
+ return evalExplode(node, rng, ctx, env);
1205
+ case "Reroll":
1206
+ return evalReroll(node, rng, ctx, env);
1207
+ case "SuccessCount":
1208
+ return evalSuccessCount(node, rng, ctx, env);
1209
+ case "Versus":
1210
+ return evalVersus(node, rng, ctx, env);
1211
+ case "FunctionCall":
1212
+ return evalFunctionCall(node, rng, ctx, env);
1213
+ default: {
1214
+ const exhaustive = node;
1215
+ throw new EvaluatorError(`Unknown node type: ${exhaustive.type}`, "UNKNOWN_NODE_TYPE", exhaustive.type);
1216
+ }
1217
+ }
1218
+ }
1219
+ function evalLiteral(value, ctx) {
1220
+ ctx.expressionParts.push(String(value));
1221
+ ctx.renderedParts.push(String(value));
1222
+ return value;
1223
+ }
1224
+ function evalDice(node, rng, ctx, env) {
1225
+ const count = evalNode(node.count, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1226
+ const sides = evalNode(node.sides, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1227
+ if (!Number.isInteger(count) || count < 0) {
1228
+ throw new EvaluatorError(`Invalid dice count: ${count}`, "INVALID_DICE_COUNT", "Dice");
1229
+ }
1230
+ if (!Number.isInteger(sides) || sides < 1) {
1231
+ throw new EvaluatorError(`Invalid dice sides: ${sides}`, "INVALID_DICE_SIDES", "Dice");
1232
+ }
1233
+ if (env.totalDiceRolled + count > env.maxDice) {
1234
+ throw new EvaluatorError(`Total dice count ${env.totalDiceRolled + count} exceeds limit of ${env.maxDice}`, "DICE_LIMIT_EXCEEDED", "Dice");
1235
+ }
1236
+ env.totalDiceRolled += count;
1237
+ const dice = [];
1238
+ for (let i = 0;i < count; i++) {
1239
+ const result = rng.nextInt(1, sides);
1240
+ dice.push(createDieResult(sides, result));
1241
+ }
1242
+ const markedDice = markAllKept(dice);
1243
+ ctx.rolls.push(...markedDice);
1244
+ const total = sumKeptDice(markedDice);
1245
+ const notation = `${count}d${sides}`;
1246
+ ctx.expressionParts.push(notation);
1247
+ ctx.renderedParts.push(`${notation}${renderDice(markedDice)}`);
1248
+ return total;
1249
+ }
1250
+ function evalFateDice(node, rng, ctx, env) {
1251
+ const count = evalNode(node.count, rng, { rolls: [], expressionParts: [], renderedParts: [] }, env);
1252
+ if (!Number.isInteger(count) || count < 0) {
1253
+ throw new EvaluatorError(`Invalid dice count: ${count}`, "INVALID_DICE_COUNT", "FateDice");
1254
+ }
1255
+ if (env.totalDiceRolled + count > env.maxDice) {
1256
+ throw new EvaluatorError(`Total dice count ${env.totalDiceRolled + count} exceeds limit of ${env.maxDice}`, "DICE_LIMIT_EXCEEDED", "FateDice");
1257
+ }
1258
+ env.totalDiceRolled += count;
1259
+ const dice = [];
1260
+ for (let i = 0;i < count; i++) {
1261
+ const result = rng.nextInt(-1, 1);
1262
+ dice.push(createFateDieResult(result));
1263
+ }
1264
+ const markedDice = markAllKept(dice);
1265
+ ctx.rolls.push(...markedDice);
1266
+ const total = sumKeptDice(markedDice);
1267
+ const notation = `${count}dF`;
1268
+ ctx.expressionParts.push(notation);
1269
+ ctx.renderedParts.push(`${notation}${renderDice(markedDice)}`);
1270
+ return total;
1271
+ }
1272
+ function evalBinaryOp(node, rng, ctx, env) {
1273
+ const leftCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1274
+ const rightCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1275
+ const left = evalNode(node.left, rng, leftCtx, env);
1276
+ const right = evalNode(node.right, rng, rightCtx, env);
1277
+ ctx.rolls.push(...leftCtx.rolls, ...rightCtx.rolls);
1278
+ const leftExpr = leftCtx.expressionParts.join("");
1279
+ const rightExpr = rightCtx.expressionParts.join("");
1280
+ const leftRendered = leftCtx.renderedParts.join("");
1281
+ const rightRendered = rightCtx.renderedParts.join("");
1282
+ ctx.expressionParts.push(`${leftExpr} ${node.operator} ${rightExpr}`);
1283
+ ctx.renderedParts.push(`${leftRendered} ${node.operator} ${rightRendered}`);
1284
+ switch (node.operator) {
1285
+ case "+":
1286
+ return left + right;
1287
+ case "-":
1288
+ return left - right;
1289
+ case "*":
1290
+ return left * right;
1291
+ case "/":
1292
+ if (right === 0) {
1293
+ throw new EvaluatorError("Division by zero", "DIVISION_BY_ZERO", "BinaryOp");
1294
+ }
1295
+ return left / right;
1296
+ case "%":
1297
+ if (right === 0) {
1298
+ throw new EvaluatorError("Modulo by zero", "MODULO_BY_ZERO", "BinaryOp");
1299
+ }
1300
+ return left % right;
1301
+ case "**":
1302
+ return left ** right;
1303
+ default: {
1304
+ const exhaustive = node.operator;
1305
+ throw new EvaluatorError(`Unknown operator: ${exhaustive}`, "UNKNOWN_OPERATOR", "BinaryOp");
1306
+ }
1307
+ }
1308
+ }
1309
+ function evalUnaryOp(node, rng, ctx, env) {
1310
+ const innerCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1311
+ const value = evalNode(node.operand, rng, innerCtx, env);
1312
+ ctx.rolls.push(...innerCtx.rolls);
1313
+ const innerExpr = innerCtx.expressionParts.join("");
1314
+ const innerRendered = innerCtx.renderedParts.join("");
1315
+ ctx.expressionParts.push(`-${innerExpr}`);
1316
+ ctx.renderedParts.push(`-${innerRendered}`);
1317
+ return -value;
1318
+ }
1319
+ function evalFunctionCall(node, rng, ctx, env) {
1320
+ const argCtxs = [];
1321
+ const values = [];
1322
+ for (const arg of node.args) {
1323
+ const argCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1324
+ values.push(evalNode(arg, rng, argCtx, env));
1325
+ argCtxs.push(argCtx);
1326
+ }
1327
+ for (const argCtx of argCtxs) {
1328
+ ctx.rolls.push(...argCtx.rolls);
1329
+ }
1330
+ const argExprs = argCtxs.map((c) => c.expressionParts.join(""));
1331
+ const argRendereds = argCtxs.map((c) => c.renderedParts.join(""));
1332
+ ctx.expressionParts.push(`${node.name}(${argExprs.join(", ")})`);
1333
+ ctx.renderedParts.push(`${node.name}(${argRendereds.join(", ")})`);
1334
+ return applyFunction(node.name, values);
1335
+ }
1336
+ function applyFunction(name, values) {
1337
+ switch (name) {
1338
+ case "floor":
1339
+ return Math.floor(requireUnaryArg(name, values));
1340
+ case "ceil":
1341
+ return Math.ceil(requireUnaryArg(name, values));
1342
+ case "round":
1343
+ return Math.round(requireUnaryArg(name, values));
1344
+ case "abs":
1345
+ return Math.abs(requireUnaryArg(name, values));
1346
+ case "max":
1347
+ return Math.max(...values);
1348
+ case "min":
1349
+ return Math.min(...values);
1350
+ default:
1351
+ throw new EvaluatorError(`Unknown function: ${name}`, "UNKNOWN_FUNCTION", "FunctionCall");
1352
+ }
1353
+ }
1354
+ function requireUnaryArg(name, values) {
1355
+ const [x] = values;
1356
+ if (x === undefined) {
1357
+ throw new EvaluatorError(`Function '${name}' requires an argument`, "UNKNOWN_FUNCTION", "FunctionCall");
1358
+ }
1359
+ return x;
1360
+ }
1361
+ function flattenModifierChain(node, rng, env) {
1362
+ const specs = [];
1363
+ let current = node;
1364
+ while (isModifier(current)) {
1365
+ const countCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1366
+ const modCount = evalNode(current.count, rng, countCtx, env);
1367
+ if (!Number.isInteger(modCount) || modCount < 0) {
1368
+ throw new EvaluatorError(`Invalid modifier count: ${modCount}`, "INVALID_MODIFIER_COUNT", "Modifier");
1369
+ }
1370
+ const code = current.modifier === "keep" ? current.selector === "highest" ? "kh" : "kl" : current.selector === "highest" ? "dh" : "dl";
1371
+ specs.push({ modifier: current.modifier, selector: current.selector, count: modCount, code });
1372
+ current = current.target;
1373
+ }
1374
+ specs.reverse();
1375
+ return { specs, baseTarget: current };
1376
+ }
1377
+ function applyModifierSpec(dice, spec) {
1378
+ if (spec.modifier === "keep") {
1379
+ return spec.selector === "highest" ? applyKeepHighest(dice, spec.count) : applyKeepLowest(dice, spec.count);
1380
+ }
1381
+ return spec.selector === "highest" ? applyDropHighest(dice, spec.count) : applyDropLowest(dice, spec.count);
1382
+ }
1383
+ function mergeDropSets(baseDice, specs) {
1384
+ const droppedIndices = new Set;
1385
+ for (const spec of specs) {
1386
+ const result = applyModifierSpec(baseDice, spec);
1387
+ for (let i = 0;i < result.length; i++) {
1388
+ if (result[i]?.modifiers.includes("dropped")) {
1389
+ droppedIndices.add(i);
1390
+ }
1391
+ }
1392
+ }
1393
+ return baseDice.map((die, index) => ({
1394
+ ...die,
1395
+ modifiers: droppedIndices.has(index) ? [...die.modifiers.filter((m) => m !== "kept" && m !== "dropped"), "dropped"] : [...die.modifiers.filter((m) => m !== "dropped" && m !== "kept"), "kept"]
1396
+ }));
1397
+ }
1398
+ function formatExplodeCode(variant, threshold, thresholdValue) {
1399
+ const marker = variant === "standard" ? "!" : variant === "compound" ? "!!" : "!p";
1400
+ if (threshold == null || thresholdValue == null)
1401
+ return marker;
1402
+ return `${marker}${threshold.operator}${thresholdValue}`;
1403
+ }
1404
+ function evalExplode(node, rng, ctx, env) {
1405
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1406
+ const targetValue = evalNode(node.target, rng, targetCtx, env);
1407
+ const targetExpr = targetCtx.expressionParts.join("");
1408
+ let thresholdValue;
1409
+ if (node.threshold != null) {
1410
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1411
+ thresholdValue = evalNode(node.threshold.value, rng, thresholdCtx, env);
1412
+ }
1413
+ const code = formatExplodeCode(node.variant, node.threshold, thresholdValue);
1414
+ if (targetCtx.rolls.length === 0) {
1415
+ ctx.expressionParts.push(`${targetExpr}${code}`);
1416
+ ctx.renderedParts.push(`${targetExpr}${code}`);
1417
+ return targetValue;
1418
+ }
1419
+ const shouldExplode = buildShouldExplode(node.threshold?.operator, thresholdValue);
1420
+ 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);
1421
+ ctx.rolls.push(...expanded);
1422
+ ctx.expressionParts.push(`${targetExpr}${code}`);
1423
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(expanded)}`);
1424
+ return sumKeptDice(expanded);
1425
+ }
1426
+ function evalReroll(node, rng, ctx, env) {
1427
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1428
+ evalNode(node.target, rng, targetCtx, env);
1429
+ const targetExpr = targetCtx.expressionParts.join("");
1430
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1431
+ const thresholdValue = evalNode(node.condition.value, rng, thresholdCtx, env);
1432
+ const code = `${node.once ? "ro" : "r"}${node.condition.operator}${thresholdValue}`;
1433
+ if (targetCtx.rolls.length === 0) {
1434
+ ctx.expressionParts.push(`${targetExpr}${code}`);
1435
+ ctx.renderedParts.push(`${targetExpr}${code}`);
1436
+ return sumKeptDice(targetCtx.rolls);
1437
+ }
1438
+ const pool = node.once ? applyRerollOnce(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env) : applyRecursiveReroll(targetCtx.rolls, node.condition.operator, thresholdValue, rng, env);
1439
+ ctx.rolls.push(...pool);
1440
+ ctx.expressionParts.push(`${targetExpr}${code}`);
1441
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(pool)}`);
1442
+ return sumKeptDice(pool);
1443
+ }
1444
+ function evalModifier(node, rng, ctx, env) {
1445
+ const { specs, baseTarget } = flattenModifierChain(node, rng, env);
1446
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1447
+ evalNode(baseTarget, rng, targetCtx, env);
1448
+ const mergedDice = mergeDropSets(targetCtx.rolls, specs);
1449
+ ctx.rolls.push(...mergedDice);
1450
+ const total = sumKeptDice(mergedDice);
1451
+ const targetExpr = targetCtx.expressionParts.join("");
1452
+ const modifierCodes = specs.map((s) => `${s.code}${s.count}`).join("");
1453
+ ctx.expressionParts.push(`${targetExpr}${modifierCodes}`);
1454
+ ctx.renderedParts.push(`${targetExpr}${renderDice(mergedDice)}`);
1455
+ return total;
1456
+ }
1457
+ function resolveThreshold(value, rng, env, role) {
1458
+ const thresholdCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1459
+ const resolved = evalNode(value, rng, thresholdCtx, env);
1460
+ if (!Number.isFinite(resolved)) {
1461
+ throw new EvaluatorError(`Invalid ${role}: ${resolved}`, "INVALID_THRESHOLD", "SuccessCount");
1462
+ }
1463
+ return resolved;
1464
+ }
1465
+ function evalSuccessCount(node, rng, ctx, env) {
1466
+ const targetCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1467
+ const targetValue = evalNode(node.target, rng, targetCtx, env);
1468
+ const targetExpr = targetCtx.expressionParts.join("");
1469
+ const thresholdValue = resolveThreshold(node.threshold.value, rng, env, "threshold");
1470
+ const failValue = node.failThreshold != null ? resolveThreshold(node.failThreshold.value, rng, env, "fail threshold") : undefined;
1471
+ const code = `${node.threshold.operator}${thresholdValue}${failValue != null ? `f${failValue}` : ""}`;
1472
+ if (targetCtx.rolls.length === 0) {
1473
+ ctx.expressionParts.push(`${targetExpr}${code}`);
1474
+ ctx.renderedParts.push(`${targetExpr}${code}`);
1475
+ return targetValue;
1476
+ }
1477
+ const result = countSuccesses(targetCtx.rolls, { operator: node.threshold.operator, value: thresholdValue }, failValue != null && node.failThreshold != null ? { operator: node.failThreshold.operator, value: failValue } : undefined);
1478
+ env.hasSuccessCount = true;
1479
+ ctx.rolls.push(...targetCtx.rolls);
1480
+ ctx.expressionParts.push(`${targetExpr}${code}`);
1481
+ ctx.renderedParts.push(`${targetExpr}${code}${renderDice(targetCtx.rolls)}`);
1482
+ return result.total;
1483
+ }
1484
+ function extractNatural(rolls) {
1485
+ const keptD20s = rolls.filter((d) => d.sides === 20 && !d.modifiers.includes("dropped") && !d.modifiers.includes("rerolled"));
1486
+ return keptD20s.length === 1 ? keptD20s[0]?.result : undefined;
1487
+ }
1488
+ function calculateDegree(total, dc, natural) {
1489
+ let degree;
1490
+ if (total >= dc + 10)
1491
+ degree = 3 /* CriticalSuccess */;
1492
+ else if (total >= dc)
1493
+ degree = 2 /* Success */;
1494
+ else if (total > dc - 10)
1495
+ degree = 1 /* Failure */;
1496
+ else
1497
+ degree = 0 /* CriticalFailure */;
1498
+ if (natural === 20 && degree < 3 /* CriticalSuccess */)
1499
+ degree++;
1500
+ if (natural === 1 && degree > 0 /* CriticalFailure */)
1501
+ degree--;
1502
+ return degree;
1503
+ }
1504
+ function degreeLabel(degree) {
1505
+ switch (degree) {
1506
+ case 0 /* CriticalFailure */:
1507
+ return "Critical Failure";
1508
+ case 1 /* Failure */:
1509
+ return "Failure";
1510
+ case 2 /* Success */:
1511
+ return "Success";
1512
+ case 3 /* CriticalSuccess */:
1513
+ return "Critical Success";
1514
+ }
1515
+ }
1516
+ function evalVersus(node, rng, ctx, env) {
1517
+ if (env.insideVersus) {
1518
+ throw new EvaluatorError("Cannot nest versus operators", "NESTED_VERSUS", "Versus");
1519
+ }
1520
+ env.insideVersus = true;
1521
+ try {
1522
+ const rollCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1523
+ const rollTotal = evalNode(node.roll, rng, rollCtx, env);
1524
+ const natural = extractNatural(rollCtx.rolls);
1525
+ const dcCtx = { rolls: [], expressionParts: [], renderedParts: [] };
1526
+ const dcTotal = evalNode(node.dc, rng, dcCtx, env);
1527
+ const degree = calculateDegree(rollTotal, dcTotal, natural);
1528
+ ctx.rolls.push(...rollCtx.rolls, ...dcCtx.rolls);
1529
+ const rollExpr = rollCtx.expressionParts.join("");
1530
+ const dcExpr = dcCtx.expressionParts.join("");
1531
+ const rollRendered = rollCtx.renderedParts.join("");
1532
+ const dcRendered = dcCtx.renderedParts.join("");
1533
+ ctx.expressionParts.push(`${rollExpr} vs ${dcExpr}`);
1534
+ ctx.renderedParts.push(`${rollRendered} vs ${dcRendered}`);
1535
+ ctx.versusMetadata = { degree, natural, dcTotal };
1536
+ return rollTotal;
1537
+ } finally {
1538
+ env.insideVersus = false;
1539
+ }
1540
+ }
1541
+ function evaluate(ast, rng, options = {}) {
1542
+ const maxDice = options.maxDice != null && Number.isFinite(options.maxDice) && options.maxDice > 0 ? Math.floor(options.maxDice) : DEFAULT_MAX_DICE;
1543
+ const maxExplodeIterations = options.maxExplodeIterations != null && Number.isFinite(options.maxExplodeIterations) && options.maxExplodeIterations >= 0 ? Math.floor(options.maxExplodeIterations) : DEFAULT_MAX_EXPLODE_ITERATIONS;
1544
+ const maxRerollIterations = options.maxRerollIterations != null && Number.isFinite(options.maxRerollIterations) && options.maxRerollIterations >= 0 ? Math.floor(options.maxRerollIterations) : DEFAULT_MAX_REROLL_ITERATIONS;
1545
+ const env = {
1546
+ maxDice,
1547
+ maxExplodeIterations,
1548
+ maxRerollIterations,
1549
+ totalDiceRolled: 0,
1550
+ hasSuccessCount: false,
1551
+ insideVersus: false
1552
+ };
1553
+ const ctx = {
1554
+ rolls: [],
1555
+ expressionParts: [],
1556
+ renderedParts: []
1557
+ };
1558
+ const total = evalNode(ast, rng, ctx, env);
1559
+ const expression = ctx.expressionParts.join("");
1560
+ const trailing = ctx.versusMetadata ? degreeLabel(ctx.versusMetadata.degree) : String(total);
1561
+ const rendered = `${ctx.renderedParts.join("")} = ${trailing}`;
1562
+ const result = {
1563
+ total,
1564
+ notation: options.notation ?? expression,
1565
+ expression,
1566
+ rendered,
1567
+ rolls: ctx.rolls
1568
+ };
1569
+ if (env.hasSuccessCount) {
1570
+ let successes = 0;
1571
+ let failures = 0;
1572
+ for (const die of ctx.rolls) {
1573
+ if (die.modifiers.includes("success"))
1574
+ successes += 1;
1575
+ else if (die.modifiers.includes("failure"))
1576
+ failures += 1;
1577
+ }
1578
+ result.successes = successes;
1579
+ result.failures = failures;
1580
+ }
1581
+ if (ctx.versusMetadata) {
1582
+ result.degree = ctx.versusMetadata.degree;
1583
+ if (ctx.versusMetadata.natural !== undefined) {
1584
+ result.natural = ctx.versusMetadata.natural;
1585
+ }
1586
+ }
1587
+ return result;
1588
+ }
1589
+ // src/roll.ts
1590
+ function roll(notation, options = {}) {
1591
+ const rng = options.rng ?? new SeededRNG(options.seed);
1592
+ const tokens = lex(notation);
1593
+ const ast = new Parser(tokens).parse();
1594
+ const evalOptions = { notation };
1595
+ if (options.maxDice != null)
1596
+ evalOptions.maxDice = options.maxDice;
1597
+ if (options.maxExplodeIterations != null) {
1598
+ evalOptions.maxExplodeIterations = options.maxExplodeIterations;
1599
+ }
1600
+ if (options.maxRerollIterations != null) {
1601
+ evalOptions.maxRerollIterations = options.maxRerollIterations;
1602
+ }
1603
+ return evaluate(ast, rng, evalOptions);
1604
+ }
1605
+ // package.json
1606
+ var package_default = {
1607
+ name: "roll-parser",
1608
+ version: "3.0.0-alpha.0",
1609
+ description: "High-performance dice notation parser for tabletop RPGs. TypeScript-first, Bun-optimized.",
1610
+ type: "module",
1611
+ main: "./dist/index.js",
1612
+ module: "./dist/index.mjs",
1613
+ types: "./dist/index.d.ts",
1614
+ bin: { "roll-parser": "./dist/cli.js" },
1615
+ sideEffects: false,
1616
+ exports: {
1617
+ ".": {
1618
+ types: "./dist/index.d.ts",
1619
+ import: "./dist/index.mjs",
1620
+ require: "./dist/index.js"
1621
+ },
1622
+ "./testing": {
1623
+ types: "./dist/testing.d.ts",
1624
+ import: "./dist/testing.mjs",
1625
+ require: "./dist/testing.js"
1626
+ }
1627
+ },
1628
+ files: [
1629
+ "dist",
1630
+ "src",
1631
+ "!src/**/*.test.ts"
1632
+ ],
1633
+ keywords: [
1634
+ "parser",
1635
+ "dice",
1636
+ "roll",
1637
+ "rpg",
1638
+ "dnd",
1639
+ "d20",
1640
+ "pratt-parser",
1641
+ "typescript",
1642
+ "bun"
1643
+ ],
1644
+ homepage: "https://github.com/edloidas/roll-parser#readme",
1645
+ bugs: {
1646
+ url: "https://github.com/edloidas/roll-parser/issues"
1647
+ },
1648
+ license: "MIT",
1649
+ author: "Mikita Taukachou <edloidas@gmail.com> (https://edloidas.com)",
1650
+ repository: {
1651
+ type: "git",
1652
+ url: "git+https://github.com/edloidas/roll-parser.git"
1653
+ },
1654
+ publishConfig: {
1655
+ access: "public"
1656
+ },
1657
+ scripts: {
1658
+ typecheck: "tsc --noEmit",
1659
+ lint: "biome lint .",
1660
+ "lint:fix": "biome lint --write .",
1661
+ format: "biome format --write .",
1662
+ "format:check": "biome format .",
1663
+ check: "bun typecheck && bun lint && bun format:check",
1664
+ "check:fix": "bun typecheck && bun lint:fix && bun format",
1665
+ "check:version": "bun scripts/check-version.ts",
1666
+ "check:changelog": "bun scripts/check-changelog.ts",
1667
+ clean: "rm -rf dist coverage",
1668
+ build: "bun run clean && bun run build:esm && bun run build:cjs && bun run build:esm:testing && bun run build:cjs:testing && bun run build:cli && bun run build:types",
1669
+ "build:esm": "bun build src/index.ts --outfile dist/index.mjs --target bun",
1670
+ "build:cjs": "bun build src/index.ts --outfile dist/index.js --target node",
1671
+ "build:esm:testing": "bun build src/testing.ts --outfile dist/testing.mjs --target bun",
1672
+ "build:cjs:testing": "bun build src/testing.ts --outfile dist/testing.js --target node",
1673
+ "build:cli": "bun build src/cli/index.ts --outfile dist/cli.js --target node",
1674
+ "build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
1675
+ test: "bun test",
1676
+ "test:node": `node -e "import('./dist/index.mjs').then(m => { const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('ESM smoke failed: total=' + r.total); console.log('Node.js ESM smoke: ok'); })" && node -e "const m = require('./dist/index.js'); const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('CJS smoke failed: total=' + r.total); console.log('Node.js CJS smoke: ok');"`,
1677
+ "test:watch": "bun test --watch",
1678
+ "test:ci": "bun test --bail",
1679
+ validate: "bun run check && bun run build && bun test:ci",
1680
+ coverage: "bun test --coverage",
1681
+ "release:dry": "bun run check:version && bun run check:changelog && bun run check && bun run build && bun test",
1682
+ prepublishOnly: "bun run release:dry"
1683
+ },
1684
+ devDependencies: {
1685
+ "@biomejs/biome": "^2.4.9",
1686
+ "@types/bun": "^1.3.0",
1687
+ "fast-check": "^4.6.0",
1688
+ typescript: "^6.0.2"
1689
+ },
1690
+ engines: {
1691
+ node: ">= 22.0.0"
1692
+ }
1693
+ };
1694
+
1695
+ // src/index.ts
1696
+ var VERSION = package_default.version;
1697
+ export {
1698
+ roll,
1699
+ parse,
1700
+ isVersus,
1701
+ isUnaryOp,
1702
+ isSuccessCount,
1703
+ isRollParserError,
1704
+ isReroll,
1705
+ isModifier,
1706
+ isLiteral,
1707
+ isFunctionCall,
1708
+ isFateDice,
1709
+ isExplode,
1710
+ isDice,
1711
+ isBinaryOp,
1712
+ evaluate,
1713
+ VERSION,
1714
+ SeededRNG,
1715
+ RollParserError,
1716
+ ParseError,
1717
+ LexerError,
1718
+ EvaluatorError,
1719
+ DegreeOfSuccess,
1720
+ DEFAULT_MAX_REROLL_ITERATIONS,
1721
+ DEFAULT_MAX_EXPLODE_ITERATIONS,
1722
+ DEFAULT_MAX_DICE
1723
+ };