roll-parser 3.0.0-alpha.0 → 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +166 -30
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/format.d.ts +1 -1
  5. package/dist/cli/format.d.ts.map +1 -1
  6. package/dist/cli.js +1110 -277
  7. package/dist/cli.js.map +28 -0
  8. package/dist/errors.d.ts +7 -4
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/evaluator/evaluator.d.ts +63 -7
  11. package/dist/evaluator/evaluator.d.ts.map +1 -1
  12. package/dist/evaluator/index.d.ts +2 -2
  13. package/dist/evaluator/index.d.ts.map +1 -1
  14. package/dist/evaluator/modifiers/compare.d.ts +1 -1
  15. package/dist/evaluator/modifiers/compare.d.ts.map +1 -1
  16. package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
  17. package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
  18. package/dist/evaluator/modifiers/explode.d.ts +8 -4
  19. package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
  20. package/dist/evaluator/modifiers/keep-drop.d.ts +1 -1
  21. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
  22. package/dist/evaluator/modifiers/reroll.d.ts +4 -4
  23. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
  24. package/dist/evaluator/modifiers/sort.d.ts +23 -0
  25. package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
  26. package/dist/evaluator/modifiers/success-count.d.ts +1 -1
  27. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
  28. package/dist/index.d.ts +13 -13
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1112 -275
  31. package/dist/index.js.map +26 -0
  32. package/dist/lexer/lexer.d.ts +18 -3
  33. package/dist/lexer/lexer.d.ts.map +1 -1
  34. package/dist/lexer/tokens.d.ts +22 -2
  35. package/dist/lexer/tokens.d.ts.map +1 -1
  36. package/dist/parser/ast.d.ts +209 -24
  37. package/dist/parser/ast.d.ts.map +1 -1
  38. package/dist/parser/parser.d.ts +48 -5
  39. package/dist/parser/parser.d.ts.map +1 -1
  40. package/dist/rng/index.d.ts +2 -2
  41. package/dist/rng/index.d.ts.map +1 -1
  42. package/dist/rng/mock.d.ts +1 -1
  43. package/dist/rng/mock.d.ts.map +1 -1
  44. package/dist/rng/seeded.d.ts +8 -1
  45. package/dist/rng/seeded.d.ts.map +1 -1
  46. package/dist/roll.d.ts +6 -2
  47. package/dist/roll.d.ts.map +1 -1
  48. package/dist/testing.d.ts +1 -1
  49. package/dist/testing.d.ts.map +1 -1
  50. package/dist/testing.js +3 -0
  51. package/dist/testing.js.map +11 -0
  52. package/dist/types.d.ts +143 -2
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +22 -18
  55. package/src/cli/args.ts +2 -1
  56. package/src/cli/format.ts +8 -4
  57. package/src/cli/index.ts +22 -5
  58. package/src/errors.ts +15 -3
  59. package/src/evaluator/evaluator.ts +826 -111
  60. package/src/evaluator/index.ts +2 -2
  61. package/src/evaluator/modifiers/compare.ts +1 -1
  62. package/src/evaluator/modifiers/crit-threshold.ts +59 -0
  63. package/src/evaluator/modifiers/explode.ts +29 -25
  64. package/src/evaluator/modifiers/keep-drop.ts +1 -1
  65. package/src/evaluator/modifiers/reroll.ts +18 -25
  66. package/src/evaluator/modifiers/sort.ts +30 -0
  67. package/src/evaluator/modifiers/success-count.ts +2 -2
  68. package/src/index.ts +33 -15
  69. package/src/lexer/lexer.ts +101 -8
  70. package/src/lexer/tokens.ts +42 -2
  71. package/src/parser/ast.ts +397 -30
  72. package/src/parser/parser.ts +590 -67
  73. package/src/rng/index.ts +2 -2
  74. package/src/rng/mock.ts +1 -1
  75. package/src/rng/seeded.ts +31 -1
  76. package/src/roll.ts +14 -6
  77. package/src/testing.ts +1 -1
  78. package/src/types.ts +127 -2
  79. package/dist/index.mjs +0 -1724
  80. package/dist/testing.mjs +0 -39
