roll-parser 3.0.0-beta.0 → 3.1.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 (164) hide show
  1. package/CHANGELOG.md +142 -9
  2. package/MIGRATION.md +219 -0
  3. package/README.md +1026 -154
  4. package/dist/cli/args.d.ts +1 -0
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +81 -0
  7. package/dist/cli/args.js.map +1 -0
  8. package/dist/cli/format.d.ts +15 -3
  9. package/dist/cli/format.d.ts.map +1 -1
  10. package/dist/cli/format.js +18 -0
  11. package/dist/cli/format.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -1
  14. package/dist/cli/index.js +14 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/cli/main.d.ts +36 -0
  17. package/dist/cli/main.d.ts.map +1 -0
  18. package/dist/cli/main.js +83 -0
  19. package/dist/cli/main.js.map +1 -0
  20. package/dist/errors.d.ts +332 -17
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +141 -0
  23. package/dist/errors.js.map +1 -0
  24. package/dist/evaluator/die.d.ts +27 -0
  25. package/dist/evaluator/die.d.ts.map +1 -0
  26. package/dist/evaluator/die.js +19 -0
  27. package/dist/evaluator/die.js.map +1 -0
  28. package/dist/evaluator/env.d.ts +89 -0
  29. package/dist/evaluator/env.d.ts.map +1 -0
  30. package/dist/evaluator/env.js +11 -0
  31. package/dist/evaluator/env.js.map +1 -0
  32. package/dist/evaluator/evaluator.d.ts +41 -75
  33. package/dist/evaluator/evaluator.d.ts.map +1 -1
  34. package/dist/evaluator/evaluator.js +914 -0
  35. package/dist/evaluator/evaluator.js.map +1 -0
  36. package/dist/evaluator/modifiers/compare.js +15 -0
  37. package/dist/evaluator/modifiers/compare.js.map +1 -0
  38. package/dist/evaluator/modifiers/crit-threshold.d.ts +57 -5
  39. package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -1
  40. package/dist/evaluator/modifiers/crit-threshold.js +42 -0
  41. package/dist/evaluator/modifiers/crit-threshold.js.map +1 -0
  42. package/dist/evaluator/modifiers/die-bound.d.ts +29 -0
  43. package/dist/evaluator/modifiers/die-bound.d.ts.map +1 -0
  44. package/dist/evaluator/modifiers/die-bound.js +17 -0
  45. package/dist/evaluator/modifiers/die-bound.js.map +1 -0
  46. package/dist/evaluator/modifiers/explode.d.ts +13 -5
  47. package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
  48. package/dist/evaluator/modifiers/explode.js +105 -0
  49. package/dist/evaluator/modifiers/explode.js.map +1 -0
  50. package/dist/evaluator/modifiers/flags.d.ts +47 -0
  51. package/dist/evaluator/modifiers/flags.d.ts.map +1 -0
  52. package/dist/evaluator/modifiers/flags.js +25 -0
  53. package/dist/evaluator/modifiers/flags.js.map +1 -0
  54. package/dist/evaluator/modifiers/keep-drop.d.ts +15 -29
  55. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
  56. package/dist/evaluator/modifiers/keep-drop.js +82 -0
  57. package/dist/evaluator/modifiers/keep-drop.js.map +1 -0
  58. package/dist/evaluator/modifiers/reroll.d.ts +12 -4
  59. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
  60. package/dist/evaluator/modifiers/reroll.js +68 -0
  61. package/dist/evaluator/modifiers/reroll.js.map +1 -0
  62. package/dist/evaluator/modifiers/sort.d.ts +5 -1
  63. package/dist/evaluator/modifiers/sort.d.ts.map +1 -1
  64. package/dist/evaluator/modifiers/sort.js +13 -0
  65. package/dist/evaluator/modifiers/sort.js.map +1 -0
  66. package/dist/evaluator/modifiers/success-count.d.ts +6 -7
  67. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
  68. package/dist/evaluator/modifiers/success-count.js +25 -0
  69. package/dist/evaluator/modifiers/success-count.js.map +1 -0
  70. package/dist/index.d.ts +33 -11
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +12 -2560
  73. package/dist/index.js.map +1 -26
  74. package/dist/lexer/lexer.d.ts +50 -5
  75. package/dist/lexer/lexer.d.ts.map +1 -1
  76. package/dist/lexer/lexer.js +260 -0
  77. package/dist/lexer/lexer.js.map +1 -0
  78. package/dist/lexer/tokens.d.ts +31 -6
  79. package/dist/lexer/tokens.d.ts.map +1 -1
  80. package/dist/lexer/tokens.js +42 -0
  81. package/dist/lexer/tokens.js.map +1 -0
  82. package/dist/parser/ast.d.ts +324 -165
  83. package/dist/parser/ast.d.ts.map +1 -1
  84. package/dist/parser/ast.js +52 -0
  85. package/dist/parser/ast.js.map +1 -0
  86. package/dist/parser/guards.d.ts +106 -0
  87. package/dist/parser/guards.d.ts.map +1 -0
  88. package/dist/parser/guards.js +121 -0
  89. package/dist/parser/guards.js.map +1 -0
  90. package/dist/parser/parser.d.ts +118 -14
  91. package/dist/parser/parser.d.ts.map +1 -1
  92. package/dist/parser/parser.js +751 -0
  93. package/dist/parser/parser.js.map +1 -0
  94. package/dist/render.d.ts +95 -0
  95. package/dist/render.d.ts.map +1 -0
  96. package/dist/render.js +227 -0
  97. package/dist/render.js.map +1 -0
  98. package/dist/rng/mock.d.ts +73 -12
  99. package/dist/rng/mock.d.ts.map +1 -1
  100. package/dist/rng/mock.js +30 -0
  101. package/dist/rng/mock.js.map +1 -0
  102. package/dist/rng/seeded.d.ts +141 -9
  103. package/dist/rng/seeded.d.ts.map +1 -1
  104. package/dist/rng/seeded.js +138 -0
  105. package/dist/rng/seeded.js.map +1 -0
  106. package/dist/rng/types.d.ts +57 -0
  107. package/dist/rng/types.d.ts.map +1 -1
  108. package/dist/rng/types.js +2 -0
  109. package/dist/rng/types.js.map +1 -0
  110. package/dist/roll.d.ts +58 -28
  111. package/dist/roll.d.ts.map +1 -1
  112. package/dist/roll.js +8 -0
  113. package/dist/roll.js.map +1 -0
  114. package/dist/testing.d.ts +5 -4
  115. package/dist/testing.d.ts.map +1 -1
  116. package/dist/testing.js +2 -41
  117. package/dist/testing.js.map +1 -11
  118. package/dist/types.d.ts +349 -47
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/types.js +8 -0
  121. package/dist/types.js.map +1 -0
  122. package/dist/version.d.ts +2 -0
  123. package/dist/version.d.ts.map +1 -0
  124. package/dist/version.js +2 -0
  125. package/dist/version.js.map +1 -0
  126. package/package.json +93 -34
  127. package/src/cli/args.ts +66 -10
  128. package/src/cli/format.ts +37 -26
  129. package/src/cli/index.ts +27 -84
  130. package/src/cli/main.ts +129 -0
  131. package/src/errors.ts +480 -27
  132. package/src/evaluator/die.ts +51 -0
  133. package/src/evaluator/env.ts +105 -0
  134. package/src/evaluator/evaluator.ts +693 -434
  135. package/src/evaluator/modifiers/crit-threshold.ts +96 -14
  136. package/src/evaluator/modifiers/die-bound.ts +48 -0
  137. package/src/evaluator/modifiers/explode.ts +70 -62
  138. package/src/evaluator/modifiers/flags.ts +78 -0
  139. package/src/evaluator/modifiers/keep-drop.ts +129 -127
  140. package/src/evaluator/modifiers/reroll.ts +44 -56
  141. package/src/evaluator/modifiers/sort.ts +21 -2
  142. package/src/evaluator/modifiers/success-count.ts +24 -12
  143. package/src/index.ts +56 -35
  144. package/src/lexer/lexer.ts +107 -34
  145. package/src/lexer/tokens.ts +31 -6
  146. package/src/parser/ast.ts +333 -346
  147. package/src/parser/guards.ts +248 -0
  148. package/src/parser/parser.ts +419 -242
  149. package/src/render.ts +392 -0
  150. package/src/rng/mock.ts +74 -13
  151. package/src/rng/seeded.ts +299 -64
  152. package/src/rng/types.ts +57 -0
  153. package/src/roll.ts +64 -47
  154. package/src/testing.ts +5 -9
  155. package/src/types.ts +353 -46
  156. package/src/version.ts +2 -0
  157. package/dist/cli.js +0 -2608
  158. package/dist/cli.js.map +0 -28
  159. package/dist/evaluator/index.d.ts +0 -8
  160. package/dist/evaluator/index.d.ts.map +0 -1
  161. package/dist/rng/index.d.ts +0 -8
  162. package/dist/rng/index.d.ts.map +0 -1
  163. package/src/evaluator/index.ts +0 -14
  164. package/src/rng/index.ts +0 -8
