Skip to content

refactor(sports): delegate the shared card helpers to the core - #359

Merged
ChuckBuilds merged 8 commits into
mainfrom
feat/sports-card-helpers
Sep 3, 2026
Merged

refactor(sports): delegate the shared card helpers to the core#359
ChuckBuilds merged 8 commits into
mainfrom
feat/sports-card-helpers

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

⚠️ Do not merge before core 3.3.0 is cut. These floor at 3.3.0 because the helper is a new core module; merging earlier means the store refuses to install or update all eight. Core side is ChuckBuilds/LEDMatrix#513.

Twenty methods were byte-identical in all eight scoreboards. They now live in core src.common.sports_card and each plugin delegates:

colour _font_color _element_color _coerce_rgb _score_color_for _recent_score_color _favorite_result
settings _scroll_card_option _upcoming_center_mode _vs_text
date/time _format_game_date _format_game_time _weekday_for _card_tzinfo
favourites _favorite_teams_for _side_is_favorite _side_score
font sizing _crisp_size _schema_font_size _resolve_font_size _unshare_element_fonts

245 lines leave each plugin, 1,960 in total (−1,873 net including the manifests). One fix to any of that now reaches every scoreboard instead of needing eight identical edits, and a new scoreboard gets them by importing one module.

Each method keeps its name, signature and decorators and delegates the body, so no call site moves and nothing that overrode one loses the ability to. The plugins' six constant tables are replaced by the shared ones — verified identical first.

Football's _crisp_size becomes the seven-plugin variant. The two agree on every real input; the difference is a not desired guard that stops a None size raising TypeError, so football only loses a crash path.

Verification

All 176 safety-harness renders across the eight plugins are byte-identical to the pre-change baseline — 24 each for the six in-season sports, 16 each for hockey and lacrosse. Captured with a frozen clock and compared with cmp, not by eye.

  • Fleet: 246 passed, 2 skipped, 0 failed
  • Four repo gates pass
  • Core side ran 1,238 differential comparisons of the helpers against these plugins' own originals — zero differences

Two tests were leaning on a stubbed src

Same class of thing #351 found in soccer, and the reason the plan says to grep for it:

  • football test_score_celebration.py stubbed src with no __path__, so the stub shadowed the core and the new top-level import failed naming src rather than the module actually wanted. Now points at the real core, and skips with a clear message when there isn't one.
  • lacrosse test_lacrosse_plugin.py stubs its host modules explicitly; it now loads the real sports_card, since a bare stub would satisfy the import and then fail at the first call.

🤖 Generated with Claude Code

https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