package/src/rng/index.ts CHANGED
@@ -4,5 +4,5 @@
4
4
  * @module rng
5
5
  */
6
6
 
7
- export type { RNG } from './types';
8
- export { SeededRNG } from './seeded';
7
+ export type { RNG } from './types.js';
8
+ export { SeededRNG } from './seeded.js';
package/src/rng/mock.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * @module rng/mock
5
5
  */
6
6
 
7
- import type { RNG } from './types';
7
+ import type { RNG } from './types.js';
8
8
 
9
9
  /**
10
10
  * Error thrown when MockRNG exhausts its predefined values.
package/src/rng/seeded.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * @module rng/seeded
5
5
  */
6
6
 
7
- import type { RNG } from './types';
7
+ import type { RNG } from './types.js';
8
8
 
9
9
  /**
10
10
  * Seedable pseudo-random number generator using xorshift128.
@@ -114,6 +114,12 @@ export class SeededRNG implements RNG {
114
114
  return lo;
115
115
  }
116
116
 
117
+ // Ranges wider than 2^32 need two draws — a single uint32 can never
118
+ // produce the upper part of the range and would silently truncate it.
119
+ if (range > 0x100000000) {
120
+ return lo + this.nextBoundedWide(range);
121
+ }
122
+
117
123
  // Rejection sampling for unbiased distribution
118
124
  // Avoids modulo bias by rejecting values that would cause uneven distribution
119
125
  const threshold = (0x100000000 - range) % range;
@@ -124,4 +130,28 @@ export class SeededRNG implements RNG {
124
130
 
125
131
  return lo + (value % range);
126
132
  }
133
+
134
+ /**
135
+ * Unbiased sampling in `[0, range)` for ranges above 2^32, built from two
136
+ * uint32 draws combined into a 53-bit integer (the largest width JS numbers
137
+ * represent exactly). Ranges beyond 2^53 cannot be sampled without bias —
138
+ * throw instead of silently degrading.
139
+ */
140
+ private nextBoundedWide(range: number): number {
141
+ const MAX_53 = 2 ** 53;
142
+ if (range > MAX_53) {
143
+ throw new RangeError(`nextInt range ${range} exceeds 2^53 and cannot be sampled exactly`);
144
+ }
145
+
146
+ // Largest multiple of `range` below 2^53 — values at or above it are
147
+ // rejected to avoid modulo bias.
148
+ const limit = Math.floor(MAX_53 / range) * range;
149
+ let value: number;
150
+ do {
151
+ // 32 high bits shifted up by 21 + top 21 bits of a second draw = 53 bits.
152
+ value = this.nextUint32() * 0x200000 + (this.nextUint32() >>> 11);
153
+ } while (value >= limit);
154
+
155
+ return value % range;
156
+ }
127
157
  }
package/src/roll.ts CHANGED
@@ -4,12 +4,12 @@
4
4
  * @module roll
5
5
  */
6
6
 
7
- import type { RNG } from './rng/types';
8
- import type { EvaluateOptions, RollResult } from './types';
9
- import { evaluate } from './evaluator/evaluator';
10
- import { lex } from './lexer/lexer';
11
- import { Parser } from './parser/parser';
12
- import { SeededRNG } from './rng/seeded';
7
+ import type { RNG } from './rng/types.js';
8
+ import type { EvaluateOptions, RollResult } from './types.js';
9
+ import { evaluate } from './evaluator/evaluator.js';
10
+ import { lex } from './lexer/lexer.js';
11
+ import { Parser } from './parser/parser.js';
12
+ import { SeededRNG } from './rng/seeded.js';
13
13
 
14
14
  /**
15
15
  * Options for the roll function.
@@ -25,6 +25,10 @@ export type RollOptions = {
25
25
  maxExplodeIterations?: number;
26
26
  /** Maximum reroll iterations allowed per die (default: 1,000) */
27
27
  maxRerollIterations?: number;
28
+ /** Variable context for `@name` / `@{name}` references (default: empty) */
29
+ context?: Record<string, number>;
30
+ /** Behavior when a referenced variable is missing from context (default: 'throw') */
31
+ onMissingVariable?: 'throw' | 'zero';
28
32
  };
29
33
 