package/src/types.ts CHANGED
@@ -7,52 +7,125 @@
7
7
  import type { ASTNode } from './parser/ast.js';
8
8
 
9
9
  /**
10
- * Comparison operator for compare points.
10
+ * Comparison operator for compare points. Spelled exactly as it appears in
11
+ * notation — `4d6!>=5` carries `'>='`, bare `f1` normalizes to `'='`.
12
+ *
13
+ * @category AST
11
14
  */
12
15
  export type CompareOp = '>' | '>=' | '<' | '<=' | '=';
13
16
 
14
17
  /**
15
- * A comparison threshold used by exploding dice, reroll, and success counting.
18
+ * A comparison threshold used by exploding dice, reroll, success counting, and
19
+ * crit-threshold overrides.
20
+ *
21
+ * The value is an {@link ASTNode} to support computed thresholds
22
+ * (`1d6!>(1d2+3)`), matching the pattern used by `DiceNode.count` and
23
+ * `DiceNode.sides`. The evaluated counterpart is {@link ResolvedComparePoint}.
16
24
  *
17
- * The value is an ASTNode to support computed thresholds (e.g., `>=ceil(5)`),
18
- * matching the pattern used by DiceNode.count and DiceNode.sides.
25
+ * @category AST
19
26
  */
20
27
  export type ComparePoint = {
21
- operator: CompareOp;
22
- value: ASTNode;
28
+ readonly operator: CompareOp;
29
+ readonly value: ASTNode;
23
30
  };
