Repository navigation
feat: adopt placement-agnostic knockout credit (HessFest) - #143
Merged
Merged
Conversation
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.
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.
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 numericknockoutPlacementAgnosticflag (default0) onScoringConfig. When1, 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) reusestageOfso the engine and analysis script can't drift.bracketScoreEntriesalready threadsctx.cfg, so FULL_BRACKET and KNOCKOUT pick it up with no change.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.scripts/adopt-placement-credit.ts) — read-only impact report by default;--write-jsonemits the immutable audit record;--commitflips 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./pool/[code]/scoringstatement page (what changed, why, what stays the same with values fromDEFAULT_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 vialib/pool/cutover.ts.Impact (verified against production, read-only)
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 buildall clean;/pool/[code]/scoringcompiles.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 --commitagainst 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.