import { LRUCache } from "../common/lru-cache.js"; import type { Bin, Rounding } from "../common/types.js"; import { DiceQuery } from "./query.js"; export declare const pmfCache: LRUCache; /** * Complete numeric model for the stacked damage-attribution chart, produced by * {@link PMF.damageAttributionChartModel}. Carries every dice-and-probability * value the chart needs; a renderer only maps these numbers into its own format * (colors, human labels, axis units, dataset objects). */ export interface DamageAttributionChartModel { /** Bucket-start value for each column. Numeric; the caller stringifies for labels. */ labels: number[]; /** Present only when the distribution was coarsened; [start,end] inclusive, per bucket. */ binRanges?: { start: number; end: number; }[]; /** Discovered outcome labels in stack order. Empty for pure (unattributed) distributions. */ outcomes: string[]; /** outcome → per-bucket probability mass (fraction 0..1). Bar heights. Σ over outcomes ≈ totals[i]. */ series: Map; /** * outcome → per-bucket conditional share (fraction 0..1) of that bucket's total. * Tooltip signal. 0 where the bucket total is below `epsilon`; otherwise Σ over * outcomes ≈ 1. */ shares: Map; /** Per-bucket total probability mass (fraction 0..1). The only signal for pure distributions. */ totals: number[]; /** Reversed-convention CCDF markers: damage at which P(X ≥ x) crosses 80/50/20%. */ percentiles: { p80: number; p50: number; p20: number; }; /** Distribution mean, `this.mean()`. */ mean: number; } /** * Probability Mass Function for discrete damage distributions. */ export declare class PMF { readonly map: Map; readonly epsilon: number; readonly normalized: boolean; readonly identifier: string; private _preservedProvenance; private static __anonIdCounter; private _support?; private _min?; private _max?; private _totalMass?; private _mean?; private _variance?; private _stdev?; private _fingerprint?; constructor(map?: Map, epsilon?: number, normalized?: boolean, identifier?: string, _preservedProvenance?: boolean); static empty(epsilon?: number, identifier?: string): PMF; static zero(epsilon?: number): PMF; static delta(value: number, epsilon?: number): PMF; /** * Point mass at damage 0 tagged with the canonical `missNone` outcome. * * Differs from {@link PMF.zero}, which labels its zero bin `miss` — the * builder's attack-resolution vocabulary. This uses the `missNone` * {@link OutcomeType} that the attribution charts and outcome stats key on, * so it is the correct "clean miss / no damage" delta for provenance-aware * mixtures feeding those consumers. */ static missNone(epsilon?: number): PMF; static emptyMass(): PMF; [Symbol.iterator](): IterableIterator<[number, Bin]>; static clearCache(): void; /** * Creates a conditional PMF from two branches (success and failure) and a probability. * This is the core logic for modeling any probabilistic event where there are two * distinct outcomes. */ static branch(successPMF: PMF, failurePMF: PMF, successProbability: number): PMF; /** * withProbability() * * A convenience wrapper around branch() for the common case where the "failure" branch is always zero(). * * Think of this as a shortcut for: * pmf.gate(p, PMF.zero()) * * Use this to model a *single* Bernoulli event — an outcome that either happens or doesn't, * like an opportunity attack that occurs with probability p, or a single attack that either hits or misses. * * This is **not** for combining multiple independent attacks or mutually exclusive multi-outcome scenarios. * - For multiple independent swings, use DiceQuery with separate PMFs for each attack. * - For modeling "first success" logic across multiple attacks (like Sneak Attack or Smite) * use query.firstSuccessSplit() to get the exact probabilities. * - For scenarios with several mutually exclusive outcomes (like crit vs hit vs none), use PMF.exclusive(). * */ static withProbability(successPMF: PMF, probability: number): PMF; /** * gate() * * A conditional wrapper around branch() that applies this PMF with probability `p`, * and applies a provided fallback PMF otherwise. * * This is useful for modeling a binary choice between two outcomes: * - The "success" outcome (this PMF) happens with probability `p`. * - The "failure" outcome (fallback PMF) happens with probability `1 - p`. * * Examples: * - 25% chance to include an opportunity attack, otherwise nothing: * attackPMF.gate(0.25, PMF.zero()) * * - 50% chance to deal fireball damage, otherwise cone of cold damage: * fireballPMF.gate(0.5, coneOfColdPMF) * * Relationship to other helpers: * - **withProbability()** is a shortcut for the common case where the fallback is `PMF.zero()`. * - **exclusive()** is for three or more mutually exclusive outcomes (e.g., crit vs hit vs none). * * @param p Probability of applying this PMF (between 0 and 1). * @param fallback PMF to apply when this PMF is *not* selected. * @returns A new PMF representing the weighted mixture of this PMF and the fallback. */ gate(p: number, fallback: PMF): PMF; /** * PMF.exclusive() * * Builds a single PMF from a set of mutually exclusive weighted outcomes. * Exactly one of the provided options will occur. * * Each option has: * - A PMF representing its outcome (e.g., damage dice). * - A weight representing its probability of being selected. * * Notes: * - If total weight < 1 (within eps), leftover mass is assumed to be PMF.zero() * * @param options Array of `{ pmf, weight }` or `[PMF, number]`. * @param eps Optional tolerance for floating point rounding. */ static exclusive(options: Array<{ pmf: PMF; weight: number; } | [PMF, number]>, eps?: number): PMF; /** * PMF.mix() * * Builds a PMF as a linear combination of input PMFs with the given weights. * Unlike `exclusive`, this does NOT: * - enforce that weights sum to 1 * - add leftover probability to δ0 (PMF.zero()) * * Use when outcomes are not mutually exclusive, or for interpolation/blending. * * @param options Array of `{ pmf, weight }` or `[PMF, number]`. * @param eps Optional tolerance for skipping tiny weights. */ static mix(options: Array<{ pmf: PMF; weight: number; } | [PMF, number]>, eps?: number): PMF; /** * Adds damage attribution metadata to this PMF based on existing count metadata. * For each bin, sets attr[outcome] = damage × count[outcome]. * * This enables damage attribution charts to work with builder-generated PMFs. * The parser generates attr automatically, but builder PMFs only have count. * * @returns New PMF with attr field populated in each bin */ /** * Returns true if this PMF already carries damage attribution metadata. * * Only the first positive-damage bin is inspected (parser-generated PMFs * populate `attr` uniformly), so this is O(1) in practice. */ hasAttribution(): boolean; withAttribution(): PMF; /** * General-purpose N-way mixture. * weights: Array of [weight, PMF]. * * Example: PMF.mixN([ * [pMiss, zero], * [pHit, hitPMF], * [pCrit, critPMF], * ]); */ static mixN(weights: [number, PMF][], eps?: number): PMF; private setPreservedProvenance; preservedProvenance(): boolean; private getPowerCacheKey; /** * Efficiently computes this PMF convolved with itself `n` times. * Uses exponentiation by squaring to reduce total convolutions. * n must be a positive integer. * * * * NOTE: This folds multiple independent attacks into a single PMF. * As a result, The power() method causes a loss of data provenance. * This is ONLY SAFE if you are trying to calculate masses. * If you want to query any atLeast probabilities, you should use the DiceQuery class instead without power(). */ power(n: number, eps?: number): PMF; replicate(n: number): PMF[]; mass(): number; outcomeMass(outcome: string): number; faceTotal(): number; normalize(): PMF; /** * Returns a copy with negligible probabilities removed (p < eps). * If keepFinalBin is true, the bin with the largest key is always kept, * even if its probability is below eps. count/attr submaps are still cleaned. */ compact(eps?: number, keepFinalBin?: boolean): PMF; support(): number[]; min(): number; max(): number; /** * Returns the expected (mean) damage value. * Cached for performance since this requires iterating through all bins. */ mean(): number; /** * Returns the variance of the damage distribution. * Cached for performance since this requires mean calculation plus iteration. */ variance(): number; /** * Returns the standard deviation of the damage distribution. */ stdev(): number; /** Deep-copies a Bin, cloning its count and (optional) attr maps. */ private static cloneBin; /** Returns a new Bin with p, count, and attr all multiplied by `factor`. */ private static scaleBin; private static mergeInto; add(other: PMF): PMF; /** * Returns a new PMF with a scaled branch added to this one. * The branch PMF is scaled by the given probability before merging * This will be very useful for conditional effects and for being * able to model "I can probably have this opportunity attack 40% of rounds" * Example: `pmf.addScaled(critBranch, 0.05)` → PMF including 5% crit outcomes */ addScaled(branch: PMF, probability: number): PMF; /** * Redistributes probability mass to model an effect that only occurs with * probability `frequency` — a conditional attack, an on-hit rider, or a * sub-one AoE target fraction. * * Every hit outcome (damage > 0) is scaled by `frequency` — probability mass, * per-label `count`, AND per-label `attr` — and the freed mass is moved into * the miss bin at damage 0, tagged with the canonical `missNone` outcome. * Total probability mass is preserved. * * Unlike a bare {@link scaleMass} or {@link mapDamage}, this keeps damage * attribution (`attr`) intact, so a frequency-scaled PMF still renders * correctly in the damage-attribution charts. * * `frequency >= 1` (or non-finite) returns this PMF unchanged; `frequency <= 0` * collapses all mass into the miss bin. The miss outcome is assumed to be * encoded at damage value 0. * * @param frequency Probability in [0, 1] that the effect occurs. */ applyHitFrequency(frequency: number): PMF; scaleMass(factor: number): PMF; mapDamage(damageTransformFunction: (damageValue: number) => number): PMF; scaleDamage(factor: number, rounding?: "floor" | "round" | "ceil"): PMF; private getPMFCombineCacheKey; /** * A content fingerprint of every bin (probability, per-label `count`, per-label `attr`) plus * the `normalized` flag, so convolution/power cache keys change whenever the underlying * numbers do. Mass/bin-count/face-sum alone are not content-unique: `mapDamage` variants can * keep the same identifier, support, mass, and face sum while differing in per-bin * probabilities or in the `count`/`attr` channels `convolve()`/`power()` actually propagate -- * that previously let `power()` return one PMF's cached result for a different PMF. Memoized * because a PMF is immutable once constructed -- this avoids re-deriving the key on every * convolve()/power() call (including cache hits). Bin order is sorted by damage value (and * label keys sorted within each bin) so two equal-content PMFs built via different code paths * fingerprint identically regardless of Map insertion order. */ fingerprint(): string; convolve(other: PMF, eps?: number, raw?: boolean): PMF; combineRaw(other: PMF, eps?: number): PMF; private static reduceConvolveLeft; /** * Convolves multiple PMFs using linear convolution with automatic caching. * Uses a left-to-right accumulation approach for maximum cache reuse. * Each convolve() call automatically uses the convolution cache for performance. * * This linear approach provides better cache hits than pairwise because: * - Intermediate results are more predictable and stable * - Similar PMF lists share common prefixes (A+B, (A+B)+C, etc.) * - Order-independent cache keys work better with consistent build patterns */ static convolveMany(pmfList: PMF[], eps?: number): PMF; /** * Returns a plain, JSON-serializable representation of this PMF. * * Follows the standard `toJSON` contract, so `JSON.stringify(pmf)` produces * the expected output (no double-encoding). Use {@link PMF.fromJSON} to * reconstruct, or {@link PMF.toJSONString} if you need the string directly. */ toJSON(): { bins: Array<[number, Bin]>; normalized: boolean; identifier: string; }; /** Serializes this PMF to a JSON string (equivalent to `JSON.stringify(pmf)`). */ toJSONString(): string; static fromJSON(jsonData: { bins: Array<[number, Bin]>; normalized?: boolean; identifier?: string; }): PMF; /** * Relative pruning with optional top-K floor. * Keeps bins with p >= epsRel * peak, always keeps min and max damage, * optionally guarantees at least `minBins` survivors by adding top-K. * Returns a new, non-normalized PMF. */ prune(epsRel: number, minBins?: number): PMF; /** Probability mass at exactly x. */ pAt(x: number): number; /** * P(any damage) — the mass on all non-zero outcomes, i.e. `1 - P(0)`. * Assumes a miss is encoded as the damage-0 bin (the convention used across * attack/save PMFs). The dual of {@link missProbability}. */ hitProbability(): number; /** P(no damage) — the mass at damage 0. The dual of {@link hitProbability}. */ missProbability(): number; /** * Coarsen the distribution into at most `maxBuckets` contiguous, equal-width * damage buckets, aggregating probability mass (and `count`/`attr` * provenance) into each bucket's start value. Returns this PMF unchanged when * its integer support already fits within `maxBuckets`. * * This is a lossy display/downsampling transform (bucket start replaces the * exact damage value) — use it for charting wide distributions, not for DPR * math. */ rebin(maxBuckets: number): PMF; /** Dense integer support from min..max (inclusive). * Useful for showing empty bars in charts. */ denseSupport(): number[]; /** CDF at x: P(X ≤ x). */ cdfAt(x: number): number; /** Quantile / inverse CDF for p in [0,1]. Returns smallest x with CDF ≥ p. */ quantile(p: number): number; /** Get outcome probability at specific damage value. */ outcomeAt(damage: number, outcome: string): number; /** Get all outcome types present in this PMF. */ outcomes(): string[]; /** Get total probability of an outcome across all damage values. */ outcomeProbability(outcome: string): number; /** Get damage attribution for an outcome at specific damage value. */ outcomeAttributionAt(damage: number, outcome: string): number; /** Get all outcome data at specific damage value. */ binAt(damage: number): { p: number; count: Record; attr?: Record; } | null; /** Check if outcome exists in this PMF. */ hasOutcome(outcome: string): boolean; /** * Split each damage value's probability mass across outcome labels, returning * per-label maps of `damage value → probability mass attributable to that * label`. Summing over labels at a given value recovers that value's `p`. * * Damage-bearing bins are split by `attr` weight (the share of damage each * outcome contributed); the clean-miss bin at 0 is split by `count` weight * (there is no damage to attribute). Attribution is computed on demand via * {@link withAttribution} when absent, so builder-generated PMFs work too. * * This is the provenance core of the stacked damage-attribution chart — the * caller only maps these series into its rendering format (colors, binning, * axis labels). */ attributionByValue(): Map>; /** * Reversed-convention CCDF percentile markers used by the attribution chart: * for each target probability t, the largest damage x still reached with * P(X ≥ x) > t%, falling back to the smallest/largest support value at the * edges. Ported verbatim from the app so `p80/p50/p20` keep their intentional * reversed meaning (p80 is the low-damage end). Computed on the full, un-binned * support. Assumes a non-empty map. */ private attributionPercentiles; /** * Full numeric model for the stacked damage-attribution chart — bar-height * masses, tooltip shares, bucket labels/ranges, percentile markers, and the * mean. The caller only maps these into a rendering format (colors, labels, * axis units); all of the dice-and-probability logic lives here. * * Built split-first-then-bin: the attribution split ({@link attributionByValue}) * runs on the un-binned distribution, then the resulting series are coarsened. * {@link rebin} is deliberately *not* used — rebinning first would fold any * sub-`binSize` damage into the damage-0 bucket, which the split then mistakes * for a clean miss and drops. * * @param options.maxBuckets Coarsen to at most this many equal-width buckets * when the integer support is wider (`range > maxBuckets`); omit for a dense, * per-integer model. * @param options.stackOrder Preferred outcome order (defaults to * {@link ALL_OUTCOME_TYPES}); labels outside it sort alphabetically after. * @param options.epsilon Bucket-total floor below which a `shares` entry is 0 * (divide-by-~0 guard). Defaults to 1e-9. */ damageAttributionChartModel(options?: { maxBuckets?: number; stackOrder?: readonly string[]; epsilon?: number; }): DamageAttributionChartModel; tailProbGE(t: number): number; tailProbGT(t: number): number; /** * Returns a new PMF containing only bins where the specified outcome has non-zero probability. * This creates a marginal distribution for the given outcome type, with probabilities * scaled to represent the unconditional mass attributable to that outcome. */ filterOutcome(outcome: string): PMF; /** * Calculates probabilities for first-success outcomes across n independent attempts. * * @param pSuccess - Total probability of any success on a single attempt. * @param pSpecial - Probability of a specific subset of successes (e.g., critical success). * @param n - Number of independent attempts. * * Returns: * - pSpecificSuccess: Probability that the first success was of the "special" type * - pGeneralSuccess: Probability that the first success was of the non-special type * - pNone: Probability that no successes occurred * - pAny: Probability that at least one success occurred */ static firstSuccessWeights(pSuccess: number, pSpecial: number, n: number): { pSpecificSuccess: number; pGeneralSuccess: number; pNone: number; pAny: number; }; mapValues(f: (v: number) => number, eps?: number, opts?: { rounding?: Rounding; preserveCounts?: boolean; }): PMF; static fromMap(m: Map, eps?: number, { requireIntegerValues }?: { requireIntegerValues?: boolean; }): PMF; query(): DiceQuery; } //# sourceMappingURL=pmf.d.ts.map