fix: show correct "Game X of 7" for future playoff dates
Enrich raw score-endpoint games with gameNumber from the series cache before parsing. The score API omits gameNumber and its seriesStatus reflects current wins, so all future games in a series computed the same number. Now we cross-reference by game id against the cached series-detail endpoint which includes the correct gameNumber per game. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -158,8 +158,6 @@ def series_badges(game):
|
||||
|
||||
def series_summary(game):
|
||||
"""Short line rendered above the card, e.g. 'Game 2 of 7'."""
|
||||
if game.get("gameState") == "FUT":
|
||||
return ""
|
||||
state = series_state(game.get("seriesStatus", {}))
|
||||
return f"Game {_game_number(game, state)} of 7"
|
||||
|
||||
|
||||
@@ -158,6 +158,55 @@ def fetch_series(series_id):
|
||||
return None
|
||||
|
||||
|
||||
# ── Game-number enrichment ────────────────────────────────────────
|
||||
|
||||
|
||||
def enrich_game_numbers(raw_games):
|
||||
"""Inject gameNumber from cached series data into raw score-endpoint games.
|
||||
|
||||
The /v1/score/{date} endpoint omits gameNumber. For future dates the
|
||||
fallback computation (top_wins + bot_wins + 1) gives every game in a
|
||||
series the same number. The series-detail endpoint includes gameNumber,
|
||||
so we cross-reference by game id.
|
||||
"""
|
||||
need = {}
|
||||
for game in raw_games or []:
|
||||
if game.get("gameType") != 3:
|
||||
continue
|
||||
if isinstance(game.get("gameNumber"), int) and game["gameNumber"] > 0:
|
||||
continue
|
||||
ss = game.get("seriesStatus") or {}
|
||||
letter = ss.get("seriesLetter") or ss.get("seriesAbbrev")
|
||||
if not letter:
|
||||
continue
|
||||
start = game.get("startTimeUTC") or ""
|
||||
try:
|
||||
year = (
|
||||
datetime.fromisoformat(start.replace("Z", "+00:00"))
|
||||
.astimezone(EASTERN)
|
||||
.year
|
||||
)
|
||||
except (ValueError, AttributeError):
|
||||
year = datetime.now(EASTERN).year
|
||||
sid = f"{year}-{letter.upper()}"
|
||||
need.setdefault(sid, []).append(game)
|
||||
|
||||
for sid, games in need.items():
|
||||
payload = fetch_series(sid)
|
||||
if not payload:
|
||||
continue
|
||||
lookup = {}
|
||||
for sg in payload.get("games") or []:
|
||||
gid = sg.get("id")
|
||||
gn = sg.get("gameNumber")
|
||||
if gid is not None and isinstance(gn, int) and gn > 0:
|
||||
lookup[gid] = gn
|
||||
for game in games:
|
||||
gid = game.get("id")
|
||||
if gid is not None and gid in lookup:
|
||||
game["gameNumber"] = lookup[gid]
|
||||
|
||||
|
||||
# ── Per-round start dates (drive the "Day N" banner) ──────────────
|
||||
|
||||
ROUND_DATES_KEY = "meta:round_start_dates"
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.playoff import today_meta
|
||||
from app.bracket_view import build_bracket_view
|
||||
from app.playoff_cache import (
|
||||
day_n_for_round,
|
||||
enrich_game_numbers,
|
||||
fetch_series,
|
||||
get_bracket,
|
||||
parse_series_id,
|
||||
@@ -118,6 +119,7 @@ def get_scoreboard():
|
||||
|
||||
if scoreboard_data:
|
||||
raw_games = scoreboard_data.get("games", [])
|
||||
enrich_game_numbers(raw_games)
|
||||
games = parse_games(scoreboard_data)
|
||||
max_round = _max_playoff_round(raw_games)
|
||||
n = day_n_for_round(max_round) if max_round else None
|
||||
|
||||
@@ -273,3 +273,98 @@ class TestSchema:
|
||||
playoff_cache._put("k", {"v": 2})
|
||||
cached, _ = playoff_cache._get("k")
|
||||
assert cached == {"v": 2}
|
||||
|
||||
|
||||
def _raw_playoff_game(game_id, series_letter="A", game_number=None):
|
||||
"""Minimal raw score-endpoint playoff game for enrichment tests."""
|
||||
game = {
|
||||
"id": game_id,
|
||||
"gameType": 3,
|
||||
"gameState": "FUT",
|
||||
"startTimeUTC": "2026-04-25T23:00:00Z",
|
||||
"seriesStatus": {"seriesLetter": series_letter, "round": 1},
|
||||
}
|
||||
if game_number is not None:
|
||||
game["gameNumber"] = game_number
|
||||
return game
|
||||
|
||||
|
||||
class TestEnrichGameNumbers:
|
||||
def test_basic_enrichment(self, tmp_db, monkeypatch):
|
||||
games = [_raw_playoff_game(101), _raw_playoff_game(102)]
|
||||
series_payload = {
|
||||
"games": [
|
||||
{"id": 101, "gameNumber": 3},
|
||||
{"id": 102, "gameNumber": 4},
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"app.playoff_cache.requests.get",
|
||||
lambda *a, **kw: _Resp(series_payload),
|
||||
)
|
||||
playoff_cache.enrich_game_numbers(games)
|
||||
assert games[0]["gameNumber"] == 3
|
||||
assert games[1]["gameNumber"] == 4
|
||||
|
||||
def test_skips_games_with_existing_game_number(self, tmp_db, monkeypatch):
|
||||
games = [_raw_playoff_game(101, game_number=2)]
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
"app.playoff_cache.requests.get",
|
||||
lambda *a, **kw: called.append(1) or _Resp({"games": []}),
|
||||
)
|
||||
playoff_cache.enrich_game_numbers(games)
|
||||
assert games[0]["gameNumber"] == 2
|
||||
assert len(called) == 0
|
||||
|
||||
def test_skips_non_playoff_games(self, tmp_db, monkeypatch):
|
||||
games = [{"id": 101, "gameType": 2, "gameState": "FUT"}]
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
"app.playoff_cache.requests.get",
|
||||
lambda *a, **kw: called.append(1) or _Resp({"games": []}),
|
||||
)
|
||||
playoff_cache.enrich_game_numbers(games)
|
||||
assert "gameNumber" not in games[0]
|
||||
assert len(called) == 0
|
||||
|
||||
def test_graceful_on_cache_miss(self, tmp_db, monkeypatch):
|
||||
import requests as req
|
||||
|
||||
games = [_raw_playoff_game(101)]
|
||||
monkeypatch.setattr(
|
||||
"app.playoff_cache.requests.get",
|
||||
lambda *a, **kw: (_ for _ in ()).throw(req.ConnectionError("x")),
|
||||
)
|
||||
playoff_cache.enrich_game_numbers(games)
|
||||
assert "gameNumber" not in games[0]
|
||||
|
||||
def test_handles_missing_id(self, tmp_db, monkeypatch):
|
||||
game = {
|
||||
"gameType": 3,
|
||||
"gameState": "FUT",
|
||||
"startTimeUTC": "2026-04-25T23:00:00Z",
|
||||
"seriesStatus": {"seriesLetter": "A", "round": 1},
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"app.playoff_cache.requests.get",
|
||||
lambda *a, **kw: _Resp({"games": [{"id": 101, "gameNumber": 3}]}),
|
||||
)
|
||||
playoff_cache.enrich_game_numbers([game])
|
||||
assert "gameNumber" not in game
|
||||
|
||||
def test_multiple_series(self, tmp_db, monkeypatch):
|
||||
games = [_raw_playoff_game(101, "A"), _raw_playoff_game(201, "B")]
|
||||
payloads = {
|
||||
"a": {"games": [{"id": 101, "gameNumber": 2}]},
|
||||
"b": {"games": [{"id": 201, "gameNumber": 5}]},
|
||||
}
|
||||
|
||||
def fake_get(url, *a, **kw):
|
||||
letter = url.rstrip("/").rsplit("/", 1)[-1]
|
||||
return _Resp(payloads[letter])
|
||||
|
||||
monkeypatch.setattr("app.playoff_cache.requests.get", fake_get)
|
||||
playoff_cache.enrich_game_numbers(games)
|
||||
assert games[0]["gameNumber"] == 2
|
||||
assert games[1]["gameNumber"] == 5
|
||||
|
||||
@@ -181,9 +181,15 @@ class TestSeriesSummary:
|
||||
)
|
||||
assert series_summary(game) == "Game 2 of 7"
|
||||
|
||||
def test_fut_game_suppresses_summary(self):
|
||||
def test_fut_game_uses_explicit_game_number(self):
|
||||
game = make_playoff_game(
|
||||
top_wins=1, bottom_wins=1, game_state="FUT", game_number=4
|
||||
)
|
||||
assert series_summary(game) == "Game 4 of 7"
|
||||
|
||||
def test_fut_game_without_game_number_uses_fallback(self):
|
||||
game = make_playoff_game(top_wins=1, bottom_wins=1, game_state="FUT")
|
||||
assert series_summary(game) == ""
|
||||
assert series_summary(game) == "Game 3 of 7"
|
||||
|
||||
|
||||
class TestIsPinned:
|
||||
|
||||
Reference in New Issue
Block a user