roll-parser 3.0.0 → 3.2.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 +61 -1
- package/MIGRATION.md +293 -4
- package/README.md +246 -55
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +8 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/evaluator/die.d.ts +2 -1
- package/dist/evaluator/die.d.ts.map +1 -1
- package/dist/evaluator/die.js.map +1 -1
- package/dist/evaluator/env.d.ts +57 -0
- package/dist/evaluator/env.d.ts.map +1 -1
- package/dist/evaluator/env.js.map +1 -1
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/evaluator.js +104 -56
- package/dist/evaluator/evaluator.js.map +1 -1
- package/dist/evaluator/modifiers/crit-threshold.d.ts +43 -5
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -1
- package/dist/evaluator/modifiers/crit-threshold.js +24 -11
- package/dist/evaluator/modifiers/crit-threshold.js.map +1 -1
- package/dist/evaluator/modifiers/die-bound.d.ts +5 -2
- package/dist/evaluator/modifiers/die-bound.d.ts.map +1 -1
- package/dist/evaluator/modifiers/die-bound.js +6 -3
- package/dist/evaluator/modifiers/die-bound.js.map +1 -1
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
- package/dist/evaluator/modifiers/explode.js +13 -5
- package/dist/evaluator/modifiers/explode.js.map +1 -1
- package/dist/evaluator/modifiers/flags.d.ts +17 -0
- package/dist/evaluator/modifiers/flags.d.ts.map +1 -1
- package/dist/evaluator/modifiers/flags.js +4 -4
- package/dist/evaluator/modifiers/flags.js.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.d.ts +5 -5
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.js +7 -7
- package/dist/evaluator/modifiers/keep-drop.js.map +1 -1
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
- package/dist/evaluator/modifiers/reroll.js +16 -10
- package/dist/evaluator/modifiers/reroll.js.map +1 -1
- package/dist/evaluator/modifiers/sort.d.ts +1 -1
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -1
- package/dist/evaluator/modifiers/sort.js +3 -3
- package/dist/evaluator/modifiers/sort.js.map +1 -1
- package/dist/evaluator/modifiers/success-count.d.ts +6 -2
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
- package/dist/evaluator/modifiers/success-count.js +9 -5
- package/dist/evaluator/modifiers/success-count.js.map +1 -1
- package/dist/notation.d.ts +13 -0
- package/dist/notation.d.ts.map +1 -0
- package/dist/notation.js +8 -0
- package/dist/notation.js.map +1 -0
- package/dist/parser/ast.d.ts +12 -7
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/ast.js.map +1 -1
- package/dist/parser/guards.d.ts +38 -0
- package/dist/parser/guards.d.ts.map +1 -1
- package/dist/parser/guards.js +51 -0
- package/dist/parser/guards.js.map +1 -1
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +26 -2
- package/dist/parser/parser.js.map +1 -1
- package/dist/render.d.ts +95 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +232 -0
- package/dist/render.js.map +1 -0
- package/dist/types.d.ts +64 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -4
- package/src/cli/format.ts +15 -23
- package/src/evaluator/die.ts +2 -1
- package/src/evaluator/env.ts +58 -0
- package/src/evaluator/evaluator.ts +192 -70
- package/src/evaluator/modifiers/crit-threshold.ts +81 -15
- package/src/evaluator/modifiers/die-bound.ts +13 -4
- package/src/evaluator/modifiers/explode.ts +21 -6
- package/src/evaluator/modifiers/flags.ts +21 -4
- package/src/evaluator/modifiers/keep-drop.ts +10 -8
- package/src/evaluator/modifiers/reroll.ts +20 -11
- package/src/evaluator/modifiers/sort.ts +9 -3
- package/src/evaluator/modifiers/success-count.ts +27 -5
- package/src/notation.ts +24 -0
- package/src/parser/ast.ts +12 -7
- package/src/parser/guards.ts +92 -1
- package/src/parser/parser.ts +56 -1
- package/src/render.ts +394 -0
- package/src/types.ts +64 -12
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,7 @@ Dice notation for tabletop RPGs, rolled into structured results.
|
|
|
12
12
|
<a href="https://www.npmjs.com/package/roll-parser"><img src="https://img.shields.io/npm/v/roll-parser?color=cb3837" alt="npm version"></a>
|
|
13
13
|
<a href="https://github.com/edloidas/roll-parser/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/edloidas/roll-parser/ci.yml?branch=master&label=CI" alt="CI status"></a>
|
|
14
14
|
<a href="https://github.com/edloidas/roll-parser/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/roll-parser?color=blue" alt="MIT license"></a>
|
|
15
|
-
|
|
16
|
-
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/node-%3E%3D22.12-5fa04e" alt="Node.js >= 22.12"></a>
|
|
15
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/node/v/roll-parser?color=5fa04e" alt="Node.js >= 22.12"></a>
|
|
17
16
|
</p>
|
|
18
17
|
|
|
19
18
|
<p align="center">
|
|
@@ -69,8 +68,9 @@ roll('4d6kh3', { rng: createMockRng([3, 6, 2, 5]) }).total; // 14, every run
|
|
|
69
68
|
- **Safe on untrusted input.** Dice count, explosion depth, reroll depth, and
|
|
70
69
|
parse depth are all bounded, and every failure is a typed error with a stable
|
|
71
70
|
code and a source span.
|
|
72
|
-
- **Small and fast.** ≈
|
|
73
|
-
`parse`, ≈213 B for the testing entry
|
|
71
|
+
- **Small and fast.** ≈12.7 kB brotli for the whole library, ≈5.5 kB for just
|
|
72
|
+
`parse`, ≈1.4 kB for `roll-parser/render`, ≈213 B for the testing entry
|
|
73
|
+
point. Zero runtime dependencies, zero
|
|
74
74
|
`node:` imports. A `1d20` round trip takes about 0.5 µs.
|
|
75
75
|
- **Tested.** 1,500+ tests behind CI-enforced coverage floors — 100% of
|
|
76
76
|
functions, 98% of lines — including every code example in this README, which
|
|
@@ -85,22 +85,28 @@ roll('4d6kh3', { rng: createMockRng([3, 6, 2, 5]) }).total; // 14, every run
|
|
|
85
85
|
|
|
86
86
|
## Contents
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
**Start here**
|
|
89
|
+
|
|
90
|
+
- [Install](#install) — [CDN](#cdn--browser-without-a-bundler) · [Upgrading](#upgrading)
|
|
91
|
+
- [Notation reference](#notation-reference) — [Dice](#dice) · [Arithmetic](#arithmetic-and-functions) · [Modifiers](#modifiers) · [Pools and checks](#pools-and-checks)
|
|
90
92
|
- [Recipes by game system](#recipes-by-game-system)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- [
|
|
95
|
-
- [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
- [
|
|
100
|
-
- [
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
|
|
94
|
+
**Using results**
|
|
95
|
+
|
|
96
|
+
- [Working with results](#working-with-results) — [RollResult](#rollresult) · [The parts tree](#the-parts-tree)
|
|
97
|
+
- [Rendering](#rendering) — [Custom markers](#custom-markers) · [Reading crit and fumble](#reading-crit-and-fumble)
|
|
98
|
+
|
|
99
|
+
**Configuring a roll**
|
|
100
|
+
|
|
101
|
+
- [Options](#options) — [Safety limits](#safety-limits)
|
|
102
|
+
- [Randomness](#randomness) — [Seeded rolls](#seeded-rolls) · [Replay](#replay-and-saveresume) · [Custom RNGs](#custom-rngs) · [Testing](#testing)
|
|
103
|
+
|
|
104
|
+
**Reference**
|
|
105
|
+
|
|
106
|
+
- [Error handling](#error-handling) — [Notation errors](#notation-errors) · [Error classes](#error-classes) · [Error codes](#error-codes)
|
|
107
|
+
- [Using the parser directly](#using-the-parser-directly) · [TypeScript](#typescript)
|
|
108
|
+
- [CLI](#cli) · [Performance](#performance) · [Known limitations](#known-limitations)
|
|
109
|
+
- [Versioning](#versioning) · [Contributing](#contributing) · [License](#license)
|
|
104
110
|
|
|
105
111
|
## Install
|
|
106
112
|
|
|
@@ -144,10 +150,11 @@ as one request:
|
|
|
144
150
|
`https://esm.sh/roll-parser` works the same way. Raw file URLs
|
|
145
151
|
(`unpkg.com/roll-parser`) also work but fetch each module separately.
|
|
146
152
|
|
|
147
|
-
### Upgrading
|
|
153
|
+
### Upgrading
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
[MIGRATION.md](MIGRATION.md) covers every upgrade path, newest first. v3 is a
|
|
156
|
+
complete rewrite and the 2.x API is not compatible; to stay on the old line,
|
|
157
|
+
pin `roll-parser@2.3.2`.
|
|
151
158
|
|
|
152
159
|
## Notation reference
|
|
153
160
|
|
|
@@ -157,8 +164,9 @@ wherever the two overlap: `kh`/`kl`/`dh`/`dl`, `!`/`!!`/`!p`, `r`/`ro`, success
|
|
|
157
164
|
and failure counting, `cs`/`cf`, grouped rolls, and `floor`/`ceil`/`round`/`abs`
|
|
158
165
|
all use Roll20's spelling and semantics. It is not a strict superset — sorting
|
|
159
166
|
(`s`), PF2e `vs` checks, `max`/`min`, and computed dice counts and sides are
|
|
160
|
-
additions, and
|
|
161
|
-
after a success count
|
|
167
|
+
additions, and three Roll20 forms are deliberately rejected (`4d6d1`, arithmetic
|
|
168
|
+
after a success count, and a counted single-sub-roll group folding its own math
|
|
169
|
+
into each die, `{3d20+5}>=21`) — see [Known limitations](#known-limitations).
|
|
162
170
|
|
|
163
171
|
Notation is case-insensitive and whitespace-tolerant — `2 D 20 KH 1` and
|
|
164
172
|
`2d20kh1` are the same expression, newlines included. The one exception is
|
|
@@ -220,6 +228,18 @@ Chained keep/drop modifiers do **not** nest. `4d6kh3dl1` flattens into two
|
|
|
220
228
|
specs applied independently to the same pool, with the dropped sets unioned —
|
|
221
229
|
the Roll20 rule.
|
|
222
230
|
|
|
231
|
+
`cs` / `cf` covers the whole pool, dice that `!`, `!p`, `r`, and `ro` add
|
|
232
|
+
included, and position does not matter — `1d6cs<2!` and `1d6!cs<2` flag the
|
|
233
|
+
same dice. The side you leave alone keeps the default rule, reading each die's
|
|
234
|
+
natural face, so writing `cf>5` does not disturb `critical`: `1d6cf>5!p` and
|
|
235
|
+
`1d6!pcf>5` both agree with plain `1d6!p` on the appended die.
|
|
236
|
+
|
|
237
|
+
One case stays order-sensitive: an explicit threshold reads a die's current
|
|
238
|
+
value, so modifiers that rewrite it rather than add a die (`!!`, `minN`,
|
|
239
|
+
`maxN`) are judged on whatever that value is when the threshold runs. With
|
|
240
|
+
`[6, 6, 3]`, `1d6cs>10!!` reports no critical while `1d6!!cs>10` reports one,
|
|
241
|
+
judging the compounded 15.
|
|
242
|
+
|
|
223
243
|
### Pools and checks
|
|
224
244
|
|
|
225
245
|
| Notation | Meaning |
|
|
@@ -228,11 +248,19 @@ the Roll20 rule.
|
|
|
228
248
|
| `fT` / `f<cmp>T` | Subtract dice meeting the failure threshold — `10d10>=6f1`, `10d10>=6f<=2` |
|
|
229
249
|
| `<roll> vs <dc>` | PF2e degree of success, with nat-20/nat-1 upgrade and downgrade |
|
|
230
250
|
| `{a, b}khN` | Grouped roll: each sub-roll's subtotal competes as one compound die |
|
|
231
|
-
| `{a+b}khN` | Single-sub-roll group: keep/drop selects across the flattened pool |
|
|
251
|
+
| `{a+b}khN` | Single-sub-roll group: keep/drop selects across the flattened pool — `+`-joined pools only, a literal or any math throws |
|
|
252
|
+
| `{a, b}<cmp>T` | Grouped roll: each sub-roll's subtotal is counted, not its dice — `{2d6, 2d6}>=10` |
|
|
253
|
+
| `{a+b}<cmp>T` | Single-sub-roll group: counts across the flattened pool — `+`-joined pools only, a literal or any math throws |
|
|
232
254
|
|
|
233
255
|
> [!IMPORTANT]
|
|
234
|
-
> Success counting is **terminal** —
|
|
235
|
-
> a parse error. Put the arithmetic inside the
|
|
256
|
+
> Success counting is **terminal** — no modifier or arithmetic applies to it
|
|
257
|
+
> directly, so `10d10>=6 + 2` is a parse error. Put the arithmetic inside the
|
|
258
|
+
> threshold, `10d10>=(4+2)`, or wrap the count in group braces to operate on its
|
|
259
|
+
> result: `{10d10>=6}+2`.
|
|
260
|
+
>
|
|
261
|
+
> Braces also let a second count re-score the same pool — `{4d6>=5}<=2f5`. The
|
|
262
|
+
> outermost count owns the result; see
|
|
263
|
+
> [RollResult](#rollresult) for what that means for the fields you read.
|
|
236
264
|
|
|
237
265
|
> [!WARNING]
|
|
238
266
|
> A bare `d` after a dice expression is rejected: `4d6d1` throws
|
|
@@ -281,7 +309,7 @@ pool.total; // 5 — successes minus failures
|
|
|
281
309
|
| `notation` | `string` | Exactly what you passed in |
|
|
282
310
|
| `expression` | `string` | Normalized form; meta-expressions appear as their resolved values |
|
|
283
311
|
| `rendered` | `string` | Markdown breakdown with per-die markers |
|
|
284
|
-
| `rolls` | `DieResult[]` | Every die in order: `sides`, `result`, `modifiers`, `critical`, `fumble` |
|
|
312
|
+
| `rolls` | `DieResult[]` | Every die in order, meta-expression dice included: `sides`, `result`, `modifiers`, `critical`, `fumble` |
|
|
285
313
|
| `parts` | `RollPart` | Typed evaluation tree mirroring the AST 1:1 |
|
|
286
314
|
| `successes` / `failures` | `number?` | Present only when success counting was used |
|
|
287
315
|
| `degree` / `natural` | `DegreeOfSuccess?` / `number?` | Present only for a top-level `vs` |
|
|
@@ -312,6 +340,12 @@ JSON.stringify(roll('3d6', { rng: createMockRng([4, 2, 6]) }));
|
|
|
312
340
|
> `rolls[]` and the `rolls[]` inside `parts` hold the *same* `DieResult`
|
|
313
341
|
> objects — no deep clone, so annotating a die is visible through both views.
|
|
314
342
|
|
|
343
|
+
When counts nest through braces (`{4d6>=5}<=2f5`), the outermost one owns the
|
|
344
|
+
result: every die is judged against its thresholds alone, so `successes`,
|
|
345
|
+
`failures`, `rolls[].modifiers`, and `rendered` all agree with it, and dice it
|
|
346
|
+
does not count come out untagged. The DC side of a `vs` is the exception — it
|
|
347
|
+
sits outside every pool pass and keeps whatever tagged it.
|
|
348
|
+
|
|
315
349
|
### The parts tree
|
|
316
350
|
|
|
317
351
|
`result.parts` is a 16-variant discriminated union mirroring the AST one-to-one.
|
|
@@ -342,9 +376,22 @@ describe(roll('4d6kh3 + 2', { rng: createMockRng([3, 6, 2, 5]) }).parts);
|
|
|
342
376
|
|
|
343
377
|
Invariants worth relying on: `result.parts.total === result.total`,
|
|
344
378
|
`successCount.total === successes - failures`, and every part's `rolls[]`
|
|
345
|
-
sharing references with `result.rolls`.
|
|
346
|
-
|
|
347
|
-
|
|
379
|
+
sharing references with `result.rolls`.
|
|
380
|
+
|
|
381
|
+
Four variants carry a `rolls[]` of their own, holding the pool the modifier
|
|
382
|
+
produced rather than the one its target rolled — read those instead of
|
|
383
|
+
descending into `target` when you want what was rendered:
|
|
384
|
+
|
|
385
|
+
- `sort` — the target's pool in sorted order. Chained sorts (`4d6s sd`) nest,
|
|
386
|
+
and the outermost one holds the order `rendered` shows.
|
|
387
|
+
- `explode` — the expanded pool. Standard and penetrating explosions append
|
|
388
|
+
dice that exist nowhere under `target`.
|
|
389
|
+
- `reroll` — discarded intermediates and their replacements, both appended
|
|
390
|
+
rather than substituted.
|
|
391
|
+
- `successCount` — the tallied pool.
|
|
392
|
+
|
|
393
|
+
Unlike the pool on a `dice` part, those four keep `'meta'` dice, so filter them
|
|
394
|
+
before counting or displaying.
|
|
348
395
|
|
|
349
396
|
Meta-expressions do **not** appear as nested parts. `(1d4)d6`, `4d6kh(1d2)`, and
|
|
350
397
|
`1d6!>(1d2+3)` surface only their resolved numbers in the owning part; their
|
|
@@ -354,7 +401,9 @@ dice land in `result.rolls` tagged `'meta'` so an audit log can still show them.
|
|
|
354
401
|
|
|
355
402
|
`result.rendered` uses markdown markers, so a Discord bot or chat log can print
|
|
356
403
|
it as-is: `~~n~~` for a die dropped by keep/drop or group selection, `**n**` for
|
|
357
|
-
a success, `__n__` for a failure.
|
|
404
|
+
a success, `__n__` for a failure. The dialect is Discord's, not a universal one
|
|
405
|
+
— Telegram MarkdownV2 rejects `**bold**` and spells strike `~n~` — so expect a
|
|
406
|
+
transform, or emit your own dialect with [`renderBreakdown`](#custom-markers).
|
|
358
407
|
|
|
359
408
|
```typescript
|
|
360
409
|
roll('4d6kh3', { seed: 'demo' }).rendered; // '4d6[1, 6, ~~1~~, 3] = 10'
|
|
@@ -369,6 +418,109 @@ success-count modifiers, but **not** keep/drop — `4d6kh3` renders as `4d6[…]
|
|
|
369
418
|
while `8d6!` renders as `8d6![…]`. Read `result.expression` when you need the
|
|
370
419
|
modifier back.
|
|
371
420
|
|
|
421
|
+
#### Custom markers
|
|
422
|
+
|
|
423
|
+
Markdown is one dialect, and `rendered` bakes it in. `roll-parser/render` is a
|
|
424
|
+
≈1.4 kB entry point that rebuilds the same breakdown from `result.parts` with
|
|
425
|
+
markers you choose — no regex round-trip through the baked string.
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { renderBreakdown } from 'roll-parser/render';
|
|
429
|
+
|
|
430
|
+
const result = roll('4d6kh3', { seed: 'demo' });
|
|
431
|
+
|
|
432
|
+
renderBreakdown(result); // '4d6[1, 6, ~~1~~, 3] = 10'
|
|
433
|
+
renderBreakdown(result, {}); // '4d6[1, 6, 1, 3] = 10'
|
|
434
|
+
renderBreakdown(result, {
|
|
435
|
+
dropped: (_die, text) => `<s>${text}</s>`,
|
|
436
|
+
critical: (_die, text) => `<b>${text}</b>`,
|
|
437
|
+
}); // '4d6[1, <b>6</b>, <s>1</s>, 3] = 10'
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Six slots are available — `dropped`, `success`, `failure`, `critical`,
|
|
441
|
+
`fumble`, and `droppedGroup` for a whole sub-roll struck by `{…}kh1`. An
|
|
442
|
+
omitted slot renders plain, so pass `{}` to strip markup entirely or spread
|
|
443
|
+
`MARKDOWN_MARKS` to keep the rest. `critical` and `fumble` read the
|
|
444
|
+
`DieResult` booleans, which `rendered` has no marker for at all.
|
|
445
|
+
|
|
446
|
+
Marks compose in a fixed order: `critical` then `fumble` innermost, then
|
|
447
|
+
exactly one of `dropped`, `success`, or `failure` — a dropped die is never
|
|
448
|
+
also shown as a success. With no marks the output is byte-identical to
|
|
449
|
+
`result.rendered`; a property test pins that.
|
|
450
|
+
|
|
451
|
+
#### Reading crit and fumble
|
|
452
|
+
|
|
453
|
+
`rendered` carries no marker for a crit or a fumble — those live on the dice.
|
|
454
|
+
`die.critical` and `die.fumble` already reflect any `cs` / `cf` override, so a
|
|
455
|
+
UI that builds its own elements reads the booleans rather than the string.
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
const result = roll('4d20cs>=19', { rng: createMockRng([20, 19, 7, 1]) });
|
|
459
|
+
|
|
460
|
+
const marked = result.rolls
|
|
461
|
+
.filter((die) => !die.modifiers.includes('meta'))
|
|
462
|
+
.map((die) => {
|
|
463
|
+
let text = String(die.result);
|
|
464
|
+
if (die.critical) text = `<b>${text}</b>`;
|
|
465
|
+
if (die.fumble) text = `<i>${text}</i>`;
|
|
466
|
+
return text;
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
marked.join(', '); // '<b>20</b>, <b>19</b>, 7, <i>1</i>'
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
The 19 is critical only because `cs>=19` said so — the booleans are where the
|
|
473
|
+
override lands. Compose the two marks rather than branching on one: overlapping
|
|
474
|
+
thresholds set both, and `1d6cs>=3cf<=4` rolling a 3 does exactly that.
|
|
475
|
+
|
|
476
|
+
The [playground](https://roll-parser.edloidas.io/) does exactly this — its
|
|
477
|
+
[source](https://github.com/edloidas/roll-parser/blob/master/site/src/dice.ts)
|
|
478
|
+
turns the same booleans into CSS classes, a tooltip, and a legend.
|
|
479
|
+
|
|
480
|
+
That loop is right for a plain pool. Four decisions it cannot dodge:
|
|
481
|
+
|
|
482
|
+
- **`'meta'` dice are not yours to show.** They resolve counts, sides, and
|
|
483
|
+
modifier arguments, and they carry ordinary crit flags — in `4d6kh(1d2)` the
|
|
484
|
+
`1d2` rolling a 2 hits its max face and arrives `critical: true`. The filter
|
|
485
|
+
above is not optional.
|
|
486
|
+
- **`'dc'` dice are a judgment call.** They do render — `1d20 vs 2d10` shows
|
|
487
|
+
`1d20[5] vs 2d10[10, 10]` — but they sit outside the roll-side pool while
|
|
488
|
+
still taking the default crit rule, so that `[10, 10]` arrives as two crits
|
|
489
|
+
nobody rolled for. Drop them and a per-die zip has no dice left for a
|
|
490
|
+
two-number bracket; keep them and the DC side lights up on its own faces.
|
|
491
|
+
[`renderBreakdown`](#custom-markers) does not decide this for you — it marks
|
|
492
|
+
every flagged die — but its callbacks receive the die, so gate on the tag.
|
|
493
|
+
For a hand-rolled walk, `result.parts` keeps the two sides apart: the
|
|
494
|
+
`versus` part carries `roll` and `dc` as separate subtrees.
|
|
495
|
+
- **Never splice `rendered` by bracket position.** `{2d6, 2d8}kh1` strikes the
|
|
496
|
+
dropped sub-roll as a whole, `{2d6[6, 6], ~~2d8[1, 1]~~}`, yet its inner dice
|
|
497
|
+
each carry `'dropped'` too — marking every `'dropped'` die into that string
|
|
498
|
+
yields `~~2d8[~~1~~, ~~1~~]~~`. Rebuild with
|
|
499
|
+
[`renderBreakdown`](#custom-markers) instead of patching the baked string.
|
|
500
|
+
- **Density is your call.** The default rule flags the max face of any pool,
|
|
501
|
+
not just d20s — `3d6` rolling a 6 is `critical: true` — so marking it
|
|
502
|
+
unconditionally makes ordinary pools glitter. Gate on `die.sides === 20`, or
|
|
503
|
+
on whatever your table's rule is. `d1` and Fate dice are the exceptions:
|
|
504
|
+
having no exceptional face, they stay `false` unless `cs` / `cf` says so.
|
|
505
|
+
|
|
506
|
+
The claims above, in code:
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
import { renderBreakdown } from 'roll-parser/render';
|
|
510
|
+
|
|
511
|
+
roll('1d6cs>=3cf<=4', { rng: createMockRng([3]) }).rolls[0]?.fumble; // true
|
|
512
|
+
roll('4d6kh(1d2)', { rng: createMockRng([2, 3, 6, 2, 5]) }).rolls[0]?.critical; // true
|
|
513
|
+
roll('1d20 vs 2d10', { rng: createMockRng([5, 10, 10]) }).rolls[2]?.critical; // true
|
|
514
|
+
roll('3dF', { rng: createMockRng([1, 0, -1]) }).rolls[0]?.critical; // false
|
|
515
|
+
roll('{2d6, 2d8}kh1', { rng: createMockRng([6, 6, 1, 1]) }).rendered;
|
|
516
|
+
// '{2d6[6, 6], ~~2d8[1, 1]~~} = 12'
|
|
517
|
+
|
|
518
|
+
// Gating a mark on the `'dc'` tag keeps the DC side unmarked
|
|
519
|
+
renderBreakdown(roll('1d20 vs 2d10', { rng: createMockRng([5, 10, 10]) }), {
|
|
520
|
+
critical: (die, text) => (die.modifiers.includes('dc') ? text : `<b>${text}</b>`),
|
|
521
|
+
}); // '1d20[5] vs 2d10[10, 10] = Critical Failure'
|
|
522
|
+
```
|
|
523
|
+
|
|
372
524
|
## Randomness
|
|
373
525
|
|
|
374
526
|
Every die is drawn through the `RNG` interface — no roll path bypasses the RNG
|
|
@@ -891,7 +1043,8 @@ precedes them. Errors go to stderr; only the result goes to stdout.
|
|
|
891
1043
|
| `1d20` | 85 ns | 164 ns | **0.49 µs** (~2.0M rolls/s) |
|
|
892
1044
|
| `2d6+3` | 98 ns | 215 ns | **0.77 µs** |
|
|
893
1045
|
| `4d6kh3` | 122 ns | 245 ns | **1.2 µs** |
|
|
894
|
-
| `
|
|
1046
|
+
| `4d6sd` | 101 ns | 211 ns | **0.84 µs** |
|
|
1047
|
+
| `10d10>=6f1` | 161 ns | 336 ns | **1.6 µs** |
|
|
895
1048
|
| `100d6` | 82 ns | 168 ns | **2.6 µs** |
|
|
896
1049
|
|
|
897
1050
|
The `roll` column pays for a fresh `SeededRNG` per call, which an injected RNG
|
|
@@ -907,13 +1060,17 @@ Values are **p50**, from
|
|
|
907
1060
|
(`.gc('inner')`), taken as the per-record median of four full
|
|
908
1061
|
`bun run bench:json` passes, every row agreeing within 5% except
|
|
909
1062
|
`lex / 4d6kh3` at 7%. Measured 2026-08-05 on Bun
|
|
910
|
-
1.3.14, Apple M2 Pro, macOS, idle and on AC power.
|
|
911
|
-
`10d10>=6f1` `roll` figures were re-measured 2026-08-06 over three passes
|
|
912
|
-
agreeing within 4%, after per-die `'dc'` tag checks entered the keep/drop and
|
|
913
|
-
success-count paths; the remaining rows re-measured within noise. Read them as
|
|
1063
|
+
1.3.14, Apple M2 Pro, macOS, idle and on AC power. Read them as
|
|
914
1064
|
two significant digits: another machine shifts every row, and a busy one
|
|
915
1065
|
inflates the heavy cases most.
|
|
916
1066
|
|
|
1067
|
+
The `4d6sd` row and the `10d10>=6f1` `roll` figure come from a later five-pass
|
|
1068
|
+
measurement on the same machine, after the per-die `'dc'` tag checks were
|
|
1069
|
+
hoisted out of the keep/drop, success-count, and sort loops. That session was
|
|
1070
|
+
not idle and those two read bimodally with roughly 10% between the modes, so
|
|
1071
|
+
take them as a recovered range rather than point values. Every other row is as
|
|
1072
|
+
first measured.
|
|
1073
|
+
|
|
917
1074
|
p50 rather than mean, because the mean here is effectively a GC-pause
|
|
918
1075
|
histogram and swings ±40% between processes. Every bench body is JIT-primed
|
|
919
1076
|
before measurement and pinned to mitata's batched sampling mode, so all cases
|
|
@@ -925,37 +1082,71 @@ mis-times mid-weight cases by 10-30x.
|
|
|
925
1082
|
Run `bun run bench` for the full suite, or `bench:lex` / `bench:parse` /
|
|
926
1083
|
`bench:evaluate` / `bench:roll` for one stage. Cross-library numbers live in
|
|
927
1084
|
the [competitor suite](https://github.com/edloidas/roll-parser/tree/master/bench/competitors)
|
|
928
|
-
(`bun run bench:competitors`).
|
|
929
|
-
|
|
1085
|
+
(`bun run bench:competitors`). Every push to `master` publishes its p50s to a
|
|
1086
|
+
[trend chart](https://edloidas.io/roll-parser/dev/bench/), and CI comments on
|
|
1087
|
+
any commit that regresses a case past 1.75x. Bundle size is gated in CI by
|
|
1088
|
+
`size-limit`; the budgets live in `package.json`.
|
|
930
1089
|
|
|
931
1090
|
## Known limitations
|
|
932
1091
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1092
|
+
### Notation the parser rejects
|
|
1093
|
+
|
|
1094
|
+
| Form | Throws | Write instead |
|
|
1095
|
+
|------|--------|---------------|
|
|
1096
|
+
| `{2d6+3}kh2`, `{2d6-1d4}kh3`, `{2d6*2}kh2`, `{2d6+0}kh2` | `INVALID_KEEP_DROP_TARGET` | `{2d6}kh2 + 3`, or one sub-roll per term: `{2d6+3, 1d8}kh1` |
|
|
1097
|
+
| `{3d20+5}>=21`, `{2d6-1d4}>=3`, `{2d6*2}>=4`, `{floor(2d6/2)}>=2` | `INVALID_SUCCESS_COUNT_TARGET` | fold the scalar into the threshold: `3d20>=16` |
|
|
1098
|
+
| `{3, 5, 7}>=4` | `INVALID_SUCCESS_COUNT_TARGET` | give the group a pool: `3d6>=4` |
|
|
1099
|
+
| `{{2d6, 2d8}}>=4`, `({2d6, 2d8})>=4`, `{2d6, 2d8}kh1>=4` | `INVALID_SUCCESS_COUNT_TARGET` | count the group directly: `{2d6, 2d8}>=4` |
|
|
1100
|
+
| `{4d6>=5}kh1` | `INVALID_SUCCESS_COUNT_TARGET` | the count is already terminal — select first, `{4d6kh1}>=5` |
|
|
1101
|
+
| `{2d6, 1d8}s` | `INVALID_SORT_TARGET` | flatten to one sub-roll: `{2d6+1d8}s` |
|
|
1102
|
+
| `4d6d1` | `AMBIGUOUS_DICE_CHAIN` | `4d6dl1` to drop a die, `(4d6)d1` for nested dice |
|
|
1103
|
+
|
|
1104
|
+
The first two rows share one cause: a single-sub-roll group compares dice one
|
|
1105
|
+
face at a time, so a literal, a subtracted or scaled pool, or a function wrapper
|
|
1106
|
+
never reaches the comparison. The check is structural rather than arithmetic, so
|
|
1107
|
+
an identity term (`{2d6+0}kh2`) is refused with the rest. Adding pools
|
|
1108
|
+
(`{4d6+2d8}kh3`, `{2d6+1d8}>=5`) is the form these braces exist for and always
|
|
1109
|
+
works.
|
|
1110
|
+
|
|
1111
|
+
A counted group needs at least one die somewhere in it — one real pool is
|
|
1112
|
+
enough, and the other sub-rolls may be literals: `{3, 1d6}>=4` scores both
|
|
1113
|
+
subtotals, the literal `3` included. A pool that is merely empty at run time
|
|
1114
|
+
(`0d6>=4`) still parses and totals 0. Subtotals exist only while the count holds
|
|
1115
|
+
the group directly, which is why the fourth row throws rather than falling back
|
|
1116
|
+
to loose faces; the DC side of a `vs` is exempt (`{1d20 vs {2d6, 2d8}}>=5`
|
|
1117
|
+
counts the d20), since no pool pass tallies a DC.
|
|
1118
|
+
|
|
1119
|
+
Sorting a multi-sub-roll group is refused rather than shipped wrong: spec-correct
|
|
1120
|
+
sorting there is hierarchical — dice within each sub-roll, then sub-rolls by
|
|
1121
|
+
total — and the evaluator only flat-sorts.
|
|
1122
|
+
|
|
1123
|
+
### Rendering and `expression`
|
|
1124
|
+
|
|
1125
|
+
- **Keep/drop does not echo into the render prefix** — see
|
|
1126
|
+
[Rendering](#rendering). The dropped die is still marked; only the `kh3` is
|
|
1127
|
+
missing, and `result.expression` has it.
|
|
944
1128
|
- **`result.expression` substitutes meta-expressions with their resolved
|
|
945
1129
|
values,** so it does not round-trip through `parse` when they are present:
|
|
946
1130
|
`roll('(1d4)d6').expression` is `'4d6'` when the `1d4` rolled 4, and
|
|
947
1131
|
`roll('1d6!>(1d2+3)').expression` is `'1d6!>5'`.
|
|
1132
|
+
- **Adjacent bare modifiers keep a space.** `cs`, `cf`, `s`, and `sd` without a
|
|
1133
|
+
threshold or count end an identifier the lexer would extend into whatever
|
|
1134
|
+
follows, so `expression` and `rendered` separate them the way the input had
|
|
1135
|
+
to: `roll('1d20cs cf').expression` is `'1d20cs cf'`, and `4d6 s kh2` comes
|
|
1136
|
+
back as `'4d6s kh2'`. Everything else stays flush — `4d6cs>4cf<2`.
|
|
948
1137
|
- **Sort flattens additive pools.** `(2d6+1d8)s` renders as one combined sorted
|
|
949
1138
|
list, `(2d6 + 1d8)s[2, 3, 6] = 11`, rather than `2d6[2, 6] + 1d8[3]`. Totals
|
|
950
1139
|
are unaffected; only the breakdown loses pool boundaries.
|
|
951
1140
|
- **Outer parentheses drop when crit thresholds collapse.** `(1d20cs>19)cs=1`
|
|
952
1141
|
reports `expression: '1d20cs>19cs=1'` because chained `cs`/`cf` fold into one
|
|
953
1142
|
node. It re-parses to the same AST; only the text differs.
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1143
|
+
|
|
1144
|
+
### Arithmetic and precedence
|
|
1145
|
+
|
|
1146
|
+
- **Threshold comparisons bind tight.** `1d6!>=5+2` parses as `(1d6!>=5)+2`;
|
|
1147
|
+
parenthesize for a computed threshold, `1d6!>=(5+2)`. Success counts bind the
|
|
1148
|
+
same way but are terminal, so `1d6>=5+2` errors outright — write
|
|
1149
|
+
`1d6>=(5+2)`, or `{1d6>=5}+2` to add to the count itself.
|
|
959
1150
|
- **Division does not floor.** `7/2` totals `3.5`, not `3` — arithmetic is plain
|
|
960
1151
|
IEEE-754 throughout. Wrap it when you need an integer: `floor(7/2)` totals `3`.
|
|
961
1152
|
- **The power operator has no overflow guard.** `2**999` totals `5.357…e+300`.
|
package/dist/cli/format.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/cli/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/cli/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAe9C;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,8EAA8E;IAC9E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,kEAAkE;IAClE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAYpF"}
|
package/dist/cli/format.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { renderBreakdown } from '../render.js';
|
|
2
|
+
const TERMINAL_MARKS = {
|
|
3
|
+
dropped: (_die, text) => `(${text})`,
|
|
4
|
+
success: (_die, text) => `[${text}]`,
|
|
5
|
+
failure: (_die, text) => `{${text}}`,
|
|
6
|
+
droppedGroup: (inner) => `(${inner})`,
|
|
7
|
+
};
|
|
1
8
|
export function formatResult(result, options = {}) {
|
|
2
9
|
const { json = false, verbose = false } = options;
|
|
3
10
|
if (json) {
|
|
@@ -6,12 +13,6 @@ export function formatResult(result, options = {}) {
|
|
|
6
13
|
if (!verbose) {
|
|
7
14
|
return String(result.total);
|
|
8
15
|
}
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
function formatRendered(rendered) {
|
|
12
|
-
return rendered
|
|
13
|
-
.replace(/\*\*(-?\d+)\*\*/g, '[$1]')
|
|
14
|
-
.replace(/__(-?\d+)__/g, '{$1}')
|
|
15
|
-
.replace(/~~([^~]+)~~/g, '($1)');
|
|
16
|
+
return renderBreakdown(result, TERMINAL_MARKS);
|
|
16
17
|
}
|
|
17
18
|
//# sourceMappingURL=format.js.map
|
package/dist/cli/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/cli/format.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/cli/format.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,eAAe,EAAE,MAAM,cAAc,CAAC;AAS9D,MAAM,cAAc,GAAa;IAC/B,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG;IACpC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG;IACpC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG;IACpC,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,GAAG;CACtC,CAAC;AAyBF,MAAM,UAAU,YAAY,CAAC,MAAkB,EAAE,OAAO,GAAkB,EAAE;IAC1E,MAAM,EAAE,IAAI,GAAG,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;IAElD,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO,eAAe,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AACjD,CAAC"}
|
package/dist/evaluator/die.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ import type { DieModifier, DieResult } from '../types.js';
|
|
|
20
20
|
export declare function createDieResult(sides: number, result: number, modifiers: DieModifier[]): DieResult;
|
|
21
21
|
/**
|
|
22
22
|
* Creates a Fate/Fudge die result. Uses `sides = 0` as a sentinel — Fate dice
|
|
23
|
-
* have no max-face concept, so `critical` and `fumble`
|
|
23
|
+
* have no max-face concept, so `critical` and `fumble` start `false` and the
|
|
24
|
+
* default rule never sets them. An explicit `cs`/`cf` threshold still can.
|
|
24
25
|
*/
|
|
25
26
|
export declare function createFateDieResult(result: number, modifiers: DieModifier[]): DieResult;
|
|
26
27
|
//# sourceMappingURL=die.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"die.d.ts","sourceRoot":"","sources":["../../src/evaluator/die.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,WAAW,EAAE,GACvB,SAAS,CAUX;AAED
|
|
1
|
+
{"version":3,"file":"die.d.ts","sourceRoot":"","sources":["../../src/evaluator/die.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,WAAW,EAAE,GACvB,SAAS,CAUX;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,SAAS,CAQvF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"die.js","sourceRoot":"","sources":["../../src/evaluator/die.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,eAAe,CAC7B,KAAa,EACb,MAAc,EACd,SAAwB;IAIxB,OAAO;QACL,KAAK;QACL,MAAM;QACN,SAAS;QACT,QAAQ,EAAE,MAAM,KAAK,KAAK,IAAI,KAAK,GAAG,CAAC;QACvC,MAAM,EAAE,MAAM,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;KAClC,CAAC;AACJ,CAAC;
|
|
1
|
+
{"version":3,"file":"die.js","sourceRoot":"","sources":["../../src/evaluator/die.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,eAAe,CAC7B,KAAa,EACb,MAAc,EACd,SAAwB;IAIxB,OAAO;QACL,KAAK;QACL,MAAM;QACN,SAAS;QACT,QAAQ,EAAE,MAAM,KAAK,KAAK,IAAI,KAAK,GAAG,CAAC;QACvC,MAAM,EAAE,MAAM,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;KAClC,CAAC;AACJ,CAAC;AAOD,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,SAAwB;IAC1E,OAAO;QACL,KAAK,EAAE,CAAC;QACR,MAAM;QACN,SAAS;QACT,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,KAAK;KACd,CAAC;AACJ,CAAC"}
|
package/dist/evaluator/env.d.ts
CHANGED
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
* @module evaluator/env
|
|
9
9
|
*/
|
|
10
10
|
import type { ASTNode } from '../parser/ast.js';
|
|
11
|
+
import type { DieResult, ResolvedCritThreshold } from '../types.js';
|
|
12
|
+
/**
|
|
13
|
+
* A resolved `cs`/`cf` pair, recorded per die so that dice minted later by
|
|
14
|
+
* explode or reroll can be judged by the rule the user declared instead of
|
|
15
|
+
* the built-in default.
|
|
16
|
+
*/
|
|
17
|
+
export type CritRule = {
|
|
18
|
+
readonly success: readonly ResolvedCritThreshold[];
|
|
19
|
+
readonly fail: readonly ResolvedCritThreshold[];
|
|
20
|
+
};
|
|
11
21
|
/**
|
|
12
22
|
* Per-evaluation shared environment (created once, shared across all branches).
|
|
13
23
|
*
|
|
@@ -23,8 +33,20 @@ export type EvalEnv = {
|
|
|
23
33
|
* Set to `true` by `evalSuccessCount`. Propagates through the shared env
|
|
24
34
|
* so `evaluate()` can include `successes`/`failures` fields even when no
|
|
25
35
|
* die was tagged (impossible threshold).
|
|
36
|
+
*
|
|
37
|
+
* ! Set *after* the target is evaluated, not before. `evalSuccessCount` reads
|
|
38
|
+
* ! the pre-set value to learn whether an inner count already tagged its pool
|
|
39
|
+
* ! — moving the assignment back to the top makes it read its own write.
|
|
26
40
|
*/
|
|
27
41
|
hasSuccessCount: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* What a multi-sub-roll group count (`{2d6, 2d6}>=10`) scored. Its units are
|
|
44
|
+
* subtotals, which exist only as throwaway synthetic dice, so these verdicts
|
|
45
|
+
* reach the top-level tally here rather than as tags on a die. Every path
|
|
46
|
+
* that releases `TALLY_FLAGS` releases these too.
|
|
47
|
+
*/
|
|
48
|
+
subtotalSuccesses: number;
|
|
49
|
+
subtotalFailures: number;
|
|
28
50
|
/**
|
|
29
51
|
* `true` while the evaluator is inside a `VersusNode`'s roll or DC
|
|
30
52
|
* sub-evaluation. `evalVersus` rejects nesting via this flag — catches
|
|
@@ -32,6 +54,41 @@ export type EvalEnv = {
|
|
|
32
54
|
* left-chain check.
|
|
33
55
|
*/
|
|
34
56
|
insideVersus: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* `true` once a `vs` has tagged its DC dice `'dc'`. Lets every pool
|
|
59
|
+
* operation skip its per-die `isVersusDc` check outright — the tag exists in
|
|
60
|
+
* a small minority of notation, and the call inside the loop cost 11-38% on
|
|
61
|
+
* keep/drop, success, and sort notation that can never carry one.
|
|
62
|
+
*
|
|
63
|
+
* ! Monotonic: set by `evalVersus`, never cleared. `insideVersus` is not a
|
|
64
|
+
* ! model for it — that flag resets in a `finally`, and it is false exactly
|
|
65
|
+
* ! when the tagged DC dice become visible to the enclosing pool.
|
|
66
|
+
*/
|
|
67
|
+
hasVersusDc: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Crit rule per die, keyed on the `DieResult` object itself. Populated by
|
|
70
|
+
* `applyCritThresholds`; read by explode and reroll so a die they mint
|
|
71
|
+
* inherits the rule of the die it descended from.
|
|
72
|
+
*
|
|
73
|
+
* Out of band rather than a `DieResult` field because `DieResult` is public
|
|
74
|
+
* and serialized. `undefined` until the first `cs`/`cf` node — declared here
|
|
75
|
+
* so the shape stays stable for notation that has none.
|
|
76
|
+
*/
|
|
77
|
+
critRules: WeakMap<DieResult, CritRule> | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Dice minted as explosion continuations by `!` / `!p`, keyed on the
|
|
80
|
+
* `DieResult` object itself. Read by `extractNatural`, which counts them out
|
|
81
|
+
* of the versus primaries — the `'exploded'` tag cannot say it alone, since
|
|
82
|
+
* a compound explode stamps it on the original die it accumulated into.
|
|
83
|
+
*
|
|
84
|
+
* Out of band for the same reason as {@link EvalEnv.critRules}: `DieResult`
|
|
85
|
+
* is public and serialized.
|
|
86
|
+
*
|
|
87
|
+
* ! Only populated while {@link EvalEnv.insideVersus} — nothing outside a
|
|
88
|
+
* ! `vs` reads it, and the `add` per appended die is not free. Any new
|
|
89
|
+
* ! reader must be on a versus path too.
|
|
90
|
+
*/
|
|
91
|
+
explosionDice: WeakSet<DieResult> | undefined;
|
|
35
92
|
/**
|
|
36
93
|
* User-supplied variable map for `@name` / `@{name}` references. Always
|
|
37
94
|
* defined — `evaluate()` defaults to an empty object so lookups can be
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/evaluator/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/evaluator/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpE;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACjD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;;;;OAQG;IACH,eAAe,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;;;;OASG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;IACpD;;;;;;;;;;;;OAYG;IACH,aAAa,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,GAAG,MAAM,CAAC;CAC9C,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CASvF;AAED,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAEvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/evaluator/env.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/evaluator/env.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AA0G9C,MAAM,UAAU,UAAU,CAAC,GAAY,EAAE,KAAa,EAAE,QAAyB;IAC/E,IAAI,GAAG,CAAC,eAAe,GAAG,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;QAC9C,MAAM,IAAI,cAAc,CACtB,oBAAoB,GAAG,CAAC,eAAe,GAAG,KAAK,qBAAqB,GAAG,CAAC,OAAO,EAAE,EACjF,qBAAqB,EACrB,QAAQ,CACT,CAAC;IACJ,CAAC;IACD,GAAG,CAAC,eAAe,IAAI,KAAK,CAAC;AAC/B,CAAC;AAGD,MAAM,UAAU,SAAS,CAAC,GAAY,EAAE,QAAyB;IAC/D,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluator/evaluator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAiB,cAAc,EAAuC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluator/evaluator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAiB,cAAc,EAAuC,MAAM,cAAc,CAAC;AAElG,OAAO,KAAK,EACV,OAAO,EAmBR,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,KAAK,EAIV,SAAS,EACT,eAAe,EAKf,UAAU,EACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAK9C,OAAO,EAKL,8BAA8B,EAC/B,MAAM,wBAAwB,CAAC;AAWhC,OAAO,EAGL,6BAA6B,EAC9B,MAAM,uBAAuB,CAAC;AAM/B,OAAO,EAAE,cAAc,EAAE,CAAC;AAM1B;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAmCvC,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,CAAC;AAMzE;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;OAKG;IACH,cAAc,CAAC,EAAE;QACf,MAAM,EAAE,eAAe,CAAC;QACxB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;QAC5B,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AA0lDF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,GAAE,eAAoB,GAAG,UAAU,CAiE1F"}
|