From 4e5fab654d5091d5a8870e71f1833ea4d6bcde02 Mon Sep 17 00:00:00 2001 From: josh Date: Wed, 22 Apr 2026 22:35:32 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20don't=20label=20a=20FINAL=20playoff=20ca?= =?UTF-8?q?rd=20as=20CLINCHER=20=E2=80=94=20those=20stakes=20belong=20to?= =?UTF-8?q?=20the=20next=20game?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seriesStatus updates with the just-played game's win, so once a card goes FINAL the is_clincher / is_game7 / is_pivotal predicates point at the upcoming game. Gate the stake badge, stake blurb, and elimination_count tally on a non-FINAL gameState so a completed Game 3 that left the series 3-0 reads "Flyers lead 3-0" instead of "Flyers can close it out — Game 3." Co-Authored-By: Claude Opus 4.7 --- app/playoff.py | 41 +++++++++++++++++++++------------ tests/test_playoff_series.py | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/app/playoff.py b/app/playoff.py index 3953c99..c4db214 100644 --- a/app/playoff.py +++ b/app/playoff.py @@ -112,15 +112,20 @@ def series_blurb(game): g = _game_number(game, state) leader_name = _leader_name(game, state) trailer_name = _trailer_name(game, state) + is_final = game.get("gameState") in ("FINAL", "OFF") - if state["is_game7"]: - return "Win-or-go-home \u2014 Game 7." - if state["is_clincher"] and leader_name: - return f"{leader_name} can close it out \u2014 Game {g}." - if state["is_pivotal"]: - return f"Series tied 2\u20112 \u2014 pivotal Game {g}." - if state["is_opener"]: - return "Series opener" + # Stake / opener blurbs describe what's *about* to happen. For a FINAL card + # the seriesStatus already includes this game, so the stake really points at + # the next matchup \u2014 fall through to a generic series-score blurb instead. + if not is_final: + if state["is_game7"]: + return "Win-or-go-home \u2014 Game 7." + if state["is_clincher"] and leader_name: + return f"{leader_name} can close it out \u2014 Game {g}." + if state["is_pivotal"]: + return f"Series tied 2\u20112 \u2014 pivotal Game {g}." + if state["is_opener"]: + return "Series opener" if leader_name and trailer_name: return f"{leader_name} lead {state['hi']}\u2011{state['lo']}" if state["hi"] == state["lo"]: @@ -138,12 +143,16 @@ def series_badges(game): ) badges.append(round_abbrev) - if state["is_game7"]: - badges.append("GAME 7") - elif state["is_clincher"]: - badges.append("CLINCHER") - elif state["is_pivotal"]: - badges.append("PIVOTAL") + # Stake badges describe the *upcoming* game. Once a game is FINAL the + # seriesStatus reflects post-game wins, so the predicate now points at the + # next card in the series — don't stamp it onto the one that's already done. + if game.get("gameState") not in ("FINAL", "OFF"): + if state["is_game7"]: + badges.append("GAME 7") + elif state["is_clincher"]: + badges.append("CLINCHER") + elif state["is_pivotal"]: + badges.append("PIVOTAL") return badges @@ -217,6 +226,10 @@ def today_meta(raw_games, now=None, day_n=None): series_letters.add(letter) state = series_state(ss) max_round = max(max_round, state["round"]) + # Only pending/live games can still become the clincher or Game 7 + # today. Once a card is FINAL its seriesStatus points at the next game. + if g.get("gameState") in ("FINAL", "OFF"): + continue if state["is_game7"]: g7 += 1 elif state["is_clincher"]: diff --git a/tests/test_playoff_series.py b/tests/test_playoff_series.py index c8f2192..b467bec 100644 --- a/tests/test_playoff_series.py +++ b/tests/test_playoff_series.py @@ -105,6 +105,22 @@ class TestSeriesBlurb: assert "1" in blurb assert "Game 3" in blurb + def test_final_clincher_falls_through_to_leader_blurb(self): + # Post-game seriesStatus (3-0) would trigger the clincher branch, but + # the FINAL card is already decided — that stake belongs to Game 4. + game = make_playoff_game( + top_wins=3, + bottom_wins=0, + top_abbrev="PHI", + bottom_abbrev="PIT", + top_is_home=True, + game_state="OFF", + ) + blurb = series_blurb(game) + assert "close it out" not in blurb + assert "lead" in blurb + assert "3‑0" in blurb + class TestSeriesBadges: def test_round_1_always_first(self): @@ -135,6 +151,12 @@ class TestSeriesBadges: badges = series_badges(make_playoff_game(top_wins=0, bottom_wins=0)) assert badges == ["R1"] + def test_no_stake_badge_on_final(self): + # Post-game seriesStatus shows is_clincher true, but CLINCHER refers to + # the upcoming Game 4, not the completed card. + game = make_playoff_game(top_wins=3, bottom_wins=0, game_state="OFF") + assert series_badges(game) == ["R1"] + class TestSeriesSummary: def test_opener_summary(self): @@ -285,3 +307,25 @@ class TestTodayMeta: games = [make_playoff_game(round_num=4, series_letter="P")] meta = today_meta(games) assert meta["round_label"] == "Stanley Cup Final" + + def test_does_not_count_final_games_as_elimination(self): + games = [ + make_playoff_game( + top_wins=3, bottom_wins=0, series_letter="A", game_state="OFF" + ), + make_playoff_game( + top_wins=3, bottom_wins=1, series_letter="B", game_state="LIVE" + ), + ] + meta = today_meta(games) + # Only the LIVE card counts; the FINAL card describes a completed game. + assert meta["elimination_count"] == 1 + + def test_does_not_count_final_game7(self): + games = [ + make_playoff_game( + top_wins=3, bottom_wins=3, series_letter="A", game_state="OFF" + ) + ] + meta = today_meta(games) + assert meta["game7_count"] == 0