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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [3.0.0-beta.0] - 2026-07-07
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **BREAKING:** package is now ESM-only. The `require` export condition shipped a file that was actually ESM (`bun build --target node` emits ESM), so it only ever worked via Node ≥22 `require(esm)` — which continues to work. Bundles are built with `--target node` (no `// @bun` pragma) and include linked sourcemaps; type declarations now resolve under `moduleResolution: node16/nodenext`, validated by `@arethetypeswrong/cli` + `publint` in CI ([#119](https://github.com/edloidas/roll-parser/issues/119))
|
|
15
|
+
- **BREAKING:** `cs`/`cf` are now independent overrides (Roll20 semantics). `1d20cf<3` keeps the default nat-20 crit instead of clearing it; `1d20cs>=19` keeps the default nat-1 fumble. The previous replace semantics required chaining `cscf<3` to preserve defaults ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
16
|
+
- **BREAKING:** bare `NdXdY` dice chains (e.g. `4d6d1`) are rejected at parse time with `AMBIGUOUS_DICE_CHAIN` instead of silently parsing as `(4d6)d1` — roll 4d6, use the result as a count of d1 dice — which is almost never intended. Use `4d6dl1` to drop dice or `(4d6)d1` for nested dice ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
17
|
+
- **BREAKING:** non-finite totals (`Infinity`/`NaN`, e.g. `2**1024`) now throw `NON_FINITE_RESULT` instead of returning a non-finite `total` ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
18
|
+
- **BREAKING:** `1d1` no longer reports `fumble: true` — a d1 always rolls 1, so it is neither critical nor fumble, mirroring the existing `critical` guard ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Rich structured `RollResult.parts` output — a 16-variant discriminated union mirroring the AST 1:1, always built during evaluation. Each part carries its sub-total, resolved thresholds/specs (`ModifierSpec`, `ResolvedComparePoint`, `ResolvedCritThreshold`), source span, and (for dice parts) the same `DieResult` objects as `RollResult.rolls`. Group parts under keep/drop carry `keptIndices`; versus parts carry `degree`; successCount parts carry `successes`/`failures` with `total === successes - failures`. JSON-serializable with `parts.total === result.total` guaranteed ([#84](https://github.com/edloidas/roll-parser/issues/84))
|
|
23
|
+
- Source spans: lexer tokens carry an exclusive `end` offset, and every parser-produced AST node carries `start`/`end` offsets (`NodeSpan`) into the notation. `EvaluatorError` is stamped with the span of the tightest failing sub-expression, and the CLI caret uses it — `2d6+1d0+3` points at `1d0` ([#120](https://github.com/edloidas/roll-parser/issues/120))
|
|
24
|
+
- Root exports for the previously missing `GroupNode`/`SortNode`/`CritThresholdNode` types and `isGroup`/`isSort`/`isCritThreshold` guards ([#120](https://github.com/edloidas/roll-parser/issues/120))
|
|
25
|
+
- Benchmark suite (`bun bench`, mitata) covering parse, evaluate, and end-to-end roll paths ([#119](https://github.com/edloidas/roll-parser/issues/119))
|
|
26
|
+
- Coverage thresholds enforced in CI (100% functions, ≥95% lines per file); coverage was previously disabled in `bunfig.toml` and unmeasurable even with `--coverage` ([#119](https://github.com/edloidas/roll-parser/issues/119))
|
|
27
|
+
- CLI prints the notation with a caret under the error position for lexer/parser errors ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
28
|
+
- Lexer hints at modifier splits for merged identifiers: `4d6khs` now suggests `kh1s` ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
29
|
+
- `"./package.json"` export and `CHANGELOG.md` in the published tarball ([#119](https://github.com/edloidas/roll-parser/issues/119))
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Dropped group sub-rolls no longer leak `success`/`failure` tags into `RollResult.successes`/`failures` — `{2d6>=4, 2d6>=4}kh1` counted successes from the dropped sub-roll, and the rendered output showed `**bold**` markers inside `~~strikethrough~~` ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
34
|
+
- `SeededRNG.nextInt` no longer silently truncates ranges above 2^32 — a 53-bit two-draw sampling path covers up to `Number.MAX_SAFE_INTEGER`, and wider ranges throw `RangeError` instead of degrading ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
35
|
+
- Deeply nested input (e.g. 20,000 parens) throws a typed `MAX_DEPTH_EXCEEDED` `ParseError` instead of an uncaught `RangeError` stack overflow that broke the `isRollParserError` contract ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
36
|
+
- PF2e nat-20/nat-1 upgrade now survives standard (`!`) and penetrating (`!p`) explosion under `vs` — explosion continuation dice no longer make `extractNatural` bail as ambiguous, matching the compound (`!!`) behavior ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
37
|
+
- Entire `package.json` (scripts, devDependencies, author email) is no longer embedded in every bundle via the `VERSION` import ([#119](https://github.com/edloidas/roll-parser/issues/119))
|
|
38
|
+
- CLI verbose output no longer leaks markdown for dropped group sub-rolls — `{1d8, 1d10}kh1 -v` printed `{~~1d8[2]~~, ...}` because only single-number spans were rewritten ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
39
|
+
- Lexer errors report full code points for astral characters (`🎲` instead of a lone surrogate `�`); parser `expect()` errors name symbols and say `end of input` instead of `Expected RPAREN but got ''` ([#118](https://github.com/edloidas/roll-parser/issues/118))
|
|
40
|
+
- Parser now rejects `SuccessCount` wrapped in meta-expression positions — modifier count, dice count/sides (infix and prefix), Fate/percentile dice count, SuccessCount threshold and bare `fN` value, and compare-point values used by Explode/Reroll. `mergeMetaRolls` also strips `success`/`failure` modifier tags on meta-forwarded dice as defense-in-depth ([#69](https://github.com/edloidas/roll-parser/issues/69))
|
|
41
|
+
- Parser now rejects `Versus` wrapped in the same meta-expression positions so a PF2e `vs` outcome cannot be silently dropped by `mergeMetaRolls`, and the `parseVersus` chain guard unwraps `Grouped` so `(1d20 vs 15) vs 10` throws `NESTED_VERSUS` at parse time instead of at eval ([#70](https://github.com/edloidas/roll-parser/issues/70))
|
|
42
|
+
- Single-sub-roll Group passthrough on `cs`/`cf` no longer smuggles a buried multi-sub Group past the parser. `{{1d20, 1d20}kh1}cs>18`, `{{1d6, 2d8}+0}cs>5`, `{abs({1d6, 2d8})}cs>5`, and `{floor({1d6, 2d8}/1)}cs>5` now reject with `INVALID_CRIT_THRESHOLD_TARGET` via a new `containsMultiSubGroup` deep-walk in `rejectGroupTarget` ([#109](https://github.com/edloidas/roll-parser/issues/109))
|
|
43
|
+
- Single-sub-roll Group passthrough no longer flips Fate `+1` faces to fumble. `{4dF+1d6}cf`, `({4dF+1d6})cf`, and `{abs(4dF)}cf` now reject via a new `deepContainsFatePool` mirror of `deepContainsDicePool` used inside `containsFatePool`'s Group case ([#109](https://github.com/edloidas/roll-parser/issues/109))
|
|
44
|
+
- Single-sub-roll Group passthrough no longer smuggles `Versus` past the meta-expression rejection, so `{1d20 vs 15}cs>18`, `{1d20 vs 15}s`, `{1d20 vs 15}kh1`, `{1+(1d20 vs 15)}cs>18`, `{abs(1d20 vs 15)}cs>18`, and `4d6>={1d20 vs 15}` now throw `NESTED_VERSUS`. `parseModifier` also gained the `rejectVersusTarget` call it was missing — closing a pre-existing inconsistency where `(1d20 vs 15)kh1` rejected with a different error code while the brace form silently dropped `degree`/`natural` ([#109](https://github.com/edloidas/roll-parser/issues/109))
|
|
45
|
+
|
|
46
|
+
### Notes
|
|
47
|
+
|
|
48
|
+
These are intentional behaviours documented for the first time, not changes — semantics are unchanged.
|
|
49
|
+
|
|
50
|
+
- Sort flattens additive pools: `(2d6+1d8)s` renders as one combined sorted list rather than per-pool brackets. Same applies to wrapped pools like `floor(4d6)s`. Totals are preserved ([#96](https://github.com/edloidas/roll-parser/issues/96))
|
|
51
|
+
- Outer parens drop from `result.expression` when chained `cs`/`cf` thresholds collapse: `(1d20cs>19)cs=1` renders as `1d20cs>19cs=1`. Re-parses to the same AST; only the textual form differs ([#96](https://github.com/edloidas/roll-parser/issues/96))
|
|
52
|
+
|
|
53
|
+
## [3.0.0-alpha.0] - 2026-04-20
|
|
54
|
+
|
|
55
|
+
First alpha of the v3 rewrite. Stage 1 (core engine) is complete; Stage 2 (dice mechanics) is largely in place.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
Core engine (Stage 1):
|
|
60
|
+
|
|
61
|
+
- Lexer and token system ([#3](https://github.com/edloidas/roll-parser/issues/3))
|
|
62
|
+
- Pratt parser and AST types ([#4](https://github.com/edloidas/roll-parser/issues/4))
|
|
63
|
+
- Seedable RNG system with `SeededRNG` (xorshift128) and `MockRNG` helpers ([#5](https://github.com/edloidas/roll-parser/issues/5))
|
|
64
|
+
- AST evaluator with keep/drop modifiers (`kh`, `kl`, `dh`, `dl`) ([#6](https://github.com/edloidas/roll-parser/issues/6))
|
|
65
|
+
- Public `roll(notation, options)` API with `RollResult` / `DieResult` types ([#7](https://github.com/edloidas/roll-parser/issues/7))
|
|
66
|
+
- `roll-parser` CLI with `--help`, `--version`, `--verbose`, `--seed` flags; ESM + CJS dual build ([#8](https://github.com/edloidas/roll-parser/issues/8))
|
|
67
|
+
|
|
68
|
+
Dice mechanics (Stage 2):
|
|
69
|
+
|
|
70
|
+
- Percentile dice notation (`d%`) ([#35](https://github.com/edloidas/roll-parser/issues/35))
|
|
71
|
+
- Fate/Fudge dice (`dF`) ([#36](https://github.com/edloidas/roll-parser/issues/36))
|
|
72
|
+
- Math functions: `floor()`, `ceil()`, `round()`, `abs()`, `max()`, `min()` ([#37](https://github.com/edloidas/roll-parser/issues/37))
|
|
73
|
+
- Exploding dice — `!` (standard), `!!` (compound), `!p` (penetrating) — with optional compare points ([#38](https://github.com/edloidas/roll-parser/issues/38))
|
|
74
|
+
- Reroll mechanics (`r`, `ro`) with compare points ([#39](https://github.com/edloidas/roll-parser/issues/39))
|
|
75
|
+
- Success counting / dice pools with `>`, `>=`, `<`, `<=`, `=` operators and `fN` failure tagging ([#40](https://github.com/edloidas/roll-parser/issues/40))
|
|
76
|
+
- PF2e Degrees of Success via the `vs` keyword, with nat-20/1 upgrade/downgrade ([#41](https://github.com/edloidas/roll-parser/issues/41))
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **BREAKING:** complete rewrite from v2.x. Public API, semantics, and notation coverage are not compatible with the 2.x line. Pin to `roll-parser@2.3.2` for the legacy implementation.
|
|
81
|
+
- Public API surface trimmed: `Lexer`, `Parser`, `lex`, `TokenType`, `Token`, and mock RNG exports removed from the root entry. `createMockRng` moved to the `roll-parser/testing` subpath. Added typed `RollParserError` base class with `RollParserErrorCode` union and `isRollParserError()` type guard ([#24](https://github.com/edloidas/roll-parser/issues/24))
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- Modifier chaining now matches the Roll20 standard: chained modifiers are flattened and each applies independently to the full pool; drop sets are unioned ([#12](https://github.com/edloidas/roll-parser/issues/12))
|
|
86
|
+
- Dice count safety limit via `maxDice` option (default 10,000), enforced across the whole expression to prevent DoS via additive groups like `5000d6+5000d6` ([#19](https://github.com/edloidas/roll-parser/issues/19))
|
|
87
|
+
- Parser and evaluator correctness: duplicate `kept` modifier entries, implicit modifier count defaulting to 1 (`4d6kh` → `4d6kh1`), `critical` flag suppression when `sides === 1`, negative `--seed` CLI values ([#21](https://github.com/edloidas/roll-parser/issues/21))
|
|
88
|
+
|
|
89
|
+
[Unreleased]: https://github.com/edloidas/roll-parser/compare/v3.0.0-beta.0...HEAD
|
|
90
|
+
[3.0.0-beta.0]: https://github.com/edloidas/roll-parser/releases/tag/v3.0.0-beta.0
|
|
91
|
+
[3.0.0-alpha.0]: https://github.com/edloidas/roll-parser/releases/tag/v3.0.0-alpha.0
|
package/README.md
CHANGED
|
@@ -13,34 +13,32 @@ TypeScript-first, Bun-optimized, Pratt parser architecture.
|
|
|
13
13
|
|
|
14
14
|
## Status
|
|
15
15
|
|
|
16
|
-
> **v3 Alpha** —
|
|
17
|
-
>
|
|
16
|
+
> **v3 Alpha** — Stages 1–3 (core engine, system compatibility, advanced
|
|
17
|
+
> features incl. rich structured `parts` output) are implemented with 1,000+
|
|
18
|
+
> tests, 100% function coverage, and a property-based test suite.
|
|
18
19
|
>
|
|
19
20
|
> For production use, install [v2.3.2](https://www.npmjs.com/package/roll-parser/v/2.3.2).
|
|
20
21
|
|
|
21
22
|
## Features
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- Variables: `1d20+@str`, `1d20+@{modifier}`
|
|
42
|
-
- Grouped rolls: `{1d8, 1d10}kh1`
|
|
43
|
-
- Rich JSON output with roll breakdown
|
|
24
|
+
- **Basic dice**: `2d6`, `d20`, `4d6+4`, percentile `d%`, Fate/Fudge `4dF`
|
|
25
|
+
- **Full arithmetic**: `+`, `-`, `*`, `/`, `%`, `**` (also `^`), parentheses
|
|
26
|
+
- **Computed dice**: `(1+1)d(3*2)`, `(1d4)d6`
|
|
27
|
+
- **Keep/Drop**: `4d6kh3`, `2d20kl1`, `4d6dl1`, shorthand `4d6k3`
|
|
28
|
+
- **Exploding dice**: standard `1d6!`, compound `1d6!!`, penetrating `1d6!p`, with compare points `1d6!>=5`
|
|
29
|
+
- **Rerolls**: recursive `2d6r<2`, once `2d6ro<3`
|
|
30
|
+
- **Success counting**: `10d10>=6`, with failure tagging `10d10>=6f1`
|
|
31
|
+
- **PF2e degrees of success**: `1d20+10 vs 25` with nat-20/nat-1 upgrade/downgrade
|
|
32
|
+
- **Math functions**: `floor()`, `ceil()`, `round()`, `abs()`, `max()`, `min()`
|
|
33
|
+
- **Variables**: `1d20+@str`, `1d20+@{sneak attack}`
|
|
34
|
+
- **Grouped rolls**: `{1d8, 1d10}kh1`, `{4d10+5d6}kh2`
|
|
35
|
+
- **Sorting**: `4d6s`, `4d6sd` (display-only)
|
|
36
|
+
- **Crit thresholds**: `1d20cs>=19`, `1d20cf<3` (display-only, independent overrides)
|
|
37
|
+
- **Structured `parts` output**: every result carries a typed evaluation tree mirroring the AST — per-sub-expression totals, dice, resolved thresholds — for chat-log/character-sheet rendering without re-parsing
|
|
38
|
+
- **Source spans**: every AST node and `RollPart` carries `start`/`end` offsets into the notation; evaluator errors point at the exact failing sub-expression
|
|
39
|
+
- **Seedable RNG** (xorshift128) for reproducible rolls, mock RNG for tests
|
|
40
|
+
- **Typed errors** with stable `code`s and input positions
|
|
41
|
+
- **Safety limits**: `maxDice`, `maxExplodeIterations`, `maxRerollIterations`, parse depth cap
|
|
44
42
|
|
|
45
43
|
## Installation
|
|
46
44
|
|
|
@@ -49,30 +47,168 @@ bun add roll-parser
|
|
|
49
47
|
npm install roll-parser
|
|
50
48
|
```
|
|
51
49
|
|
|
50
|
+
The package is **ESM-only**. On Node.js ≥ 22 (required), CommonJS consumers
|
|
51
|
+
can still `require('roll-parser')` via `require(esm)`.
|
|
52
|
+
|
|
52
53
|
## Usage
|
|
53
54
|
|
|
54
55
|
```typescript
|
|
55
56
|
import { roll } from 'roll-parser';
|
|
56
57
|
|
|
57
58
|
const result = roll('4d6kh3');
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
result.total; // e.g. 14
|
|
60
|
+
result.notation; // '4d6kh3'
|
|
61
|
+
result.rendered; // '4d6kh3[3, 6, ~~2~~, 5] = 14'
|
|
62
|
+
result.rolls; // per-die results with kept/dropped/critical flags
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Options
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { roll, SeededRNG } from 'roll-parser';
|
|
69
|
+
|
|
70
|
+
roll('4d6', { seed: 'character-1' }); // reproducible rolls
|
|
71
|
+
roll('4d6', { rng: new SeededRNG(42) }); // custom RNG instance (takes precedence)
|
|
72
|
+
|
|
73
|
+
roll('1d20+@str', {
|
|
74
|
+
context: { str: 4 }, // variable values
|
|
75
|
+
onMissingVariable: 'zero', // or 'throw' (default)
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
roll('100d6!', {
|
|
79
|
+
maxDice: 1_000, // total dice cap, default 10,000
|
|
80
|
+
maxExplodeIterations: 100, // per-die explosion cap, default 1,000
|
|
81
|
+
maxRerollIterations: 100, // per-die reroll cap, default 1,000
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Results
|
|
86
|
+
|
|
87
|
+
| Field | Type | Notes |
|
|
88
|
+
|-------|------|-------|
|
|
89
|
+
| `total` | `number` | Final computed total (always finite — overflow throws) |
|
|
90
|
+
| `notation` | `string` | Original input |
|
|
91
|
+
| `expression` | `string` | Normalized form; meta-expressions render as their resolved values |
|
|
92
|
+
| `rendered` | `string` | Markdown breakdown: `~~n~~` dropped, `**n**` success, `__n__` failure |
|
|
93
|
+
| `rolls` | `DieResult[]` | Every die: `sides`, `result`, `modifiers`, `critical`, `fumble` |
|
|
94
|
+
| `parts` | `RollPart` | Typed evaluation tree mirroring the AST 1:1 (see below) |
|
|
95
|
+
| `successes` / `failures` | `number?` | Present when success counting was used |
|
|
96
|
+
| `degree` / `natural` | `DegreeOfSuccess?` / `number?` | Present for top-level `vs` expressions |
|
|
97
|
+
|
|
98
|
+
### Structured breakdown (`parts`)
|
|
99
|
+
|
|
100
|
+
`result.parts` is a discriminated union (16 variants — `dice`, `binaryOp`,
|
|
101
|
+
`modifier`, `versus`, …) where each part holds its own sub-total, resolved
|
|
102
|
+
thresholds/specs, and — for dice parts — the same `DieResult` objects as
|
|
103
|
+
`result.rolls`. JSON-serializable; `parts.total === result.total` always.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const r = roll('4d6kh3 + 2');
|
|
107
|
+
// r.parts = { type: 'binaryOp', operator: '+', total: 16,
|
|
108
|
+
// left: { type: 'modifier', specs: [{ kind: 'keep', selector: 'highest', count: 3 }],
|
|
109
|
+
// target: { type: 'dice', count: 4, sides: 6, rolls: [...], ... }, total: 14 },
|
|
110
|
+
// right: { type: 'literal', value: 2, total: 2 } }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Meta-expressions (`(1d4)d6` counts, computed thresholds) appear as resolved
|
|
114
|
+
numbers in the owning part; their dice live in `result.rolls` tagged `'meta'`.
|
|
115
|
+
|
|
116
|
+
### Error handling
|
|
117
|
+
|
|
118
|
+
All errors extend `RollParserError` with a typed `code`
|
|
119
|
+
(e.g. `DICE_LIMIT_EXCEEDED`, `DIVISION_BY_ZERO`, `AMBIGUOUS_DICE_CHAIN`);
|
|
120
|
+
lexer and parser errors also carry the input `position`.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { isRollParserError } from 'roll-parser';
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
roll(userInput);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (isRollParserError(error)) console.error(error.code, error.message);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Testing your integrations
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { createMockRng } from 'roll-parser/testing';
|
|
136
|
+
|
|
137
|
+
const result = roll('3d6', { rng: createMockRng([4, 2, 6]) });
|
|
138
|
+
result.total; // 12 — throws if the roll consumes more values than provided
|
|
60
139
|
```
|
|
61
140
|
|
|
62
141
|
### CLI
|
|
63
142
|
|
|
64
143
|
```bash
|
|
65
144
|
roll-parser 2d6+3
|
|
66
|
-
roll-parser 4d6kh3
|
|
145
|
+
roll-parser 4d6kh3 --verbose --seed test
|
|
67
146
|
roll-parser --help
|
|
68
147
|
```
|
|
69
148
|
|
|
149
|
+
## Notation reference
|
|
150
|
+
|
|
151
|
+
| Notation | Meaning |
|
|
152
|
+
|----------|---------|
|
|
153
|
+
| `NdX`, `dX`, `Nd%`, `NdF` | Roll N X-sided / percentile / Fate dice |
|
|
154
|
+
| `khN` `klN` `dhN` `dlN` (`kN`) | Keep/drop highest/lowest N (default 1) |
|
|
155
|
+
| `!` `!!` `!p` | Explode: standard / compound / penetrating (optional `>=T` etc.) |
|
|
156
|
+
| `r<T`, `ro<T` | Reroll recursively / once while condition matches |
|
|
157
|
+
| `>=T`, `>T`, `=T`, `<T`, `<=T` | Count successes against threshold (terminal) |
|
|
158
|
+
| `fT`, `f<T` | Failure threshold after success counting |
|
|
159
|
+
| `vs DC` | PF2e degree of success against a DC |
|
|
160
|
+
| `s`, `sd` | Sort dice ascending/descending (display-only) |
|
|
161
|
+
| `csT`, `cfT` | Override crit/fumble display thresholds independently |
|
|
162
|
+
| `@name`, `@{any name}` | Variable from `context` |
|
|
163
|
+
| `{a, b}khN` | Grouped rolls; keep/drop by sub-roll subtotal |
|
|
164
|
+
| `floor() ceil() round() abs() max() min()` | Math functions |
|
|
165
|
+
|
|
166
|
+
## Performance
|
|
167
|
+
|
|
168
|
+
Run `bun bench` for the mitata suite (parse, evaluate, and end-to-end paths).
|
|
169
|
+
Indicative numbers on a modern x86-64 container (Bun 1.3):
|
|
170
|
+
|
|
171
|
+
| Operation | Time |
|
|
172
|
+
|-----------|------|
|
|
173
|
+
| `parse('2d6+3')` | ~0.4 µs |
|
|
174
|
+
| `roll('1d20+5')` | ~2.5 µs (~400k rolls/s) |
|
|
175
|
+
| `roll('4d6kh3')` | ~5 µs |
|
|
176
|
+
| `evaluate(parse('100d6'))` | ~17 µs |
|
|
177
|
+
|
|
178
|
+
Numbers include building the structured `parts` tree (always on).
|
|
179
|
+
|
|
70
180
|
## Known Limitations
|
|
71
181
|
|
|
72
|
-
- **`4d6d1`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
182
|
+
- **`4d6d1` is a parse error, not "drop 1".** The bare `d` token is the dice
|
|
183
|
+
operator, and `4d6d1` reading as "roll 4d6, use the result as a count of
|
|
184
|
+
d1 dice" is a silent trap — so chaining `d` directly onto a dice expression
|
|
185
|
+
throws `AMBIGUOUS_DICE_CHAIN`. Use `4d6dl1` to drop dice, or `(4d6)d1` for
|
|
186
|
+
nested dice.
|
|
187
|
+
- **Threshold comparisons bind tight.** In explode and reroll thresholds,
|
|
188
|
+
`1d6!>=5+2` parses as `(1d6!>=5)+2` — the comparison binds to the dice
|
|
189
|
+
pool, not the arithmetic after it. For a computed threshold, parenthesize:
|
|
190
|
+
`1d6!>=(5+2)`. Success-count thresholds bind the same way, but wrapping the
|
|
191
|
+
resulting `SuccessCountNode` is a parse error (terminal by design), so
|
|
192
|
+
`1d6>=5+2` throws — parenthesize: `1d6>=(5+2)`.
|
|
193
|
+
- **Meta-expressions in `result.expression` are substituted with their
|
|
194
|
+
resolved values.** Dice counts, sides, modifier counts, and threshold
|
|
195
|
+
expressions are rendered as the integer they evaluated to — so
|
|
196
|
+
`result.expression` does not round-trip through `parse` when
|
|
197
|
+
meta-expressions are present.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
roll('(1d4)d6').expression; // '2d6' (if 1d4 rolled 2)
|
|
201
|
+
roll('1d6!>(1d2+3)').expression; // '1d6!>5'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- **Sort flattens additive pools and drops per-pool brackets.** `(2d6+1d8)s`
|
|
205
|
+
renders the combined pool as one sorted list — `(2d6 + 1d8)s[3, 4, 5]` —
|
|
206
|
+
rather than the per-pool form `2d6[3, 4] + 1d8[5]`. Totals are preserved;
|
|
207
|
+
only the rendered breakdown loses pool boundaries.
|
|
208
|
+
- **Outer parens drop from `result.expression` after threshold collapse.**
|
|
209
|
+
`(1d20cs>19)cs=1` evaluates with `result.expression = '1d20cs>19cs=1'`
|
|
210
|
+
because chained `cs` thresholds collapse into a single node. The notation
|
|
211
|
+
re-parses to the same AST; only the textual form differs.
|
|
76
212
|
|
|
77
213
|
## License
|
|
78
214
|
|
package/dist/cli/args.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA0CzD"}
|
package/dist/cli/format.d.ts
CHANGED
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;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/cli/format.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAMzE"}
|