Phase B2 of the sports unification. Both features exist in only some of
the nine scoreboards, so they ship as capabilities the plugin composes,
never as `if self.<feature>_enabled` branches inside the base classes: a
sport that does not opt in has none of this code in its MRO.
CelebrationMixin (afl, nrl, soccer, football)
The two lineages spelled this differently -- _check_for_goal /
celebrate_opponent_goals vs _check_for_score / celebrate_opponent_scores
-- but the bodies were identical apart from three things, each now a
seam rather than a branch:
- wording -> score_phrase() / win_phrase() hooks
- follow-up suppression -> COALESCE_SCORING_SEQUENCE, on for football
where a touchdown lands as +6 then +1, off where two increments are
two real goals
- team identity -> _favorite_key, so nrl matches on team id without
core learning why its abbreviations are ambiguous
Both config spellings are read, so a plugin adopting the mixin keeps
working with the keys already in its published schema.
Rotation strategies
The three "dialects" turned out to be one algorithm (SWRR) in two
shapes: an incremental picker holding state across calls, and a
precomputed per-cycle list. They agree within a cycle and differ only at
the boundary, so core ships both behind a name registry rather than
declaring a winner. weight_for is supplied by the host, so rotation.py
never learns what a favorite is; an unknown name degrades to "simple"
because it arrives from user config.
Each strategy is checked against a verbatim transcription of the plugin
code it replaces, over every live-game shape up to four games -- the
differential B5 will delete the bundled copies on the strength of.
185 new tests. Gates: 648 core unit, 60 plugin safety.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
5.9 KiB
Changelog
Notable changes to the LEDMatrix core. The version below is the value of
src.__version__, which the plugin loader reports to compatibility checks and
which plugin manifests reference via ledmatrix_min_version.
Why this file exists: the plugin monorepo bundles fallback copies of several
core modules (see docs/plugin-development/08-shared-sports-code.md in
ledmatrix-plugins). A plugin
may delete its bundled copy only when its manifest floors on the first core
release that ships the module — which requires module additions to be recorded
here, against a version number. When you add a module plugins will import via
src.*, note it in the Unreleased section and bump src/__init__.py in the
release that ships it.
Unreleased
Added
-
src/element_style.py— per-element style resolver backing thex-style-elementsconfig-schema extension. Already consumed (behind guarded imports with classic fallbacks) by theof-the-day,ledmatrix-music, andfootball-scoreboardplugins. -
Core unit-test CI job enrolling the previously unenrolled suites (skin system, data sources, API extractors, scroll helper, adaptive layout, loader compatibility warning) plus new characterization tests for
src/base_classes/sports.pyahead of the shared sports-code unification. -
src/base_classes/sports/—sports.pyis now a package (core.py+modes.py). The import path is unchanged:from src.base_classes.sports import SportsCorestill works. -
Nine methods promoted onto the sports base classes from the plugins' bundled copies, plus the override points
_favorite_key,_config_schema_pathand_font_rootand the class attributesFINAL_PERIOD/CLOCK_COUNTS_DOWN. Seedocs/SPORTS_UNIFICATION.md. A plugin may start calling these once its manifest floorsledmatrix_min_versionat the release that ships them. -
src/base_classes/sports/capabilities/— opt-in capabilities for the sports scoreboards, composed by inheritance rather than gated by config branches inside the base classes:CelebrationMixin— the score/win takeover, merging the goal and score dialects behind thescore_phrase()/win_phrase()hooks, theCOALESCE_SCORING_SEQUENCEclass attribute and the_favorite_keyseam. Reads both thecelebrate_opponent_goalsandcelebrate_opponent_scoresconfig spellings. Sports that do not mix it in have none of this code in their MRO.RotationStrategy+ a name registry (swrr,weighted,simple, plusregister_rotation_strategyfor plugin-supplied orderings). Each built-in is verified against a verbatim transcription of the plugin implementation it replaces. An unknown name degrades tosimple.
Changed
- Live games are no longer dropped when the feed omits a game clock.
SportsLive._is_game_really_overpreviously (in the baseball and UFC plugin lineages) coerced a missing or non-string clock to the literal"0:00"and then treated the game as finished onceperiod >= 4. Baseball has no game clock andperiodis the inning, so live MLB games disappeared from the scoreboard from the 5th inning onward; UFC was affected the same way. The clock check is now skipped when the clock is unusable, and the period threshold is the per-sportFINAL_PERIOD(hockey ends in P3). Sports whose clocks count up — soccer, AFL, NRL — setCLOCK_COUNTS_DOWN = Falseand never run the check at all, since0:00there means kickoff rather than expiry.
Fixed
FontManagerresolvesassets/fontsagainst the core install root instead of the process working directory, so font loading works when the process starts elsewhere (e.g. the plugin safety harness on CI).- Hockey events whose competitors carry no
statisticsarray are no longer discarded. The extractor readcompetitor["statistics"]unguarded, so aKeyErrorinside the generator dropped the entire event despite valid scores and status; shot counts now fall back to0. - Live baseball events that populate status only at the competition level are
no longer discarded. The extractor read the event top-level
game_event["status"]for the inning; real ESPN events duplicate it, but MiLB events synthesized from the MLB Stats API do not, so the lookup raised a bareKeyError. It now reads the already-validated competition-level status. SportsLive._is_game_really_overno longer crashes the live-update pass when a feed sends an explicit nullperiod.None >= FINAL_PERIODraisedTypeError, and the only caller (_detect_stale_games) has notry/except— the same failure shape as the already-fixed nullperiod_text.- An expired clock spelled
"00:00"now ends the game. The check compared the colon-stripped clock against a hand-listed set of literals, which"0000"is not a member of, so a finished game with a two-digit-minute clock stayed on the scoreboard indefinitely. The comparison is now numeric. SportsCore._load_fontsresolvesassets/fontsthrough the_font_root()seam instead of the process working directory. Started outside the install root, every scoreboard font silently degraded to PIL's default bitmap face.SportsCore._should_logno longer raisesAttributeErroron the first warning of a run;_last_warning_timeis initialized in__init__rather than lazily by an unrelated method.SportsCore._resolve_project_pathresolved relative logo directories against<root>/srcinstead of the repo root aftersports.pybecame a package — the class bodies moved byte-identically but__file__gained a directory. Both it and_font_rootnow derive from one_INSTALL_ROOTconstant.
3.1.0
Baseline for this changelog. Highlights already shipped at this version: skin system for sports scoreboards (#419), Vegas continuous-scroll overhaul (#423), plugin update surfacing (#421).