30
34
  /**
@@ -62,5 +66,9 @@ export function roll(notation: string, options: RollOptions = {}): RollResult {
62
66
  if (options.maxRerollIterations != null) {
63
67
  evalOptions.maxRerollIterations = options.maxRerollIterations;
64
68
  }
69
+ if (options.context != null) evalOptions.context = options.context;
70
+ if (options.onMissingVariable != null) {
71
+ evalOptions.onMissingVariable = options.onMissingVariable;
72
+ }
65
73
  return evaluate(ast, rng, evalOptions);
66
74
  }
package/src/testing.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  import {
11
11
  MockRNGExhaustedError as _MockRNGExhaustedError,
12
12
  createMockRng as _createMockRng,
13
- } from './rng/mock';
13
+ } from './rng/mock.js';
14
14
 
15
15
  export const createMockRng = _createMockRng;
16
16
  export const MockRNGExhaustedError = _MockRNGExhaustedError;
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * @module types
5
5
  */
6
6
 
7
- import type { ASTNode } from './parser/ast';
7
+ import type { ASTNode } from './parser/ast.js';
8
8
 
9
9
  /**
10
10
  * Comparison operator for compare points.
@@ -22,10 +22,32 @@ export type ComparePoint = {
22
22
  value: ASTNode;
23
23
  };
24
24
 
25
+ /**
26
+ * A ComparePoint whose value has been evaluated to a number. Used in
27
+ * `RollPart` where meta-expressions are already resolved.
28
+ */
29
+ export type ResolvedComparePoint = {
30
+ operator: CompareOp;
31
+ value: number;
32
+ };
33
+
34
+ /**
35
+ * A resolved crit threshold — `'default'` means the per-die default rule
36
+ * (`result === sides` for critical, `result === 1` for fumble).
37
+ */
38
+ export type ResolvedCritThreshold = ResolvedComparePoint | 'default';
39
+
25
40
  /**
26
41
  * Modifier flags applied to individual die results.
27
42
  */
28
- export type DieModifier = 'dropped' | 'kept' | 'exploded' | 'rerolled' | 'success' | 'failure';
43
+ export type DieModifier =
44
+ | 'dropped'
45
+ | 'kept'
46
+ | 'exploded'
47
+ | 'rerolled'
48
+ | 'success'
49
+ | 'failure'
50
+ | 'meta';
29
51
 
30
52
  /**
31
53
  * PF2e Degree of Success. Produced by the `vs` operator when comparing a
@@ -51,6 +73,13 @@ export type DieResult = {
51
73
  sides: number;
52
74
  /** The rolled value */
53
75
  result: number;
76
+ /**
77
+ * Raw first roll before any mutation (e.g., compound-explode accumulation).
78
+ * Only populated when `result` has been overwritten with a computed value.
79
+ * Consumers that need the original face (nat-20 / nat-1 detection) should
80
+ * read `initialResult ?? result`.
81
+ */
82
+ initialResult?: number;
54
83
  /** Modifiers applied to this die */
55
84
  modifiers: DieModifier[];
56
85
  /** True if rolled the maximum value (always false for Fate dice) */
@@ -59,6 +88,96 @@ export type DieResult = {
59
88
  fumble: boolean;
60
89
  };
61
90
 
