@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
@@ -1,1129 +0,0 @@
1
- /**
2
- * Simple LRU cache implementation
3
- */
4
- declare class LRUCache<K, V> {
5
- private readonly maxSize;
6
- private cache;
7
- constructor(maxSize?: number);
8
- get(key: K): V | undefined;
9
- delete(key: K): void;
10
- set(key: K, value: V): this;
11
- clear(): void;
12
- get size(): number;
13
- has(key: K): boolean;
14
- keys(): IterableIterator<K>;
15
- values(): IterableIterator<V>;
16
- }
17
-
18
- /** Mapping from outcome label to probability mass or damage attribution. */
19
- type OutcomeLabelMap = Partial<Record<string, number>>;
20
- /** Computational epsilon for pruning negligible probabilities. */
21
- declare const EPS = 1e-12;
22
- /** A probability bin for a specific damage value. */
23
- interface Bin {
24
- /** Total probability mass at this damage value. */
25
- p: number;
26
- /** Per-outcome probability mass contributions at this damage. */
27
- count: OutcomeLabelMap;
28
- /** Optional per-outcome damage attribution at this damage. */
29
- attr?: OutcomeLabelMap;
30
- }
31
- interface CritConfig {
32
- critThreshold: number;
33
- }
34
- /** Simple mapping from damage value to probability. */
35
- type DamageDistribution = Record<number, number>;
36
- /** Canonical outcome labels supported by the query helpers. */
37
- type OutcomeType = "crit" | "hit" | "missNone" | "missDamage" | "saveHalf" | "saveFail" | "pc";
38
- type Rounding = "none" | "floor" | "round" | "ceil";
39
- /** How a d20 attack roll resolves: single die, keep-highest of 2/3, or keep-lowest of 2. */
40
- type RollType = "flat" | "advantage" | "disadvantage" | "elven accuracy";
41
- /**
42
- * P(critical hit) for the given crit window and d20 {@link RollType}.
43
- *
44
- * `critRange` is the number of top faces that crit (1 for a natural 20, 2 for
45
- * 19–20, …), so a single die crits with probability `critRange / 20`. Advantage
46
- * rolls two d20s / elven accuracy three, keeping the best; disadvantage keeps
47
- * the worst of two.
48
- */
49
- declare function critProbability(critRange: number, rollType?: RollType): number;
50
- /**
51
- * The canonical "clean miss" outcome — a point of zero damage with no rider.
52
- * This is the {@link OutcomeType} that attribution charts and outcome stats key
53
- * on, and is distinct from the builder's attack-resolution `miss` weight label.
54
- */
55
- declare const MISS_NONE_OUTCOME: OutcomeType;
56
- /**
57
- * All outcome types in canonical severity order — clean miss → crit. This is
58
- * also the natural stacking order for attribution charts (least- to
59
- * most-impactful, bottom → top). Enumerates every {@link OutcomeType} exactly
60
- * once; use it instead of hand-maintained per-consumer outcome tables.
61
- */
62
- declare const ALL_OUTCOME_TYPES: OutcomeType[];
63
- /**
64
- * Outcome types in display order for stats / breakdown rows — most prominent
65
- * first (crit, hit, …) down to the clean miss.
66
- */
67
- declare const OUTCOME_DISPLAY_ORDER: OutcomeType[];
68
- /**
69
- * Sort outcome labels by a canonical order (defaults to {@link ALL_OUTCOME_TYPES}).
70
- * Labels not present in `order` sort after known ones, alphabetically — so
71
- * ad-hoc/test labels outside the {@link OutcomeType} union stay stable.
72
- */
73
- declare function sortOutcomes<T extends string>(outcomes: Iterable<T>, order?: readonly string[]): T[];
74
- declare const onAnyHit: OutcomeType[];
75
- declare const onCritOnly: OutcomeType[];
76
- declare const onHitOnly: OutcomeType[];
77
- declare const onMissOnly: OutcomeType[];
78
- declare const onMissDamageOnly: OutcomeType[];
79
- declare const onSaveHalfOnly: OutcomeType[];
80
- declare const onSaveFailOnly: OutcomeType[];
81
- declare const onPotentCantripOnly: OutcomeType[];
82
-
83
- /**
84
- * Query interface for analyzing dice roll probability distributions.
85
- *
86
- * Combines multiple attack PMFs and provides statistical analysis methods for:
87
- * - Basic statistics (mean, variance, min/max, percentiles)
88
- * - Probability queries (hit chances, success rates, exact counts)
89
- * - Damage analysis (ranges by outcome type, expected values)
90
- * - Data export (charts, tables, visualizations)
91
- *
92
- */
93
- declare class DiceQuery {
94
- readonly singles: PMF[];
95
- private readonly _eps;
96
- private readonly _combinedProvided;
97
- private _combined?;
98
- private _combinedWithAttr?;
99
- constructor(singles: PMF | PMF[], combined?: PMF, eps?: number);
100
- /**
101
- * The combined damage distribution of all single PMFs (their convolution),
102
- * normalized to total probability 1.
103
- *
104
- * Computed lazily on first access and cached. Queries that only need
105
- * additive statistics — {@link DiceQuery.mean}, {@link DiceQuery.variance},
106
- * {@link DiceQuery.stddev} — never trigger this convolution.
107
- */
108
- get combined(): PMF;
109
- private static readonly DEFAULT_OUTCOMES;
110
- /**
111
- * Returns a new PMF with damage attribution metadata populated.
112
- *
113
- * This method computes attribution on-demand for builder-generated PMFs,
114
- * enabling them to work with damage attribution charts. The `attr` field
115
- * tracks how much damage each outcome type contributes at each damage value.
116
- *
117
- * For each bin at damage D: sum(attr.values()) ≈ D × P(damage = D)
118
- *
119
- * Performance: Cached after first call. Adds minimal overhead vs `combined`.
120
- *
121
- * @returns PMF with attr field populated for damage attribution charts
122
- *
123
- * @example
124
- * const attack = d20.plus(5).ac(15).onHit(d(2,6).plus(3)).onCrit(d(2,6))
125
- * const query = attack.toQuery()
126
- * const pmf = query.combinedWithAttribution()
127
- * // Now pmf can be used with toDamageAttributionChartSeries()
128
- */
129
- combinedWithAttribution(): PMF;
130
- /**
131
- * Per-label `damage value → probability mass` series for the combined,
132
- * attribution-carrying distribution — the provenance core of the stacked
133
- * damage-attribution chart. Convenience for
134
- * `combinedWithAttribution().attributionByValue()`; see
135
- * {@link PMF.attributionByValue}.
136
- */
137
- attributionByValue(): Map<string, Map<number, number>>;
138
- /**
139
- * How many of the independent single PMFs can produce the given outcome
140
- * label. Useful for "all of them succeeded" style probabilities where the
141
- * exponent is the number of contributing attacks (see
142
- * {@link DiceQuery.probExactlyK}).
143
- */
144
- countSinglesWith(label: string): number;
145
- /**
146
- * Returns the expected damage across all possible outcomes.
147
- *
148
- * Example: `query.mean()` → 12.5
149
- * Use case: "What's my average damage per round?"
150
- */
151
- mean(): number;
152
- /**
153
- * Returns the variance of the damage distribution.
154
- *
155
- * Example: `query.variance()` → 45.2
156
- * Use case: "How much does my damage vary from the average?"
157
- * High variance means higher risk/reward. Lower variance means more consistent damage.
158
- */
159
- variance(): number;
160
- /**
161
- * Returns the standard deviation of the damage distribution.
162
- *
163
- * Example: `query.stdev()` → 6.7
164
- * Use case: "What's the typical spread around my average damage?"
165
- * Used to determine how consistent the damage is.
166
- */
167
- stddev(): number;
168
- /** Alias of {@link DiceQuery.stddev}, matching {@link PMF.stdev}. */
169
- stdev(): number;
170
- /**
171
- * Returns the Cumulative Distribution Function.
172
- */
173
- cdf(x: number): number;
174
- /**
175
- * Returns the probability of dealing X damage or less.
176
- * In statistics, this is called the cumulative distribution function (CDF).
177
- * Example: `query.cdf(20)` → 0.75
178
- * Use case: "What's the chance I deal 20 damage or less?"
179
- */
180
- probTotalAtMost(x: number): number;
181
- /**
182
- * Returns the Complementary Cumulative Distribution Function.
183
- */
184
- ccdf(x: number): number;
185
- /**
186
- * Returns the probability of dealing at least X damage.
187
- *
188
- * Example: `query.probTotalAtLeast(25)` → 0.35
189
- * Use case: "What's the chance I deal at least 25 damage to finish the enemy?"
190
- */
191
- probTotalAtLeast(threshold: number): number;
192
- /**
193
- * Returns damage values at specific percentiles.
194
- *
195
- * Example: `query.percentiles([0.25, 0.5, 0.75])` → [8, 12, 18]
196
- * Use case: "What are my 25th, 50th, and 75th percentile damage values?"
197
- */
198
- percentiles(percentileValues: number[]): number[];
199
- /**
200
- * Returns the minimum possible damage.
201
- *
202
- * Example: `query.min()` → 0
203
- * Use case: "What's the worst-case damage if everything misses?"
204
- */
205
- min(): number;
206
- /**
207
- * Returns the maximum possible damage.
208
- *
209
- * Example: `query.max()` → 56
210
- * Use case: "What's the best-case damage if everything crits and rolls max?"
211
- */
212
- max(): number;
213
- private singleProb;
214
- /**
215
- * Full count distribution [P(0), P(1), …, P(n)] for "an attack succeeds if it
216
- * carries ANY of `labels`", over the n independent singles.
217
- *
218
- * Each single's per-event success probability is the Poisson-binomial
219
- * marginal P(≥1 of labels) from {@link probabilityOf} (i.e. probAtLeastOne),
220
- * computed exactly once. The binomial DP then runs once to produce the whole
221
- * distribution, so the array-label paths of probExactlyK / probAtLeastK /
222
- * probAtMostK can slice or sum from it instead of rebuilding a DiceQuery and
223
- * re-running the DP per requested k.
224
- */
225
- private countDistribution;
226
- probAtLeastK(labels: OutcomeType | OutcomeType[], k: number): number;
227
- /**
228
- * Returns the probability that at least one attack has the specified outcome(s).
229
- * - This is the complement of probAtMostK(labels, 0)
230
- *
231
- * Examples:
232
- * - `query.probAtLeastOne('hit')` → 0.88 (88% chance at least one attack hits)
233
- * - `query.probAtLeastOne(['hit', 'crit'])` → 0.96 (96% chance at least one succeeds)
234
- *
235
- * Use cases:
236
- * - "What's the chance at least one of my attacks connects?"
237
- *
238
- * Note:
239
- *
240
- * - You have to pass in an array of labels to avoid double-counting if you are
241
- * using multiple labels. You cannot just add them.
242
- */
243
- probAtLeastOne(labels: OutcomeType | OutcomeType[]): number;
244
- /**
245
- * Computes binomial probabilities for exactly 0, 1, 2, ..., maxK occurrences of a label.
246
- *
247
- * Uses dynamic programming to efficiently calculate the probability distribution
248
- * of how many attacks will have the specified outcome, accounting for different
249
- * success probabilities across individual attacks.
250
- *
251
- * Example: For 3 attacks with 50% hit chance each, returns:
252
- * [0.125, 0.375, 0.375, 0.125] = [P(0 hits), P(1 hit), P(2 hits), P(3 hits)]
253
- *
254
- * @param label - The outcome type to count
255
- * @param maxK - Maximum number of occurrences to calculate (usually number of attacks)
256
- * @returns Array where index K contains P(exactly K attacks have the label)
257
- */
258
- private computeBinomialProbabilities;
259
- /**
260
- * Returns the probability that exactly K attacks result in the specified outcome(s).
261
- *
262
- * Single label examples:
263
- * - probExactlyK('hit', 2) = probability exactly 2 attacks hit
264
- * - probExactlyK('crit', 1) = probability exactly 1 attack crits
265
- * - probExactlyK('crit', 0) = probability no attacks crit
266
- *
267
- * Array examples:
268
- * - probExactlyK(['hit', 'crit'], 2) = probability exactly 2 attacks succeed
269
- * - probExactlyK(['hit', 'crit'], 1) = probability exactly 1 attack succeeds
270
- * - probExactlyK(['missDamage', 'missNone'], 0) = probability no attacks miss
271
- *
272
- * Use cases:
273
- * - "What's the chance exactly one of my attacks hits?"
274
- * - "How likely am I to get exactly 2 successes out of 3 attacks?"
275
- * - "What's the probability that exactly half my attacks succeed?"
276
- *
277
- * Note: For arrays, an attack counts as a "success" if it has any of the specified labels.
278
- * This is different from probAtMostK, which counts an attack as a "success" if it has ALL of the specified labels.
279
- */
280
- probExactlyK(labels: OutcomeType | OutcomeType[], k: number): number;
281
- /**
282
- * Returns the probability that AT MOST K attacks result in the specified outcome(s).
283
- *
284
- * Single label examples:
285
- * - probAtMostK('hit', 1) = probability 0 or 1 attacks hit (at most 1)
286
- * - probAtMostK('crit', 0) = probability no attacks crit
287
- * - probAtMostK('missDamage', 2) = probability at most 2 attacks miss
288
- *
289
- * Array examples:
290
- * - probAtMostK(['hit', 'crit'], 1) = probability at most 1 attack succeeds
291
- * - probAtMostK(['hit', 'crit'], 0) = probability no attacks succeed (all miss)
292
- *
293
- * Use cases:
294
- * - "What's the chance that at most one attack hits?" (rest miss)
295
- * - "How likely am I to have mostly failures?" (at most 1 success)
296
- * - "What's the probability of a really bad turn?" (at most 0 successes)
297
- *
298
- */
299
- probAtMostK(labels: OutcomeType | OutcomeType[], k: number): number;
300
- /**
301
- * Returns the expected damage attributed to specific outcome types.
302
- *
303
- * Single label examples:
304
- * - expectedDamageFrom('hit') = expected damage from hit components
305
- * - expectedDamageFrom('crit') = expected damage from crit components
306
- *
307
- * Array examples:
308
- * - expectedDamageFrom(['hit', 'crit']) = expected damage from any success
309
- * - expectedDamageFrom(['missDamage', 'missNone']) = expected damage from misses
310
- *
311
- * Use cases:
312
- * - "How much damage do I expect from successful attacks?"
313
- * - "What's the damage contribution from critical hits specifically?"
314
- * - "How much damage comes from miss effects (like save-for-half spells)?"
315
- */
316
- expectedDamageFrom(labels: OutcomeType | OutcomeType[]): number;
317
- /**
318
- * Returns damage statistics for scenarios where AT LEAST ONE attack results in
319
- * the specified outcome(s).
320
- *
321
- * This method answers "What happens when things go reasonably well?" rather than
322
- * "What's the theoretical maximum?" It includes mixed scenarios which are more
323
- * common and tactically relevant than pure scenarios.
324
- *
325
- * Single label examples:
326
- * - damageStatsFrom('hit') = damage range when at least one attack hits
327
- * - damageStatsFrom('crit') = damage range when at least one attack crits
328
- *
329
- * Array examples:
330
- * - damageStatsFrom(['hit', 'crit']) = damage range when at least one attack succeeds
331
- * - damageStatsFrom(['missDamage', 'missNone']) = damage range when at least one attack misses
332
- *
333
- * Tactical Use Cases:
334
- * - "Given that I don't completely whiff (99% of turns), what damage should I expect?"
335
- * - "When planning to kill a 60 HP enemy, what's my damage range on successful turns?"
336
- * - "Should I use this risky spell if it has good damage when it works?"
337
- * - "What's my damage potential when something goes right?" (vs pure failure)
338
- *
339
- * Combat Planning Examples:
340
- * - 4 attacks with 90% hit chance: "96% of the time you'll do 25-150 damage, avg 52"
341
- * (Much more useful than "You average 50 damage including complete misses")
342
- * - Risk assessment: "80% of successful turns do 40-80 damage, but 20% do 80-150"
343
- * - Resource management: "If I hit anything, I'll likely finish this enemy"
344
- *
345
- * Statistical Note:
346
- * This includes mixed scenarios (2 hits + 1 crit, 3 hits + 1 miss, etc.) which
347
- * occur far more frequently than pure scenarios. For pure scenarios, use combinedDamageStats.
348
- *
349
- * KNOWN LIMITATION (multi-attack, single label): the returned `count` is an
350
- * EXPECTED COUNT (E[#label], so > 1 for N≥2 attacks, not a probability), and
351
- * `avg` is the size-biased conditional mean E[dmg·#label]/E[#label] rather than
352
- * E[dmg | the label occurs]. For a single attack both are the plain
353
- * conditional figures. Use {@link probAtLeastOne} for the scenario probability.
354
- *
355
- * @example
356
- * // High-level tactical planning
357
- * const successStats = query.damageStatsFrom('hit')
358
- * const successChance = query.probAtLeastOne('hit')
359
- * console.log(`${(successChance*100).toFixed(1)}% chance to do ${successStats.min}-${successStats.max} damage`)
360
- */
361
- damageStatsFrom(labels: OutcomeType | OutcomeType[]): {
362
- min: number;
363
- max: number;
364
- avg: number;
365
- count: number;
366
- };
367
- /**
368
- * Returns damage statistics for scenarios where ALL attacks result in the specified
369
- * outcome, calculated by leveraging the pure partition of singles.
370
- *
371
- * This method answers "What's the theoretical best/worst case?" and "What are the
372
- * clean mathematical boundaries?" It provides pure scenarios without mixing outcomes.
373
- *
374
- * Examples:
375
- * - combinedDamageStats('hit') = damage range when all attacks hit (none crit, none miss)
376
- * - combinedDamageStats('crit') = damage range when all attacks crit (none just hit)
377
- *
378
- * UI and Display Use Cases:
379
- * - Statistics panels showing "MAX Hit Damage" (users expect pure hits, not mixed)
380
- * - "Best case scenario" vs "worst case scenario" analysis
381
- * - Mathematical verification: "Does our hit damage calculation match manual math?"
382
- * - Clean damage type attribution: "How much comes from base hits vs crits?"
383
- *
384
- * Design and Balance Use Cases:
385
- * - Game designers: "What's the damage ceiling if someone gets lucky?"
386
- * - Character optimization: "What's my absolute maximum potential?"
387
- * - Ability comparison: "Which build has higher crit ceiling?"
388
- * - Minimum guaranteed damage: "What's the worst I can do if everything hits?"
389
- *
390
- * Mathematical Use Cases:
391
- * - Validating complex calculations against simple manual math
392
- * - Understanding damage component contributions in isolation
393
- * - Separating luck (crit variance) from consistency (hit variance)
394
- * - Building intuition about damage sources
395
- *
396
- * When to Use This vs damageStatsFrom():
397
- * - Use THIS for: UI max/min displays, theoretical limits, clean comparisons
398
- * - Use damageStatsFrom() for: tactical planning, realistic expectations, mixed scenarios
399
- *
400
- * Statistical Note:
401
- * Pure scenarios (all hits, all crits) are rare but represent clear mathematical
402
- * boundaries. These stats help understand the "shape" of your damage potential.
403
- *
404
- * @example
405
- * // UI display logic
406
- * const pureHitMax = query.combinedDamageStats('hit').max // Clean "MAX Hit Damage: 90"
407
- * const pureCritMax = query.combinedDamageStats('crit').max // Clean "MAX Crit Damage: 168"
408
- *
409
- * // vs tactical planning (use damageStatsFrom instead)
410
- * const realisticRange = query.damageStatsFrom('hit') // Includes mixed scenarios
411
- */
412
- combinedDamageStats(targetLabel: OutcomeType): {
413
- min: number;
414
- max: number;
415
- avg: number;
416
- count: number;
417
- };
418
- /**
419
- * Returns the probability that at least one attack carries ANY of the
420
- * specified labels (the marginal P(≥1) across the independent attacks).
421
- *
422
- * Examples:
423
- * - `query.probabilityOf('hit')` → 0.88 (probability at least one hit occurs)
424
- * - `query.probabilityOf(['hit', 'crit'])` → 0.96 (probability of any success)
425
- *
426
- * Use cases:
427
- * - "What's the chance my resolution includes a success label?"
428
- * - "How likely am I to get any hits or crits across all attacks?"
429
- *
430
- * Note: this must NOT be computed by summing `combined` bin probabilities. A
431
- * single combined damage total is reachable by many outcome combinations and
432
- * a bin can hold several labels at once, so summing `bin.p` over bins that
433
- * contain a label over-counts. The correct marginal is the Poisson-binomial
434
- * complement over the per-attack probabilities, i.e. {@link probAtLeastOne}.
435
- */
436
- probabilityOf(labels: OutcomeType | OutcomeType[]): number;
437
- /**
438
- * Returns the probability of missing (any type of miss).
439
- *
440
- * Example: `query.missChance()` → 0.04
441
- * Use case: "What's the chance I miss completely this turn?"
442
- */
443
- missChance(): number;
444
- /**
445
- * Returns data formatted for plotting damage probability distribution.
446
- *
447
- * Example: `query.toChartSeries()` → [{x: 0, y: 0.04}, {x: 6, y: 0.1}, ...]
448
- * Use case: "I want to visualize my damage distribution in a chart."
449
- */
450
- toChartSeries(): Array<{
451
- x: number;
452
- y: number;
453
- }>;
454
- /**
455
- * Returns tabular data showing damage values and their probability breakdowns.
456
- *
457
- * Example: `query.toLabeledTable(['hit', 'crit'])` →
458
- * [{damage: 6, total: 0.01, hit: 0.008, crit: 0}, ...]
459
- *
460
- * Use case: "I want to see exactly how hit/crit probabilities contribute to each damage value."
461
- */
462
- toLabeledTable(labels?: OutcomeType[]): Array<{
463
- damage: number;
464
- total: number;
465
- } & Record<string, number>>;
466
- /**
467
- * Returns data for stacked charts with unconditional per-label probability mass per damage.
468
- *
469
- * - Each dataset value equals the unconditional probability mass for that label at that damage
470
- * (i.e., `bin.count[label]`).
471
- * - Column sums may be less than the total probability `bin.p` when you omit labels or when
472
- * there is unlabeled mass. Include all relevant outcome labels if you need the sum to match.
473
- * - This behavior matches tests that expect raw per-label mass (not proportional scaling).
474
- * - NOTE: This implementation may break dprcalc.com chart binning at large n, need to test it more.
475
- *
476
- * @example
477
- * query.toStackedChartData(['hit', 'crit'])
478
- * // → {labels: [0, 6, 12, ...], datasets: [{label: 'hit', data: [0, 0.03, ...]}, ...]}
479
- */
480
- toStackedChartData(labels?: OutcomeType[], epsilon?: number): {
481
- labels: number[];
482
- datasets: Array<{
483
- label: string;
484
- data: number[];
485
- }>;
486
- };
487
- /**
488
- * Returns pure mathematical data for attribution charts showing outcome contributions.
489
- *
490
- * Automatically discovers all outcome types present in the PMF, applies filtering rules,
491
- * and returns proportional data suitable for stacked visualization.
492
- *
493
- * @param options Configuration options
494
- * @param options.stackOrder Preferred order for outcome types (unknowns placed at end)
495
- * @param options.filterRules Function to determine if outcome should be included for a given damage value
496
- * @param options.asPercentages Whether to return percentages (0-100) or probabilities (0-1)
497
- * @returns Pure data structure with support, outcomes, and proportional data
498
- *
499
- * @example
500
- * query.toAttributionChartSeries()
501
- * // → {support: [0, 6, 12], outcomes: ['hit', 'crit'], data: {hit: [5.2, 8.1, ...], crit: [0, 2.3, ...]}}
502
- */
503
- toAttributionChartSeries(options?: {
504
- stackOrder?: string[];
505
- filterRules?: (outcome: string, damage: number) => boolean;
506
- asPercentages?: boolean;
507
- }): {
508
- support: number[];
509
- outcomes: string[];
510
- data: {
511
- [outcome: string]: number[];
512
- };
513
- };
514
- /**
515
- * Returns pure mathematical data for damage attribution charts showing damage contribution
516
- * from each outcome type at each damage value.
517
- *
518
- * Similar to toAttributionChartSeries() but uses bin.attr (damage attribution) instead of
519
- * bin.count (probability attribution).
520
- *
521
- * @param options Configuration options
522
- * @param options.stackOrder Preferred order for outcome types (unknowns placed at end)
523
- * @param options.filterRules Function to determine if outcome should be included for a given damage value
524
- * @param options.asPercentages Whether to return percentages (0-100) or raw damage values (0+)
525
- * @returns Pure data structure with support, outcomes, and damage attribution data
526
- *
527
- * @example
528
- * query.toDamageAttributionChartSeries()
529
- * // → {support: [0, 6, 12], outcomes: ['hit', 'crit'], data: {hit: [3.2, 5.1, ...], crit: [0, 1.8, ...]}}
530
- */
531
- toDamageAttributionChartSeries(options?: {
532
- stackOrder?: string[];
533
- filterRules?: (outcome: string, damage: number) => boolean;
534
- asPercentages?: boolean;
535
- }): {
536
- support: number[];
537
- outcomes: string[];
538
- data: {
539
- [outcome: string]: number[];
540
- };
541
- };
542
- /**
543
- * Returns pure mathematical data for outcome attribution charts showing which
544
- * attack outcome combinations can produce each damage value.
545
- *
546
- * Unlike toDamageAttributionChartSeries() which tracks damage sources, this tracks
547
- * outcome combinations - answering "what attack outcomes produced this damage?"
548
- *
549
- * @param options Configuration options
550
- * @param options.stackOrder Preferred order for outcome types (unknowns placed at end)
551
- * @param options.filterRules Function to determine if outcome should be included for a given damage value
552
- * @param options.asPercentages Whether to return percentages (0-100) or probabilities (0-1)
553
- * @returns Pure data structure with support, outcomes, and outcome combination probabilities
554
- *
555
- * @example
556
- * query.toOutcomeAttributionChartSeries()
557
- * // → {support: [0, 6, 12], outcomes: ['all_miss', 'mixed', 'all_hit'], data: {all_miss: [15, 0, 0], mixed: [60, 80, 20], all_hit: [25, 20, 80]}}
558
- */
559
- toOutcomeAttributionChartSeries(options?: {
560
- stackOrder?: string[];
561
- filterRules?: (outcome: string, damage: number) => boolean;
562
- asPercentages?: boolean;
563
- }): {
564
- support: number[];
565
- outcomes: string[];
566
- data: {
567
- [outcome: string]: number[];
568
- };
569
- };
570
- /**
571
- * Returns pure mathematical data for cumulative distribution function (CDF).
572
- * Shows P(X ≤ x) - the probability of getting at most x damage.
573
- *
574
- * @param asPercentages Whether to return percentages (0-100) or probabilities (0-1)
575
- * @returns Pure data structure with support and cumulative probabilities
576
- *
577
- * @example
578
- * query.toCDFSeries()
579
- * // → {support: [0, 6, 12], data: [5.2, 18.3, 45.1]}
580
- */
581
- toCDFSeries(asPercentages?: boolean): {
582
- support: number[];
583
- data: number[];
584
- };
585
- /**
586
- * Returns pure mathematical data for complementary cumulative distribution function (CCDF).
587
- * Shows P(X ≥ x) - the probability of getting at least x damage.
588
- *
589
- * @param asPercentages Whether to return percentages (0-100) or probabilities (0-1)
590
- * @returns Pure data structure with support and complementary cumulative probabilities
591
- *
592
- * @example
593
- * query.toCCDFSeries()
594
- * // → {support: [0, 6, 12], data: [100, 94.8, 81.7]}
595
- */
596
- toCCDFSeries(asPercentages?: boolean): {
597
- support: number[];
598
- data: number[];
599
- };
600
- /** Probability of doing strictly more than threshold damage (default >0). */
601
- probDamageGreaterThan(threshold?: number): number;
602
- /** All outcome keys actually present (typed & ordered if you pass an order). */
603
- outcomeKeys(order?: OutcomeType[]): OutcomeType[];
604
- /** Total probability per outcome across the PMF. */
605
- outcomeTotals(outcomes?: OutcomeType[]): Map<OutcomeType, number>;
606
- /** Conditional damage range per outcome (min/avg/max of X | outcome). */
607
- outcomeDamageRanges(outcomes?: OutcomeType[]): Map<OutcomeType, {
608
- min: number;
609
- avg: number;
610
- max: number;
611
- }>;
612
- /**
613
- * Snapshot of the distribution in the exact shape the UI consumes.
614
- * - outcome probabilities are "at least one" (and equal to "all" for a single PMF)
615
- * - damageRange is conditional on the outcome occurring
616
- *
617
- * The outcome probabilities use the correct Poisson-binomial marginals
618
- * (`atLeastOneProbability` = P(≥1 attack has it), `allProbability` = P(all do)),
619
- * so they are always valid probabilities in [0,1].
620
- *
621
- * KNOWN LIMITATION (multi-attack): `damageRange.avg` is still aggregated from
622
- * the combined PMF's `count`, which the convolution accumulates as an EXPECTED
623
- * COUNT, so for N≥2 attacks it is the size-biased mean E[dmg·#label]/E[#label]
624
- * rather than a clean conditional expectation. It is correct for a single
625
- * attack.
626
- */
627
- snapshot(order?: readonly OutcomeType[]): Snapshot;
628
- /**
629
- * PMF Transformation Methods
630
- *
631
- * These methods provide a fluent API for transforming dice queries by wrapping
632
- * the underlying PMF transformation methods. All operations work on the combined
633
- * PMF and return new DiceQuery instances.
634
- */
635
- /**
636
- * Returns a new DiceQuery with normalized probabilities (ensuring they sum to 1.0).
637
- *
638
- * @returns New DiceQuery with normalized combined PMF
639
- */
640
- normalize(): DiceQuery;
641
- /**
642
- * Returns a new DiceQuery with low-probability outcomes removed.
643
- *
644
- * @param eps Minimum probability threshold (defaults to PMF epsilon)
645
- * @param keepFinalBin Whether to keep the highest damage bin regardless of probability
646
- * @returns New DiceQuery with compacted combined PMF
647
- */
648
- compact(eps?: number, keepFinalBin?: boolean): DiceQuery;
649
- /**
650
- * Returns a new DiceQuery with an additional scaled branch added.
651
- * Useful for conditional outcomes like "30% chance of opportunity attack".
652
- *
653
- * @param branch DiceQuery to add as a scaled branch
654
- * @param probability Probability of the branch occurring (0-1)
655
- * @returns New DiceQuery combining this query with the scaled branch
656
- *
657
- * @example
658
- * const baseAttack = parse("(d20 + 5 AC 15) * (2d6 + 3)");
659
- * const opportunityAttack = parse("(d20 + 5 AC 15) * (1d8 + 3)");
660
- * const withOpportunity = baseAttack.addScaled(opportunityAttack, 0.3);
661
- */
662
- addScaled(branch: DiceQuery, probability: number): DiceQuery;
663
- /**
664
- * Returns a new DiceQuery with all probabilities scaled by a factor.
665
- * Used for conditional scenarios where the entire outcome has reduced probability.
666
- *
667
- * @param factor Scaling factor for probabilities
668
- * @returns New DiceQuery with scaled probabilities
669
- *
670
- * @example
671
- * const fullAttack = parse("(d20 + 5 AC 15) * (2d6 + 3)");
672
- * const conditionalAttack = fullAttack.scaleMass(0.3); // 30% chance scenario
673
- */
674
- scaleMass(factor: number): DiceQuery;
675
- totalMass(): number;
676
- /**
677
- * Returns a new DiceQuery with damage values transformed by a function.
678
- * Useful for applying modifiers, resistances, or other damage transformations.
679
- *
680
- * @param damageTransformFunction Function to transform each damage value
681
- * @returns New DiceQuery with transformed damage values
682
- *
683
- * @example
684
- * const baseAttack = parse("2d6 + 3");
685
- * const withResistance = baseAttack.mapDamage(dmg => Math.floor(dmg / 2)); // Half damage
686
- * const withBonus = baseAttack.mapDamage(dmg => dmg + 5); // +5 damage
687
- */
688
- mapDamage(damageTransformFunction: (damageValue: number) => number): DiceQuery;
689
- /**
690
- * Returns a new DiceQuery with damage values scaled by a factor.
691
- * Convenient wrapper around mapDamage for multiplicative scaling.
692
- *
693
- * @param factor Scaling factor for damage values
694
- * @param rounding Rounding method: "floor" (default), "round", or "ceil"
695
- * @returns New DiceQuery with scaled damage values
696
- *
697
- * @example
698
- * const baseAttack = parse("2d6 + 3");
699
- * const doubled = baseAttack.scaleDamage(2); // Double damage
700
- * const halfDamage = baseAttack.scaleDamage(0.5, "round"); // Half damage, rounded
701
- */
702
- scaleDamage(factor: number, rounding?: "floor" | "round" | "ceil"): DiceQuery;
703
- /**
704
- * Returns a new DiceQuery combining this query with another via convolution.
705
- * Equivalent to rolling both queries independently and adding results.
706
- * It is important to use this rather than combing()ing the PMFs directly!
707
- * This method maintains the provenance of the PMFs which is needed for damage attribution.
708
- * Combining the .combined PMFs directly is still valid for DPR calculations but
709
- * is not statistically sound for queries.
710
- *
711
- * @param other DiceQuery to combine with
712
- * @param eps Optional epsilon for precision control
713
- * @returns New DiceQuery representing the combined outcome
714
- *
715
- * @example
716
- * const mainAttack = parse("(d20 + 5 AC 15) * (2d6 + 3)");
717
- * const bonusAttack = parse("(d20 + 3 AC 15) * (1d6 + 1)");
718
- * const bothAttacks = mainAttack.convolve(bonusAttack);
719
- */
720
- convolve(other: DiceQuery): DiceQuery;
721
- /**
722
- * First-success split over an ordered list of DISTINCT single-swing PMFs.
723
- * Each PMF may have different success/subset probabilities (from labels).
724
- *
725
- * successOutcome: e.g., ["success"] or ["hit", "crit"]
726
- * subsetOutcome: e.g., ["subset"] or ["crit"] where subset ⊆ success
727
- *
728
- * Returns tuple: [pFirstNonSubset, pFirstSubset, pAnySuccess, pNone]
729
- */
730
- firstSuccessSplit(successOutcome: OutcomeType | OutcomeType[], subsetOutcome: OutcomeType | OutcomeType[], eps?: number): readonly [pSuccess: number, pSubset: number, pAny: number, pNone: number];
731
- }
732
- type OutcomeSnapshot = {
733
- atLeastOneProbability: number;
734
- allProbability: number;
735
- damageRange: {
736
- min: number;
737
- avg: number;
738
- max: number;
739
- };
740
- };
741
- type Snapshot = {
742
- averageDPR: number;
743
- damageChance: number;
744
- percentiles: {
745
- p25: number;
746
- p50: number;
747
- p75: number;
748
- };
749
- outcomes: Map<OutcomeType, OutcomeSnapshot>;
750
- };
751
-
752
- declare const pmfCache: LRUCache<string, PMF>;
753
- /**
754
- * Probability Mass Function for discrete damage distributions.
755
- */
756
- declare class PMF {
757
- readonly map: Map<number, Bin>;
758
- readonly epsilon: number;
759
- readonly normalized: boolean;
760
- readonly identifier: string;
761
- private _preservedProvenance;
762
- private static __anonIdCounter;
763
- private _support?;
764
- private _min?;
765
- private _max?;
766
- private _totalMass?;
767
- private _mean?;
768
- private _variance?;
769
- private _stdev?;
770
- private _fingerprint?;
771
- constructor(map?: Map<number, Bin>, epsilon?: number, normalized?: boolean, identifier?: string, _preservedProvenance?: boolean);
772
- static empty(epsilon?: number, identifier?: string): PMF;
773
- static zero(epsilon?: number): PMF;
774
- static delta(value: number, epsilon?: number): PMF;
775
- /**
776
- * Point mass at damage 0 tagged with the canonical `missNone` outcome.
777
- *
778
- * Differs from {@link PMF.zero}, which labels its zero bin `miss` — the
779
- * builder's attack-resolution vocabulary. This uses the `missNone`
780
- * {@link OutcomeType} that the attribution charts and outcome stats key on,
781
- * so it is the correct "clean miss / no damage" delta for provenance-aware
782
- * mixtures feeding those consumers.
783
- */
784
- static missNone(epsilon?: number): PMF;
785
- static emptyMass(): PMF;
786
- [Symbol.iterator](): IterableIterator<[number, Bin]>;
787
- static clearCache(): void;
788
- /**
789
- * Creates a conditional PMF from two branches (success and failure) and a probability.
790
- * This is the core logic for modeling any probabilistic event where there are two
791
- * distinct outcomes.
792
- */
793
- static branch(successPMF: PMF, failurePMF: PMF, successProbability: number): PMF;
794
- /**
795
- * withProbability()
796
- *
797
- * A convenience wrapper around branch() for the common case where the "failure" branch is always zero().
798
- *
799
- * Think of this as a shortcut for:
800
- * pmf.gate(p, PMF.zero())
801
- *
802
- * Use this to model a *single* Bernoulli event — an outcome that either happens or doesn't,
803
- * like an opportunity attack that occurs with probability p, or a single attack that either hits or misses.
804
- *
805
- * This is **not** for combining multiple independent attacks or mutually exclusive multi-outcome scenarios.
806
- * - For multiple independent swings, use DiceQuery with separate PMFs for each attack.
807
- * - For modeling "first success" logic across multiple attacks (like Sneak Attack or Smite)
808
- * use query.firstSuccessSplit() to get the exact probabilities.
809
- * - For scenarios with several mutually exclusive outcomes (like crit vs hit vs none), use PMF.exclusive().
810
- *
811
- */
812
- static withProbability(successPMF: PMF, probability: number): PMF;
813
- /**
814
- * gate()
815
- *
816
- * A conditional wrapper around branch() that applies this PMF with probability `p`,
817
- * and applies a provided fallback PMF otherwise.
818
- *
819
- * This is useful for modeling a binary choice between two outcomes:
820
- * - The "success" outcome (this PMF) happens with probability `p`.
821
- * - The "failure" outcome (fallback PMF) happens with probability `1 - p`.
822
- *
823
- * Examples:
824
- * - 25% chance to include an opportunity attack, otherwise nothing:
825
- * attackPMF.gate(0.25, PMF.zero())
826
- *
827
- * - 50% chance to deal fireball damage, otherwise cone of cold damage:
828
- * fireballPMF.gate(0.5, coneOfColdPMF)
829
- *
830
- * Relationship to other helpers:
831
- * - **withProbability()** is a shortcut for the common case where the fallback is `PMF.zero()`.
832
- * - **exclusive()** is for three or more mutually exclusive outcomes (e.g., crit vs hit vs none).
833
- *
834
- * @param p Probability of applying this PMF (between 0 and 1).
835
- * @param fallback PMF to apply when this PMF is *not* selected.
836
- * @returns A new PMF representing the weighted mixture of this PMF and the fallback.
837
- */
838
- gate(p: number, fallback: PMF): PMF;
839
- /**
840
- * PMF.exclusive()
841
- *
842
- * Builds a single PMF from a set of mutually exclusive weighted outcomes.
843
- * Exactly one of the provided options will occur.
844
- *
845
- * Each option has:
846
- * - A PMF representing its outcome (e.g., damage dice).
847
- * - A weight representing its probability of being selected.
848
- *
849
- * Notes:
850
- * - If total weight < 1 (within eps), leftover mass is assumed to be PMF.zero()
851
- *
852
- * @param options Array of `{ pmf, weight }` or `[PMF, number]`.
853
- * @param eps Optional tolerance for floating point rounding.
854
- */
855
- static exclusive(options: Array<{
856
- pmf: PMF;
857
- weight: number;
858
- } | [PMF, number]>, eps?: number): PMF;
859
- /**
860
- * PMF.mix()
861
- *
862
- * Builds a PMF as a linear combination of input PMFs with the given weights.
863
- * Unlike `exclusive`, this does NOT:
864
- * - enforce that weights sum to 1
865
- * - add leftover probability to δ0 (PMF.zero())
866
- *
867
- * Use when outcomes are not mutually exclusive, or for interpolation/blending.
868
- *
869
- * @param options Array of `{ pmf, weight }` or `[PMF, number]`.
870
- * @param eps Optional tolerance for skipping tiny weights.
871
- */
872
- static mix(options: Array<{
873
- pmf: PMF;
874
- weight: number;
875
- } | [PMF, number]>, eps?: number): PMF;
876
- /**
877
- * Adds damage attribution metadata to this PMF based on existing count metadata.
878
- * For each bin, sets attr[outcome] = damage × count[outcome].
879
- *
880
- * This enables damage attribution charts to work with builder-generated PMFs.
881
- * The parser generates attr automatically, but builder PMFs only have count.
882
- *
883
- * @returns New PMF with attr field populated in each bin
884
- */
885
- /**
886
- * Returns true if this PMF already carries damage attribution metadata.
887
- *
888
- * Only the first positive-damage bin is inspected (parser-generated PMFs
889
- * populate `attr` uniformly), so this is O(1) in practice.
890
- */
891
- hasAttribution(): boolean;
892
- withAttribution(): PMF;
893
- /**
894
- * General-purpose N-way mixture.
895
- * weights: Array of [weight, PMF].
896
- *
897
- * Example: PMF.mixN([
898
- * [pMiss, zero],
899
- * [pHit, hitPMF],
900
- * [pCrit, critPMF],
901
- * ]);
902
- */
903
- static mixN(weights: [number, PMF][], eps?: number): PMF;
904
- private setPreservedProvenance;
905
- preservedProvenance(): boolean;
906
- private getPowerCacheKey;
907
- /**
908
- * Efficiently computes this PMF convolved with itself `n` times.
909
- * Uses exponentiation by squaring to reduce total convolutions.
910
- * n must be a positive integer.
911
- * *
912
- * * NOTE: This folds multiple independent attacks into a single PMF.
913
- * As a result, The power() method causes a loss of data provenance.
914
- * This is ONLY SAFE if you are trying to calculate masses.
915
- * If you want to query any atLeast probabilities, you should use the DiceQuery class instead without power().
916
- */
917
- power(n: number, eps?: number): PMF;
918
- replicate(n: number): PMF[];
919
- mass(): number;
920
- outcomeMass(outcome: string): number;
921
- faceTotal(): number;
922
- normalize(): PMF;
923
- /**
924
- * Returns a copy with negligible probabilities removed (p < eps).
925
- * If keepFinalBin is true, the bin with the largest key is always kept,
926
- * even if its probability is below eps. count/attr submaps are still cleaned.
927
- */
928
- compact(eps?: number, keepFinalBin?: boolean): PMF;
929
- support(): number[];
930
- min(): number;
931
- max(): number;
932
- /**
933
- * Returns the expected (mean) damage value.
934
- * Cached for performance since this requires iterating through all bins.
935
- */
936
- mean(): number;
937
- /**
938
- * Returns the variance of the damage distribution.
939
- * Cached for performance since this requires mean calculation plus iteration.
940
- */
941
- variance(): number;
942
- /**
943
- * Returns the standard deviation of the damage distribution.
944
- */
945
- stdev(): number;
946
- /** Deep-copies a Bin, cloning its count and (optional) attr maps. */
947
- private static cloneBin;
948
- /** Returns a new Bin with p, count, and attr all multiplied by `factor`. */
949
- private static scaleBin;
950
- private static mergeInto;
951
- add(other: PMF): PMF;
952
- /**
953
- * Returns a new PMF with a scaled branch added to this one.
954
- * The branch PMF is scaled by the given probability before merging
955
- * This will be very useful for conditional effects and for being
956
- * able to model "I can probably have this opportunity attack 40% of rounds"
957
- * Example: `pmf.addScaled(critBranch, 0.05)` → PMF including 5% crit outcomes
958
- */
959
- addScaled(branch: PMF, probability: number): PMF;
960
- /**
961
- * Redistributes probability mass to model an effect that only occurs with
962
- * probability `frequency` — a conditional attack, an on-hit rider, or a
963
- * sub-one AoE target fraction.
964
- *
965
- * Every hit outcome (damage > 0) is scaled by `frequency` — probability mass,
966
- * per-label `count`, AND per-label `attr` — and the freed mass is moved into
967
- * the miss bin at damage 0, tagged with the canonical `missNone` outcome.
968
- * Total probability mass is preserved.
969
- *
970
- * Unlike a bare {@link scaleMass} or {@link mapDamage}, this keeps damage
971
- * attribution (`attr`) intact, so a frequency-scaled PMF still renders
972
- * correctly in the damage-attribution charts.
973
- *
974
- * `frequency >= 1` (or non-finite) returns this PMF unchanged; `frequency <= 0`
975
- * collapses all mass into the miss bin. The miss outcome is assumed to be
976
- * encoded at damage value 0.
977
- *
978
- * @param frequency Probability in [0, 1] that the effect occurs.
979
- */
980
- applyHitFrequency(frequency: number): PMF;
981
- scaleMass(factor: number): PMF;
982
- mapDamage(damageTransformFunction: (damageValue: number) => number): PMF;
983
- scaleDamage(factor: number, rounding?: "floor" | "round" | "ceil"): PMF;
984
- private getPMFCombineCacheKey;
985
- /**
986
- * A small content fingerprint (mass + bin count + face sum) so convolution
987
- * cache keys change if the underlying numbers do. Memoized because a PMF is
988
- * immutable once constructed — this avoids re-summing every key on each
989
- * convolve() call (including cache hits).
990
- */
991
- fingerprint(): string;
992
- convolve(other: PMF, eps?: number, raw?: boolean): PMF;
993
- combineRaw(other: PMF, eps?: number): PMF;
994
- private static reduceConvolveLeft;
995
- /**
996
- * Convolves multiple PMFs using linear convolution with automatic caching.
997
- * Uses a left-to-right accumulation approach for maximum cache reuse.
998
- * Each convolve() call automatically uses the convolution cache for performance.
999
- *
1000
- * This linear approach provides better cache hits than pairwise because:
1001
- * - Intermediate results are more predictable and stable
1002
- * - Similar PMF lists share common prefixes (A+B, (A+B)+C, etc.)
1003
- * - Order-independent cache keys work better with consistent build patterns
1004
- */
1005
- static convolveMany(pmfList: PMF[], eps?: number): PMF;
1006
- /**
1007
- * Returns a plain, JSON-serializable representation of this PMF.
1008
- *
1009
- * Follows the standard `toJSON` contract, so `JSON.stringify(pmf)` produces
1010
- * the expected output (no double-encoding). Use {@link PMF.fromJSON} to
1011
- * reconstruct, or {@link PMF.toJSONString} if you need the string directly.
1012
- */
1013
- toJSON(): {
1014
- bins: Array<[number, Bin]>;
1015
- normalized: boolean;
1016
- identifier: string;
1017
- };
1018
- /** Serializes this PMF to a JSON string (equivalent to `JSON.stringify(pmf)`). */
1019
- toJSONString(): string;
1020
- static fromJSON(jsonData: {
1021
- bins: Array<[number, Bin]>;
1022
- normalized?: boolean;
1023
- identifier?: string;
1024
- }): PMF;
1025
- /**
1026
- * Relative pruning with optional top-K floor.
1027
- * Keeps bins with p >= epsRel * peak, always keeps min and max damage,
1028
- * optionally guarantees at least `minBins` survivors by adding top-K.
1029
- * Returns a new, non-normalized PMF.
1030
- */
1031
- prune(epsRel: number, minBins?: number): PMF;
1032
- /** Probability mass at exactly x. */
1033
- pAt(x: number): number;
1034
- /**
1035
- * P(any damage) — the mass on all non-zero outcomes, i.e. `1 - P(0)`.
1036
- * Assumes a miss is encoded as the damage-0 bin (the convention used across
1037
- * attack/save PMFs). The dual of {@link missProbability}.
1038
- */
1039
- hitProbability(): number;
1040
- /** P(no damage) — the mass at damage 0. The dual of {@link hitProbability}. */
1041
- missProbability(): number;
1042
- /**
1043
- * Coarsen the distribution into at most `maxBuckets` contiguous, equal-width
1044
- * damage buckets, aggregating probability mass (and `count`/`attr`
1045
- * provenance) into each bucket's start value. Returns this PMF unchanged when
1046
- * its integer support already fits within `maxBuckets`.
1047
- *
1048
- * This is a lossy display/downsampling transform (bucket start replaces the
1049
- * exact damage value) — use it for charting wide distributions, not for DPR
1050
- * math.
1051
- */
1052
- rebin(maxBuckets: number): PMF;
1053
- /** Dense integer support from min..max (inclusive).
1054
- * Useful for showing empty bars in charts.
1055
- */
1056
- denseSupport(): number[];
1057
- /** CDF at x: P(X ≤ x). */
1058
- cdfAt(x: number): number;
1059
- /** Quantile / inverse CDF for p in [0,1]. Returns smallest x with CDF ≥ p. */
1060
- quantile(p: number): number;
1061
- /** Get outcome probability at specific damage value. */
1062
- outcomeAt(damage: number, outcome: string): number;
1063
- /** Get all outcome types present in this PMF. */
1064
- outcomes(): string[];
1065
- /** Get total probability of an outcome across all damage values. */
1066
- outcomeProbability(outcome: string): number;
1067
- /** Get damage attribution for an outcome at specific damage value. */
1068
- outcomeAttributionAt(damage: number, outcome: string): number;
1069
- /** Get all outcome data at specific damage value. */
1070
- binAt(damage: number): {
1071
- p: number;
1072
- count: Record<string, number>;
1073
- attr?: Record<string, number>;
1074
- } | null;
1075
- /** Check if outcome exists in this PMF. */
1076
- hasOutcome(outcome: string): boolean;
1077
- /**
1078
- * Split each damage value's probability mass across outcome labels, returning
1079
- * per-label maps of `damage value → probability mass attributable to that
1080
- * label`. Summing over labels at a given value recovers that value's `p`.
1081
- *
1082
- * Damage-bearing bins are split by `attr` weight (the share of damage each
1083
- * outcome contributed); the clean-miss bin at 0 is split by `count` weight
1084
- * (there is no damage to attribute). Attribution is computed on demand via
1085
- * {@link withAttribution} when absent, so builder-generated PMFs work too.
1086
- *
1087
- * This is the provenance core of the stacked damage-attribution chart — the
1088
- * caller only maps these series into its rendering format (colors, binning,
1089
- * axis labels).
1090
- */
1091
- attributionByValue(): Map<string, Map<number, number>>;
1092
- tailProbGE(t: number): number;
1093
- tailProbGT(t: number): number;
1094
- /**
1095
- * Returns a new PMF containing only bins where the specified outcome has non-zero probability.
1096
- * This creates a marginal distribution for the given outcome type, with probabilities
1097
- * scaled to represent the unconditional mass attributable to that outcome.
1098
- */
1099
- filterOutcome(outcome: string): PMF;
1100
- /**
1101
- * Calculates probabilities for first-success outcomes across n independent attempts.
1102
- *
1103
- * @param pSuccess - Total probability of any success on a single attempt.
1104
- * @param pSpecial - Probability of a specific subset of successes (e.g., critical success).
1105
- * @param n - Number of independent attempts.
1106
- *
1107
- * Returns:
1108
- * - pSpecificSuccess: Probability that the first success was of the "special" type
1109
- * - pGeneralSuccess: Probability that the first success was of the non-special type
1110
- * - pNone: Probability that no successes occurred
1111
- * - pAny: Probability that at least one success occurred
1112
- */
1113
- static firstSuccessWeights(pSuccess: number, pSpecial: number, n: number): {
1114
- pSpecificSuccess: number;
1115
- pGeneralSuccess: number;
1116
- pNone: number;
1117
- pAny: number;
1118
- };
1119
- mapValues(f: (v: number) => number, eps?: number, opts?: {
1120
- rounding?: Rounding;
1121
- preserveCounts?: boolean;
1122
- }): PMF;
1123
- static fromMap(m: Map<number, number>, eps?: number, { requireIntegerValues }?: {
1124
- requireIntegerValues?: boolean;
1125
- }): PMF;
1126
- query(): DiceQuery;
1127
- }
1128
-
1129
- export { ALL_OUTCOME_TYPES as A, type Bin as B, type CritConfig as C, type DamageDistribution as D, EPS as E, LRUCache as L, MISS_NONE_OUTCOME as M, type OutcomeLabelMap as O, PMF as P, type Rounding as R, type Snapshot as S, type OutcomeType as a, type RollType as b, critProbability as c, OUTCOME_DISPLAY_ORDER as d, onCritOnly as e, onHitOnly as f, onMissOnly as g, onMissDamageOnly as h, onSaveHalfOnly as i, onSaveFailOnly as j, onPotentCantripOnly as k, DiceQuery as l, type OutcomeSnapshot as m, onAnyHit as o, pmfCache as p, sortOutcomes as s };