@yipe/dice 0.5.0 → 0.6.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 (57) hide show
  1. package/dist/builder/ac.d.ts +25 -0
  2. package/dist/builder/ac.d.ts.map +1 -0
  3. package/dist/builder/ast.d.ts +9 -0
  4. package/dist/builder/ast.d.ts.map +1 -0
  5. package/dist/builder/attack.d.ts +41 -0
  6. package/dist/builder/attack.d.ts.map +1 -0
  7. package/dist/builder/d20.d.ts +6 -0
  8. package/dist/builder/d20.d.ts.map +1 -0
  9. package/dist/builder/dc.d.ts +26 -0
  10. package/dist/builder/dc.d.ts.map +1 -0
  11. package/dist/builder/example.d.ts +356 -0
  12. package/dist/builder/example.d.ts.map +1 -0
  13. package/dist/builder/factory.d.ts +17 -0
  14. package/dist/builder/factory.d.ts.map +1 -0
  15. package/dist/builder/index.d.ts +8 -429
  16. package/dist/builder/index.d.ts.map +1 -0
  17. package/dist/builder/nodes.d.ts +61 -0
  18. package/dist/builder/nodes.d.ts.map +1 -0
  19. package/dist/builder/prob.d.ts +3 -0
  20. package/dist/builder/prob.d.ts.map +1 -0
  21. package/dist/builder/roll.d.ts +205 -0
  22. package/dist/builder/roll.d.ts.map +1 -0
  23. package/dist/builder/save.d.ts +19 -0
  24. package/dist/builder/save.d.ts.map +1 -0
  25. package/dist/builder/types.d.ts +66 -0
  26. package/dist/builder/types.d.ts.map +1 -0
  27. package/dist/common/bounce.d.ts +32 -0
  28. package/dist/common/bounce.d.ts.map +1 -0
  29. package/dist/common/errors.d.ts +26 -0
  30. package/dist/common/errors.d.ts.map +1 -0
  31. package/dist/common/lru-cache.d.ts +17 -0
  32. package/dist/common/lru-cache.d.ts.map +1 -0
  33. package/dist/common/types.d.ts +65 -0
  34. package/dist/common/types.d.ts.map +1 -0
  35. package/dist/index.d.ts +9 -111
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/parser/dice.d.ts +70 -0
  38. package/dist/parser/dice.d.ts.map +1 -0
  39. package/dist/parser/parser.d.ts +14 -0
  40. package/dist/parser/parser.d.ts.map +1 -0
  41. package/dist/pmf/mixture.d.ts +37 -0
  42. package/dist/pmf/mixture.d.ts.map +1 -0
  43. package/dist/pmf/pmf.d.ts +380 -0
  44. package/dist/pmf/pmf.d.ts.map +1 -0
  45. package/dist/{pmf-D5VRghZI.d.cts → pmf/query.d.ts} +6 -464
  46. package/dist/pmf/query.d.ts.map +1 -0
  47. package/package.json +16 -15
  48. package/.claude/worktrees/amazing-matsumoto-27220c/LICENSE +0 -21
  49. package/.claude/worktrees/amazing-matsumoto-27220c/README.md +0 -518
  50. package/.claude/worktrees/vibrant-lovelace-0cc9e7/LICENSE +0 -21
  51. package/.claude/worktrees/vibrant-lovelace-0cc9e7/README.md +0 -518
  52. package/.claude/worktrees/wizardly-mclean-e375de/LICENSE +0 -21
  53. package/.claude/worktrees/wizardly-mclean-e375de/README.md +0 -518
  54. package/CHANGELOG.md +0 -239
  55. package/dist/builder/index.d.cts +0 -429
  56. package/dist/index.d.cts +0 -111
  57. package/dist/pmf-D5VRghZI.d.ts +0 -1129
