Commit Graph
2 Commits
Author SHA1 Message Date
ChuckandClaude Opus 5 0d7e409d77 fix(sports): rebuild un-shared faces through the pinned layout engine
unshare_element_fonts re-instantiates a duplicate font face so two
elements can be told apart by id(). It did so through bare
ImageFont.truetype, which takes PIL's default layout engine rather than
the one src/common/font_layout.py pins. Raqm and Basic disagree on
fractional advances -- that disagreement is the reason the pin exists,
having broken golden images across machines -- so a rebuilt face could
measure differently from the shared face it replaced, on any host where
Raqm is installed.

These were the only two call sites in src/ bypassing the pin.

The guard asserts that the rebuild goes through the pinned loader rather
than comparing engine values: where Raqm is absent, bare truetype returns
BASIC anyway, so an engine comparison passes whether or not the pin is
honoured. The first draft of this test did exactly that and passed with
the bug reintroduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 09:59:53 -04:00
ChuckandClaude Opus 5 10da2f97fd feat(sports): share the card helpers the eight scoreboards each carried (#513)
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>
2026-09-02 16:22:09 -04:00