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
+48
View File
@@ -56,6 +56,24 @@ def main() -> int:
help='Display mode to render, for plugins that declare '
'more than one in their manifest (e.g. nrl_live). '
'Omitted, the plugin picks its own default.')
parser.add_argument('--vegas', action='store_true',
help="Render the plugin's block of the Vegas ticker strip "
"instead of display(): its live elements if it has "
"them, else its Vegas content, laid out as the "
"ticker lays them out. Also writes the live "
"elements' keys and columns to <output>.json")
parser.add_argument('--no-live', action='store_true',
help="With --vegas: ignore live elements and render the "
"plugin's ordinary Vegas content (for before/after)")
parser.add_argument('--timeline', type=int, default=0, metavar='ROWS',
help="With --vegas: render ROWS rows, each the block a "
"--timeline-step later as the ticker would update it "
"in place (animated elements redrawn for that moment)")
parser.add_argument('--timeline-step', type=float, default=0.25, metavar='SECONDS',
help="Seconds between --timeline rows (default 0.25)")
parser.add_argument('--timeline-update', action='store_true',
help="With --timeline: run update() before each row and "
"redraw every live element from the new data")
args = parser.parse_args()
@@ -145,6 +163,36 @@ def main() -> int:
except Exception as e:
logger.warning("update() raised: %s — continuing to display()", e)
if args.vegas and args.timeline > 1:
from src.plugin_system.testing.vegas import render_vegas_timeline
image, rows = render_vegas_timeline(
plugin_instance, args.plugin, display_manager, steps=args.timeline,
step_seconds=args.timeline_step, run_update=args.timeline_update)
if image is None:
logger.error("Plugin '%s' has no Vegas content", args.plugin)
return 1
image.save(args.output)
logger.info("Saved a %d-row Vegas timeline (%dx%d) to %s",
rows, image.width, image.height, args.output)
return 0
if args.vegas:
from src.plugin_system.testing.vegas import render_vegas_strip
block, layout = render_vegas_strip(
plugin_instance, args.plugin, display_manager, live=not args.no_live)
if block is None:
logger.error("Plugin '%s' has no Vegas content", args.plugin)
return 1
block.save(args.output)
sidecar = Path(args.output).with_suffix('.json')
sidecar.write_text(json.dumps(
{"width": block.width, "height": block.height,
"live_elements": [{"key": k, "x": x, "width": w} for x, k, w in layout]},
indent=2) + "\n", encoding="utf-8")
logger.info("Saved Vegas strip %dx%d (%d live element(s)) to %s and %s",
block.width, block.height, len(layout), args.output, sidecar)
return 0
# A plugin that declares several display modes usually renders nothing
# useful without being told which one to draw: the scoreboards keep their
# state on per-mode sub-managers and their no-argument path returns False.