Add provenance-preserving PMF.applyHitFrequency and PMF.missNone#3
Merged
Conversation
Push down damage-attribution helpers that the dprcalc app hand-rolled over PMF internals, so the provenance model stays owned by @yipe/dice. - PMF.applyHitFrequency(frequency): scales every hit bin (damage > 0) by the frequency and moves the freed mass into a canonical missNone bin, preserving total mass. Unlike a bare scaleMass/mapDamage it scales per-label count AND attr. This fixes a latent bug in the app's applyFrequencyToPMF, which copied count but dropped attr — silently breaking the damage-attribution chart for frequency-scaled actions. - PMF.missNone(epsilon?): canonical "clean miss" point mass at 0 tagged with the missNone OutcomeType, distinct from PMF.zero's builder-side 'miss' label. MISS_NONE_OUTCOME exports the label as a single source of truth. Adds tests/apply-hit-frequency.test.ts (mass preservation, count+attr scaling, mean scaling, miss-bin accumulation, edge frequencies) and a CHANGELOG entry under 0.3.0 Added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013E5WBwsr61ysxR4qqGqXjD
Guards that applyHitFrequency leaves the source PMF (p, count, and attr) untouched, since it is a copy-on-write transform. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013E5WBwsr61ysxR4qqGqXjD
Add the library-owned helpers the dprcalc app had hand-rolled over PMF internals, and bump to 0.4.0. - PMF.hitProbability()/missProbability() — the 1 - P(0) miss idiom. - PMF.rebin(maxBuckets) — provenance-preserving chart downsampling. - PMF.attributionByValue() / DiceQuery.attributionByValue() — per-label value→mass split; the provenance core of the stacked attribution chart. - DiceQuery.countSinglesWith(label). - ALL_OUTCOME_TYPES / OUTCOME_DISPLAY_ORDER / sortOutcomes — canonical outcome enumeration + orderings. - critProbability(critRange, rollType); RollType now exported from the root as well as /builder. - calculateBounceOdds + BounceOddsOptions moved out of the app. Base and Elemental-Adept cases are now computed EXACTLY (elementary symmetric polynomial), verified against brute-force enumeration; the old Elemental-Adept approximation drifted up to ~3.5%. Full suite green (1216 tests); typecheck, lint, build, and npm pack all clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013E5WBwsr61ysxR4qqGqXjD
Pre-PR review pass. Extends the bounds test to faces 6/8, min 0–4, reroll 0–3, and dice up to `faces` — covering the Empowered Spell + Elemental Adept edge where keptDice exceeds the collapsed face count. Asserts the result stays finite (no NaN from Math.pow on a negative base) and within [0, 1]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013E5WBwsr61ysxR4qqGqXjD
Surfaced while adding Empowered-Spell coverage: when rerollDamageDice >= diceCount (keptDice === 0), calculateBounceOdds returned 1.0 — a guaranteed match — because pRerollDieMissesAll fell back to 0 (implying certainty of matching a kept die when there are none). With no kept dice the only match path is among the rerolled dice, i.e. a second independent roll, so the correct value is 1 - (1 - pMatch)^2 (e.g. 3xd8 reroll-all: 1.0 -> 0.5693). Adds an Empowered-Spell test block: reroll-all equals two independent rolls (exact), monotonic in reroll count, clamped at diceCount, combines with Elemental Adept, and regression-pinned model values. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013E5WBwsr61ysxR4qqGqXjD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The dprcalc app (
yipe/dpr) hand-rolls several transforms over PMF provenance internals (bin.count/bin.attr) that really belong in@yipe/dice, which already owns the provenance model. This is the first, self-contained slice of pushing that logic down. It also fixes a latent correctness bug.What
PMF.applyHitFrequency(frequency)— models an effect that only occurs with some probability (a conditional attack, an on-hit rider, a sub-one AoE target fraction). It scales every hit bin (damage > 0) byfrequencyand moves the freed mass into amissNonebin, preserving total probability mass.The app's equivalent (
applyFrequencyToPMFinTurnActionDamageCalculator.ts) copied per-labelcountbut droppedattr, so any frequency-scaled action silently lost its damage attribution in the attribution chart. This version scalescountandattrvia the existingscaleBinprimitive, fixing that.PMF.missNone(epsilon?)— a canonical "clean miss" point mass at damage 0 tagged with themissNoneOutcomeType. This is distinct fromPMF.zero(), which labels its zero binmiss(the builder's attack-resolution vocabulary) — the two must not collide. Replaces the app's ad-hoczeroPMF().MISS_NONE_OUTCOME— exports themissNonelabel as a single source of truth, used by both additions.Tests
tests/apply-hit-frequency.test.tscovers: total-mass preservation, hit-bincountscaling, freed mass landing in amissNonebin, accumulation onto a pre-existing miss bin, mean scaling for a pure-hit PMF, the attr-preservation regression guard, and thefrequency >= 1/<= 0/ non-finite edges. PlusPMF.missNoneshape and its label-distinctness fromPMF.zero.Full suite green (1185 passed, +9);
typecheck,lint, andbuildall clean.Follow-ups (not in this PR)
Once this is released, the dpr side can consume it: replace
applyFrequencyToPMF/zeroPMFwith these methods, and (separately) dedupe the reimplemented outcome queries (hasOutcome,outcomes,tailProbGE,quantile) and the triplicated canonical outcome-ordering tables.