From aa15e0497c675e486de4e3c9e9d7387face52f4e Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:01:16 -0400 Subject: [PATCH 1/4] scoreboards: inherit core's SportsGameOverMixin; drop the game-over copies (family 5) Core 3.8.1 ships src.common.sports_game_over, whose SportsGameOverMixin._is_game_really_over is the body the nine copies were reconciled to in #625. Each plugin's SportsLive now inherits it, listed before SportsLiveSharedMixin (the order core documents and tests), keeps its FINAL_PERIOD declaration and deletes its copy. The import is plain; the next commit floors every plugin on 3.8.1. Baseball keeps BaseballLive._is_game_really_over, the postponed/suspended check; its super() call now reaches core's method. No behaviour change: scripts/test_game_over_check.py gives the same 3,115 answers, and every harness render is byte-identical. Co-Authored-By: Claude Opus 5.5 --- plugins/afl-scoreboard/sports.py | 57 ++----------------------- plugins/baseball-scoreboard/sports.py | 57 ++----------------------- plugins/basketball-scoreboard/sports.py | 57 ++----------------------- plugins/football-scoreboard/sports.py | 57 ++----------------------- plugins/hockey-scoreboard/sports.py | 57 ++----------------------- plugins/lacrosse-scoreboard/sports.py | 57 ++----------------------- plugins/nrl-scoreboard/sports.py | 57 ++----------------------- plugins/soccer-scoreboard/sports.py | 57 ++----------------------- plugins/ufc-scoreboard/sports.py | 57 ++----------------------- 9 files changed, 27 insertions(+), 486 deletions(-) diff --git a/plugins/afl-scoreboard/sports.py b/plugins/afl-scoreboard/sports.py index f0561877..df7e9587 100644 --- a/plugins/afl-scoreboard/sports.py +++ b/plugins/afl-scoreboard/sports.py @@ -39,6 +39,7 @@ from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsCardOptionsMixin, SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin from src.common.sports_celebration import SportsCelebrationMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. @@ -3111,9 +3112,9 @@ def display(self, force_clear=False) -> bool: return True -class SportsLive(SportsCelebrationMixin, SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsCelebrationMixin, SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). #: None: the clock counts up, so only the final status ends a game. FINAL_PERIOD: Optional[int] = None @@ -3556,58 +3557,6 @@ def format_score(score): except Exception as e: self.logger.error(f"Error displaying live game: {e}", exc_info=True) - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def update(self): """Update live game data and handle game switching.""" if not self.is_enabled: diff --git a/plugins/baseball-scoreboard/sports.py b/plugins/baseball-scoreboard/sports.py index b5add8f6..d090678f 100644 --- a/plugins/baseball-scoreboard/sports.py +++ b/plugins/baseball-scoreboard/sports.py @@ -57,6 +57,7 @@ def resolve_font_name(font_name: str) -> str: from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsCardOptionsMixin, SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. from src.common.sports_helpers import ( @@ -2986,9 +2987,9 @@ def display(self, force_clear=False) -> bool: return True -class SportsLive(SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). #: None: innings, not a clock. FINAL_PERIOD: Optional[int] = None @@ -3049,58 +3050,6 @@ def __init__( self.game_update_timestamps = {} # {game_id: {"clock": timestamp, "score": timestamp, "last_seen": timestamp}} self.stale_game_timeout = self.mode_config.get("stale_game_timeout", 300) # 5 minutes default - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def _is_favorite_game(self, game: Dict) -> bool: return ( game.get("home_abbr") in self.favorite_teams diff --git a/plugins/basketball-scoreboard/sports.py b/plugins/basketball-scoreboard/sports.py index 99b27e5c..a9833de8 100644 --- a/plugins/basketball-scoreboard/sports.py +++ b/plugins/basketball-scoreboard/sports.py @@ -40,6 +40,7 @@ from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsCardOptionsMixin, SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. from src.common.sports_helpers import ( @@ -3265,9 +3266,9 @@ def display(self, force_clear=False) -> bool: return True -class SportsLive(SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). FINAL_PERIOD: Optional[int] = 4 def __init__( @@ -3393,58 +3394,6 @@ def _build_weighted_schedule(self, games: List[Dict]) -> List[str]: current_weight[best_gid] -= total_weight return schedule - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def _keep_final_for_vegas(self, details: Dict) -> None: """Hold a game that just went final, so its live Vegas card shows FINAL. diff --git a/plugins/football-scoreboard/sports.py b/plugins/football-scoreboard/sports.py index 1f66f336..7c896649 100644 --- a/plugins/football-scoreboard/sports.py +++ b/plugins/football-scoreboard/sports.py @@ -41,6 +41,7 @@ from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsCardOptionsMixin, SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin from src.common.sports_celebration import SportsCelebrationMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. @@ -3196,9 +3197,9 @@ def display(self, force_clear=False) -> bool: return True -class SportsLive(SportsCelebrationMixin, SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsCelebrationMixin, SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). FINAL_PERIOD: Optional[int] = 4 def __init__( @@ -3625,58 +3626,6 @@ def display(self, force_clear: bool = False) -> bool: self._advance_live_game_if_due() return super().display(force_clear) - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def update(self): """Update live game data and handle game switching.""" if not self.is_enabled: diff --git a/plugins/hockey-scoreboard/sports.py b/plugins/hockey-scoreboard/sports.py index f88155df..83a38301 100644 --- a/plugins/hockey-scoreboard/sports.py +++ b/plugins/hockey-scoreboard/sports.py @@ -44,6 +44,7 @@ from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsCardOptionsMixin, SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin from src.common.sports_celebration import SportsCelebrationMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. @@ -2859,9 +2860,9 @@ def display(self, force_clear=False) -> bool: return False -class SportsLive(SportsCelebrationMixin, SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsCelebrationMixin, SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). FINAL_PERIOD: Optional[int] = 3 def __init__( @@ -3007,58 +3008,6 @@ def _build_rotation_schedule(self, games: List[Dict]) -> List[str]: schedule.append(best_id) return schedule - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def update(self): """Update live game data and handle game switching.""" if not self.is_enabled: diff --git a/plugins/lacrosse-scoreboard/sports.py b/plugins/lacrosse-scoreboard/sports.py index 7a506ffd..8f3ba70b 100644 --- a/plugins/lacrosse-scoreboard/sports.py +++ b/plugins/lacrosse-scoreboard/sports.py @@ -41,6 +41,7 @@ from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsCardOptionsMixin, SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. from src.common.sports_helpers import ( @@ -2852,9 +2853,9 @@ def _swrr_schedule(weighted_ids: List[Tuple[str, int]]) -> List[str]: return order -class SportsLive(SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). FINAL_PERIOD: Optional[int] = 4 def __init__( @@ -2924,58 +2925,6 @@ def _is_favorite_game(self, game) -> bool: or game.get("away_abbr") in self.favorite_teams ) - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def _keep_final_for_vegas(self, details: Dict) -> None: """Hold a game that just went final, so its live Vegas card shows Final. diff --git a/plugins/nrl-scoreboard/sports.py b/plugins/nrl-scoreboard/sports.py index c5a1e8d7..4feb2aac 100644 --- a/plugins/nrl-scoreboard/sports.py +++ b/plugins/nrl-scoreboard/sports.py @@ -39,6 +39,7 @@ from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsCardOptionsMixin, SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin from src.common.sports_celebration import SportsCelebrationMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. @@ -3217,9 +3218,9 @@ def display(self, force_clear=False) -> bool: return True -class SportsLive(SportsCelebrationMixin, SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsCelebrationMixin, SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). #: None: the clock counts up, so only the final status ends a game. FINAL_PERIOD: Optional[int] = None @@ -3664,58 +3665,6 @@ def format_score(score): except Exception as e: self.logger.error(f"Error displaying live game: {e}", exc_info=True) - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def update(self): """Update live game data and handle game switching.""" if not self.is_enabled: diff --git a/plugins/soccer-scoreboard/sports.py b/plugins/soccer-scoreboard/sports.py index 2ced5766..e85d3cc8 100644 --- a/plugins/soccer-scoreboard/sports.py +++ b/plugins/soccer-scoreboard/sports.py @@ -40,6 +40,7 @@ from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsCardOptionsMixin, SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin from src.common.sports_celebration import SportsCelebrationMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. @@ -3125,9 +3126,9 @@ def display(self, force_clear=False) -> bool: return True -class SportsLive(SportsCelebrationMixin, SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsCelebrationMixin, SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). #: None: the clock counts up, so only the final status ends a game. FINAL_PERIOD: Optional[int] = None @@ -3573,58 +3574,6 @@ def format_score(score): except Exception as e: self.logger.error(f"Error displaying live game: {e}", exc_info=True) - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def update(self): """Update live game data and handle game switching.""" if not self.is_enabled: diff --git a/plugins/ufc-scoreboard/sports.py b/plugins/ufc-scoreboard/sports.py index 38272add..1d4a1b08 100644 --- a/plugins/ufc-scoreboard/sports.py +++ b/plugins/ufc-scoreboard/sports.py @@ -30,6 +30,7 @@ from src.common.sports_fetch import SportsFetchMixin from src.common.sports_display_rules import SportsGameRulesMixin from src.common.sports_font_path import resolve_font_path as _resolve_font_path +from src.common.sports_game_over import SportsGameOverMixin # Helpers every scoreboard carried a private copy of; core ships them from # 3.5.0 (the manifest's floor). The private aliases keep this module's names. from src.common.sports_helpers import ( @@ -2629,9 +2630,9 @@ def display(self, force_clear=False) -> bool: return True -class SportsLive(SportsLiveSharedMixin, SportsCore): +class SportsLive(SportsGameOverMixin, SportsLiveSharedMixin, SportsCore): SKIN_MODE = "live" - #: Period from which a 0:00 clock ends a game (see _is_game_really_over). + #: Period from which a 0:00 clock ends a game (core's SportsGameOverMixin). #: None: a bout ends only on ESPN's final status, never on a round's clock. FINAL_PERIOD: Optional[int] = None @@ -2693,58 +2694,6 @@ def __init__( self.game_update_timestamps = {} # {game_id: {"clock": timestamp, "score": timestamp, "last_seen": timestamp}} self.stale_game_timeout = self.mode_config.get("stale_game_timeout", 300) # 5 minutes default - def _is_game_really_over(self, game: Dict) -> bool: - """Whether a game ESPN still lists as live has in fact ended. - - It has when its period text says final. From period ``FINAL_PERIOD`` - on, a clock at 0:00 ends it too, unless the score is level: a tie at - the end of regulation goes to overtime, and a game that does end tied - says final. With ``FINAL_PERIOD = None`` the clock never ends a game. - """ - game_str = f"{game.get('away_abbr')}@{game.get('home_abbr')}" - - # ESPN can send the key as null, and .get()'s default only covers a - # missing key, so a None here crashed the whole live update. - raw_period_text = game.get("period_text") - period_text = raw_period_text.lower() if isinstance(raw_period_text, str) else "" - if "final" in period_text: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - 'final' in period_text='{period_text}'" - ) - return True - - # Same for a null or non-numeric period: treat it as period 0. - try: - period = int(game.get("period") or 0) - except (TypeError, ValueError, OverflowError): - period = 0 - # Only a clock string is read: "0:00" and ":00" are zero; ":40" is not. - clock = game.get("clock") - clock_at_zero = isinstance(clock, str) and clock.replace(":", "").strip() in ("000", "00") - - if self.FINAL_PERIOD is not None and period >= self.FINAL_PERIOD and clock_at_zero: - try: - tied = int(game["away_score"]) == int(game["home_score"]) - except (KeyError, TypeError, ValueError, OverflowError): - tied = False # a missing or unreadable score leaves it to the clock - if not tied: - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning True - clock at 0:00 (clock='{clock}', period={period})" - ) - return True - self.logger.debug( - f"_is_game_really_over({game_str}): " - f"returning False - tied at 0:00 (period={period}), overtime next" - ) - return False - - self.logger.debug( - f"_is_game_really_over({game_str}): returning False" - ) - return False - def _keep_final_for_vegas(self, details: Dict) -> None: """Hold a fight that just ended, so its live Vegas card shows the result. From 2a56c9fbfe4913a83781ad915bd9b6fadd4f19db Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:01:26 -0400 Subject: [PATCH 2/4] scoreboards: floor on core 3.8.1 (minor bumps) afl 1.36.0, baseball 1.58.0, basketball 1.43.0, football 3.19.0, hockey 1.43.0, lacrosse 1.37.0, nrl 1.35.0, soccer 2.40.0, ufc 1.20.0. versions[0].ledmatrix_min_version and compatible_versions rise to 3.8.1, the first release that ships src.common.sports_game_over, which the previous commit imports plainly. A floor raise is a minor bump, as for the 3.7.0 and 3.8.0 raises before it. plugins.json regenerated with update_registry.py. Co-Authored-By: Claude Opus 5.5 --- plugins.json | 45 +++++++++------------ plugins/afl-scoreboard/CHANGELOG.md | 17 ++++++++ plugins/afl-scoreboard/manifest.json | 10 ++++- plugins/baseball-scoreboard/CHANGELOG.md | 19 +++++++++ plugins/baseball-scoreboard/manifest.json | 10 ++++- plugins/basketball-scoreboard/CHANGELOG.md | 17 ++++++++ plugins/basketball-scoreboard/manifest.json | 10 ++++- plugins/football-scoreboard/CHANGELOG.md | 17 ++++++++ plugins/football-scoreboard/manifest.json | 10 ++++- plugins/hockey-scoreboard/CHANGELOG.md | 17 ++++++++ plugins/hockey-scoreboard/manifest.json | 10 ++++- plugins/lacrosse-scoreboard/CHANGELOG.md | 17 ++++++++ plugins/lacrosse-scoreboard/manifest.json | 10 ++++- plugins/nrl-scoreboard/CHANGELOG.md | 17 ++++++++ plugins/nrl-scoreboard/manifest.json | 10 ++++- plugins/soccer-scoreboard/CHANGELOG.md | 17 ++++++++ plugins/soccer-scoreboard/manifest.json | 10 ++++- plugins/ufc-scoreboard/CHANGELOG.md | 17 ++++++++ plugins/ufc-scoreboard/manifest.json | 10 ++++- 19 files changed, 245 insertions(+), 45 deletions(-) diff --git a/plugins.json b/plugins.json index 2d53b2c7..599b37d6 100644 --- a/plugins.json +++ b/plugins.json @@ -80,9 +80,8 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.57.4", - "ledmatrix_min_version": "3.8.0", - "commit": "a5194139de39c4999562ee0d90d9e079943d1861" + "latest_version": "1.58.0", + "ledmatrix_min_version": "3.8.1" }, { "id": "basketball-scoreboard", @@ -107,9 +106,8 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.42.2", - "ledmatrix_min_version": "3.8.0", - "commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c" + "latest_version": "1.43.0", + "ledmatrix_min_version": "3.8.1" }, { "id": "calendar", @@ -283,9 +281,8 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "3.18.9", - "ledmatrix_min_version": "3.8.0", - "commit": "7b232936a6503ac585006946327fb584dc875d62" + "latest_version": "3.19.0", + "ledmatrix_min_version": "3.8.1" }, { "id": "geochron", @@ -385,10 +382,9 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.42.5", + "latest_version": "1.43.0", "icon": "fas fa-hockey-puck", - "ledmatrix_min_version": "3.8.0", - "commit": "a5194139de39c4999562ee0d90d9e079943d1861" + "ledmatrix_min_version": "3.8.1" }, { "id": "lacrosse-scoreboard", @@ -411,10 +407,9 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.36.4", + "latest_version": "1.37.0", "icon": "fas fa-baseball-ball", - "ledmatrix_min_version": "3.8.0", - "commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c" + "ledmatrix_min_version": "3.8.1" }, { "id": "birdnet-go", @@ -847,9 +842,8 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "2.39.5", - "ledmatrix_min_version": "3.8.0", - "commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c" + "latest_version": "2.40.0", + "ledmatrix_min_version": "3.8.1" }, { "id": "static-image", @@ -1007,10 +1001,9 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.19.5", + "latest_version": "1.20.0", "icon": "fas fa-fist-raised", - "ledmatrix_min_version": "3.8.0", - "commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c" + "ledmatrix_min_version": "3.8.1" }, { "id": "weather", @@ -1164,10 +1157,9 @@ "downloads": 0, "verified": true, "screenshot": "", - "latest_version": "1.35.3", + "latest_version": "1.36.0", "last_updated": "2026-10-05", - "ledmatrix_min_version": "3.8.0", - "commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c" + "ledmatrix_min_version": "3.8.1" }, { "id": "tidbyt-baseball-scoreboard", @@ -1213,9 +1205,8 @@ "last_updated": "2026-10-05", "verified": true, "screenshot": "", - "latest_version": "1.34.3", - "ledmatrix_min_version": "3.8.0", - "commit": "8db9d3ebe374dc4155906ad29f6f5baa5536fe1c" + "latest_version": "1.35.0", + "ledmatrix_min_version": "3.8.1" }, { "id": "jellyfin-now-playing", diff --git a/plugins/afl-scoreboard/CHANGELOG.md b/plugins/afl-scoreboard/CHANGELOG.md index 3556c938..cf35a1c9 100644 --- a/plugins/afl-scoreboard/CHANGELOG.md +++ b/plugins/afl-scoreboard/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.36.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (None). + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [1.35.3] - 2026-10-05 ### Changed diff --git a/plugins/afl-scoreboard/manifest.json b/plugins/afl-scoreboard/manifest.json index 6b4c7e54..ef2d50ff 100644 --- a/plugins/afl-scoreboard/manifest.json +++ b/plugins/afl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "afl-scoreboard", "name": "AFL Scoreboard", - "version": "1.35.3", + "version": "1.36.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming AFL (Australian Football League) games with real-time scores and game status.", "category": "sports", @@ -18,6 +18,12 @@ "afl_upcoming" ], "versions": [ + { + "version": "1.36.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (None). No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "1.35.3", "released": "2026-10-05", @@ -552,6 +558,6 @@ "class_name": "AflScoreboardPlugin", "config_schema": "config_schema.json", "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ] } diff --git a/plugins/baseball-scoreboard/CHANGELOG.md b/plugins/baseball-scoreboard/CHANGELOG.md index bf4d2b79..9fb79a8d 100644 --- a/plugins/baseball-scoreboard/CHANGELOG.md +++ b/plugins/baseball-scoreboard/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [1.58.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (None). +- `BaseballLive._is_game_really_over` (the postponed/suspended check) + stays; its `super()` call now reaches core's. + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [1.57.4] - 2026-10-05 ### Fixed diff --git a/plugins/baseball-scoreboard/manifest.json b/plugins/baseball-scoreboard/manifest.json index 2c4647c6..a714ae39 100644 --- a/plugins/baseball-scoreboard/manifest.json +++ b/plugins/baseball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "baseball-scoreboard", "name": "Baseball Scoreboard", - "version": "1.57.4", + "version": "1.58.0", "update_interval": 60, "author": "ChuckBuilds", "description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules", @@ -31,6 +31,12 @@ "branch": "main", "plugin_path": "plugins/baseball-scoreboard", "versions": [ + { + "version": "1.58.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (None). BaseballLive keeps its postponed/suspended check, which now defers to core's. No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "1.57.4", "released": "2026-10-05", @@ -874,6 +880,6 @@ "class_name": "BaseballScoreboardPlugin", "entry_point": "manager.py", "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ] } diff --git a/plugins/basketball-scoreboard/CHANGELOG.md b/plugins/basketball-scoreboard/CHANGELOG.md index 8ddcdc55..e0d6a94f 100644 --- a/plugins/basketball-scoreboard/CHANGELOG.md +++ b/plugins/basketball-scoreboard/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.43.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (4). + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [1.42.2] - 2026-10-05 ### Fixed diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 305889ea..9471c44b 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "basketball-scoreboard", "name": "Basketball Scoreboard", - "version": "1.42.2", + "version": "1.43.0", "update_interval": 60, "description": "Live, recent, and upcoming basketball games across NBA, NCAA Men's, NCAA Women's, and WNBA with real-time scores, schedules, and March Madness tournament support", "author": "ChuckBuilds", @@ -19,6 +19,12 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "version": "1.43.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (4). No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "1.42.2", "released": "2026-10-05", @@ -650,6 +656,6 @@ "entry_point": "manager.py", "class_name": "BasketballScoreboardPlugin", "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ] } diff --git a/plugins/football-scoreboard/CHANGELOG.md b/plugins/football-scoreboard/CHANGELOG.md index bd05efcb..bd12566c 100644 --- a/plugins/football-scoreboard/CHANGELOG.md +++ b/plugins/football-scoreboard/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [3.19.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (4). + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [3.18.9] - 2026-10-05 ### Fixed diff --git a/plugins/football-scoreboard/manifest.json b/plugins/football-scoreboard/manifest.json index 1f7ff46d..a3b83f7f 100644 --- a/plugins/football-scoreboard/manifest.json +++ b/plugins/football-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "football-scoreboard", "name": "Football Scoreboard", - "version": "3.18.9", + "version": "3.19.0", "update_interval": 60, "author": "ChuckBuilds", "class_name": "FootballScoreboardPlugin", @@ -25,6 +25,12 @@ "ncaa_fb_live" ], "versions": [ + { + "version": "3.19.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (4). No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "3.18.9", "released": "2026-10-05", @@ -973,7 +979,7 @@ "config_schema": "config_schema.json", "entry_point": "manager.py", "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ], "display": { "design_size": { diff --git a/plugins/hockey-scoreboard/CHANGELOG.md b/plugins/hockey-scoreboard/CHANGELOG.md index 23ae7c24..dd85de5b 100644 --- a/plugins/hockey-scoreboard/CHANGELOG.md +++ b/plugins/hockey-scoreboard/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.43.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (3). + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [1.42.5] - 2026-10-05 ### Fixed diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index f5f36f04..b3992ac0 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.42.5", + "version": "1.43.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", @@ -18,7 +18,7 @@ ], "icon": "fas fa-hockey-puck", "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ], "requires": { "python": ">=3.9", @@ -54,6 +54,12 @@ } ], "versions": [ + { + "version": "1.43.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (3). No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "1.42.5", "released": "2026-10-05", diff --git a/plugins/lacrosse-scoreboard/CHANGELOG.md b/plugins/lacrosse-scoreboard/CHANGELOG.md index cf54f7e5..c5453c12 100644 --- a/plugins/lacrosse-scoreboard/CHANGELOG.md +++ b/plugins/lacrosse-scoreboard/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.37.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (4). + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [1.36.4] - 2026-10-05 ### Fixed diff --git a/plugins/lacrosse-scoreboard/manifest.json b/plugins/lacrosse-scoreboard/manifest.json index df381bb1..d79f56bd 100644 --- a/plugins/lacrosse-scoreboard/manifest.json +++ b/plugins/lacrosse-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "lacrosse-scoreboard", "name": "Lacrosse Scoreboard", - "version": "1.36.4", + "version": "1.37.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming NCAA men's and women's lacrosse games with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/lacrosse-scoreboard", @@ -17,7 +17,7 @@ ], "icon": "fas fa-baseball-ball", "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ], "requires": { "python": ">=3.9", @@ -50,6 +50,12 @@ } ], "versions": [ + { + "version": "1.37.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (4). No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "1.36.4", "released": "2026-10-05", diff --git a/plugins/nrl-scoreboard/CHANGELOG.md b/plugins/nrl-scoreboard/CHANGELOG.md index bd0ea7c8..3642edea 100644 --- a/plugins/nrl-scoreboard/CHANGELOG.md +++ b/plugins/nrl-scoreboard/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.35.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (None). + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [1.34.3] - 2026-10-05 ### Changed diff --git a/plugins/nrl-scoreboard/manifest.json b/plugins/nrl-scoreboard/manifest.json index 0302f685..d7c1365e 100644 --- a/plugins/nrl-scoreboard/manifest.json +++ b/plugins/nrl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "nrl-scoreboard", "name": "NRL Scoreboard", - "version": "1.34.3", + "version": "1.35.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming NRL (National Rugby League) games with real-time scores and game status.", "category": "sports", @@ -18,6 +18,12 @@ "nrl_upcoming" ], "versions": [ + { + "version": "1.35.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (None). No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "1.34.3", "released": "2026-10-05", @@ -562,6 +568,6 @@ "class_name": "NrlScoreboardPlugin", "config_schema": "config_schema.json", "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ] } diff --git a/plugins/soccer-scoreboard/CHANGELOG.md b/plugins/soccer-scoreboard/CHANGELOG.md index fdc5b36f..35564b2d 100644 --- a/plugins/soccer-scoreboard/CHANGELOG.md +++ b/plugins/soccer-scoreboard/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [2.40.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (None). + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [2.39.5] - 2026-10-05 ### Changed diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 30382cc2..282010f2 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "2.39.5", + "version": "2.40.0", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, Champions League, Europa League, and FIFA World Cup", "category": "sports", @@ -53,6 +53,12 @@ "soccer_usa.1_upcoming" ], "versions": [ + { + "version": "2.40.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (None). No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "2.39.5", "released": "2026-10-05", @@ -732,6 +738,6 @@ } ], "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ] } diff --git a/plugins/ufc-scoreboard/CHANGELOG.md b/plugins/ufc-scoreboard/CHANGELOG.md index 19a49fa7..ad3dc8d0 100644 --- a/plugins/ufc-scoreboard/CHANGELOG.md +++ b/plugins/ufc-scoreboard/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.20.0] - 2026-10-05 + +### Changed +- Requires LEDMatrix core 3.8.1 (`ledmatrix_min_version` and + `compatible_versions`), the first release that ships + `src/common/sports_game_over.py`. The store refuses to install or update + this version onto an older core. +- `SportsLive` inherits `SportsGameOverMixin` (`src.common.sports_game_over`) + and keeps declaring `FINAL_PERIOD` (None). + +### Removed +- `SportsLive._is_game_really_over`, this plugin's copy of core's game-over + check. + +No change in behaviour: every game-over answer and every safety-harness +render is identical. + ## [1.19.5] - 2026-10-05 ### Changed diff --git a/plugins/ufc-scoreboard/manifest.json b/plugins/ufc-scoreboard/manifest.json index 4253f4c6..d13b2348 100644 --- a/plugins/ufc-scoreboard/manifest.json +++ b/plugins/ufc-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "ufc-scoreboard", "name": "UFC Scoreboard", - "version": "1.19.5", + "version": "1.20.0", "author": "LegoGuy1000", "contributors": [ { @@ -32,6 +32,12 @@ "default_duration": 15, "config_schema": "config_schema.json", "versions": [ + { + "version": "1.20.0", + "released": "2026-10-05", + "ledmatrix_min_version": "3.8.1", + "notes": "Requires LEDMatrix core 3.8.1, the first release that ships src.common.sports_game_over. SportsLive uses core's game-over check (SportsGameOverMixin) and its own copy of _is_game_really_over is removed; FINAL_PERIOD stays declared here (None). No change in behaviour: every game-over answer and every safety-harness render is identical." + }, { "version": "1.19.5", "released": "2026-10-05", @@ -409,6 +415,6 @@ "verified": true, "screenshot": "", "compatible_versions": [ - ">=3.8.0" + ">=3.8.1" ] } From b647f180461d2c3c6269f4a83c8189971b18aaa0 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:01:26 -0400 Subject: [PATCH 3/4] scripts: sunset guard for the game-over copies scripts/test_game_over_mixin_copies.py, in the shape of the stage 3 and stage 4 guards. For each of the nine scoreboards it fails when: - a runtime file defines _is_game_really_over again (any class, or module level), except baseball's BaseballLive, which must call super(); - sports.py's import of SportsGameOverMixin is missing, aliased or inside a try that catches ImportError; - SportsLive does not inherit the mixin, or lists it after SportsLiveSharedMixin; - SportsLive's FINAL_PERIOD is not the owner's value (hockey 3; basketball, football and lacrosse 4; the rest None), or another class sets it; - with a core checkout, the mixin defines anything but the method and FINAL_PERIOD. A self-check plants each failure. CI picks it up through the scripts/test_*.py glob in test-plugins.yml. The shared sports code page records the 3.8.1 convergence. Co-Authored-By: Claude Opus 5.5 --- .../08-shared-sports-code.md | 9 + scripts/test_game_over_mixin_copies.py | 231 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 scripts/test_game_over_mixin_copies.py diff --git a/docs/plugin-development/08-shared-sports-code.md b/docs/plugin-development/08-shared-sports-code.md index 6770b220..ef19974a 100644 --- a/docs/plugin-development/08-shared-sports-code.md +++ b/docs/plugin-development/08-shared-sports-code.md @@ -148,6 +148,15 @@ the core actually ships: `_swrr_advance` and `_refresh_switch_mode_managers`, or the multi-league plugins' `_resolve_managers_for_mode` and `_extract_mode_type`). They go when the code around them is reconciled. +- **Converged at 3.8.1 (sports family 5, the game-over check):** + `src.common.sports_game_over`. `SportsLive` (all nine) inherits + `SportsGameOverMixin`, listed before `SportsLiveSharedMixin`, and keeps + declaring `FINAL_PERIOD`, the period from which a 0:00 clock ends a game + (hockey 3; basketball, football and lacrosse 4; the rest `None`). Baseball's + `BaseballLive` keeps its postponed/suspended override, which calls + `super()`. `scripts/test_game_over_mixin_copies.py` fails if a copy, a + guarded import, a missing or misplaced base or a changed `FINAL_PERIOD` + comes back; `scripts/test_game_over_check.py` pins the answers. - **Not converging (documented forks):** `dynamic_team_resolver` (plugin copies take `cache_manager` in the constructor; the core's does not — different API), ufc's `base_odds_manager` (MMA athlete-odds fork), and — until the core diff --git a/scripts/test_game_over_mixin_copies.py b/scripts/test_game_over_mixin_copies.py new file mode 100644 index 00000000..c7f5df51 --- /dev/null +++ b/scripts/test_game_over_mixin_copies.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Keep the game-over copies gone: ``_is_game_really_over`` lives in core. + +LEDMatrix core 3.8.1 ships ``src.common.sports_game_over`` -- +``SportsGameOverMixin._is_game_really_over``, the body the nine scoreboards' +``SportsLive`` copies were reconciled to (sports family 5). Each plugin floors +on 3.8.1, inherits the mixin and deleted its copy; the one per-sport fact, +``FINAL_PERIOD``, stays declared on its ``SportsLive``. A copy that comes back +is dead weight at best; at worst it is a fix made on one side only, overriding +core's without anyone noticing. So, for every scoreboard: + +1. No runtime file (tests excluded) defines ``_is_game_really_over``, on any + class or at module level. The one exception is baseball's + ``BaseballLive``, which ends postponed and suspended games first; it must + defer to core's through ``super()``. +2. ``sports.py`` imports the mixin from core plainly -- a ``try`` around it + can only hide which module was missing -- and ``SportsLive`` lists it as a + base before ``SportsLiveSharedMixin``, the order core documents (the shared + mixin's ``_detect_stale_games`` calls the method). +3. ``SportsLive`` declares ``FINAL_PERIOD`` with the value in ``FINAL_PERIOD`` + below, and no other class sets it. +4. With a core checkout, the mixin defines exactly the names listed here. + +Self-check: planted sources must fail each of the checks above, so a broken +finder cannot report success. + +Exit: 0 clean, 1 failure. The core comparison is skipped (with a note), never +failed, when no core checkout ships the module. + +Run: python scripts/test_game_over_mixin_copies.py +""" + +from __future__ import annotations + +import ast +import os +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +PLUGINS = REPO / "plugins" + +MODULE = "src.common.sports_game_over" +MIXIN = "SportsGameOverMixin" +METHOD = "_is_game_really_over" + +#: The period from which a 0:00 clock ends a game; None: the clock never does. +FINAL_PERIOD = {"afl": None, "baseball": None, "basketball": 4, "football": 4, + "hockey": 3, "lacrosse": 4, "nrl": None, "soccer": None, "ufc": None} + +#: (plugin, file, class) that overrides the method on purpose; it must call super(). +#: Baseball ends postponed and suspended games before core's check runs. +OVERRIDES = {("baseball", "baseball.py", "BaseballLive")} + +_CATCHES_IMPORT_ERROR = {"ImportError", "ModuleNotFoundError", "Exception", "BaseException"} + + +def _catches_import_error(handler: ast.ExceptHandler) -> bool: + kind = handler.type + if kind is None: + return True + names = kind.elts if isinstance(kind, ast.Tuple) else [kind] + return any(isinstance(n, ast.Name) and n.id in _CATCHES_IMPORT_ERROR for n in names) + + +def _calls_super(func: ast.FunctionDef) -> bool: + """Whether the body calls ``super().(...)``.""" + return any(isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) + and n.func.attr == METHOD and isinstance(n.func.value, ast.Call) + and isinstance(n.func.value.func, ast.Name) and n.func.value.func.id == "super" + for n in ast.walk(func)) + + +def copies(source: str, sport: str, filename: str) -> list[str]: + """Where the method is defined again, or an allowed override skips super().""" + tree = ast.parse(source) + found = [f"module-level {METHOD}" for n in tree.body + if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == METHOD] + for cls in (n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)): + for item in cls.body: + if not (isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and item.name == METHOD): + continue + if (sport, filename, cls.name) not in OVERRIDES: + found.append(f"{cls.name}.{METHOD} is back -- {MODULE} ships it; delete the copy") + elif not _calls_super(item): + found.append(f"{cls.name}.{METHOD} no longer calls super(): core's check is skipped") + return found + + +def _final_period_sets(tree) -> list[tuple[str, ast.expr]]: + """(class, value) for every ``FINAL_PERIOD`` set in a class body.""" + sets = [] + for cls in (n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)): + for item in cls.body: + if isinstance(item, ast.Assign) and any( + isinstance(t, ast.Name) and t.id == "FINAL_PERIOD" for t in item.targets): + sets.append((cls.name, item.value)) + elif (isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name) + and item.target.id == "FINAL_PERIOD" and item.value is not None): + sets.append((cls.name, item.value)) + return sets + + +def adoption_problems(source: str, final_period) -> list[str]: + """``sports.py``: plain import, the base before the shared mixin, FINAL_PERIOD's value.""" + tree = ast.parse(source) + problems = [] + imports = [n for n in ast.walk(tree) if isinstance(n, ast.ImportFrom) and n.module == MODULE + and any(a.name == MIXIN and a.asname is None for a in n.names)] + if not imports: + problems.append(f"does not import {MIXIN} from {MODULE}") + guarded = {n.lineno for t in ast.walk(tree) + if isinstance(t, ast.Try) and any(_catches_import_error(h) for h in t.handlers) + for stmt in t.body for n in ast.walk(stmt) if hasattr(n, "lineno")} + problems += [f"line {n.lineno}: the {MODULE} import is inside a try" + for n in imports if n.lineno in guarded] + live = next((n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "SportsLive"), None) + if live is None: + return problems + ["no class SportsLive"] + bases = [b.id for b in live.bases if isinstance(b, ast.Name)] + if MIXIN not in bases: + problems.append(f"SportsLive does not inherit {MIXIN}") + elif "SportsLiveSharedMixin" in bases and bases.index(MIXIN) > bases.index("SportsLiveSharedMixin"): + problems.append(f"SportsLive lists {MIXIN} after SportsLiveSharedMixin") + declared = [ast.literal_eval(v) for c, v in _final_period_sets(tree) if c == "SportsLive"] + if declared != [final_period]: + problems.append(f"SportsLive.FINAL_PERIOD is {declared or 'not declared'}, " + f"expected {final_period}") + return problems + + +def runtime_files(plugin: Path): + for path in sorted(plugin.rglob("*.py")): + relative = path.relative_to(plugin) + if relative.parts[0] == "test" or path.name.startswith("test_"): + continue + yield path + + +def check_plugins() -> list[str]: + failures = [] + for sport, final_period in FINAL_PERIOD.items(): + plugin = PLUGINS / f"{sport}-scoreboard" + target = plugin / "sports.py" + if not target.is_file(): + failures.append(f"{sport}: sports.py is missing") + continue + failures += [f"{sport} sports.py: {p}" for p in + adoption_problems(target.read_text(encoding="utf-8"), final_period)] + for path in runtime_files(plugin): + source = path.read_text(encoding="utf-8") + failures += [f"{sport} {path.relative_to(plugin)}: {w}" + for w in copies(source, sport, path.name)] + failures += [f"{sport} {path.relative_to(plugin)}: {cls} sets FINAL_PERIOD; " + f"only sports.py's SportsLive declares it" + for cls, _v in _final_period_sets(ast.parse(source)) + if not (path == target and cls == "SportsLive")] + return failures + + +def find_core() -> Path | None: + for candidate in (os.environ.get("LEDMATRIX_CORE", ""), str(REPO.parent / "LEDMatrix")): + if candidate and (Path(candidate) / "src" / "plugin_system").is_dir(): + return Path(candidate) + return None + + +def check_core(core: Path) -> list[str]: + """Core's mixin defines exactly the method and FINAL_PERIOD.""" + path = core / (MODULE.replace(".", "/") + ".py") + if not path.is_file(): + return [] # a core older than 3.8.1: nothing to compare + tree = ast.parse(path.read_text(encoding="utf-8")) + cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == MIXIN) + names = {n.name for n in cls.body if isinstance(n, ast.FunctionDef)} + names |= {n.target.id for n in cls.body + if isinstance(n, ast.AnnAssign) and n.value is not None} + if names != {METHOD, "FINAL_PERIOD"}: + return [f"{MODULE}: {MIXIN} defines {sorted(names)}, expected {METHOD} and FINAL_PERIOD"] + return [] + + +def self_check() -> list[str]: + failures = [] + planted = ("def _is_game_really_over(game):\n pass\n" + "class SportsLive:\n def _is_game_really_over(self, game):\n return False\n") + if len(copies(planted, "hockey", "sports.py")) != 2: + failures.append("self-check: planted copies were not all found") + no_super = ("class BaseballLive:\n def _is_game_really_over(self, game):\n" + " return False\n") + if not any("super()" in p for p in copies(no_super, "baseball", "baseball.py")): + failures.append("self-check: an override without super() was not flagged") + good = ("from src.common.sports_game_over import SportsGameOverMixin\n" + "class SportsLive(SportsGameOverMixin, SportsLiveSharedMixin, SportsCore):\n" + " FINAL_PERIOD: Optional[int] = 3\n") + if adoption_problems(good, 3): + failures.append(f"self-check: a correct adoption was flagged: {adoption_problems(good, 3)}") + for label, source, expect in ( + ("a guarded import", "try:\n " + good.replace("\nclass", "\nexcept ImportError:\n" + " SportsGameOverMixin = object\nclass", 1), + "inside a try"), + ("a missing base", good.replace("SportsGameOverMixin, Sports", "Sports", 1), "does not inherit"), + ("the base after the shared mixin", + good.replace("SportsGameOverMixin, SportsLiveSharedMixin", "SportsLiveSharedMixin, SportsGameOverMixin"), + "after SportsLiveSharedMixin"), + ("a drifted FINAL_PERIOD", good.replace("= 3", "= 4"), "expected 3"), + ("a missing FINAL_PERIOD", good.replace(" FINAL_PERIOD: Optional[int] = 3\n", " pass\n"), + "not declared")): + if not any(expect in p for p in adoption_problems(source, 3)): + failures.append(f"self-check: {label} was not flagged") + return failures + + +def main() -> int: + failures = self_check() + check_plugins() + core = find_core() + if core is None: + print(" [note] no LEDMatrix core checkout: the names are not compared with core") + else: + failures += check_core(core) + for f in failures: + print(f" [FAIL] {f}") + if failures: + return 1 + print(f" [pass] no {METHOD} copy is back in {len(FINAL_PERIOD)} scoreboards; " + f"all inherit core's {MIXIN}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 41bed9e3c84fdf443e7a2f50f32a9ae7bab1951e Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:13:02 -0400 Subject: [PATCH 4/4] scripts: date the modules core v3.8.1 ships Core v3.8.1 (a669d781) is tagged. Every None entry in the min-core table is in it (checked with git show v3.8.1:): fetch_service, sports_game_over, display_arbiter, screen_runner and plugin_local_files are now "3.8.1", and the test's tag list gains 3.8.1, so its tag section checks them. v3.8.1 reports __version__ "3.8.1"; REPORTED_AS needs no entry. This clears check_min_core_version for the scoreboards' plain import of src.common.sports_game_over. Co-Authored-By: Claude Opus 5.5 --- scripts/check_min_core_version.py | 11 ++++++----- scripts/test_check_min_core_version.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/check_min_core_version.py b/scripts/check_min_core_version.py index ebdbf3c1..96383a98 100644 --- a/scripts/check_min_core_version.py +++ b/scripts/check_min_core_version.py @@ -164,14 +164,15 @@ "src.ipc.client": "3.8.0", # core #706 "src.ipc.contract": "3.8.0", # core #706 "src.ipc.server": "3.8.0", # core #706 + # 3.8.1 (core #702-#770) + "src.common.fetch_service": "3.8.1", # core #702 (shared fetch service) + "src.common.sports_game_over": "3.8.1", # core #770 (sports family 5, game-over check) + "src.display_arbiter": "3.8.1", # core #733 (run() stage 2 arbiter) + "src.screen_runner": "3.8.1", # core #762 (run() stage 3 screen runner) + "src.plugin_system.plugin_local_files": "3.8.1", # core #755 (tokens and local files kept across store updates) # on core main, in no tagged release yet: add as None. When the next # release is tagged, set each None entry here to that tag -- the test's # tag-verification section insists on it once the tag is in its list. - "src.common.fetch_service": None, # core #702 (shared fetch service) - "src.common.sports_game_over": None, # core #770 (sports family 5, game-over check) - "src.display_arbiter": None, # core #733 (run() stage 2 arbiter) - "src.screen_runner": None, # core #762 (run() stage 3 screen runner) - "src.plugin_system.plugin_local_files": None, # core #755 (tokens and local files kept across store updates) } #: Releases whose ``src/__init__.py`` ``__version__`` lags their tag. The diff --git a/scripts/test_check_min_core_version.py b/scripts/test_check_min_core_version.py index 71bc4aeb..0b525c8e 100644 --- a/scripts/test_check_min_core_version.py +++ b/scripts/test_check_min_core_version.py @@ -217,7 +217,7 @@ def tree(ref): return set(out.stdout.split()) if out.returncode == 0 else None -tags = ["3.0.0", "3.1.0", "3.2.0", "3.3.0", "3.3.1", "3.4.0", "3.5.0", "3.6.0", "3.6.1", "3.6.2", "3.7.0", "3.8.0"] +tags = ["3.0.0", "3.1.0", "3.2.0", "3.3.0", "3.3.1", "3.4.0", "3.5.0", "3.6.0", "3.6.1", "3.6.2", "3.7.0", "3.8.0", "3.8.1"] trees = {t: tree(f"v{t}") for t in tags} if core and os.path.isdir(core) else {} if not trees or any(v is None for v in trees.values()): print(" SKIP LEDMATRIX_CORE is not a core git checkout with release tags")