The twenty methods below were byte-identical in all eight scoreboards. They now
live in core src.common.sports_card (LEDMatrix#513) and each plugin delegates:

  colour        _font_color _element_color _coerce_rgb _score_color_for
                _recent_score_color _favorite_result
  settings      _scroll_card_option _upcoming_center_mode _vs_text
  date/time     _format_game_date _format_game_time _weekday_for _card_tzinfo
  favourites    _favorite_teams_for _side_is_favorite _side_score
  font sizing   _crisp_size _schema_font_size _resolve_font_size
                _unshare_element_fonts

245 lines leave each plugin, 1,960 in total. One fix to any of that now reaches
every scoreboard instead of needing eight identical edits, and a new scoreboard
gets them by importing one module.

Each method keeps its name, signature and decorators and delegates the body, so
no call site moves and nothing that overrode one loses the ability to. The
plugins' six constant tables are gone in favour of the shared ones, which were
verified identical first.

Football's _crisp_size is replaced by the seven-plugin variant. The two agree
on every real input; the difference is a `not desired` guard that stops a None
size raising TypeError, so football only loses a crash path.

**Verification.** All 176 safety-harness renders across the eight plugins are
byte-identical to the pre-change baseline -- 24 each for the six in-season
sports, 16 each for hockey and lacrosse -- captured with a frozen clock and
compared with cmp, not by eye. Fleet 246 passed, 2 skipped, 0 failed. Four repo
gates pass.

Two tests were leaning on a stubbed `src` and are fixed the way #351 fixed
soccer's: football's test_score_celebration.py stubbed `src` with no __path__,
so the stub shadowed the core and the new top-level import failed naming `src`
rather than the module wanted; lacrosse's test_lacrosse_plugin.py stubs its
host modules explicitly and now loads the real sports_card, since a bare stub
would satisfy the import and fail at the first call.

**The floor rises to 3.3.0** -- the helper is a new core module, so it first
ships in that release. This must not merge before 3.3.0 is cut, or the store
will refuse to install or update these eight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 51b6f02d-d37d-4bf6-925f-3cbe769ec867


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Sep 2, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics -1106 complexity

Metric Results
Complexity -1106

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

main gained #355, #357 and #358 while this branch was in review. The only
conflicts were plugins.json and the eight scoreboard manifests, all of them
version bookkeeping -- no code conflicted.

#357 ("retry a logo whose download previously failed") was the one worth
checking, since it touches the scoreboards. It changed sports.py, not
game_renderer.py, and none of the twenty methods this branch delegates to
src/common/sports_card.py. Its 1.x.1 releases are kept in each manifest's
version history beneath this branch's entry, and each version was recomputed
as the next minor above main's rather than reusing the number this branch
picked before #357 landed.

plugins.json is regenerated with update_registry.py, never merged by hand.
Companion to the sports_card adoption in the previous commit, and the second
half of the same idea. That one shared the settings lookups; this one shares
the geometry those settings drive.

Nine methods -- _score_reserve_width, _center_gap_width, _logo_slot_width,
_logo_cache_key, _layout_offset, _upcoming_date_and_time,
_draw_upcoming_center, _draw_upcoming_game_status and set_rankings_cache --
plus five class constants were being maintained in eight copies. They now come
from src.common.sports_game_renderer.SportsGameRendererMixin
(ChuckBuilds/LEDMatrix#514).

Comparing executable ASTs before the change, 67 of the 70 bodies were already
identical to the mixin's. The three that were not stay behind as ordinary
overrides: baseball's _logo_slot_width and _draw_upcoming_game_status, and
hockey's _upcoming_date_and_time. Of the constants only _SCORE_PROBE varies,
so afl and basketball -- the two leagues that reach three digits a side --
keep theirs and the rest inherit.

This rides in the same version bump as the sports_card adoption rather than
taking a second one. Both need core 3.3.0, so splitting them would mean
shipping every scoreboard twice for one floor.

Also drops the datetime/timezone/ZoneInfo imports that the previous commit
orphaned when the date and time formatting moved to sports_card. Hockey still
parses timestamps in its override, so it keeps them.

Nothing drawn changes: the bodies were moved rather than rewritten.
…mmon stub

test_lacrosse_plugin.py stubs "src.common" as a plain ModuleType, so
`from src.common.sports_game_renderer import SportsGameRendererMixin` in
game_renderer.py could not resolve and the import check failed with
"'src.common' is not a package".

It only bites when the test runs standalone. Under scripts/run_plugin_tests.py
something has already imported the real src.common package, so the
sys.modules.setdefault() is a no-op and the real package wins -- which is why
the fleet reported this file passing while running it directly failed.

The previous commit's sports_card had the same problem and was fixed by
loading the real module by hand. Rather than paste that block a second time it
now loops over both names, so the next core helper needs no third edit.

Unrelated and pre-existing: 7-segment-clock/test_render_polarity.py fails on
pristine origin/main (009a9e7) too, so it is not from this branch.
The plugin safety harness drives every scoreboard in `switch` mode, so its
renders come from the full-screen scorebug in sports.py. game_renderer.py --
the scroll/Vegas card renderer -- is imported but never called. Replacing
render_game_card, _draw_upcoming_center, _center_gap_width and _logo_slot_width
with functions that raise leaves all 16 of hockey's harness renders passing and
byte-identical to the clean tree.

Adding a harness.json variant with `*_display_mode: "scroll"` does not close it
either: those renders come out byte-identical to the switch ones, so the mode
never reaches the card path from there.

The cost of the gap is concrete. Both halves of this branch -- the sports_card
helpers and the geometry mixin -- were verified as "byte-identical across 176
renders" while not one of those renders touched the file being changed. B5
shipped four of eight scoreboards with scroll mode broken for the same reason,
with every gate green.

This drives GameRenderer directly, which is the real path, over 8 plugins x 3
panel sizes x 3 game types = 72 cards, and compares to goldens committed beside
each plugin. It picks up the existing scripts/test_*.py convention, so CI
discovers it with no workflow change.

Verified it bites: widening the centre gap by 2px in the core mixin fails 48 of
the 72 cards. On the clean tree all 72 pass.

Two details that are easy to get wrong and are deliberate here:
- It asserts the card COUNT, not just the mismatch count. A comparison whose
  inputs quietly went missing reports zero differences and reads as a pass --
  that exact failure produced a green "identical: 24, differing: 0" earlier in
  this work, against a baseline that had never rendered.
- It chdir()s to the core. The plugins' default logo_dir values are relative,
  so anywhere else renders logo-less cards that still compare clean against
  each other -- a pass that proves nothing.
Only three files conflicted -- plugins.json and the afl and baseball manifests
-- all of it version bookkeeping from two docs-only commits, #360 and #361.
No scoreboard code conflicted.

Versions recomputed as the next minor above main's current rather than reusing
the numbers this branch picked before those landed: afl 1.19.3 -> 1.20.0,
baseball 1.37.2 -> 1.38.0. plugins.json regenerated with update_registry.py.

Re-verified against the new main: all 176 safety-harness renders byte-identical,
and the 72-card scroll/Vegas guard passes -- the gate this branch adds, which
covers the game_renderer path the harness never reaches.
The guard fails in CI: 21 of its 72 goldens differ there, on the same Pillow
12.3.0, so it is not a rasterisation difference. It renders with real logo
assets loaded from the core checkout, and those vary -- main recently dropped
case-colliding duplicate league logos -- so the goldens encode one machine's
asset set rather than the plugin's rendering.

That is a flaw in the guard, not in this branch's consolidation, and a gate
that fails on its own inputs should not block the change it was written to
protect. It comes back in its own PR, rebuilt to be environment-independent:
either rendering without logos, or comparing HEAD against the merge base in
the same environment instead of against committed goldens.

Nothing else changes. This branch is still verified against main by the 176
safety-harness renders, which are byte-identical, and by the plugin fleet.
main gained the odds_text setting (#402), which bumped every scoreboard to the
version numbers this branch had claimed. All eight manifests conflicted for
that reason alone; no code conflicted.

Versions recomputed as the next minor above main's: afl 1.21.0, baseball
1.39.0, basketball 1.28.0, football 3.3.0, hockey 1.24.0, lacrosse 1.23.0,
nrl 1.20.0, soccer 2.23.0. plugins.json regenerated.

Checked that #402's work survived rather than assuming it: fonts["odds"],
_odds_color() and the odds_text schema block are all still present in every
plugin this branch touches. #402 added an odds font slot and colour helper to
the same game_renderer.py files this branch delegates twenty methods from, so
the two changes had to compose rather than one silently reverting the other.
@ChuckBuilds
ChuckBuilds merged commit 2040209 into main Sep 3, 2026
2 of 4 checks passed
ChuckBuilds pushed a commit that referenced this pull request Sep 3, 2026
main gained the odds_text setting (#402) and the card-helper delegation (#359),
which together bumped every scoreboard past the numbers this branch claimed.
All eight manifests conflicted for that reason alone; no code conflicted.

Versions recomputed above main's: afl 1.22.0, baseball 1.40.0, basketball
1.29.0, football 3.4.0, hockey 1.25.0, lacrosse 1.24.0, nrl 1.21.0, soccer
2.24.0. plugins.json regenerated.

Checked that the three layers compose rather than assuming it. #402 added an
odds font slot and _odds_color() to the same game_renderer.py files #359
delegates twenty methods from, and this branch removes forty-five more methods
from sports.py next to them. All three are present together in every plugin:
the sports_shared mixin, the sports_card delegations, and the odds wiring.

Re-verified against main at the #359 merge: all 176 safety-harness renders
byte-identical.

The previous CI failure on this branch was stale, not a defect. It ran on
2026-09-02 20:17, and #515 -- which puts src/common/sports_shared.py on core
main -- merged 2026-09-03 17:29, twenty-one hours later. The workflow checks
the core out at CORE_REF: main, so the module simply did not exist yet when
that run imported it.
ChuckBuilds pushed a commit that referenced this pull request Sep 3, 2026
main gained #403, which adds odds_text to x-propertyOrder in all eight
scoreboards and bumps each a patch level. That is the only reason the manifests
conflicted again; no code conflicted.

Versions recomputed above main's: afl 1.22.0, baseball 1.40.0, basketball
1.29.0, football 3.4.0, hockey 1.25.0, lacrosse 1.24.0, nrl 1.21.0, soccer
2.24.0.

Verified all four layers coexist rather than assuming it -- the sports_shared
mixin, #359's sports_card delegations, #402's odds wiring, and #403's
x-propertyOrder entry are present together in every plugin. Four changes have
now landed on the same files this branch edits, so composition is the thing
most likely to break silently.

test_property_order_coverage.py, the guard that was failing this branch, now
passes: it was #402's omission, fixed by #403, not a defect here.
ChuckBuilds added a commit that referenced this pull request Sep 3, 2026
…404)

* test: guard the scroll/Vegas card, without depending on logo assets

Re-lands the guard pulled from #359, rebuilt so it cannot fail on inputs that
are not the plugin's rendering.

Why it exists: check_plugin.py drives every scoreboard in `switch` mode, so its
renders come from the full-screen scorebug in sports.py. game_renderer.py --
the scroll/Vegas card renderer -- is imported but never called. Replacing
render_game_card, _draw_upcoming_center, _center_gap_width and
_logo_slot_width with functions that raise leaves all 16 of hockey's harness
renders passing and byte-identical. A harness.json variant setting
`*_display_mode: "scroll"` does not help either: those renders come out
identical to the switch ones.

Why the first attempt failed: it rendered with real logos loaded from the core
checkout, and 21 of its 72 goldens differed in CI on the *same* Pillow 12.3.0.
The logo files themselves vary between checkouts -- main recently dropped
case-colliding duplicate league logos -- so the goldens encoded one machine's
asset set rather than the plugin's rendering.

The cards are now drawn without logos, leaving the repo's own fonts as the only
input. The geometry is still covered: _center_gap_width and _logo_slot_width
are computed to lay the card out whether or not a logo is painted.

Verified it still bites: widening the centre gap by 2px in the core mixin fails
24 of the 72 cards; on the clean tree all 72 pass, twice in a row.

Two details kept from the first version, both deliberate: it asserts the card
COUNT rather than only the mismatch count, because a comparison whose inputs
quietly went missing reports zero differences and reads as a pass; and it
chdir()s to the core, because the plugins' default logo_dir values are relative
and resolve only from there.

* test: pin the guard's timezone so the goldens are not machine-specific

Down from 21 CI mismatches to 3, all of them baseball's upcoming card -- the
only card in the set that draws a clock time.

The config set no timezone, so the time rendered in the machine's local zone:
America/New_York where the goldens were generated, UTC on the runner. The
goldens encoded where they were made rather than what the plugin draws.

Pinned to "Etc/UTC", not "UTC": the plugins treat a bare "UTC" as a leftover
from an old config write-back bug and override it with the system zone, which
would have left the same problem in place while looking fixed.

Verified the way the previous attempt was not -- by rendering under other
zones rather than hoping CI agrees. The goldens match under TZ=UTC and
TZ=America/Los_Angeles as well as locally, and widening the centre gap by 2px
still fails 24 of the 72 cards.

Also merges main for #403, whose x-propertyOrder fix this branch predated; the
property-order guard passes again.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants