Twenty methods were byte-identical in all eight scoreboards' game_renderer.py:
the colour pickers, the scroll_card settings lookup, the date and time
formatting, the favourite-team rules and the font-size grid snapping. Every fix
to any of it had to be made eight times, and a new scoreboard began by copying
them a ninth.
src/common/sports_card.py holds them once. 245 lines leave each plugin.
**Free functions, not a base class.** Every helper takes config/logger/fonts as
arguments rather than reading them off an instance, so a plugin keeps its
method and delegates the body -- call sites, signatures and override points are
all untouched. Adoption is therefore per-function and reversible, which is what
let all eight move with byte-identical renders.
The bodies are the plugins' code moved, not rewritten. Two deliberate
differences, both verified:
- crisp_size takes the seven-plugin guard (`not desired`) rather than
football's. They agree on every real input; the extra guard only stops a
None size raising TypeError, so adopting it is a no-op for seven plugins and
removes a crash path for the eighth.
- schema_font_size caches per schema PATH. The plugins cached on their own
class, which is the same distinction expressed without a class to hang it
on; two plugins never share an entry. The path has to be passed in because
the plugins derived it from __file__, and __file__ here is the core's.
Verified before any plugin was touched: 534 differential comparisons of the
helpers against afl's originals and 704 more of the font-sizing chain against
all eight plugins' originals -- 1,238 comparisons, zero differences. Writing
the constant tables by hand introduced two errors that check caught: the tie
colour was (255,255,0) instead of the plugins' (255,200,0), and a
"five_by_seven" alias that does not exist. Both are now taken from the plugins
verbatim.
43 tests pin the contract, including the cases the plugins' own comments record
as having bitten: a three-character string must not iterate into a colour, a
shared font face must give up rather than guess an element, a font_size equal
to the schema default carries no intent, and a bad timezone falls back to UTC
rather than blanking the card.
Full suite 3768 passed, 6 skipped.
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>