package/CHANGELOG.md DELETED
@@ -1,239 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project are documented here.
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
- ## [0.5.0]
9
-
10
- Pushes damage-attribution / provenance and D&D-probability logic that the
11
- consuming app (dprcalc) had hand-rolled over PMF internals down into the
12
- library, so the provenance model and dice math stay owned here, and adds a
13
- **composable scale node** so a scaled/rounded sub-roll can nest inside a larger
14
- damage payload (per-damage-type resistance / immunity / vulnerability). All
15
- additive except the Elemental-Adept bounce fix noted below.
16
-
17
- ### Added
18
-
19
- - **`RollBuilder.scaleResult(numerator, denominator = 1, rounding = 'floor')`** —
20
- wraps a builder in a composable `scale` AST node that scales its resolved PMF
21
- by `numerator / denominator` with the given rounding. Unlike the old
22
- `.half()` wrapper, a scaled builder composes: it survives `sumRolls(...)`
23
- instead of being dropped on a flat-config merge, so a per-type resisted or
24
- doubled sub-roll keeps its own scaling inside a larger hit/crit payload. The
25
- rendered expression reflects it — `denominator === 1 → "N * (child)"`,
26
- `numerator === 1 → "(child) // D"`, general → `"(child) * N // D"`. `.half()`
27
- is now `scaleResult(1, 2, 'floor')`.
28
- - **`sumRolls(parts: RollBuilder[])`** — additive factory whose `toAST()` is an
29
- `add` node over each part's AST, letting scaled and plain children sit side by
30
- side without the flat `.plus()` merge collapsing them. `toExpression()` joins
31
- the parts with ` + ` and `toPMF()` convolves them.
32
- - **`PMF.applyHitFrequency(frequency)`** — provenance-preserving mass
33
- redistribution for effects that only occur with some probability (conditional
34
- attacks, on-hit riders, sub-one AoE fractions): scales every hit bin (damage
35
- > 0) by `frequency` and moves the freed mass into a `missNone` bin. Unlike a
36
- bare `scaleMass`/`mapDamage`, it scales per-label `count` **and** `attr`, so a
37
- frequency-scaled PMF still renders correctly in the damage-attribution charts.
38
- Replaces the app's hand-rolled `applyFrequencyToPMF`, which dropped `attr`.
39
- - **`PMF.missNone(epsilon?)`** / **`MISS_NONE_OUTCOME`** — canonical "clean miss"
40
- delta (point mass at 0 tagged with the `missNone` `OutcomeType`, distinct from
41
- `PMF.zero`'s builder-side `miss` label), and the label as a single source of
42
- truth.
43
- - **`PMF.hitProbability()` / `PMF.missProbability()`** — the `1 - P(0)` idiom
44
- (miss encoded at damage 0), centralized.
45
- - **`PMF.rebin(maxBuckets)`** — coarsen a wide distribution into ≤ N contiguous
46
- equal-width buckets, aggregating `count`/`attr` provenance. For charting wide
47
- distributions, not DPR math.
48
- - **`PMF.attributionByValue()` / `DiceQuery.attributionByValue()`** — split each
49
- damage value's probability mass across outcome labels (by `attr` for
50
- damage-bearing bins, by `count` for the clean-miss bin), returning per-label
51
- `value → mass` series. The provenance core of the stacked attribution chart.
52
- - **`DiceQuery.countSinglesWith(label)`** — how many independent single PMFs can
53
- produce a given outcome label.
54
- - **`ALL_OUTCOME_TYPES`**, **`OUTCOME_DISPLAY_ORDER`**, **`sortOutcomes()`** —
55
- canonical `OutcomeType` enumeration + stack / display orderings, replacing
56
- per-consumer outcome tables.
57
- - **`critProbability(critRange, rollType)`** and **`RollType`** (now exported
58
- from the package root as well as `@yipe/dice/builder`) — advantage-aware
59
- P(crit) for a given crit window.
60
- - **`calculateBounceOdds(diceCount, dieFaces, options?)`** and
61
- **`BounceOddsOptions`** — the "birthday problem" for bouncing damage dice
62
- (Chromatic Orb), honoring Elemental Adept and Empowered Spell. Moved out of the
63
- app; the base and Elemental-Adept cases are now computed **exactly** (verified
64
- against brute-force enumeration in `tests/bounce.test.ts`).
65
-
66
- ### Fixed
67
-
68
- - **`calculateBounceOdds` Elemental Adept was approximate.** The former
69
- hand-derived adjustment factor drifted from the exact value by up to ~3.5%
70
- (e.g. 3×d8, min-roll 3: 0.4965 → 0.5313). The Elemental-Adept branch now uses
71
- an exact elementary-symmetric-polynomial computation. Consumers relying on the
72
- old numbers for bouncing spells with Elemental Adept will see small DPR shifts.
73
- - **`calculateBounceOdds` Empowered Spell returned certainty when rerolling all
74
- dice.** When `rerollDamageDice >= diceCount` (no dice kept), the model claimed
75
- a guaranteed match (1.0) instead of treating the reroll as a second
76
- independent roll. It now correctly yields `1 - (1 - pMatch)^2` in that case
77
- (e.g. 3×d8 reroll-all: 1.0 → 0.5693).
78
-
79
- ## [0.3.0]
80
-
81
- ### Fixed (mathematical correctness)
82
-
83
- Every fix is verified against an independent brute-force enumeration (see
84
- `tests/math-correctness.test.ts`).
85
-
86
- - **`DiceQuery.probabilityOf(label)` over-counted.** It summed the full `bin.p`
87
- of every combined bin that merely *contained* a label, but bins hold multiple
88
- mutually-exclusive outcomes — so `probabilityOf('crit')` returned 0.49 where
89
- the true P(crit)=0.05. It now returns the correct Poisson-binomial marginal
90
- (= `probAtLeastOne`). `missChance()` is fixed by the same change.
91
- - **`DiceQuery.probExactlyK([labels], k)` array-path** delegated to the buggy
92
- `probabilityOf`, disagreeing with the (correct) single-label string path; both
93
- now match the true binomial.
94
- - **`DiceQuery.variance()/stddev()`** used the unstable `E[X²]−E[X]²` form and
95
- lost all precision under a large constant damage offset (`1d6 + 1e8` gave
96
- variance 2 instead of 35/12). Now uses the centered, additive-per-single form.
97
- - **`DiceQuery.mean()/variance()`** now stay consistent with an explicitly
98
- supplied `combined` that diverges from `convolve(singles)`.
99
- - **`PMF.convolve()` produced `NaN`** for a zero-mass operand (divide-by-zero in
100
- the mass rescale), silently poisoning `DiceQuery.combined`. A zero-mass
101
- convolution now correctly yields mass 0.
102
- - **`probAtLeastOne` is now mass-invariant** (per-attack probability divided by
103
- the single's mass) and clamped to `[0,1]` (was returning `1.0000000002`).
104
- - **`PMF.firstSuccessWeights`** throws on `pSpecial > pSuccess` instead of
105
- returning out-of-range probabilities.
106
- - **Parser `hd6`/`hd20` (reroll-one)** used a weighted union giving
107
- `P(1)=1/(2s−1)`; now uses `reroll(1)` for the correct `P(1)=1/s²`. The parser
108
- `hd` distribution now matches the builder's `reroll(1)` exactly (the
109
- previously loosened tests are tightened).
110
- - **`DiceQuery.snapshot()` outcome probabilities** (`atLeastOneProbability`,
111
- `allProbability`) were aggregated as expected counts and could exceed 1 for
112
- multi-attack queries. They now use the correct Poisson-binomial marginals
113
- (P(≥1) and P(all)) and are always in [0,1]. (`damageRange.avg` remains a
114
- size-biased mean for N≥2 — see Known limitations.)
115
- - **Parser save-for-half mislabeled outcomes** on odd/constant damage (e.g.
116
- `(d20 DC 15) * (3) save half`): the brittle "2×half ∈ hit" detection
117
- false-negatived, tagging the success mass as `saveFail` and the failure mass
118
- as `hit`. Detection is now deterministic (the presence of a save distribution),
119
- so `saveHalf`/`saveFail` are always labeled correctly.
120
- - **`PMF.compact()` corrupted PMFs that shared bin objects.** It deleted
121
- sub-epsilon `count`/`attr` entries *in place* and reused that same bin
122
- reference in the compacted map. Because bins are shared by reference across
123
- PMFs (the `branch()` / `addScaled()` / `scaleMass()` fast paths can carry
124
- another PMF's bin objects), this silently mutated the source PMF — and the
125
- receiver's own bins. `compact()` now clones each surviving bin before pruning;
126
- the compacted result is unchanged.
127
-
128
- ### Security / hardening
129
-
130
- - **Parser resource-exhaustion guards.** Adversarial expressions are rejected
131
- with a `DiceParseError` instead of exhausting CPU/memory: a die over 1,000,000
132
- faces, a dice count over 10,000, a keep whose `faces^count` enumeration would
133
- exceed 1,000,000 outcomes, and a binary operation whose `faces₁ × faces₂` work
134
- would exceed 100,000,000 face pairs. The last closes a gap the per-operand
135
- caps missed — two individually-legal large dice (e.g. `d100000 + d100000`,
136
- ~10¹⁰ operations) previously hung for tens of seconds. All legitimate
137
- expressions, including `d100000`, still parse.
138
-
139
- ### Known limitations (documented; recommend maintainer review)
140
-
141
- These are real but require API/architecture decisions, so they are documented
142
- and pinned by tests rather than changed blindly:
143
-
144
- - **Parser crit probability with bonus to-hit dice is wrong.** With bonus dice in
145
- the to-hit (e.g. Bless, `d20 + 5 + 1d4`), the string parser collapses crit to
146
- `1/(20·∏bonusSides)` (and the DPR is off by a few %), because the natural-20
147
- slice can't be separated after the bonus dice are convolved. **The builder API
148
- computes it correctly** — use `d20.plus(..).plus(bonusDie).ac(..).onCrit(..)`.
149
- - **Multi-attack conditional damage `avg` is size-biased.** The `avg` returned by
150
- `damageStatsFrom()` (single label), `outcomeDamageRanges()` and
151
- `snapshot().damageRange` aggregates the combined PMF's `count` (an *expected
152
- count* for N≥2 attacks), so it is the size-biased mean E[dmg·#label]/E[#label]
153
- rather than a clean conditional expectation. It is correct for a single attack.
154
- (The associated *probabilities* are now correct — see Fixed.)
155
- - **`PMF.mixN`/`gate`/`branch` build O(2ⁿ) identifier strings**, which can blow up
156
- (multi-MB, eventual `RangeError`) for very deep (≈20+) gate chains. Prefer
157
- `PMF.exclusive`/`PMF.mix` for large mixtures.
158
-
159
- ### Breaking
160
-
161
- - **`PMF.toJSON()` now returns a plain object** (`{ bins, normalized, identifier }`)
162
- instead of a JSON string, following the standard `toJSON` contract. This means
163
- `JSON.stringify(pmf)` no longer double-encodes. If you relied on the old string
164
- return, call the new `PMF.toJSONString()` instead.
165
- - **`DiceQuery.firstSuccessSplit()` is typed as `OutcomeType | OutcomeType[]`**
166
- (previously `string | string[]`). Only affects callers passing arbitrary strings;
167
- valid outcome labels are unchanged.
168
-
169
- ### Added
170
-
171
- - **`DiceParseError`** — `parse()` now throws this typed error (a subclass of
172
- `Error`) instead of a plain `Error`. Existing `try/catch` and message checks keep
173
- working; you can now narrow with `instanceof DiceParseError` and read
174
- `error.expression` / `error.cause`.
175
- - **`PMF.toJSONString()`** — returns the JSON string form (the previous
176
- `toJSON()` behavior).
177
- - **`DiceQuery.stdev()`** — alias of `stddev()`, matching `PMF.stdev()`.
178
- - **`PMF.hasAttribution()`** — O(1) check for whether a PMF already carries
179
- damage-attribution metadata.
180
-
181
- ### Performance
182
-
183
- - **`DiceQuery.mean()` / `variance()` / `stddev()` use moment additivity**
184
- (`E[ΣX]=ΣE[X]`, `Var[ΣX]=ΣVar[X]`) computed directly from the single PMFs.
185
- - **`DiceQuery.combined` is now built lazily** (on first access) instead of in
186
- the constructor. Combined with the above, a query used only for DPR / mean /
187
- variance never performs the N-way convolution — multi-attack stats-only
188
- queries are ~10000× faster (e.g. ~17 ms → ~0.001 ms for a heavy 4-attack
189
- expression). The materialized `combined` distribution is unchanged; mean and
190
- variance may differ from the previous convolution-based values by at most a
191
- few ULP (well within the library's tolerances).
192
- - **`DiceQuery.combinedWithAttribution()` reuses `combined`** when every single
193
- already carries attribution (as parser-generated PMFs do), avoiding a
194
- redundant convolution pass. Result is bit-for-bit identical.
195
- - **`PMF.convolve()` inner loop accumulates directly into destination bins**
196
- instead of allocating a temporary bin per term and merging — ~1.6× faster
197
- convolution (the cost of building the combined distribution for charts). The
198
- probability channel is bit-identical; per-label `count`/`attr` provenance may
199
- re-associate by at most a few ULP (≤1e-14 even at 16 attacks, ~100× below the
200
- eps pruning threshold).
201
- - **Convolution cache-key fingerprint is memoized** on each (immutable) PMF
202
- instead of re-summing every bin key on every `convolve()` call — ~36% faster
203
- on warm cache hits. Bit-identical (`PMF.fingerprint()` returns the same string).
204
- - **`Dice.calculateHitDistribution()` no longer clones outcome distributions per
205
- face** — it reads the stored maps once instead of `O(faces × outcomes)` clones,
206
- ~10% faster cold parsing of wide-support expressions. Bit-identical.
207
- - **`DiceQuery.toStackedChartData()` drops a dead `O(N×L)` precomputation pass**
208
- whose result was discarded — ~2× faster. Bit-identical.
209
- - Minor bit-identical cleanups on the parse path (`Dice.toPMF` iterates the
210
- internal face map directly; `multiplyDiceByDice` uses a `Map`).
211
- - **`DiceQuery` count queries (`probExactlyK` / `probAtLeastK` / `probAtMostK`,
212
- array-label paths)** compute each attack's success probability and the binomial
213
- DP once instead of rebuilding a query per requested count (~3× on the looped
214
- variants).
215
- - **`PMF.branch()` assembles its Bernoulli mixture in a single pass** rather than
216
- chaining two `addScaled` calls (which copied the failure branch's bins twice).
217
- - **`keepSumPMF` packs its DP state into a single integer key** instead of a
218
- `"used|r"` string parsed on every transition.
219
- - **`computeMaxOfPMF` walks the support once with a running CDF** for large pools,
220
- reducing the max-of computation from O(N²) to O(N).
221
- - **`Dice.reroll()` uses a `Set` for membership** and **`Dice.binaryOp()` hoists
222
- the inner die's face list** out of its loop.
223
-
224
- All of the above were verified bit-for-bit identical (probabilities, counts,
225
- means, variance) across the full expression corpus.
226
-
227
- ### Changed
228
-
229
- - Removed the stale `package-lock.json` (the project uses Yarn 4) and dropped the
230
- unused `ts-node` / `tsconfig-paths` dev dependencies.
231
- - Removed `console.error` calls from the parser so the library no longer writes to
232
- a consumer's console.
233
- - Internal refactors with no behavioral change: deduplicated `Bin` clone/scale
234
- logic in `PMF`, removed dead code and impossible iterator branches, and tightened
235
- internal `any` usage.
236
- - `Dice.outcomeData` is typed `Partial<Record<OutcomeType, …>>` (dropping an
237
- unsound `as Record<…>` cast); `getFullOutcomeDistribution()`'s return type
238
- matches. Type-only change; runtime output is unchanged.
239
- - Added a `yarn format` script (ESLint autofix).
@@ -1,429 +0,0 @@
1
- import { b as RollType, P as PMF, l as DiceQuery, C as CritConfig, L as LRUCache } from '../pmf-D5VRghZI.cjs';
2
-
3
- type RollFactory = {
4
- (count: number, sides?: number, modifier?: number): RollBuilder;
5
- (count: number, die: RollBuilder, modifier?: number): RollBuilder;
6
- d(sides: number | string): RollBuilder;
7
- hd20(): RollBuilder;
8
- d4(): RollBuilder;
9
- d6(): RollBuilder;
10
- d8(): RollBuilder;
11
- d10(): RollBuilder;
12
- d12(): RollBuilder;
13
- d20(): RollBuilder;
14
- d100(): RollBuilder;
15
- flat(n: number): RollBuilder;
16
- };
17
- type KeepMode = "highest" | "lowest";
18
- type RollConfig = {
19
- count: number;
20
- sides: number;
21
- modifier: number;
22
- reroll: number;
23
- explode: number;
24
- minimum: number;
25
- bestOf: number;
26
- keep: {
27
- total: number;
28
- count: number;
29
- mode: KeepMode;
30
- } | undefined;
31
- rollType: RollType;
32
- isSubtraction?: boolean;
33
- };
34
- type Resolution = {
35
- pmf: PMF;
36
- check: PMF;
37
- weights: {
38
- [key: string]: number;
39
- };
40
- };
41
- type AttackResolution = Resolution & {
42
- hit: PMF;
43
- crit: PMF;
44
- miss: PMF;
45
- weights: {
46
- hit: number;
47
- crit: number;
48
- miss: number;
49
- };
50
- };
51
- type SaveResolution = Resolution & {
52
- saveFail: PMF;
53
- saveSuccess: PMF;
54
- weights: {
55
- success: number;
56
- fail: number;
57
- };
58
- };
59
- interface CheckBuilder {
60
- resolve(eps?: number): Resolution;
61
- toExpression(): string;
62
- readonly pmf: PMF;
63
- }
64
-
65
- type SaveOutcome = "normal" | "half";
66
- declare class SaveBuilder implements CheckBuilder {
67
- readonly check: DCBuilder;
68
- private readonly failureEffect?;
69
- private readonly saveOutcome;
70
- constructor(check: DCBuilder, failureEffect?: RollBuilder | undefined, saveOutcome?: SaveOutcome);
71
- saveHalf(): SaveBuilder;
72
- toExpression(): string;
73
- resolve(eps?: number): SaveResolution;
74
- toPMF(eps?: number): PMF;
75
- get pmf(): PMF;
76
- toQuery(eps?: number): DiceQuery;
77
- }
78
-
79
- interface SaveConfig {
80
- dc: number;
81
- }
82
- declare class DCBuilder extends RollBuilder {
83
- private readonly saveConfig;
84
- constructor(baseRoll: RollBuilder, saveConfig?: SaveConfig);
85
- dc(saveDC: number): DCBuilder;
86
- get saveDC(): number;
87
- add(anotherRoll: RollBuilder): DCBuilder;
88
- addRoll(count?: number): DCBuilder;
89
- onSaveFailure(val: number): SaveBuilder;
90
- onSaveFailure(val: string): SaveBuilder;
91
- onSaveFailure(val: RollBuilder): SaveBuilder;
92
- onSaveFailure(count: number, die: RollBuilder): SaveBuilder;
93
- onSaveFailure(count: number, sides: number): SaveBuilder;
94
- onSaveFailure(count: number, die: RollBuilder, modifier: number): SaveBuilder;
95
- onSaveFailure(count: number, sides: number, modifier: number): SaveBuilder;
96
- withElvenAccuracy(): never;
97
- toExpression(): string;
98
- toPMF(eps?: number): PMF;
99
- }
100
-
101
- type ExpressionNode = DieNode | ConstantNode | SumNode | AddNode | KeepNode | D20RollNode | HalfNode | MaxOfNode | ScaleNode;
102
- type DieNode = {
103
- type: "die";
104
- sides: number;
105
- reroll?: number;
106
- minimum?: number;
107
- explode?: number;
108
- };
109
- type ConstantNode = {
110
- type: "constant";
111
- value: number;
112
- };
113
- type SumNode = {
114
- type: "sum";
115
- count: number;
116
- child: ExpressionNode;
117
- };
118
- type AddNode = {
119
- type: "add";
120
- children: {
121
- node: ExpressionNode;
122
- sign: 1 | -1;
123
- }[];
124
- };
125
- type KeepNode = {
126
- type: "keep";
127
- mode: "highest" | "lowest";
128
- count: number;
129
- child: SumNode | KeepNode;
130
- };
131
- type D20RollNode = {
132
- type: "d20Roll";
133
- rollType: "advantage" | "disadvantage" | "elven accuracy";
134
- child: ExpressionNode;
135
- };
136
- type HalfNode = {
137
- type: "half";
138
- child: ExpressionNode;
139
- };
140
- type MaxOfNode = {
141
- type: "maxOf";
142
- count: number;
143
- child: ExpressionNode;
144
- };
145
- /**
146
- * Scale the child's result by `numerator / denominator`, then round.
147
- *
148
- * Unlike {@link HalfNode} (a fixed `// 2` with floor), this is a general, composable
149
- * multiplier/divider — the building block for damage-type resistance (`1/2`, floor),
150
- * vulnerability (`2/1`), and similar per-source transforms. It renders as
151
- * `N * (child)` when the denominator is 1, `(child) // D` when the numerator is 1,
152
- * and `(child) * N // D` otherwise.
153
- */
154
- type ScaleNode = {
155
- type: "scale";
156
- numerator: number;
157
- denominator: number;
158
- rounding: "floor" | "round" | "ceil";
159
- child: ExpressionNode;
160
- };
161
-
162
- declare const defaultConfig: RollConfig;
163
- declare class RollBuilder {
164
- protected readonly subRollConfigs: readonly RollConfig[];
165
- constructor(countOrConfigs?: number | readonly RollConfig[]);
166
- protected create(configs: readonly RollConfig[]): RollBuilder;
167
- protected get lastConfig(): RollConfig;
168
- hasHiddenState(): boolean;
169
- getSubRollConfigs(): readonly RollConfig[];
170
- static fromConfig(config: Partial<RollConfig>): RollBuilder;
171
- static fromConfigs(configs: Partial<RollConfig>[]): RollBuilder;
172
- static fromArgs(...args: any[]): RollBuilder;
173
- d(sides: number | undefined): RollBuilder;
174
- plus(modOrRoll: number | RollBuilder | undefined): RollBuilder;
175
- plus(count: number, die: RollBuilder): RollBuilder;
176
- minus(modOrRoll: number | RollBuilder | undefined): RollBuilder;
177
- minus(count: number, die: RollBuilder): RollBuilder;
178
- /** Apply one-pass reroll threshold (k): reroll faces 1..k once, must keep. */
179
- reroll(value: number): RollBuilder;
180
- /** Set finite explode count for max-face explosions (Infinity allowed). */
181
- explode(count?: number | undefined): RollBuilder;
182
- /** Apply per-die minimum value (min > 0). */
183
- minimum(val: number | undefined): RollBuilder;
184
- bestOf(count: number | undefined): RollBuilder;
185
- keepHighest(total: number, count: number): RollBuilder;
186
- keepLowest(total: number, count: number): RollBuilder;
187
- keepHighestAll(total: number, count: number): PooledRollBuilder;
188
- keepLowestAll(total: number, count: number): PooledRollBuilder;
189
- withAdvantage(): RollBuilder;
190
- withDisadvantage(): RollBuilder;
191
- add(anotherRoll: RollBuilder | undefined): RollBuilder;
192
- withBonus(anotherRoll: RollBuilder): RollBuilder;
193
- addRoll(count?: number): RollBuilder;
194
- scaleDice(scale: number): RollBuilder;
195
- doubleDice(): RollBuilder;
196
- alwaysHits(): AlwaysHitBuilder;
197
- alwaysCrits(): AlwaysCritBuilder;
198
- copy(): RollBuilder;
199
- d4: () => RollBuilder;
200
- d6: () => RollBuilder;
201
- d8: () => RollBuilder;
202
- d10: () => RollBuilder;
203
- d12: () => RollBuilder;
204
- d20: () => RollBuilder;
205
- d100: () => RollBuilder;
206
- withElvenAccuracy(): RollBuilder;
207
- toExpression(): string;
208
- toPMF(eps?: number): PMF;
209
- get pmf(): PMF;
210
- toQuery(eps?: number): DiceQuery;
211
- toAST(): ExpressionNode;
212
- private configToSingleExpressionWithoutModifier;
213
- getRootDieConfig(): RollConfig | undefined;
214
- getAllDieConfigs(): readonly RollConfig[];
215
- getBonusDiceConfigs(): RollConfig[];
216
- getBonusDicePMFs(check: RollBuilder, eps?: number): PMF[];
217
- get modifier(): number;
218
- get rollType(): RollType;
219
- get baseReroll(): number;
220
- half(): HalfRollBuilder;
221
- /**
222
- * Scale this roll's result by `numerator / denominator`, rounding each outcome.
223
- * A general, composable form of {@link half} — used to model damage-type resistance
224
- * (`scaleResult(1, 2)` → `(expr) // 2`) and vulnerability (`scaleResult(2)` → `2 * (expr)`).
225
- * Compose several of these (and plain rolls) into one payload with {@link sumRolls}.
226
- */
227
- scaleResult(numerator: number, denominator?: number, rounding?: "floor" | "round" | "ceil"): ScaleRollBuilder;
228
- maxOf(count: number): MaxOfRollBuilder;
229
- ac(_targetAC: number): ACBuilder;
230
- dc(_saveDC: number): DCBuilder;
231
- }
232
- declare class HalfRollBuilder extends RollBuilder {
233
- private readonly innerRoll;
234
- constructor(innerRoll: RollBuilder);
235
- hasHiddenState(): boolean;
236
- get lastConfig(): RollConfig;
237
- getSubRollConfigs(): readonly RollConfig[];
238
- toExpression(): string;
239
- toAST(): ExpressionNode;
240
- toPMF(eps?: number): PMF;
241
- copy(): HalfRollBuilder;
242
- }
243
- /**
244
- * A roll whose result is scaled by `numerator / denominator` and rounded — the composable
245
- * generalization of {@link HalfRollBuilder}. Renders as `N * (inner)`, `(inner) // D`, or
246
- * `(inner) * N // D`. Terminal (like `half`): use {@link sumRolls} to combine with other rolls.
247
- */
248
- declare class ScaleRollBuilder extends RollBuilder {
249
- private readonly innerRoll;
250
- private readonly numerator;
251
- private readonly denominator;
252
- private readonly rounding;
253
- constructor(innerRoll: RollBuilder, numerator: number, denominator?: number, rounding?: "floor" | "round" | "ceil");
254
- hasHiddenState(): boolean;
255
- get lastConfig(): RollConfig;
256
- getSubRollConfigs(): readonly RollConfig[];
257
- toExpression(): string;
258
- toAST(): ExpressionNode;
259
- toPMF(eps?: number): PMF;
260
- copy(): ScaleRollBuilder;
261
- }
262
- declare class MaxOfRollBuilder extends RollBuilder {
263
- private readonly innerRoll;
264
- private readonly count;
265
- private readonly diceCount?;
266
- private readonly diceSides?;
267
- constructor(innerRoll: RollBuilder, count: number, diceCount?: number | undefined, diceSides?: number | undefined);
268
- hasHiddenState(): boolean;
269
- get lastConfig(): RollConfig;
270
- getSubRollConfigs(): readonly RollConfig[];
271
- toExpression(): string;
272
- toAST(): ExpressionNode;
273
- toPMF(eps?: number): PMF;
274
- copy(): MaxOfRollBuilder;
275
- }
276
- declare class AlwaysHitBuilder extends RollBuilder {
277
- readonly attackConfig: CritConfig;
278
- constructor(baseRoll: RollBuilder, attackConfig?: CritConfig);
279
- protected create(configs: readonly RollConfig[]): RollBuilder;
280
- onHit(val: number): AttackBuilder;
281
- onHit(val: string): AttackBuilder;
282
- onHit(val: RollBuilder): AttackBuilder;
283
- onHit(count: number, die: RollBuilder): AttackBuilder;
284
- onHit(count: number, sides: number): AttackBuilder;
285
- onHit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
286
- onHit(count: number, sides: number, modifier: number): AttackBuilder;
287
- get critThreshold(): number;
288
- critOn(critThreshold: number): AlwaysHitBuilder;
289
- alwaysCrits(): AlwaysCritBuilder;
290
- toExpression(): string;
291
- toPMF(): PMF;
292
- copy(): AlwaysHitBuilder;
293
- }
294
- declare class AlwaysCritBuilder extends RollBuilder {
295
- readonly attackConfig: CritConfig & {
296
- ac?: number;
297
- };
298
- readonly fromAlwaysHit: boolean;
299
- constructor(baseRoll: RollBuilder, attackConfig?: CritConfig & {
300
- ac?: number;
301
- }, fromAlwaysHit?: boolean);
302
- protected create(configs: readonly RollConfig[]): RollBuilder;
303
- onHit(val: number): AttackBuilder;
304
- onHit(val: string): AttackBuilder;
305
- onHit(val: RollBuilder): AttackBuilder;
306
- onHit(count: number, die: RollBuilder): AttackBuilder;
307
- onHit(count: number, sides: number): AttackBuilder;
308
- onHit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
309
- onHit(count: number, sides: number, modifier: number): AttackBuilder;
310
- get critThreshold(): number;
311
- critOn(critThreshold: number): AlwaysCritBuilder;
312
- toExpression(): string;
313
- toPMF(): PMF;
314
- copy(): AlwaysCritBuilder;
315
- }
316
- declare class ParsedRollBuilder extends RollBuilder {
317
- private readonly cachedPMF;
318
- private readonly originalExpression;
319
- constructor(expression: string);
320
- hasHiddenState(): boolean;
321
- protected create(configs: readonly RollConfig[]): RollBuilder;
322
- toPMF(_eps?: number): PMF;
323
- toExpression(): string;
324
- toAST(): ExpressionNode;
325
- copy(): ParsedRollBuilder;
326
- doubleDice(): ParsedRollBuilder;
327
- }
328
- declare class PooledRollBuilder extends RollBuilder {
329
- private readonly baseAST;
330
- private readonly baseExpression;
331
- constructor(baseAST: ExpressionNode, baseExpression: string, configs?: readonly RollConfig[]);
332
- protected create(configs: readonly RollConfig[]): PooledRollBuilder;
333
- hasHiddenState(): boolean;
334
- d(_sides: number | undefined): RollBuilder;
335
- reroll(_value: number): RollBuilder;
336
- explode(_count?: number | undefined): RollBuilder;
337
- minimum(_val: number | undefined): RollBuilder;
338
- bestOf(_count: number | undefined): RollBuilder;
339
- keepHighest(_total: number, _count: number): RollBuilder;
340
- keepLowest(_total: number, _count: number): RollBuilder;
341
- withAdvantage(): RollBuilder;
342
- withDisadvantage(): RollBuilder;
343
- withElvenAccuracy(): RollBuilder;
344
- toAST(): ExpressionNode;
345
- toExpression(): string;
346
- copy(): PooledRollBuilder;
347
- scaleDice(scale: number): RollBuilder;
348
- times(count: number): PooledRollBuilder;
349
- }
350
- /**
351
- * Combine several rolls into one additive payload whose PMF is their convolution and whose
352
- * expression is them joined with ` + `. Unlike `a.plus(b)`, this preserves parts that carry
353
- * hidden state (e.g. `roll.scaleResult(1, 2)` / `roll.half()`), so per-damage-type resistance
354
- * and vulnerability survive into both the distribution and the rendered expression.
355
- * Empty parts collapse to `0`; a single part is returned unwrapped.
356
- */
357
- declare function sumRolls(parts: readonly RollBuilder[]): RollBuilder;
358
-
359
- type ActionEffect = RollBuilder;
360
- declare class AttackBuilder implements CheckBuilder {
361
- readonly check: ACBuilder | AlwaysHitBuilder | AlwaysCritBuilder;
362
- private readonly hitEffect?;
363
- private readonly critEffect?;
364
- private readonly missEffect?;
365
- constructor(check: ACBuilder | AlwaysHitBuilder | AlwaysCritBuilder, hitEffect?: ActionEffect | undefined, critEffect?: (ActionEffect | null) | undefined, missEffect?: ActionEffect | undefined);
366
- onCrit(val: number): AttackBuilder;
367
- onCrit(val: string): AttackBuilder;
368
- onCrit(val: RollBuilder): AttackBuilder;
369
- onCrit(count: number, die: RollBuilder): AttackBuilder;
370
- onCrit(count: number, sides: number): AttackBuilder;
371
- onCrit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
372
- onCrit(count: number, sides: number, modifier: number): AttackBuilder;
373
- onMiss(val: number): AttackBuilder;
374
- onMiss(val: string): AttackBuilder;
375
- onMiss(val: RollBuilder): AttackBuilder;
376
- onMiss(count: number, die: RollBuilder): AttackBuilder;
377
- onMiss(count: number, sides: number): AttackBuilder;
378
- onMiss(count: number, die: RollBuilder, modifier: number): AttackBuilder;
379
- onMiss(count: number, sides: number, modifier: number): AttackBuilder;
380
- noCrit(): AttackBuilder;
381
- toExpression(): string;
382
- resolveProbabilities(check: ACBuilder | AlwaysHitBuilder | AlwaysCritBuilder, eps?: number): {
383
- pSuccess: number;
384
- pHit: number;
385
- pCrit: number;
386
- pMiss: number;
387
- };
388
- resolve(eps?: number): AttackResolution;
389
- toPMF(eps?: number): PMF;
390
- get pmf(): PMF;
391
- toQuery(eps?: number): DiceQuery;
392
- }
393
-
394
- interface AttackConfig {
395
- ac: number;
396
- critThreshold: number;
397
- }
398
- declare class ACBuilder extends RollBuilder {
399
- readonly attackConfig: AttackConfig;
400
- constructor(baseRoll: RollBuilder, ac: number, attackConfig?: AttackConfig);
401
- onHit(val: number): AttackBuilder;
402
- onHit(val: string): AttackBuilder;
403
- onHit(val: RollBuilder): AttackBuilder;
404
- onHit(count: number, die: RollBuilder): AttackBuilder;
405
- onHit(count: number, sides: number): AttackBuilder;
406
- onHit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
407
- onHit(count: number, sides: number, modifier: number): AttackBuilder;
408
- get critThreshold(): number;
409
- critOn(threshold: number): ACBuilder;
410
- alwaysCrits(): AlwaysCritBuilder;
411
- toExpression(): string;
412
- toPMF(eps?: number): PMF;
413
- copy(): ACBuilder;
414
- }
415
-
416
- declare function d(sides: number | string): RollBuilder;
417
- declare const d4: RollBuilder;
418
- declare const d6: RollBuilder;
419
- declare const d8: RollBuilder;
420
- declare const d10: RollBuilder;
421
- declare const d12: RollBuilder;
422
- declare const d20: RollBuilder;
423
- declare const hd20: RollBuilder;
424
- declare const d100: RollBuilder;
425
- declare const flat: (n: number) => RollBuilder;
426
- declare const roll: RollFactory;
427
- declare const builderPMFCache: LRUCache<string, PMF>;
428
-
429
- export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, AttackBuilder, type AttackConfig, type AttackResolution, type CheckBuilder, DCBuilder, HalfRollBuilder, type KeepMode, MaxOfRollBuilder, ParsedRollBuilder, PooledRollBuilder, type Resolution, RollBuilder, type RollConfig, type RollFactory, RollType, SaveBuilder, type SaveOutcome, type SaveResolution, ScaleRollBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll, sumRolls };