Skip to content

feat: adopt placement-agnostic knockout credit (HessFest) - #143

Merged
dommango merged 4 commits into
mainfrom
feat/placement-credit-adoption
Jul 21, 2026
Merged

dommango merged 4 commits into
mainfrom
feat/placement-credit-adoption

Conversation

@dommango

Copy link
Copy Markdown
Owner

What & why

After deliberation, the commissioner adopted placement-agnostic knockout credit for the HessFest pool. Today, a knockout pick only scores if your team wins the exact bracket slot you seeded it into (lib/scoring/score.ts), so a team you placed in the wrong group position enters a different R32 slot than reality and its entire real run is stranded — you get no credit for correctly backing a deep run. This PR implements the rule, publishes a before/after, and links a statement explaining it.

Follows the analysis from #142. The change is additive and config-gated so the load-bearing oracle-parity invariant is untouched: default off reproduces the original slot-based scoring byte-for-byte; only the HessFest tournament config flips it on.

How it works

  • lib/scoring/score.ts — new numeric knockoutPlacementAgnostic flag (default 0) on ScoringConfig. When 1, a round's points are awarded for every team the entry picked to win a match in that round that actually won a match in that round, regardless of slot. Shared round-boundary helpers (winnersByStage, stagePoints, KNOCKOUT_BUCKET) reuse stageOf so the engine and analysis script can't drift. bracketScoreEntries already threads ctx.cfg, so FULL_BRACKET and KNOCKOUT pick it up with no change.
  • Live projection (lib/pool/projected.ts) made placement-agnostic under the flag, excluding teams already banked via a decided result so a round is never double-paid.
  • Cutover (scripts/adopt-placement-credit.ts) — read-only impact report by default; --write-json emits the immutable audit record; --commit flips the flag and recomputes with snapshots suppressed (a rule change isn't a live "top mover") and no push blast. Guarded on the final being recorded.
  • UI — /pool/[code]/scoring statement page (what changed, why, what stays the same with values from DEFAULT_SCORING, and the full before/after board), a time-boxed leaderboard banner, and per-row "Rule change ▲n · +p" chips. All cutover UI is pool-scoped and window-boxed via lib/pool/cutover.ts.

Impact (verified against production, read-only)

  • HessFest: 39/43 brackets gain points, 28 change rank. Leader flips from a Brendan/Joe tiebreaker coin-flip to Joe Van Der Bogart outright (115 vs 113).
  • Every other surface is byte-identical: all other KNOCKOUT pools and all 10 standalone brackets show 0 gain / 0 rank change — knockout brackets seed from the real qualifiers, so nothing is stranded. The tournament-wide flip has no collateral impact.

Test / verify

  • npx vitest run — 751 pass. Oracle sweep and cross-game parity stay green (flag off ≡ original); new cases cover the flag on (superset property, stranded run rescued, final unchanged, bronze still unscored, dedup per round).
  • npm run typecheck, npm run lint, npm run build all clean; /pool/[code]/scoring compiles.

Rollout (deliberate ordering)

The flag lives in the DB but the code that explains it is in this PR. To avoid changing scores with no visible explanation, the sequence is: merge & deploy this PR first, then run scripts/adopt-placement-credit.ts --commit against prod. The flag is off until then, so merging is safe and changes nothing on its own.

Note: the final (match 104 = ESP) is already in the answer key, so the before/after isolates purely the rule's effect.

dommango-sys added 4 commits July 20, 2026 20:27
Add knockoutPlacementAgnostic flag (numeric, default 0) to ScoringConfig.
When on, a knockout round's points are awarded for every team the entry
picked to win a match in that round that actually won a match in that
round, regardless of bracket slot — rescuing runs stranded by a wrong
group-position pick. Default off reproduces the original slot-based
scoring byte-for-byte; the oracle and cross-game parity gates stay green.

Shared round-boundary helpers (winnersByStage, stagePoints, KNOCKOUT_BUCKET)
reuse stageOf so the engine and analysis script can't drift.
projectedLivePoints now mirrors placement-agnostic official scoring when
the flag is on: a round's points project for each distinct team the entry
picked to win any match in that round that is currently leading a live
match in that round, minus teams already banked via a decided result (no
double-pay). Caller passes the official knockout map. Slot-based path and
all existing behaviour unchanged when the flag is off.

expected-points EV stays slot-based by design (documented): the per-round
value is unchanged, and modelling placement upside needs a probability the
win model doesn't expose — only affects still-undecided matches.
scripts/adopt-placement-credit.ts: read-only impact report across every
surface the tournament-wide flag would touch (HessFest, other KNOCKOUT
pools, standalone brackets); --write-json emits the immutable audit record
(before/after per HessFest entry); --commit additionally flips the flag and
recomputes with snapshots suppressed. Guarded on the final being recorded.

Impact verified against production: only HessFest moves (39/43 gain, Joe
outright leader) — every KNOCKOUT pool and standalone bracket is byte-
identical, since knockout brackets seed from the real qualifiers and have no
placement to strand.

recomputePool gains a captureSnapshots opt-out so the cutover doesn't surface
as a live 'top mover'.
- /pool/[code]/scoring: the pool's scoring statement — what placement-agnostic
  credit means, why it was adopted, what stays the same (point values from
  DEFAULT_SCORING), and the full before/after board from the audit record.
- ScoringChangeBanner: time-boxed leaderboard notice linking to the statement.
- Leaderboard 'Rule change ▲n · +p' chip per row, from LeaderboardRow.cutover.
- Leaderboard page enriches HessFest rows with the cutover move and adds a
  persistent Scoring link. All cutover UI is pool-scoped and window-boxed via
  lib/pool/cutover.ts; other pools show a plain slot-based scoring note.
@dommango
dommango merged commit 7f211e0 into main Jul 21, 2026
1 check passed
@dommango
dommango deleted the feat/placement-credit-adoption branch July 21, 2026 00:59
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