Skip to content

Add provenance-preserving PMF.applyHitFrequency and PMF.missNone#3

Merged
yipe merged 5 commits into
mainfrom
claude/damage-attribution-refactor-u8fwjk
Jul 12, 2026
Merged

Add provenance-preserving PMF.applyHitFrequency and PMF.missNone#3
yipe merged 5 commits into
mainfrom
claude/damage-attribution-refactor-u8fwjk

Conversation

@yipe

@yipe yipe commented Jul 11, 2026

Copy link
Copy Markdown
Owner

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) by frequency and moves the freed mass into a missNone bin, preserving total probability mass.

The app's equivalent (applyFrequencyToPMF in TurnActionDamageCalculator.ts) copied per-label count but dropped attr, so any frequency-scaled action silently lost its damage attribution in the attribution chart. This version scales count and attr via the existing scaleBin primitive, fixing that.

PMF.missNone(epsilon?) — a canonical "clean miss" point mass at damage 0 tagged with the missNone OutcomeType. This is distinct from PMF.zero(), which labels its zero bin miss (the builder's attack-resolution vocabulary) — the two must not collide. Replaces the app's ad-hoc zeroPMF().

MISS_NONE_OUTCOME — exports the missNone label as a single source of truth, used by both additions.

Tests

tests/apply-hit-frequency.test.ts covers: total-mass preservation, hit-bin count scaling, freed mass landing in a missNone bin, accumulation onto a pre-existing miss bin, mean scaling for a pure-hit PMF, the attr-preservation regression guard, and the frequency >= 1 / <= 0 / non-finite edges. Plus PMF.missNone shape and its label-distinctness from PMF.zero.

Full suite green (1185 passed, +9); typecheck, lint, and build all clean.

Follow-ups (not in this PR)

Once this is released, the dpr side can consume it: replace applyFrequencyToPMF / zeroPMF with these methods, and (separately) dedupe the reimplemented outcome queries (hasOutcome, outcomes, tailProbGE, quantile) and the triplicated canonical outcome-ordering tables.

claude added 3 commits July 11, 2026 23:43
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
@yipe yipe marked this pull request as ready for review July 12, 2026 02:49
claude added 2 commits July 12, 2026 02:55
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
@yipe yipe merged commit f6da55e into main Jul 12, 2026
8 checks passed
@yipe yipe deleted the claude/damage-attribution-refactor-u8fwjk branch July 12, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants