roll-parser 2.3.2 → 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 (109) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +175 -170
  3. package/dist/cli/args.d.ts +35 -0
  4. package/dist/cli/args.d.ts.map +1 -0
  5. package/dist/cli/format.d.ts +20 -0
  6. package/dist/cli/format.d.ts.map +1 -0
  7. package/dist/cli/index.d.ts +8 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli.js +2608 -0
  10. package/dist/cli.js.map +28 -0
  11. package/dist/errors.d.ts +42 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/evaluator/evaluator.d.ts +121 -0
  14. package/dist/evaluator/evaluator.d.ts.map +1 -0
  15. package/dist/evaluator/index.d.ts +8 -0
  16. package/dist/evaluator/index.d.ts.map +1 -0
  17. package/dist/evaluator/modifiers/compare.d.ts +15 -0
  18. package/dist/evaluator/modifiers/compare.d.ts.map +1 -0
  19. package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
  20. package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
  21. package/dist/evaluator/modifiers/explode.d.ts +48 -0
  22. package/dist/evaluator/modifiers/explode.d.ts.map +1 -0
  23. package/dist/evaluator/modifiers/keep-drop.d.ts +44 -0
  24. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -0
  25. package/dist/evaluator/modifiers/reroll.d.ts +33 -0
  26. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -0
  27. package/dist/evaluator/modifiers/sort.d.ts +23 -0
  28. package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
  29. package/dist/evaluator/modifiers/success-count.d.ts +28 -0
  30. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -0
  31. package/dist/index.d.ts +20 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +2560 -0
  34. package/dist/index.js.map +26 -0
  35. package/dist/lexer/lexer.d.ts +90 -0
  36. package/dist/lexer/lexer.d.ts.map +1 -0
  37. package/dist/lexer/tokens.d.ts +107 -0
  38. package/dist/lexer/tokens.d.ts.map +1 -0
  39. package/dist/parser/ast.d.ts +368 -0
  40. package/dist/parser/ast.d.ts.map +1 -0
  41. package/dist/parser/parser.d.ts +145 -0
  42. package/dist/parser/parser.d.ts.map +1 -0
  43. package/dist/rng/index.d.ts +8 -0
  44. package/dist/rng/index.d.ts.map +1 -0
  45. package/dist/rng/mock.d.ts +36 -0
  46. package/dist/rng/mock.d.ts.map +1 -0
  47. package/dist/rng/seeded.d.ts +40 -0
  48. package/dist/rng/seeded.d.ts.map +1 -0
  49. package/dist/rng/types.d.ts +24 -0
  50. package/dist/rng/types.d.ts.map +1 -0
  51. package/dist/roll.d.ts +51 -0
  52. package/dist/roll.d.ts.map +1 -0
  53. package/dist/testing.d.ts +11 -0
  54. package/dist/testing.d.ts.map +1 -0
  55. package/dist/testing.js +41 -0
  56. package/dist/testing.js.map +11 -0
  57. package/dist/types.d.ts +246 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/package.json +66 -43
  60. package/src/cli/args.ts +73 -0
  61. package/src/cli/format.ts +49 -0
  62. package/src/cli/index.ts +92 -0
  63. package/src/errors.ts +90 -0
  64. package/src/evaluator/evaluator.ts +1552 -0
  65. package/src/evaluator/index.ts +14 -0
  66. package/src/evaluator/modifiers/compare.ts +29 -0
  67. package/src/evaluator/modifiers/crit-threshold.ts +59 -0
  68. package/src/evaluator/modifiers/explode.ts +219 -0
  69. package/src/evaluator/modifiers/keep-drop.ts +151 -0
  70. package/src/evaluator/modifiers/reroll.ts +165 -0
  71. package/src/evaluator/modifiers/sort.ts +30 -0
  72. package/src/evaluator/modifiers/success-count.ts +59 -0
  73. package/src/index.ts +90 -0
  74. package/src/lexer/lexer.ts +374 -0
  75. package/src/lexer/tokens.ts +187 -0
  76. package/src/parser/ast.ts +628 -0
  77. package/src/parser/parser.ts +1239 -0
  78. package/src/rng/index.ts +8 -0
  79. package/src/rng/mock.ts +65 -0
  80. package/src/rng/seeded.ts +157 -0
  81. package/src/rng/types.ts +25 -0
  82. package/src/roll.ts +74 -0
  83. package/src/testing.ts +16 -0
  84. package/src/types.ts +237 -0
  85. package/bin/help.txt +0 -19
  86. package/bin/roll-parser.js +0 -23
  87. package/dist/roll-parser.js +0 -697
  88. package/dist/roll-parser.min.js +0 -1
  89. package/index.js +0 -24
  90. package/src/complex/parse.js +0 -37
  91. package/src/complex/parseAndRoll.js +0 -21
  92. package/src/complex/parseAndRollClassic.js +0 -20
  93. package/src/complex/parseAndRollSimple.js +0 -19
  94. package/src/complex/parseAndRollWod.js +0 -20
  95. package/src/complex/parseClassicRoll.js +0 -22
  96. package/src/complex/parseSimpleRoll.js +0 -21
  97. package/src/complex/parseWodRoll.js +0 -21
  98. package/src/converter.js +0 -45
  99. package/src/grammar.js +0 -14
  100. package/src/mapper.js +0 -50
  101. package/src/normalizer.js +0 -51
  102. package/src/object/Result.js +0 -23
  103. package/src/object/Roll.js +0 -26
  104. package/src/object/Type.js +0 -5
  105. package/src/object/WodRoll.js +0 -29
  106. package/src/parser.js +0 -43
  107. package/src/random.js +0 -22
  108. package/src/roller.js +0 -106
  109. package/src/stringifier.js +0 -33
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
@@ -1,210 +1,215 @@
1
1
  <h1 align="center">Roll Parser</h1>