24
31
 
25
32
  /**
26
- * A ComparePoint whose value has been evaluated to a number. Used in
27
- * `RollPart` where meta-expressions are already resolved.
33
+ * A {@link ComparePoint} whose value has been evaluated to a number. Used
34
+ * throughout {@link RollPart}, where meta-expressions are already resolved
35
+ * `1d6!>(1d2+3)` surfaces as `{ operator: '>', value: 5 }`.
36
+ *
37
+ * @category Results
28
38
  */
29
39
  export type ResolvedComparePoint = {
30
- operator: CompareOp;
31
- value: number;
40
+ readonly operator: CompareOp;
41
+ readonly value: number;
32
42
  };
33
43
 
34
44
  /**
35
- * A resolved crit threshold — `'default'` means the per-die default rule
36
- * (`result === sides` for critical, `result === 1` for fumble).
45
+ * A resolved crit threshold — `'default'` means the per-die default rule,
46
+ * which is what bare `cs` / `cf` produce. It reads the natural face
47
+ * (`initialResult ?? result`): critical when it equals `sides`, fumble when
48
+ * it equals 1, both only for `sides > 1`. An explicit ComparePoint instead
49
+ * reads the die's current `result`, so a preceding modifier that rewrote it
50
+ * is visible to the comparison.
51
+ *
52
+ * @category Results
37
53
  */
38
54
  export type ResolvedCritThreshold = ResolvedComparePoint | 'default';
39
55
 
