diff --git a/README.md b/README.md index 812a069d..7677c729 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \ | [F1 Scoreboard](./plugins/f1-scoreboard/) | Formula 1 race results, schedules, and standings | | | [UFC Scoreboard](./plugins/ufc-scoreboard/) | UFC/MMA live fights, fighter headshots, records, odds & results — *by [LegoGuy1000](https://github.com/legoguy1000)* | | | [Masters Tournament](./plugins/masters-tournament/) | Live Masters golf leaderboard, hole tracking, player cards | | -| [March Madness](./plugins/march-madness/) | NCAA tournament bracket tracker with round branding and live scores | | +| [March Madness](./plugins/march-madness/) | NCAA tournament bracket tracker with round branding and live scores | march-madness on an LED panel | | [NFL Draft](./plugins/nfl-draft/) | Projected & live NFL draft picks from ESPN | | | [Odds Ticker](./plugins/odds-ticker/) | Betting odds & lines across NFL, NBA, MLB, NHL, NCAA | | | [Sports Leaderboard](./plugins/ledmatrix-leaderboard/) | League standings, rankings, conference records | | diff --git a/docs/assets/march-madness/game-states.png b/docs/assets/march-madness/game-states.png new file mode 100644 index 00000000..96275bee Binary files /dev/null and b/docs/assets/march-madness/game-states.png differ diff --git a/docs/assets/march-madness/hero.png b/docs/assets/march-madness/hero.png new file mode 100644 index 00000000..1b02e94a Binary files /dev/null and b/docs/assets/march-madness/hero.png differ diff --git a/docs/assets/march-madness/highlight-upsets.png b/docs/assets/march-madness/highlight-upsets.png new file mode 100644 index 00000000..ce82029f Binary files /dev/null and b/docs/assets/march-madness/highlight-upsets.png differ diff --git a/docs/assets/march-madness/panel-sizes.png b/docs/assets/march-madness/panel-sizes.png new file mode 100644 index 00000000..ae91338f Binary files /dev/null and b/docs/assets/march-madness/panel-sizes.png differ diff --git a/docs/assets/march-madness/shots.json b/docs/assets/march-madness/shots.json new file mode 100644 index 00000000..dcfba5cc --- /dev/null +++ b/docs/assets/march-madness/shots.json @@ -0,0 +1,301 @@ +{ + "plugin": "march-madness", + "_comment": "Games are seeded onto the plugin instance rather than fetched from ESPN. Scrolling is frame-based here, so shots step frames; teams, seeds and scores are invented.", + "defaults": { + "width": 128, + "height": 32, + "scale": 6, + "freeze_time": "2026-03-21T19:10:00+00:00", + "skip_update": true, + "frames": 20, + "config": { + "enabled": true + }, + "attrs": { + "games_data": [ + { + "tournament_round": "Sweet 16", + "tournament_region": "East", + "away_abbr": "DUKE", + "away_seed": 2, + "home_abbr": "UCONN", + "home_seed": 1, + "away_score": 58, + "home_score": 61, + "is_live": true, + "is_final": false, + "is_halftime": false, + "is_upset": false, + "winner_side": "", + "period_text": "2nd", + "clock": "4:12", + "game_date": "3/21", + "game_time": "7:10 PM" + }, + { + "tournament_round": "Sweet 16", + "tournament_region": "West", + "away_abbr": "FLA", + "away_seed": 11, + "home_abbr": "HOU", + "home_seed": 1, + "away_score": 74, + "home_score": 71, + "is_live": false, + "is_final": true, + "is_halftime": false, + "is_upset": true, + "winner_side": "away", + "period_text": "Final", + "clock": "", + "game_date": "3/21", + "game_time": "7:10 PM" + }, + { + "tournament_round": "Sweet 16", + "tournament_region": "South", + "away_abbr": "PUR", + "away_seed": 3, + "home_abbr": "TENN", + "home_seed": 2, + "away_score": 0, + "home_score": 0, + "is_live": false, + "is_final": false, + "is_halftime": false, + "is_upset": false, + "winner_side": "", + "period_text": "", + "clock": "", + "game_date": "3/21", + "game_time": "7:10 PM" + }, + { + "tournament_round": "Elite 8", + "tournament_region": "Midwest", + "away_abbr": "ILL", + "away_seed": 4, + "home_abbr": "IOWA", + "home_seed": 8, + "away_score": 68, + "home_score": 80, + "is_live": false, + "is_final": true, + "is_halftime": false, + "is_upset": false, + "winner_side": "home", + "period_text": "Final", + "clock": "", + "game_date": "3/21", + "game_time": "7:10 PM" + } + ] + } + }, + "shots": [ + { + "name": "hero", + "frames": 190, + "width": 256 + }, + { + "name": "state-live", + "frames": 190, + "width": 256, + "standalone": false + }, + { + "name": "state-upset", + "frames": 310, + "width": 256, + "standalone": false + }, + { + "name": "state-upcoming", + "frames": 395, + "width": 256, + "standalone": false + }, + { + "name": "seeds-on", + "frames": 190, + "width": 256, + "standalone": false, + "config": { + "display_options": { + "show_seeds": true + } + } + }, + { + "name": "seeds-off", + "frames": 190, + "width": 256, + "standalone": false, + "config": { + "display_options": { + "show_seeds": false + } + } + }, + { + "name": "roundlogo-on", + "frames": 110, + "width": 256, + "standalone": false, + "config": { + "display_options": { + "show_round_logos": true + } + } + }, + { + "name": "roundlogo-off", + "frames": 110, + "width": 256, + "standalone": false, + "config": { + "display_options": { + "show_round_logos": false + } + } + }, + { + "name": "upset-on", + "frames": 310, + "width": 256, + "standalone": false, + "config": { + "display_options": { + "highlight_upsets": true + } + } + }, + { + "name": "upset-off", + "frames": 310, + "width": 256, + "standalone": false, + "config": { + "display_options": { + "highlight_upsets": false + } + } + }, + { + "name": "size-64x32", + "width": 64, + "frames": 190, + "standalone": false + }, + { + "name": "size-128x32", + "width": 128, + "frames": 190, + "standalone": false + }, + { + "name": "size-128x64", + "width": 128, + "height": 64, + "frames": 190, + "standalone": false + }, + { + "name": "size-256x32", + "width": 256, + "frames": 190, + "standalone": false + } + ], + "composites": [ + { + "name": "game-states", + "columns": 1, + "cells": [ + { + "shot": "state-live", + "label": "Live", + "sublabel": "period, clock and the running score" + }, + { + "shot": "state-upset", + "label": "Final, an upset", + "sublabel": "an 11 seed past a 1 seed" + }, + { + "shot": "state-upcoming", + "label": "Not started", + "sublabel": "tip-off date and time" + } + ] + }, + { + "name": "show-seeds", + "columns": 1, + "cells": [ + { + "shot": "seeds-on", + "label": "show_seeds: true", + "sublabel": "(2)DUKE and UCONN(1)" + }, + { + "shot": "seeds-off", + "label": "show_seeds: false" + } + ] + }, + { + "name": "show-round-logos", + "columns": 1, + "cells": [ + { + "shot": "roundlogo-on", + "label": "show_round_logos: true", + "sublabel": "the round header carries the tournament mark" + }, + { + "shot": "roundlogo-off", + "label": "show_round_logos: false" + } + ] + }, + { + "name": "highlight-upsets", + "columns": 1, + "cells": [ + { + "shot": "upset-on", + "label": "highlight_upsets: true", + "sublabel": "the upset score is picked out" + }, + { + "shot": "upset-off", + "label": "highlight_upsets: false" + } + ] + }, + { + "name": "panel-sizes", + "columns": 2, + "cells": [ + { + "shot": "size-64x32", + "label": "64 x 32" + }, + { + "shot": "size-128x32", + "label": "128 x 32" + }, + { + "shot": "size-128x64", + "label": "128 x 64" + }, + { + "shot": "size-256x32", + "label": "256 x 32" + } + ] + } + ] +} diff --git a/docs/assets/march-madness/show-round-logos.png b/docs/assets/march-madness/show-round-logos.png new file mode 100644 index 00000000..f91242cc Binary files /dev/null and b/docs/assets/march-madness/show-round-logos.png differ diff --git a/docs/assets/march-madness/show-seeds.png b/docs/assets/march-madness/show-seeds.png new file mode 100644 index 00000000..bf5d3fbf Binary files /dev/null and b/docs/assets/march-madness/show-seeds.png differ diff --git a/docs/assets/march-madness/smoke.png b/docs/assets/march-madness/smoke.png new file mode 100644 index 00000000..112169eb Binary files /dev/null and b/docs/assets/march-madness/smoke.png differ diff --git a/plugins.json b/plugins.json index ac1f5e85..ca963af8 100644 --- a/plugins.json +++ b/plugins.json @@ -460,10 +460,10 @@ "plugin_path": "plugins/march-madness", "stars": 0, "downloads": 0, - "last_updated": "2026-08-05", + "last_updated": "2026-09-03", "verified": true, "screenshot": "", - "latest_version": "1.0.6" + "latest_version": "1.1.0" }, { "id": "masters-tournament", diff --git a/plugins/march-madness/README.md b/plugins/march-madness/README.md index 18d943d7..ae06f65c 100644 --- a/plugins/march-madness/README.md +++ b/plugins/march-madness/README.md @@ -13,6 +13,13 @@ # March Madness Plugin +![A live tournament game on a 256x32 panel: Duke's logo, the seeds, the period +and clock, and the running score](../../docs/assets/march-madness/hero.png) + +*Every image in this README is real plugin output, rendered at the true panel +size from seeded games so it reproduces exactly. Teams, seeds and scores are +invented; the team marks are the logo assets the plugin already ships.* + A plugin for LEDMatrix that tracks the NCAA Division I basketball tournaments. It shows a horizontally-scrolling ticker of tournament games grouped by round — from the Round of 64 through the National Championship — with team seeds, round @@ -65,6 +72,15 @@ pip install -r requirements.txt The plugin exposes one screen; its length adapts to how many games are on the board (see `dynamic_duration` below). +A game is drawn differently depending on where it is: + +![A live game, an upset final, and one not yet tipped off](../../docs/assets/march-madness/game-states.png) + +Rounds are separated by a header carrying the round name and the tournament +mark, and the ticker runs through them in bracket order. + +![The same ticker on four panel sizes](../../docs/assets/march-madness/panel-sizes.png) + ## Configuration The web UI form is generated from `config_schema.json`, which is the source of @@ -85,8 +101,8 @@ truth. The keys below are the ones you'll typically set. |-----|---------|-------|-------| | `show_seeds` | `true` | — | Show tournament seeds (1–16) next to team names. | | `show_round_logos` | `true` | — | Show round-logo separators between game groups. | -| `highlight_upsets` | `true` | — | Highlight upset winners (higher seed beating lower seed) in gold. | -| `show_bracket_progress` | `true` | — | Show which teams are still alive in each region. | +| `highlight_upsets` | `true` | — | Draw an upset winner's name and score in gold. An upset is a bigger seed number beating a smaller one — an 11 seed past a 1 seed. | +| `show_bracket_progress` | `true` | — | **Not implemented.** The value is read into the plugin and never used again; nothing on the panel changes. See [issue #406](https://github.com/ChuckBuilds/ledmatrix-plugins/issues/406). | | `scroll_speed` | `1.0` | 0.5–5.0 | Scroll speed in pixels per frame. | | `scroll_delay` | `0.02` | 0.001–0.1 | Delay between scroll frames, in seconds (smaller = smoother, more CPU). | | `target_fps` | `120` | 30–200 | Target frames per second for the scroll. | @@ -95,6 +111,14 @@ truth. The keys below are the ones you'll typically set. | `min_duration` | `30` | 10–300 | Minimum display duration in seconds (used with `dynamic_duration`). | | `max_duration` | `300` | 30–600 | Maximum display duration in seconds (used with `dynamic_duration`). | +Three of these change what you see on the panel directly: + +![show_seeds on and off](../../docs/assets/march-madness/show-seeds.png) + +![show_round_logos on and off](../../docs/assets/march-madness/show-round-logos.png) + +![highlight_upsets on and off](../../docs/assets/march-madness/highlight-upsets.png) + ### Data Settings | Key | Default | Range | Notes | diff --git a/plugins/march-madness/manager.py b/plugins/march-madness/manager.py index 8310379c..91e3aebc 100644 --- a/plugins/march-madness/manager.py +++ b/plugins/march-madness/manager.py @@ -7,11 +7,10 @@ import re import threading import time -from datetime import datetime, timedelta, timezone +from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional -import numpy as np import pytz import requests from PIL import Image, ImageDraw, ImageFont @@ -649,7 +648,11 @@ def _create_game_tile(self, game: Dict) -> Image.Image: # Score (bottom center of center block, for live/final) if score_text: score_x = center_mid - score_w // 2 - score_y = height - 13 + # A final game also draws its date at height - 6. The score font is + # 10px tall starting at height - 13, so the two used to share three + # rows -- and because both are offsets from height, they collided at + # every panel size. Lift the score clear when a date is coming. + score_y = height - (19 if (game["is_final"] and game.get("game_date")) else 13) # Upset highlighting if game["is_final"] and game["is_upset"] and self.highlight_upsets: score_color = COLOR_GOLD diff --git a/plugins/march-madness/manifest.json b/plugins/march-madness/manifest.json index 11a9b0f9..14b91e86 100644 --- a/plugins/march-madness/manifest.json +++ b/plugins/march-madness/manifest.json @@ -1,7 +1,7 @@ { "id": "march-madness", "name": "March Madness", - "version": "1.0.6", + "version": "1.1.0", "description": "NCAA March Madness tournament bracket tracker with round branding, seeded matchups, live scores, and upset highlighting", "author": "ChuckBuilds", "category": "sports", @@ -20,6 +20,12 @@ ">=2.0.0" ], "versions": [ + { + "version": "1.1.0", + "released": "2026-09-03", + "notes": "Final-game layout, and documentation. A finished game drew its date at height - 6 while the 10px score started at height - 13, so the two shared three rows and the date sat inside the score digits -- and because both were offsets from height, they collided at every panel size. The score now lifts clear when a date is coming. Adds real rendered screenshots of the three game states, three of the display toggles and four panel sizes. Marks show_bracket_progress as not implemented: it is read once and never used (#406). Drops three unused imports.", + "ledmatrix_min_version": "2.0.0" + }, { "version": "1.0.6", "released": "2026-08-19", @@ -63,7 +69,7 @@ ], "stars": 0, "downloads": 0, - "last_updated": "2026-08-05", + "last_updated": "2026-09-03", "verified": true, "screenshot": "", "display_modes": [