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.
- package/CHANGELOG.md +91 -0
- package/README.md +166 -30
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/format.d.ts +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli.js +1110 -277
- package/dist/cli.js.map +28 -0
- package/dist/errors.d.ts +7 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/evaluator/evaluator.d.ts +63 -7
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/index.d.ts +2 -2
- package/dist/evaluator/index.d.ts.map +1 -1
- package/dist/evaluator/modifiers/compare.d.ts +1 -1
- package/dist/evaluator/modifiers/compare.d.ts.map +1 -1
- package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
- package/dist/evaluator/modifiers/explode.d.ts +8 -4
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
- package/dist/evaluator/modifiers/reroll.d.ts +4 -4
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
- package/dist/evaluator/modifiers/sort.d.ts +23 -0
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
- package/dist/evaluator/modifiers/success-count.d.ts +1 -1
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1112 -275
- package/dist/index.js.map +26 -0
- package/dist/lexer/lexer.d.ts +18 -3
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/tokens.d.ts +22 -2
- package/dist/lexer/tokens.d.ts.map +1 -1
- package/dist/parser/ast.d.ts +209 -24
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/parser.d.ts +48 -5
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/rng/index.d.ts +2 -2
- package/dist/rng/index.d.ts.map +1 -1
- package/dist/rng/mock.d.ts +1 -1
- package/dist/rng/mock.d.ts.map +1 -1
- package/dist/rng/seeded.d.ts +8 -1
- package/dist/rng/seeded.d.ts.map +1 -1
- package/dist/roll.d.ts +6 -2
- package/dist/roll.d.ts.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +11 -0
- package/dist/types.d.ts +143 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +22 -18
- package/src/cli/args.ts +2 -1
- package/src/cli/format.ts +8 -4
- package/src/cli/index.ts +22 -5
- package/src/errors.ts +15 -3
- package/src/evaluator/evaluator.ts +826 -111
- package/src/evaluator/index.ts +2 -2
- package/src/evaluator/modifiers/compare.ts +1 -1
- package/src/evaluator/modifiers/crit-threshold.ts +59 -0
- package/src/evaluator/modifiers/explode.ts +29 -25
- package/src/evaluator/modifiers/keep-drop.ts +1 -1
- package/src/evaluator/modifiers/reroll.ts +18 -25
- package/src/evaluator/modifiers/sort.ts +30 -0
- package/src/evaluator/modifiers/success-count.ts +2 -2
- package/src/index.ts +33 -15
- package/src/lexer/lexer.ts +101 -8
- package/src/lexer/tokens.ts +42 -2
- package/src/parser/ast.ts +397 -30
- package/src/parser/parser.ts +590 -67
- package/src/rng/index.ts +2 -2
- package/src/rng/mock.ts +1 -1
- package/src/rng/seeded.ts +31 -1
- package/src/roll.ts +14 -6
- package/src/testing.ts +1 -1
- package/src/types.ts +127 -2
- package/dist/index.mjs +0 -1724
- package/dist/testing.mjs +0 -39
package/src/rng/index.ts
CHANGED
package/src/rng/mock.ts
CHANGED
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 =
|
|
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
|
};
|