40
56
  /**
41
- * Modifier flags applied to individual die results.
57
+ * Tags attached to a {@link DieResult} by the evaluator. A die can carry more
58
+ * than one (an exploded die that was later dropped is `['dropped',
59
+ * 'exploded']`), and the set drives the markers in `RollResult.rendered`.
60
+ *
61
+ * | Tag | Meaning | Rendered as |
62
+ * |-----|---------|-------------|
63
+ * | `'kept'` | Counted toward the total. Every non-dropped die carries it. | plain |
64
+ * | `'dropped'` | Excluded from the total by `kh`/`kl`/`dh`/`dl` or group selection. | `~~n~~` |
65
+ * | `'exploded'` | Produced by, or the trigger of, an explosion (`!`, `!!`, `!p`). | plain |
66
+ * | `'rerolled'` | A discarded intermediate from `r` / `ro`, always paired with `'dropped'` — the replacement die carries no tag. | `~~n~~` (via `'dropped'`) |
67
+ * | `'min'` | Raised to a `minN` bound; `initialResult` keeps the raw face. | plain |
68
+ * | `'max'` | Lowered to a `maxN` bound; `initialResult` keeps the raw face. | plain |
69
+ * | `'success'` | Met a success-count threshold (`>=6`). | `**n**` |
70
+ * | `'failure'` | Met a failure threshold (`f1`). | `__n__` |
71
+ * | `'meta'` | Rolled by a meta-expression rather than by the visible pool. | not shown |
72
+ * | `'dc'` | The DC side of a `vs` comparison. Never part of the roll-side pool. | plain |
73
+ *
74
+ * `'meta'` is the one tag with no counterpart in the notation. Dice counts,
75
+ * sides, modifier counts and computed thresholds may themselves be dice
76
+ * (`(1d4)d6`, `4d6kh(1d2)`, `1d6!>(1d2+3)`). Those inner dice belong to no
77
+ * pool: they never appear on a `dice` or `fateDice` part, and they are
78
+ * appended to `RollResult.rolls` tagged `'meta'` so an audit log can still
79
+ * show what the meta-expression rolled.
80
+ *
81
+ * The four whole-pool views — `sort`, `explode`, `reroll`, and `successCount`
82
+ * `rolls` — are snapshots of an evaluation context rather than of a pool, so
83
+ * they do carry meta dice. Filter them out when summing or displaying a pool.
84
+ *
85
+ * `'dc'` marks the DC side of a `vs` comparison. Unlike `'meta'` these dice do
86
+ * render — `1d20[3] vs 2d10[5, 6]` shows both sides — but they are not part of
87
+ * the roll-side pool, so no modifier may sum, select, clamp, reroll, explode,
88
+ * or tally them. Filter them out when summing a pool, exactly as with `'meta'`.
89
+ *
90
+ * @category Results
42
91
  */
43
92
  export type DieModifier =
44
93
  | 'dropped'
45
94
  | 'kept'
46
95
  | 'exploded'
47
96
  | 'rerolled'
97
+ | 'min'
98
+ | 'max'
48
99
  | 'success'
49
100
  | 'failure'
50
- | 'meta';
101
+ | 'meta'
102
+ | 'dc';
51
103
 
52
104
  /**
53
105
  * PF2e Degree of Success. Produced by the `vs` operator when comparing a
54
106
  * roll against a Difficulty Class. Ordering is significant — natural 20
55
107
  * upgrades one step and natural 1 downgrades one step.
108
+ *
109
+ * Numeric by design: the enum members are plain numbers, so `--json` CLI
110
+ * output and `JSON.stringify` emit `0`–`3` and comparisons like
111
+ * `degree >= DegreeOfSuccess.Success` work.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * import { DegreeOfSuccess, roll } from 'roll-parser';
116
+ * import { createMockRng } from 'roll-parser/testing';
117
+ *
118
+ * const result = roll('1d20+7 vs 15', { rng: createMockRng([12]) });
119
+ * result.degree; // DegreeOfSuccess.Success (2)
120
+ * result.natural; // 12 — the raw d20 face, used for the ±1 step
121
+ * result.rendered; // '1d20[12] + 7 vs 15 = Success'
122
+ *
123
+ * if (result.degree != null && result.degree >= DegreeOfSuccess.Success) {
124
+ * // hit
125
+ * }
126
+ * ```
127
+ *
128
+ * @category Results
56
129
  */