91
+ /**
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).
95
+ */
96
+ export type ModifierSpec = {
97
+ kind: 'keep' | 'drop';
98
+ selector: 'highest' | 'lowest';
99
+ count: number;
100
+ };
101
+
102
+ /**
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.
106
+ */
107
+ type RollPartBase = {
108
+ /** Sub-total this part contributed to its parent. */
109
+ total: number;
110
+ start?: number;
111
+ end?: number;
112
+ };
113
+
114
+ /**
115
+ * Structured breakdown of an evaluated expression, mirroring the AST 1:1 —
116
+ * every ASTNode produces exactly one RollPart. Discriminants are lowercase
117
+ * camelCase to distinguish evaluation-tree types from `ASTNode.type`
118
+ * (PascalCase) at a glance.
119
+ *
120
+ * Invariants:
121
+ * - `RollResult.parts.total === RollResult.total`.
122
+ * - `successCount.total === successes - failures`.
123
+ * - `literal.total === value` and `variable.total === value`.
124
+ * - Each part's `rolls[]` shares `DieResult` references with
125
+ * `RollResult.rolls[]`; both reflect post-evaluation state (explode
126
+ * accumulation, reroll flags, keep/drop flags). No deep clone.
127
+ *
128
+ * Meta-expression sub-trees (`4d6kh(1d2)`, `(1+1)d6` counts/sides, computed
129
+ * thresholds) are not surfaced as nested parts — their resolved numbers
130
+ * appear in the owning part, and their dice are inspectable in
131
+ * `RollResult.rolls` via the `'meta'` modifier tag.
132
+ */
133
+ export type RollPart =
134
+ | (RollPartBase & { type: 'literal'; value: number })
135
+ | (RollPartBase & { type: 'variable'; name: string; value: number })
136
+ | (RollPartBase & { type: 'dice'; count: number; sides: number; rolls: DieResult[] })
137
+ | (RollPartBase & { type: 'fateDice'; count: number; rolls: DieResult[] })
138
+ | (RollPartBase & { type: 'grouped'; inner: RollPart })
139
+ | (RollPartBase & {
140
+ type: 'binaryOp';
141
+ operator: '+' | '-' | '*' | '/' | '%' | '**';
142
+ left: RollPart;
143
+ right: RollPart;
144
+ })
145
+ | (RollPartBase & { type: 'unaryOp'; operator: '-'; operand: RollPart })
146
+ | (RollPartBase & { type: 'modifier'; specs: ModifierSpec[]; target: RollPart })
147
+ | (RollPartBase & {
148
+ type: 'explode';
149
+ variant: 'standard' | 'compound' | 'penetrating';
150
+ threshold?: ResolvedComparePoint;
151
+ target: RollPart;
152
+ })
153
+ | (RollPartBase & {
154
+ type: 'reroll';
155
+ once: boolean;
156
+ condition: ResolvedComparePoint;
157
+ target: RollPart;
158
+ })
159
+ | (RollPartBase & {
160
+ type: 'successCount';
161
+ threshold: ResolvedComparePoint;
162
+ failThreshold?: ResolvedComparePoint;
163
+ target: RollPart;
164
+ successes: number;
165
+ failures: number;
166
+ })
167
+ | (RollPartBase & { type: 'versus'; roll: RollPart; dc: RollPart; degree: DegreeOfSuccess })
168
+ | (RollPartBase & { type: 'functionCall'; name: string; args: RollPart[] })
169
+ | (RollPartBase & { type: 'group'; parts: RollPart[]; keptIndices?: number[] })
170
+ | (RollPartBase & { type: 'sort'; order: 'ascending' | 'descending'; target: RollPart })
171
+ | (RollPartBase & {
172
+ type: 'critThreshold';
173
+ successThresholds: ResolvedCritThreshold[];
174
+ failThresholds: ResolvedCritThreshold[];
175
+ target: RollPart;
176
+ });
177
+
178
+ /** Convenience alias for consumers writing exhaustive switches. */
179
+ export type RollPartType = RollPart['type'];
180
+
62
181
  /**
63
182
  * Complete roll result with all metadata.
64
183
  */
@@ -73,6 +192,8 @@ export type RollResult = {
73
192
  rendered: string;
74
193
  /** All individual die results */
75
194
  rolls: DieResult[];
195
+ /** Structured breakdown of the evaluated expression, mirroring the AST 1:1. */
196
+ parts: RollPart;
76
197
  /**
77
198
  * Number of dice tagged as success across the whole expression. Present
78
199
  * only when a success-counting modifier was used. Independent of `total` —
@@ -109,4 +230,8 @@ export type EvaluateOptions = {
109
230
  maxExplodeIterations?: number;
110
231
  /** Maximum reroll iterations allowed per die (default: 1,000) */
111
232
  maxRerollIterations?: number;
233
+ /** Variable context for `@name` / `@{name}` references (default: empty) */
234
+ context?: Record<string, number>;
235
+ /** Behavior when a referenced variable is missing from context (default: 'throw') */
236
+ onMissingVariable?: 'throw' | 'zero';
112
237
  };