Skip to content

fix(statistics): stop suppressing victim deaths so the F13a cap stops undercounting - #18

Merged
Afgan0r merged 1 commit into
masterfrom
fix/f13a-deaths-fold-undercount
Jun 14, 2026
Merged

Afgan0r merged 1 commit into
masterfrom
fix/f13a-deaths-fold-undercount

Conversation

@Afgan0r

@Afgan0r Afgan0r commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Problem

After F12 + F13a (PR #17) + a full recalc, sg deaths.total runs ~5% below legacy, while games/kills are within ~1.5%. Per-player, the recalc's deaths.total is 3-6% lower than the parser's own death-game signal (replays where player_counter.deaths_total > 0, i.e. parser player.d > 0), which itself matches legacy. The parser is correct; the loss is in server-2's death aggregation.

Root cause

The F13a per-game cap (foldCappedDeaths) was added, but the pre-F13a counterDeathReferences suppression was left in place in calculatePlayerAndSquadAggregates (src/modules/statistics/service/service.ts):

  • counterDeathReferences held observedPlayerRef for every player_counter whose counterDeaths(payload) is DEFINED. An explicit-zero counter (deaths_total:0) still yields a defined {total:0,…}, so it landed in the set.
  • applyVictimDeath / applySquadEvent suppressed the victim kill-row death whenever counterDeathReferences.has(String(victim_entity_id)).
  • applyCounterEvent dropped the death when its observedPlayerRef did not resolve in playersByEntity, and incrementDeathsByCounter adds 0 for an explicit-zero counter.

So for an entity with a present-but-non-contributing counter plus a real victim kill row, the death was lost from both paths. The two paths also key differently (counter: observedPlayerRef; victim: String(victim_entity_id)), so any resolution mismatch leaked a death.

Fix

With F13a's per-replay cap already collapsing any double-count to <=1 death and <=1 teamkill death per replay, the suppression is redundant — its only remaining effect was this leak. Remove counterDeathReferences and the two victim-path suppression checks; let whichever path resolves the entity record the capped death.

Result: all-time deaths.total equals the count of distinct replays the player died in (legacy "games-died-in" semantics). One-life cap preserved (d=3 in one replay still counts 1); by_teamkills <= total still holds.

Service-layer only — no parser, schema, repository SQL, or API-contract change.

Tests (TDD)

Added to service/tests/aggregates.test.ts (failing repro written first):

  • Leak repro: zero counter + victim kill row for the same entity → deaths.total: 1 (was 0).
  • Teamkill variant asserting by_teamkills <= total.
  • N-distinct-replays invariant: died in 3 replays → total: 3.

Validation

  • pnpm lint, pnpm typecheck, pnpm format — green.
  • pnpm test (unit) — 657 passed (78 files); aggregates.test.ts 14/14.
  • Coverage: changed service.ts is 100% (statements/branches/functions/lines) from the unit suite. The full pnpm test:coverage gate runs the integration files (*/tests/postgres.test.ts, src/test/integration/**) which need Docker/testcontainers; Docker was unavailable in this environment, so the repo-wide 100% gate (dependent on those integration files for the repository/route/infra layers) was not exercised here.

Planning doc: .planning/quick/260614-f14-f13a-deaths-fold-undercount/.

🤖 Generated with Claude Code

… undercounting

The F13a per-game cap left the pre-F13a `counterDeathReferences` suppression
in place, which leaks deaths. The set held `observedPlayerRef` for every
`player_counter` with a DEFINED `counterDeaths(payload)` (an explicit-zero
`deaths_total:0` still yields `{total:0}`, which is defined), so the victim
kill-row death path was suppressed whenever the entity had any counter — even
when the counter path then recorded nothing (explicit-zero magnitude, or an
`observedPlayerRef` that did not resolve in `playersByEntity`). The two paths
also key differently (counter on `observedPlayerRef`, victim on
`String(victim_entity_id)`), so a resolution mismatch dropped the death from
both paths. Result: recalc `deaths.total` ran ~5% below legacy and below the
parser's own `d>0` replay count.

With F13a's per-replay cap (`foldCappedDeaths`, <=1 total and <=1 by_teamkills
per replay) already in place, the suppression is redundant: double-counting
between the counter path and the victim path collapses to a single death. Drop
`counterDeathReferences` and the two victim-path suppression checks; let
whichever path resolves the entity record the capped death. All-time
`deaths.total` now equals the count of distinct replays the player died in,
matching legacy "games-died-in" semantics. The one-life cap is preserved
(d=3 in one replay still counts 1) and `by_teamkills <= total` still holds.

Service-layer only: no parser, schema, repository SQL, or API-contract change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Afgan0r
Afgan0r marked this pull request as ready for review June 14, 2026 16:35
@Afgan0r
Afgan0r merged commit 4495bc7 into master Jun 14, 2026
3 checks passed
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.

1 participant