Files
scoreboard/modes/fantasy.py
T
2026-06-25 23:54:47 -04:00

283 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import requests
from PIL import Image
from rgbmatrix import graphics
import utils.logos as logos
from utils.vars import Colors, font, font_small, PANEL_WIDTH, PANEL_HEIGHT, GAME_WIDTH, DIVIDER_COLOR
from time import time
FETCH_INTERVAL = 60 # seconds between API calls
# Logo layout: 14x14 centered vertically in the 32px panel
LOGO_SIZE = 14
LOGO_Y = 9 # (32 - 14) // 2 = 9
TEXT_X = 17 # x offset for text after logo
# Off-enum colors
_COLOR_GREEN = (0, 200, 80)
# --- State ---
_data = None
_last_fetch = 0
_cells = []
_scroll_x = 0
_scroll_speed = 1
_frames_per_tick = 2
_tick = 0
_virtual_canvas = None
_virtual_dirty = True
# ---------------------------------------------------------------------------
# RBG channel correction for Waveshare P2.5 panels
# Hardware physical order is R-B-G, so swap G and B before passing to API.
# ---------------------------------------------------------------------------
def _rbg(rgb):
r, g, b = rgb
return graphics.Color(r, b, g)
# ---------------------------------------------------------------------------
# Data fetch
# ---------------------------------------------------------------------------
def _fetch():
try:
resp = requests.get("https://api.alexav.gg/v4/sports/fantasy", timeout=5)
resp.raise_for_status()
return resp.json()
except Exception as e:
print(f"[fantasy] fetch error: {e}")
return None
# ---------------------------------------------------------------------------
# Formatting helpers
# ---------------------------------------------------------------------------
def _player_name(player):
"""Return a short display name (≤9 chars). DEF units use team abbreviation."""
if player.get("position") == "DEF":
return player.get("team", "DEF")[:9]
first = player.get("first_name", "")
last = player.get("last_name", "")
if first and last:
return f"{first[0]}.{last}"[:9]
return (last or first)[:9]
def _team_label(name, maxlen=12):
"""Strip parenthetical suffixes and truncate team name."""
paren = name.find(" (")
label = name[:paren] if paren > 0 else name
return label[:maxlen]
def _fmt_pts(pts):
"""Format fantasy points to one decimal place."""
return f"{float(pts):.1f}"
# ---------------------------------------------------------------------------
# Build the ordered cell list from API data
# Structure: [Team1 Header] [T1 Player ...] [Team2 Header] [T2 Player ...]
# ---------------------------------------------------------------------------
def _build_cells(data):
cells = []
t1, t2 = data["team1"], data["team2"]
for team, opp in [(t1, t2), (t2, t1)]:
meta = team["owner"].get("metadata", {})
team_name = meta.get("team_name", team["owner"]["display_name"])
winning = float(team["points"]) >= float(opp["points"])
# ── Team summary header cell ──────────────────────────────────────
cells.append({
"type": "team_info",
"owner": team["owner"]["display_name"],
"team_name": team_name,
"points": team["points"],
"opp_points": opp["points"],
"winning": winning,
})
# ── One cell per starter ─────────────────────────────────────────
pp = team["players_points"]
for player in team["starters"]:
pid = player["player_id"]
injury = player.get("injury_status")
cells.append({
"type": "player",
"nfl_team": player.get("team", ""),
"name": _player_name(player),
"position": player.get("position", ""),
"points": pp.get(pid, 0),
"injury": injury,
})
return cells
# ---------------------------------------------------------------------------
# PIL canvas: logos + dividers (blit-able background layer)
# ---------------------------------------------------------------------------
def _build_virtual_canvas(cells):
if not cells:
return None
n = len(cells)
total_w = GAME_WIDTH * (n + 4) # +4 tail so wrap never shows black
img = Image.new("RGB", (total_w, PANEL_HEIGHT), (0, 0, 0))
for i, cell in enumerate(cells * 2):
if i >= n + 4:
break
_render_pil(img, cell, i * GAME_WIDTH)
return img, n
def _render_pil(img, cell, x):
# right-edge divider
for y in range(PANEL_HEIGHT):
img.putpixel((x + GAME_WIDTH - 1, y), DIVIDER_COLOR)
# NFL team logo for player cells
if cell["type"] == "player" and cell["nfl_team"]:
logo = logos.load_logo("nfl", cell["nfl_team"])
if logo:
img.paste(logo.resize((LOGO_SIZE, LOGO_SIZE)), (x + 1, LOGO_Y))
# ---------------------------------------------------------------------------
# Blit a PANEL_WIDTH-wide slice from the PIL canvas to the matrix
# ---------------------------------------------------------------------------
def _blit_slice(canvas, img, offset):
w = img.width
for x in range(PANEL_WIDTH):
src_x = (offset + x) % w
for y in range(PANEL_HEIGHT):
r, g, b = img.getpixel((src_x, y))
canvas.SetPixel(x, y, r, b, g) # RBG panels: swap G and B
# ---------------------------------------------------------------------------
# Text overlay (drawn on top of the blitted PIL layer)
# ---------------------------------------------------------------------------
def _draw_overlay(canvas, cells, scroll_x):
total_w = GAME_WIDTH * len(cells)
for i, cell in enumerate(cells):
bx = i * GAME_WIDTH - scroll_x
for wrap in [0, total_w]:
x = bx + wrap
if x + GAME_WIDTH < 0 or x >= PANEL_WIDTH:
continue
if cell["type"] == "team_info":
_draw_team_info(canvas, cell, x)
else:
_draw_player(canvas, cell, x)
def _draw_team_info(canvas, cell, x):
"""
Layout (64 × 32 px cell):
y= 8 │ Owner display name (yellow)
y=17 │ Fantasy team name (white)
y=28 │ 144.0-134.3 score line (green if winning, red if losing)
"""
score_color = _COLOR_GREEN if cell["winning"] else Colors.RED.value
graphics.DrawText(canvas, font_small, x + 2, 8,
_rbg(Colors.YELLOW.value),
cell["owner"][:12])
graphics.DrawText(canvas, font_small, x + 2, 17,
_rbg(Colors.WHITE.value),
_team_label(cell["team_name"]))
score_line = f"{_fmt_pts(cell['points'])}-{_fmt_pts(cell['opp_points'])}"
graphics.DrawText(canvas, font_small, x + 2, 28,
_rbg(score_color),
score_line)
def _draw_player(canvas, cell, x):
"""
Layout (64 × 32 px cell):
Left 14 px │ NFL team logo (rendered in PIL layer)
x+17, y=13 │ Player name — white / yellow (Q) / red (Out/IR)
x+17, y=26 │ "POS pts"
top-right │ injury indicator letter (Q / D / X)
"""
injury = cell.get("injury")
# Name color reflects injury severity
if injury in ("Out", "IR"):
name_color = Colors.RED.value
elif injury in ("Questionable", "Doubtful"):
name_color = Colors.YELLOW.value
else:
name_color = Colors.WHITE.value
graphics.DrawText(canvas, font_small, x + TEXT_X, 13,
_rbg(name_color), cell["name"])
# Injury indicator letter in top-right corner of the cell
if injury:
ind_map = {"Questionable": "Q", "Doubtful": "D", "Out": "X", "IR": "IR"}
indicator = ind_map.get(injury, "?")
ind_color = Colors.YELLOW.value if injury in ("Questionable", "Doubtful") else Colors.RED.value
graphics.DrawText(canvas, font_small, x + GAME_WIDTH - 10, 7,
_rbg(ind_color), indicator)
# Position + fantasy points
pts_line = f"{cell['position']} {_fmt_pts(cell['points'])}"
graphics.DrawText(canvas, font_small, x + TEXT_X, 26,
_rbg(Colors.WHITE.value), pts_line)
# ---------------------------------------------------------------------------
# Public entry point — call from the main render loop
# ---------------------------------------------------------------------------
def draw_frame(canvas):
global _data, _last_fetch, _cells
global _virtual_canvas, _virtual_dirty, _scroll_x, _tick
now = time()
# Refresh data on interval
if now - _last_fetch > FETCH_INTERVAL or _data is None:
fresh = _fetch()
if fresh:
_data = fresh
_cells = _build_cells(_data)
_virtual_dirty = True
_last_fetch = now
if not _cells:
canvas.Clear()
graphics.DrawText(canvas, font, 8, 20,
_rbg(Colors.RED.value), "No fantasy")
return canvas
# Rebuild PIL background layer when data changes
if _virtual_dirty or _virtual_canvas is None:
result = _build_virtual_canvas(_cells)
if result:
_virtual_canvas, _ = result
_virtual_dirty = False
_scroll_x = 0
total_scroll_w = GAME_WIDTH * len(_cells)
canvas.Clear()
if _virtual_canvas:
_blit_slice(canvas, _virtual_canvas, _scroll_x)
_draw_overlay(canvas, _cells, _scroll_x)
# Advance scroll position
_tick += 1
if _tick >= _frames_per_tick:
_tick = 0
_scroll_x = (_scroll_x + _scroll_speed) % total_scroll_w
return canvas