57
130
  export enum DegreeOfSuccess {
58
131
  CriticalFailure = 0,
@@ -62,7 +135,25 @@ export enum DegreeOfSuccess {
62
135
  }
63
136
 
64
137
  /**
65
- * Individual die roll result with metadata.
138
+ * One physical die and everything the evaluator learned about it.
139
+ *
140
+ * The same object is shared between `RollResult.rolls` and the `rolls[]` of
141
+ * the {@link RollPart} that produced it — there is no deep clone, so mutating
142
+ * a die is visible through both views.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * import { roll } from 'roll-parser';
147
+ * import { createMockRng } from 'roll-parser/testing';
148
+ *
149
+ * const result = roll('4d6kh3', { rng: createMockRng([3, 6, 2, 5]) });
150
+ * result.rolls[1];
151
+ * // { sides: 6, result: 6, modifiers: ['kept'], critical: true, fumble: false }
152
+ * result.rolls[2];
153
+ * // { sides: 6, result: 2, modifiers: ['dropped'], critical: false, fumble: false }
154
+ * ```
155
+ *
156
+ * @category Results
66
157
  */
67
158
  export type DieResult = {
68
159
  /**
@@ -82,29 +173,49 @@ export type DieResult = {
82
173
  initialResult?: number;
83
174
  /** Modifiers applied to this die */
84
175
  modifiers: DieModifier[];
85
- /** True if rolled the maximum value (always false for Fate dice) */
176
+ /**
177
+ * True if the die met its critical criteria — by default, rolling the
178
+ * maximum face on a die with more than one side. That default never fires
179
+ * on `d1` or on Fate dice (`sides = 0` has no maximum face), but an
180
+ * explicit `cs` threshold does: `4dFcs>0` flags every `+1`.
181
+ */
86
182
  critical: boolean;
87
- /** True if rolled 1 (always false for Fate dice) */
183
+ /**
184
+ * True if the die met its fumble criteria — by default, rolling a 1 on a
185
+ * die with more than one side. As with {@link DieResult.critical}, the
186
+ * default never fires on `d1` or Fate dice, while an explicit `cf`
187
+ * threshold does: `4dFcf=-1` flags every `-1`.
188
+ */
88
189
  fumble: boolean;
89
190
  };
90
191
 
91
192
  /**
92
- * Per-spec keep/drop entry inside a flattened modifier chain. Counts are
93
- * resolved at evaluation time (meta-expressions like `kh(1d2)` become the
94
- * rolled number).
193
+ * One entry inside a flattened keep/drop chain. Counts are resolved at
194
+ * evaluation time (meta-expressions like `kh(1d2)` become the rolled number).
195
+ *
196
+ * Chained keep/drop modifiers flatten into one list — `4d6kh3dl1` yields two
197
+ * specs against a single pool, each applied independently, with drop sets
198
+ * unioned (Roll20 semantics).
199
+ *
200
+ * @category Results
95
201
  */
96
- export type ModifierSpec = {
97
- kind: 'keep' | 'drop';
98
- selector: 'highest' | 'lowest';
99
- count: number;
202
+ export type KeepDropSpec = {
203
+ readonly kind: 'keep' | 'drop';
204
+ readonly selector: 'highest' | 'lowest';
205
+ readonly count: number;
100
206
  };
101
207
 
102
208
  /**
103
- * Fields shared by every RollPart variant. `start`/`end` mirror the source
104
- * span of the AST node the part was evaluated from — present whenever the
105
- * AST came from `parse()`, absent on hand-built ASTs.
209
+ * Fields shared by every {@link RollPart} variant. `start`/`end` mirror the
210
+ * source span of the AST node the part was evaluated from — present whenever
211
+ * the AST came from `parse()`, absent on hand-built ASTs.
212
+ *
213
+ * Useful for code that walks the parts tree generically: any part can be
214
+ * narrowed to this shape without switching on `type` first.
215
+ *
216
+ * @category Results
106
217
  */
107
- type RollPartBase = {
218
+ export type RollPartBase = {
108
219
  /** Sub-total this part contributed to its parent. */
109
220
  total: number;
110
221
  start?: number;
@@ -129,6 +240,63 @@ type RollPartBase = {
129
240
  * thresholds) are not surfaced as nested parts — their resolved numbers
130
241
  * appear in the owning part, and their dice are inspectable in
131
242
  * `RollResult.rolls` via the `'meta'` modifier tag.
243
+ *
244
+ * @example Walking the tree with an exhaustive switch
245
+ * ```typescript
246
+ * import type { RollPart } from 'roll-parser';
247
+ *
248
+ * function describe(part: RollPart): string {
249
+ * switch (part.type) {
250
+ * case 'literal':
251
+ * return String(part.value);
252
+ * case 'variable':
253
+ * return `@${part.name}`;
254
+ * case 'dice':
255
+ * return `${part.count}d${part.sides}[${part.rolls.map((d) => d.result).join(', ')}]`;
256
+ * case 'fateDice':
257
+ * return `${part.count}dF`;
258
+ * case 'grouped':
259
+ * return `(${describe(part.inner)})`;
260
+ * case 'binaryOp':
261
+ * return `${describe(part.left)} ${part.operator} ${describe(part.right)}`;
262
+ * case 'unaryOp':
263
+ * return `-${describe(part.operand)}`;
264
+ * case 'keepDrop':
265
+ * return `${describe(part.target)} [${part.specs.length} keep/drop]`;
266
+ * case 'explode':
267
+ * return `${describe(part.target)} (${part.variant} explode)`;
268
+ * case 'reroll':
269
+ * return `${describe(part.target)} (reroll${part.once ? ' once' : ''})`;
270
+ * case 'dieBound':
271
+ * return `${describe(part.target)} (${part.bound} ${part.value})`;
272
+ * case 'successCount':
273
+ * return `${describe(part.target)} => ${part.successes}-${part.failures}`;
274
+ * case 'versus':
275
+ * return `${describe(part.roll)} vs ${describe(part.dc)}`;
276
+ * case 'functionCall':
277
+ * return `${part.name}(${part.args.map(describe).join(', ')})`;
278
+ * case 'group':
279
+ * return `{${part.parts.map(describe).join(', ')}}`;
280
+ * case 'sort':
281
+ * return `${describe(part.target)} (${part.order})`;
282
+ * case 'critThreshold':
283
+ * return `${describe(part.target)} (crit override)`;
284
+ * }
285
+ * }
286
+ * ```
287
+ *
288
+ * @example Reading a concrete tree
289
+ * ```typescript
290
+ * import { roll } from 'roll-parser';
291
+ * import { createMockRng } from 'roll-parser/testing';
292
+ *
293
+ * const result = roll('4d6kh3 + 2', { rng: createMockRng([3, 6, 2, 5]) });
294
+ * result.parts.type; // 'binaryOp'
295
+ * result.parts.total; // 16 — always equal to result.total
296
+ * describe(result.parts); // '4d6[3, 6, 2, 5] [1 keep/drop] + 2'
297
+ * ```
298
+ *
299
+ * @category Results
132
300
  */
133
301
  export type RollPart =
134
302
  | (RollPartBase & { type: 'literal'; value: number })
@@ -143,23 +311,47 @@ export type RollPart =
143
311
  right: RollPart;
144
312
  })
145
313
  | (RollPartBase & { type: 'unaryOp'; operator: '-'; operand: RollPart })
146
- | (RollPartBase & { type: 'modifier'; specs: ModifierSpec[]; target: RollPart })
314
+ | (RollPartBase & { type: 'keepDrop'; specs: KeepDropSpec[]; target: RollPart })
147
315
  | (RollPartBase & {
148
316
  type: 'explode';
149
317
  variant: 'standard' | 'compound' | 'penetrating';
150
318
  threshold?: ResolvedComparePoint;
319
+ /**
320
+ * The expanded pool. Standard and penetrating explosions append dice
321
+ * that exist nowhere under `target`, so this is the only view of the
322
+ * pool the modifier actually produced. Compound explosions accumulate
323
+ * in place, making it the same dice as `target` carries.
324
+ *
325
+ * Like `RollResult.rolls` — and unlike the pool under `target` — this
326
+ * keeps `'meta'` dice. Filter them out before counting or displaying.
327
+ */
328
+ rolls: DieResult[];
151
329
  target: RollPart;
152
330
  })
153
331
  | (RollPartBase & {
154
332
  type: 'reroll';
155
333
  once: boolean;
156
334
  condition: ResolvedComparePoint;
335
+ /**
336
+ * The post-reroll pool: discarded intermediates (`'rerolled'` +
337
+ * `'dropped'`) alongside their replacements. Both are appended rather
338
+ * than substituted, so neither appears under `target`.
339
+ *
340
+ * Keeps `'meta'` dice, as `RollResult.rolls` does.
341
+ */
342
+ rolls: DieResult[];
157
343
  target: RollPart;
158
344
  })
345
+ | (RollPartBase & { type: 'dieBound'; bound: 'min' | 'max'; value: number; target: RollPart })
159
346
  | (RollPartBase & {
160
347
  type: 'successCount';
161
348
  threshold: ResolvedComparePoint;
162
349
  failThreshold?: ResolvedComparePoint;
350
+ /**
351
+ * The tallied pool, sharing `DieResult` references with `target`.
352
+ * Keeps `'meta'` dice, as `RollResult.rolls` does.
353
+ */
354
+ rolls: DieResult[];
163
355
  target: RollPart;
164
356
  successes: number;
165
357
  failures: number;
@@ -167,7 +359,18 @@ export type RollPart =
167
359
  | (RollPartBase & { type: 'versus'; roll: RollPart; dc: RollPart; degree: DegreeOfSuccess })
168
360
  | (RollPartBase & { type: 'functionCall'; name: string; args: RollPart[] })
169
361
  | (RollPartBase & { type: 'group'; parts: RollPart[]; keptIndices?: number[] })
170
- | (RollPartBase & { type: 'sort'; order: 'ascending' | 'descending'; target: RollPart })
362
+ | (RollPartBase & {
363
+ type: 'sort';
364
+ order: 'ascending' | 'descending';
365
+ /**
366
+ * Every die the target produced, in sorted order, sharing `DieResult`
367
+ * references with `target` — so flags set after the sort (`4d6s dl1`)
368
+ * show through both. Like `RollResult.rolls` it keeps `'meta'` dice,
369
+ * which `rendered` omits from the bracket.
370
+ */
371
+ rolls: DieResult[];
372
+ target: RollPart;
373
+ })
171
374
  | (RollPartBase & {
172
375
  type: 'critThreshold';
173
376
  successThresholds: ResolvedCritThreshold[];
@@ -175,13 +378,53 @@ export type RollPart =
175
378
  target: RollPart;
176
379
  });
177
380
 
178
- /** Convenience alias for consumers writing exhaustive switches. */
381
+ /**
382
+ * The 17 discriminant strings of {@link RollPart}. Convenience alias for
383
+ * consumers writing exhaustive switches or part-type lookup tables.
384
+ *
385
+ * @category Results
386
+ */
179
387
  export type RollPartType = RollPart['type'];
180
388
 
181
389
  /**
182
- * Complete roll result with all metadata.
390
+ * Complete roll result with all metadata — what {@link roll} and
391
+ * {@link evaluate} return.
392
+ *
393
+ * The top level is `Readonly` — a result describes one completed evaluation
394
+ * and is never re-targeted. The `rolls` array and the `parts` tree stay
395
+ * mutable so consumers can annotate or re-sort their own views.
396
+ *
397
+ * Fully JSON-serializable; this is exactly what the CLI's `--json` flag emits.
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * import { roll } from 'roll-parser';
402
+ * import { createMockRng } from 'roll-parser/testing';
403
+ *
404
+ * const result = roll('3d6', { rng: createMockRng([4, 2, 6]) });
405
+ *
406
+ * JSON.stringify(result);
407
+ * // {
408
+ * // "total": 12,
409
+ * // "notation": "3d6",
410
+ * // "expression": "3d6",
411
+ * // "rendered": "3d6[4, 2, 6] = 12",
412
+ * // "rolls": [
413
+ * // { "sides": 6, "result": 4, "modifiers": ["kept"], "critical": false, "fumble": false },
414
+ * // { "sides": 6, "result": 2, "modifiers": ["kept"], "critical": false, "fumble": false },
415
+ * // { "sides": 6, "result": 6, "modifiers": ["kept"], "critical": true, "fumble": false }
416
+ * // ],
417
+ * // "parts": {
418
+ * // "type": "dice", "count": 3, "sides": 6,
419
+ * // "rolls": [ ...the same three objects... ],
420
+ * // "total": 12, "start": 0, "end": 3
421
+ * // }
422
+ * // }
423
+ * ```
424
+ *
425
+ * @category Results
183
426
  */
184
- export type RollResult = {
427
+ export type RollResult = Readonly<{
185
428
  /** Final computed total */
186
429
  total: number;
187
430
  /** Original input notation */
@@ -190,20 +433,28 @@ export type RollResult = {
190
433
  expression: string;
191
434
  /** Rendered result with individual rolls shown */
192
435
  rendered: string;
193
- /** All individual die results */
436
+ /**
437
+ * All individual die results, in evaluation order.
438
+ *
439
+ * Shares `DieResult` object references with the `rolls[]` arrays inside
440
+ * `parts` — no deep clone. Mutating a die here is visible through the part
441
+ * tree and vice versa; clone first if that matters.
442
+ */
194
443
  rolls: DieResult[];
195
444
  /** Structured breakdown of the evaluated expression, mirroring the AST 1:1. */
196
445
  parts: RollPart;
197
446
  /**
198
447
  * Number of dice tagged as success across the whole expression. Present
199
448
  * only when a success-counting modifier was used. Independent of `total` —
200
- * arithmetic on top of a success count (e.g. `5d6>=5 * 2`) affects `total`
201
- * but not `successes`.
449
+ * arithmetic on top of a success count (e.g. `{5d6>=5}+2`) affects `total`
450
+ * but not `successes`. Success counts are terminal, so the group braces are
451
+ * required: `5d6>=5 * 2` is an `INVALID_SUCCESS_COUNT_TARGET` parse error.
202
452
  */
203
453
  successes?: number;
204
454
  /**
205
455
  * Number of dice tagged as failure across the whole expression. Present
206
- * only when a success-counting modifier with a fail threshold was used.
456
+ * whenever a success-counting modifier was used `0` when no failure
457
+ * threshold was given, since nothing can be tagged as a failure.
207
458
  */
208
459
  failures?: number;
209
460
  /**
@@ -216,22 +467,78 @@ export type RollResult = {
216
467
  * exactly one kept d20 was rolled on the roll side of a `vs` expression.
217
468
  */
218
469
  natural?: number;
219
- };
470
+ }>;
220
471
 
221
472
  /**
222
- * Options for the evaluate function.
473
+ * Evaluation guardrails and variable resolution, shared by `EvaluateOptions`
474
+ * and `RollOptions`. Every field is optional — the defaults bound adversarial
475
+ * notation without capping any realistic expression.
476
+ *
477
+ * Tighten them when the notation comes from users you do not control; the
478
+ * caps are the difference between a typed `EvaluatorError` and a request that
479
+ * rolls ten million dice. Parse depth is capped separately and
480
+ * unconditionally by {@link MAX_PARSE_DEPTH}.
481
+ *
482
+ * The three numeric limits fail closed: omit one — or pass `undefined` or
483
+ * `null`, the no-options path a partial config produces — and it takes its
484
+ * default, but supply anything else that is not a safe integer in range — a
485
+ * string, `NaN`, `±Infinity`, a negative, a fraction — and evaluation throws
486
+ * `INVALID_EVALUATION_LIMIT` before rolling. `maxDice` accepts `>= 1`, the two
487
+ * iteration limits `>= 0`. No coercion: `maxDice: Number(input)` on
488
+ * unparseable input rejects rather than quietly reverting to the permissive
489
+ * default.
490
+ *
491
+ * @example Untrusted input
492
+ * ```typescript
493
+ * import { isRollParserError, roll } from 'roll-parser';
494
+ *
495
+ * const limits = {
496
+ * maxDice: 100,
497
+ * maxExplodeIterations: 20,
498
+ * maxRerollIterations: 20,
499
+ * };
500
+ *
501
+ * try {
502
+ * roll('99999d6', limits).total;
503
+ * } catch (error) {
504
+ * isRollParserError(error) && error.code; // 'DICE_LIMIT_EXCEEDED'
505
+ * }
506
+ * ```
507
+ *
508
+ * @example Variables
509
+ * ```typescript
510
+ * import { roll } from 'roll-parser';
511
+ *
512
+ * roll('1d20+@str', { context: { str: 4 }, seed: 'demo' }).total; // 5
513
+ * roll('1d20+@str', { onMissingVariable: 'zero', seed: 'demo' }).total; // 1
514
+ * ```
515
+ *
516
+ * @category Core
223
517
  */
224
- export type EvaluateOptions = {
225
- /** Original notation string (for result metadata) */
226
- notation?: string;
227
- /** Maximum total dice allowed per evaluation (default: 10,000) */
518
+ export type EvaluationOptions = {
519
+ /** Maximum total dice allowed per evaluation; integer >= 1 (default: 10,000) */
228
520
  maxDice?: number;
229
- /** Maximum explosion iterations allowed per die (default: 1,000) */
521
+ /** Maximum explosion iterations allowed per die; integer >= 0 (default: 1,000) */
230
522
  maxExplodeIterations?: number;
231
- /** Maximum reroll iterations allowed per die (default: 1,000) */
523
+ /** Maximum reroll iterations allowed per die; integer >= 0 (default: 1,000) */
232
524
  maxRerollIterations?: number;
233
525
  /** Variable context for `@name` / `@{name}` references (default: empty) */
234
- context?: Record<string, number>;
526
+ context?: Readonly<Record<string, number>>;
235
527
  /** Behavior when a referenced variable is missing from context (default: 'throw') */
236
528
  onMissingVariable?: 'throw' | 'zero';
237
529
  };
530
+
531
+ /**
532
+ * Options for {@link evaluate}: the shared {@link EvaluationOptions} plus the
533
+ * original notation, which `evaluate` cannot recover from an AST.
534
+ *
535
+ * @category Core
536
+ */
537
+ export type EvaluateOptions = EvaluationOptions & {
538
+ /**
539
+ * Original notation string, echoed back as `RollResult.notation`. When
540
+ * omitted, falls back to the normalized `expression` reconstructed from the
541
+ * AST — {@link roll} always forwards the string the caller typed.
542
+ */
543
+ notation?: string;
544
+ };
package/src/version.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Generated by `bun run generate:version` from package.json — do not edit.
2
+ export const version = '3.1.0';