mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-02 01:08:05 +00:00
fix(sports): harden the live game-over check and font/log init
Follow-up review findings on the promoted base-class methods. _is_game_really_over: - `period` present-but-None raised TypeError on `None >= FINAL_PERIOD`, taking down the whole live-update pass (_detect_stale_games has no try/except). Same failure shape as the null `period_text` already fixed. - An expired clock spelled "00:00" normalizes to "0000", which matched none of the hand-listed literals, so a finished game with a two-digit minute clock stayed on the scoreboard forever. Compare numerically. SportsCore: - _load_fonts kept the cwd-relative "assets/fonts/..." literals the _font_root() seam exists to remove, so every scoreboard font degraded to PIL's default face outside the install root. - _should_log read self._last_warning_time unguarded while only an unrelated method initialized it lazily; the first warning of a run raised AttributeError. Initialize it in __init__. Also documents that game_update_timestamps is written by subclasses, not by the base class, so the staleness branch is inert until B5 adoption. 14 new tests. Gates: 463 core unit, 60 plugin safety. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
This commit is contained in:
@@ -185,6 +185,10 @@ class SportsCore(ABC):
|
||||
}
|
||||
self.last_update = 0
|
||||
self.current_game = None
|
||||
# Cooldown clock for _should_log(). Initialized here rather than lazily:
|
||||
# _should_log() reads it unguarded, so whichever warning fires first
|
||||
# would otherwise raise AttributeError instead of logging.
|
||||
self._last_warning_time = 0
|
||||
self.fonts = self._load_fonts()
|
||||
|
||||
# Optional visual skin (see docs/SKIN_SYSTEM.md). "skin" is either a
|
||||
@@ -425,17 +429,26 @@ class SportsCore(ABC):
|
||||
|
||||
|
||||
def _load_fonts(self):
|
||||
"""Load fonts used by the scoreboard."""
|
||||
"""Load fonts used by the scoreboard.
|
||||
|
||||
Paths go through :meth:`_resolve_font_path` so the bundled fonts are
|
||||
found regardless of the process working directory — a bare
|
||||
``"assets/fonts/..."`` silently degraded every scoreboard to the PIL
|
||||
default font whenever the process started elsewhere (the plugin safety
|
||||
harness on CI being the case that surfaced it).
|
||||
"""
|
||||
fonts = {}
|
||||
press_start = self._resolve_font_path("PressStart2P-Regular.ttf")
|
||||
four_by_six = self._resolve_font_path("4x6-font.ttf")
|
||||
try:
|
||||
fonts['score'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
|
||||
fonts['time'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
||||
fonts['team'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
||||
fonts['status'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) # Using 4x6 for status
|
||||
fonts['detail'] = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) # Added detail font
|
||||
fonts['rank'] = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 10)
|
||||
fonts['score'] = ImageFont.truetype(press_start, 10)
|
||||
fonts['time'] = ImageFont.truetype(press_start, 8)
|
||||
fonts['team'] = ImageFont.truetype(press_start, 8)
|
||||
fonts['status'] = ImageFont.truetype(four_by_six, 6) # Using 4x6 for status
|
||||
fonts['detail'] = ImageFont.truetype(four_by_six, 6) # Added detail font
|
||||
fonts['rank'] = ImageFont.truetype(press_start, 10)
|
||||
logging.info("Successfully loaded fonts") # Changed log prefix
|
||||
except IOError:
|
||||
except OSError:
|
||||
logging.warning("Fonts not found, using default PIL font.") # Changed log prefix
|
||||
fonts['score'] = ImageFont.load_default()
|
||||
fonts['time'] = ImageFont.load_default()
|
||||
|
||||
@@ -842,8 +842,11 @@ class SportsLive(SportsCore):
|
||||
self.count_log_interval = 5 # Only log count data every 5 seconds
|
||||
# Initialize test_mode - defaults to False (live mode)
|
||||
self.test_mode = self.mode_config.get("test_mode", False)
|
||||
# Freshness bookkeeping for _detect_stale_games():
|
||||
# {game_id: {"clock": ts, "score": ts, "last_seen": ts}}
|
||||
# Freshness bookkeeping for _detect_stale_games(). The base class only
|
||||
# *reads* this map; a subclass's update() stamps entries as it ingests a
|
||||
# feed: {game_id: {"clock": ts, "score": ts, "last_seen": ts}}.
|
||||
# Until a subclass writes "last_seen", the staleness branch of
|
||||
# _detect_stale_games is inert and only the game-over check applies.
|
||||
self.game_update_timestamps = {}
|
||||
self.stale_game_timeout = self.mode_config.get("stale_game_timeout", 300) # 5 minutes default
|
||||
|
||||
@@ -884,7 +887,11 @@ class SportsLive(SportsCore):
|
||||
return False
|
||||
|
||||
raw_clock = game.get("clock")
|
||||
period = game.get("period", 0)
|
||||
# `or 0` rather than a get() default: feeds routinely send an explicit
|
||||
# null period, and `None >= FINAL_PERIOD` raises TypeError — which would
|
||||
# take down the whole live-update pass, since the only caller
|
||||
# (_detect_stale_games) has no try/except around it.
|
||||
period = game.get("period") or 0
|
||||
|
||||
# Only check clock-based finish if we have a valid clock string. A
|
||||
# missing or non-string clock is NOT coerced to "0:00": sports without a
|
||||
@@ -892,8 +899,11 @@ class SportsLive(SportsCore):
|
||||
# otherwise be declared over from the FINAL_PERIOD-th period onward.
|
||||
if isinstance(raw_clock, str) and raw_clock.strip() and period >= self.FINAL_PERIOD:
|
||||
clock = raw_clock
|
||||
# Compare numerically rather than against a literal set: feeds spell
|
||||
# an expired clock "0:00", ":00" and "00:00" depending on sport, and
|
||||
# a membership test silently misses every spelling not listed.
|
||||
clock_normalized = clock.replace(":", "").strip()
|
||||
if clock_normalized in ("000", "00") or clock in ("0:00", ":00"):
|
||||
if clock_normalized.isdigit() and int(clock_normalized) == 0:
|
||||
self.logger.debug(
|
||||
f"_is_game_really_over({game_str}): "
|
||||
f"returning True - clock at 0:00 (clock='{clock}', period={period})"
|
||||
|
||||
Reference in New Issue
Block a user