2
2
 
3
3
  <p align="center">
4
- Parser for classic (2d6+1), simple (2 6 1), and WoD (4d10!>6f1) dice rolls.
4
+ High-performance dice notation parser for tabletop RPGs.<br>
5
+ TypeScript-first, Bun-optimized, Pratt parser architecture.
5
6
  </p>
6
7
 
7
- [![Travis Build Status][travis-image]][travis-url]
8
- [![AppVeyor Build Status][appveyor-image]][appveyor-url]
9
- [![Coverage Status][coveralls-image]][coveralls-url]
10
- [![devDependency Status][devdep-image]][devdep-url]
11
- <!-- [![Dependency Status][dep-image]][dep-url] -->
12
-
13
- [![Node.js version][node-image]][node-url]
14
- [![Project is on npm][npm-image]][npm-url]
15
-
16
- ## Documentation
17
-
18
- Please review the [API documentation](http://edloidas.com/roll-parser/).
19
-
20
- ## Install
21
-
22
- #### Node:
23
-
24
- ```
25
- npm install roll-parser
26
- ```
27
-
28
- Then in the console or JS file:
29
- ```js
30
- const rollParser = require('roll-parser');
31
- ```
32
-
33
- #### Browser:
34
-
35
- ```html
36
- <script src="https://unpkg.com/roll-parser/dist/roll-parser.js"></script>
37
- ```
38
- Minified version:
39
- ```html
40
- <script src="https://unpkg.com/roll-parser/dist/roll-parser.min.js"></script>
41
- ```
42
-
43
- Then access all functions from `rollParser` object.
8
+ <p align="center">
9
+ <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-6-blue.svg" alt="TypeScript"></a>
10
+ <a href="https://bun.sh/"><img src="https://img.shields.io/badge/Bun-1.3+-black.svg" alt="Bun"></a>
11
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-22%2B-339933.svg" alt="Node.js"></a>
12
+ </p>
44
13
 
45
- #### Console:
14
+ ## Status
15
+
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.
19
+ >
20
+ > For production use, install [v2.3.2](https://www.npmjs.com/package/roll-parser/v/2.3.2).
21
+
22
+ ## Features
23
+
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
42
+
43
+ ## Installation
46
44
 
47
45
  ```bash
48
- $ roll-parser [options] [<rolls>]
46
+ bun add roll-parser
47
+ npm install roll-parser
49
48
  ```
50
49
 
51
- Run `roll-parser --help` for more details.
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
53
  ## Usage
54
54
 
55
- ```js
56
- const { parse, roll, parseAndRoll, Roll } = require('roll-parser');
57
-
58
- // `parse()` function parses any notation and returns `Roll` or `WodRoll` object
59
- //=> { dice: 6, count: 4, modifier: 1 }
60
- const parsedRoll = parse('4d6+1');
61
-
62
- // `Roll` or `WodRoll` can be stringified
63
- //=> '4d6+1'
64
- const rollNotation = parsedRoll.toString();
65
-
66
- //=> { notation: '4d6+1', value: 16, rolls: [3, 1, 6, 5] }
67
- const result1 = roll(parsedRoll);
68
- //=> { notation: '2d20-3', value: 23, rolls: [11, 15] }
69
- const result2 = roll(new Roll(20, 2, -3));
70
- // Can also accept plain objects
71
- //=> { notation: '2d10>7', value: 1, rolls: [4, 8] }
72
- const result3 = roll({dice: 10, count: 2, success: 7});
73
-
74
- // `parseAndRoll()` function can parse any notation and then roll the dice
75
- // Any invalid arguments, except `null` or `undefined`, will be parsed as default `Roll`
76
- //=> { notation: '3d10!>8f1', value: 2, rolls: [3, 10, 7, 9] }
77
- const result4 = parseAndRoll('3d10!>8f1');
78
-
79
- //=> '(3d10!>8f1) 2 [3,10,7,9]'
80
- const resultNotation = result4.toString();
81
- ```
82
-
83
- Specific parsers can be used.
84
-
85
- __Classic (D&D):__
86
-
87
- ```js
88
- const {
89
- parseClassicRoll,
90
- rollClassic,
91
- parseAndRollClassic,
92
- Roll
93
- } = require('roll-parser');
94
-
95
- //=> { dice: 10, count: 1, modifier: 0 }
96
- const parsedRoll = parseClassicRoll('d10');
97
-
98
- //=> { notation: 'd10', value: 7, rolls: [7] }
99
- const result1 = rollClassic(parsedRoll);
55
+ ```typescript
56
+ import { roll } from 'roll-parser';
100
57
 
101
- //=> { notation: '2d20', value: 26, rolls: [11, 15] }
102
- const result2 = rollClassic(new Roll(20, 2));
103
-
104
- //=> { notation: '4d10+1', value: 22, rolls: [4, 6, 2, 9] }
105
- const result3 = rollClassic({ dice: 10, count: 4, modifier: 1 });
106
-
107
- //=> { notation: '3d6', value: 15, rolls: [6, 6, 3] }
108
- const result4 = parseAndRollClassic('3d6');
58
+ const result = roll('4d6kh3');
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
109
63
  ```
110
64
 
111
- __WoD (World of Darkness):__
112
-
113
- ```js
114
- const {
115
- parseWodRoll,
116
- rollWod,
117
- parseAndRollWod,
118
- WodRoll
119
- } = require('roll-parser');
65
+ ### Options
120
66
 
121
- //=> { dice: 10, count: 1, again: false, success: 6, fail: 0 }
122
- const parsedRoll = parseWodRoll('d10>6');
67
+ ```typescript
68
+ import { roll, SeededRNG } from 'roll-parser';
123
69
 
124
- // Returns notation, number of success rolls and list of all dice rolls
125
- //=> { notation: 'd10', value: 1, rolls: [7] }
126
- const result1 = rollWod(parsedRoll);
70
+ roll('4d6', { seed: 'character-1' }); // reproducible rolls
71
+ roll('4d6', { rng: new SeededRNG(42) }); // custom RNG instance (takes precedence)
127
72
 
128
- //=> { notation: '4d10>6f1', value: 1, rolls: [4, 10, 5, 2] }
129
- const result2 = rollWod(new WodRoll(10, 4, false, 6, 1));
73
+ roll('1d20+@str', {
74
+ context: { str: 4 }, // variable values
75
+ onMissingVariable: 'zero', // or 'throw' (default)
76
+ });
130
77
 
131
- //=> { notation: '4d10!>8f1', value: 22, rolls: [1, 8, 5, 10, 10, 4] }
132
- const result3 = rollWod({ dice: 10, count: 2, again: true, success: 8, fail: 1 });
133
-
134
- //=> { notation: '4d10>7f4', value: 1, rolls: [6, 3, 8, 4] }
135
- const result4 = parseAndRollWod('4d10>7f4');
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
+ });
136
83
  ```
137
84
 
138
- __Simple (D&D, space-separated):__
139
-
140
- ```js
141
- const { parseSimpleRoll, parseAndRollSimple } = require('roll-parser');
142
-
143
- //=> { dice: 10, count: 1, modifier: 0 }
144
- const parsedRoll = parseSimpleRoll('10');
145
-
146
- //=> { notation: '4d10-1', value: 23, rolls: [3, 6, 8, 7] }
147
- const result = parseAndRollSimple('4 10 -1');
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 } }
148
111
  ```
149
112
 
150
- Random number generator can be used to roll the dice.
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'`.
151
115
 
152
- ```js
153
- const { random } = require('roll-parser');
116
+ ### Error handling
154
117
 
155
- //=> 84 - d100-like roll
156
- random(100);
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`.
157
121
 
158
- //=> 7 - d10-like roll
159
- random(10);
122
+ ```typescript
123
+ import { isRollParserError } from 'roll-parser';
160
124
 
161
- //=> [2, 5, 2, 6] - 4d6-like roll
162
- [...Array(4)].map(() => random(6));
125
+ try {
126
+ roll(userInput);
127
+ } catch (error) {
128
+ if (isRollParserError(error)) console.error(error.code, error.message);
129
+ }
163
130
  ```
164
131
 
165
- Even so the parse&roll functions uses checks to convert non-standard objects to `Roll` or `WodRoll`, explicit conversion can be used in some cases:
166
-
167
- ```js
168
- const { convert } = require('roll-parser');
132
+ ### Testing your integrations
169
133
 
170
- //=> new Roll(undefined, 4, -3)
171
- convert({ count: 4, modifier: -3 });
134
+ ```typescript
135
+ import { createMockRng } from 'roll-parser/testing';
172
136
 
173
- //=> new WodRoll(10, 6, true, undefined, 2)
174
- convert({ dice: 10, count: 6, again: true, fail: 2 });
137
+ const result = roll('3d6', { rng: createMockRng([4, 2, 6]) });
138
+ result.total; // 12 throws if the roll consumes more values than provided
175
139
  ```
176
140
 
177
- ## Releases
178
-
179
- Please review the [changelog](https://github.com/edloidas/roll-parser/releases).
141
+ ### CLI
180
142
 
181
- ## Contributing
143
+ ```bash
144
+ roll-parser 2d6+3
145
+ roll-parser 4d6kh3 --verbose --seed test
146
+ roll-parser --help
147
+ ```
182
148
 
183
- [roll-parser](https://github.com/edloidas/roll-parser) and want to get involved?<br>
184
- Please, check the [guide](CONTRIBUTING.md) first.
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
+
180
+ ## Known Limitations
181
+
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.
185
212
 
186
213
  ## License
187
214
 
188
215
  [MIT](LICENSE) © [Mikita Taukachou](https://edloidas.com)
189
-
190
- <!-- Links -->
191
- [travis-url]: https://travis-ci.org/edloidas/roll-parser
192
- [travis-image]: https://img.shields.io/travis/edloidas/roll-parser.svg?label=linux%20build
193
-
194
- [appveyor-url]: https://ci.appveyor.com/project/edloidas/roll-parser
195
- [appveyor-image]: https://img.shields.io/appveyor/ci/edloidas/roll-parser.svg?label=windows%20build
196
-
197
- [coveralls-url]: https://coveralls.io/github/edloidas/roll-parser?branch=master
198
- [coveralls-image]: https://coveralls.io/repos/github/edloidas/roll-parser/badge.svg?branch=master
199
-
200
- [dep-url]: https://david-dm.org/edloidas/roll-parser
201
- [dep-image]: https://david-dm.org/edloidas/roll-parser.svg
202
-
203
- [devdep-url]: https://david-dm.org/edloidas/roll-parser#info=devDependencies
204
- [devdep-image]: https://david-dm.org/edloidas/roll-parser/dev-status.svg
205
-
206
- [node-url]: https://nodejs.org
207
- [node-image]: https://img.shields.io/badge/node-≥%206.0.0-green.svg
208
-
209
- [npm-url]: https://www.npmjs.com/package/roll-parser
210
- [npm-image]: https://img.shields.io/badge/npm-roll--parser-blue.svg
@@ -0,0 +1,35 @@
1
+ /**
2
+ * CLI argument parser.
3
+ *
4
+ * Pure function that parses process.argv-style string arrays into typed options.
5
+ *
6
+ * @module cli/args
7
+ */
8
+ /**
9
+ * Parsed CLI arguments.
10
+ */
11
+ export type CliArgs = {
12
+ notation: string | undefined;
13
+ verbose: boolean;
14
+ seed: string | undefined;
15
+ showHelp: boolean;
16
+ showVersion: boolean;
17
+ };
18
+ /**
19
+ * Result of parsing CLI arguments — either success or a usage error.
20
+ */
21
+ export type ParseArgsResult = {
22
+ ok: true;
23
+ args: CliArgs;
24
+ } | {
25
+ ok: false;
26
+ error: string;
27
+ };
28
+ /**
29
+ * Parses a raw argument array into typed CLI options.
30
+ *
31
+ * @param argv - Arguments to parse (typically `process.argv.slice(2)`)
32
+ * @returns Parsed result or an error message for usage errors
33
+ */
34
+ export declare function parseArgs(argv: string[]): ParseArgsResult;
35
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +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,CA0CzD"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * CLI output formatting.
3
+ *
4
+ * Transforms RollResult into terminal-appropriate strings.
5
+ *
6
+ * @module cli/format
7
+ */
8
+ import type { RollResult } from '../types.js';
9
+ /**
10
+ * Formats a roll result for terminal display.
11
+ *
12
+ * In normal mode, returns just the total. In verbose mode, returns the
13
+ * rendered breakdown with terminal-safe formatting for dropped dice.
14
+ *
15
+ * @param result - The roll result to format
16
+ * @param verbose - Whether to show the detailed breakdown
17
+ * @returns Formatted string for terminal output
18
+ */
19
+ export declare function formatResult(result: RollResult, verbose: boolean): string;
20
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +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,aAAa,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAMzE"}
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for roll-parser.
4
+ *
5
+ * @module cli/index
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA;;;;GAIG"}