Rank the third place game winner instead of leaving them in contention #137

Merged
chrisp merged 1 commit from claude/third-place-game-elimination into main 2026-07-19 02:36:37 +00:00
Owner

Problem

On the FIFA World Cup bracket, setting the Third Place Game result left the winner displayed under In Contention. They should take 3rd place points and then be done.

Cause

The scoring side was already correct — TEMPLATE_ROUND_CONFIG.fifa_48 finalizes the 3PG winner at finalPosition = 3 with isPartialScore = false, so the 50 points were being awarded.

The bug was display-only. PlayoffBracket re-derives rankings from match records rather than participant results, and only ever ranks losers. The 3PG winner lost the semifinal but was correctly excluded there by the double-chance guard (they appear in a later round), then won the 3PG — so they were never a loser in any match, never ranked, and fell through into the active list.

Also fixed

The same code double-counted positions 3–4 when labelling ranks:

Before After
Finals loser T2 T2
3PG winner missing, "In Contention" 3
3PG loser T3 4
Quarterfinal losers T6 T5

Approach

Rankings now go through computeRankedEntries, which resolves a consolation round at its feeder round: the winner takes the top of the tier the feeder's losers would otherwise share, the loser the next position, and the consolation round itself consumes none. Positions are derived rather than hardcoded to 3/4, so a consolation round hanging off a different feeder lands correctly too.

Only matches that can be placed exactly are withheld from the loser-driven walk, so an incomplete or partially hydrated consolation match falls back to the old path rather than being silently dropped.

loserFeedsInto is set on exactly one round in the templates (fifa_48 Semifinals), so NCAA / NBA / AFL / CFP / tennis brackets are unaffected. This fixes mode="rankings" as well, which shared the derivation.

Testing

32 tests in PlayoffBracket.test.tsx, up from 22 — covering the 3PG winner/loser placement, rank ordering, the pre-game state, a consolation round in a non-3rd/4th position, score lookup by slot, and fallback cases. Two assert against the rendered DOM, not just the helper.

Each fix has a negative control: reverting any one of them individually makes tests fail. Full suite 2855/2855 green, typecheck and lint clean.

Not yet verified

This has not been driven against real World Cup data in a running app — the coverage is unit plus rendered-DOM. Worth a look at the actual page before merging.

Known limitation (unchanged, not in scope)

Third-place handling is still coupled to the fifa_48 template id in two places on the scoring side (TEMPLATE_ROUND_CONFIG in scoring-calculator.ts and DISTINCT_34_TEMPLATE_IDS in scoring-rules.ts), and generic ROUND_CONFIG has no "Third Place Game" entry. A future template adding one would fall into the "unrecognized round" branch and get a T5–T8 floor instead of 3rd.

🤖 Generated with Claude Code

## Problem On the FIFA World Cup bracket, setting the Third Place Game result left the **winner** displayed under **In Contention**. They should take 3rd place points and then be done. ## Cause The scoring side was already correct — `TEMPLATE_ROUND_CONFIG.fifa_48` finalizes the 3PG winner at `finalPosition = 3` with `isPartialScore = false`, so the 50 points were being awarded. The bug was display-only. `PlayoffBracket` re-derives rankings from match records rather than participant results, and only ever ranks *losers*. The 3PG winner lost the semifinal but was correctly excluded there by the double-chance guard (they appear in a later round), then **won** the 3PG — so they were never a loser in any match, never ranked, and fell through into the active list. ## Also fixed The same code double-counted positions 3–4 when labelling ranks: | | Before | After | |---|---|---| | Finals loser | T2 | T2 | | 3PG winner | *missing, "In Contention"* | **3** | | 3PG loser | T3 | **4** | | Quarterfinal losers | T6 | **T5** | ## Approach Rankings now go through `computeRankedEntries`, which resolves a consolation round **at its feeder round**: the winner takes the top of the tier the feeder's losers would otherwise share, the loser the next position, and the consolation round itself consumes none. Positions are derived rather than hardcoded to 3/4, so a consolation round hanging off a different feeder lands correctly too. Only matches that can be placed *exactly* are withheld from the loser-driven walk, so an incomplete or partially hydrated consolation match falls back to the old path rather than being silently dropped. `loserFeedsInto` is set on exactly one round in the templates (`fifa_48` Semifinals), so NCAA / NBA / AFL / CFP / tennis brackets are unaffected. This fixes `mode="rankings"` as well, which shared the derivation. ## Testing 32 tests in `PlayoffBracket.test.tsx`, up from 22 — covering the 3PG winner/loser placement, rank ordering, the pre-game state, a consolation round in a non-3rd/4th position, score lookup by slot, and fallback cases. Two assert against the **rendered DOM**, not just the helper. Each fix has a negative control: reverting any one of them individually makes tests fail. Full suite 2855/2855 green, typecheck and lint clean. ## Not yet verified This has **not** been driven against real World Cup data in a running app — the coverage is unit plus rendered-DOM. Worth a look at the actual page before merging. ## Known limitation (unchanged, not in scope) Third-place handling is still coupled to the `fifa_48` template id in two places on the scoring side (`TEMPLATE_ROUND_CONFIG` in `scoring-calculator.ts` and `DISTINCT_34_TEMPLATE_IDS` in `scoring-rules.ts`), and generic `ROUND_CONFIG` has no `"Third Place Game"` entry. A future template adding one would fall into the "unrecognized round" branch and get a T5–T8 floor instead of 3rd. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chrisp added 1 commit 2026-07-19 02:25:36 +00:00
Rank the third place game winner instead of leaving them in contention
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m8s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m18s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 3m28s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m22s
🚀 Deploy / 🐳 Build (push) Successful in 1m22s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
b85f387c79
The Third Place Game winner kept showing under "In Contention" on the FIFA
World Cup bracket even after the game was scored.

The scoring side was already correct — TEMPLATE_ROUND_CONFIG.fifa_48 finalizes
the 3PG winner at finalPosition 3 with isPartialScore false, so they got their
50 points. The bug was in PlayoffBracket, which re-derives rankings from match
records rather than participant results and only ever ranks losers. The 3PG
winner lost the semifinal but was correctly excluded there by the double-chance
guard, then won the 3PG — so they were never a loser anywhere, never ranked,
and fell through into the active list.

Fixing that also fixes the rank labels, which double-counted positions 3-4:
the 3PG loser was labelled T3 rather than 4, and quarterfinal losers T6 rather
than T5.

Rankings are now built by computeRankedEntries, which resolves a consolation
round at its feeder round: the winner takes the top of the tier the feeder's
losers would otherwise share and the loser the next position, with the
consolation round itself consuming none. Positions are derived rather than
hardcoded to 3/4, so a consolation round hanging off a different feeder lands
correctly too. Only matches that can be placed exactly are withheld from the
loser-driven walk, so an incomplete or partially hydrated consolation match
falls back to the old path instead of being dropped.

loserFeedsInto is set on exactly one round in the templates (fifa_48
Semifinals), so every other bracket keeps its existing behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chrisp merged commit b85f387c79 into main 2026-07-19 02:36:37 +00:00
chrisp deleted branch claude/third-place-game-elimination 2026-07-19 02:36:37 +00:00
Sign in to join this conversation.
No description provided.