feat(sports): live Vegas cards for the scoreboards (shared layer)

One live element per game, drawn only when what the card shows changes, so
a score changes on a card already crossing the panel. The shared part, so
each scoreboard adopts it in a few lines:

- src/common/sports_vegas.py: game_key, game_fingerprint (the whole game
  dict, frozen: no drawn field can be missed), dedupe_games, VegasCardCache,
  StickyOdds (odds a live poll left out stay drawn), finished_games /
  with_finished_games (a game that just went final keeps its card, after its
  league's live games; one a heuristic only judged over keeps its live
  state, so a tied end of regulation never shows FINAL early).
- SportsScrollDisplay.make_vegas_renderer() is the override point;
  build_vegas_elements() and SportsScrollDisplayManager
  .get_vegas_elements_for() do the rest. A card's version includes its
  teams' ranks, which the renderer draws from the rankings cache.
- SportsLiveSharedMixin._record_finished_game() / finished_games_snapshot():
  held for FINISHED_GAME_TTL after it leaves the live list.

A sport that does not implement make_vegas_renderer keeps its ordinary Vegas
content, so no scoreboard changes until it opts in.

scripts/render_plugin.py --vegas renders a plugin's Vegas block as the
ticker lays it out, and --timeline stacks it at successive moments as
the ticker would update it in place; the join is now
render_pipeline.join_plugin_rows().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-30 18:36:39 -04:00
co-authored by Claude Opus 5.5
parent 876130e93f
commit 8cce532ea9
11 changed files with 1126 additions and 37 deletions
+28
View File
@@ -15,6 +15,7 @@ from src.plugin_system.testing.harness import _instantiate # noqa: E402
from src.plugin_system.testing.loading import build_full_config, load_harness_spec, load_manifest # noqa: E402
from src.plugin_system.testing.vegas import ( # noqa: E402
check_vegas_elements, implements_vegas_elements, render_vegas_elements,
render_vegas_strip, render_vegas_timeline,
)
from src.plugin_system.testing.visual_display_manager import VisualTestDisplayManager # noqa: E402
from src.plugin_system.vegas_elements import VegasElement # noqa: E402
@@ -178,3 +179,30 @@ def test_check_plugin_reports_a_failing_element_check_and_carries_on(monkeypatch
False, None, None)
vegas = [r for r in results if r.mode == "vegas elements"]
assert len(vegas) == 1 and "boom" in vegas[0].error
def test_a_timeline_moves_what_animates_and_nothing_else():
import numpy as np
plugin, dm = _stub(dot_speed=200)
_block, layout = render_vegas_strip(plugin, "vegas-live-stub", dm)
image, rows = render_vegas_timeline(plugin, "vegas-live-stub", dm, steps=3,
step_seconds=0.5)
assert rows == 3 and image.height == 3 * H + 2
pixels = np.asarray(image)
first, last = pixels[:H], pixels[2 * (H + 1):]
columns = {key: (x, width) for x, key, width in layout}
x, width = columns["map"]
assert (first[:, x:x + width] != last[:, x:x + width]).any()
x, width = columns["card:0"]
assert (first[:, x:x + width] == last[:, x:x + width]).all()
def test_a_timeline_with_updates_redraws_the_cards_in_place():
import numpy as np
plugin, dm = _stub(map_hz=0)
_block, layout = render_vegas_strip(plugin, "vegas-live-stub", dm)
image, rows = render_vegas_timeline(plugin, "vegas-live-stub", dm, steps=2,
run_update=True)
pixels = np.asarray(image)
x, width = {key: (x, width) for x, key, width in layout}["card:0"]
assert (pixels[:H, x:x + width] != pixels[H + 1:, x:x + width]).any()