add fantasy mode
This commit is contained in:
@@ -0,0 +1,283 @@
|
|||||||
|
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
|
||||||
+8
-1
@@ -6,7 +6,10 @@ from PIL import Image, ImageDraw, ImageFont
|
|||||||
from rgbmatrix import RGBMatrix, RGBMatrixOptions
|
from rgbmatrix import RGBMatrix, RGBMatrixOptions
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from utils.vars import Colors, LOGO_DIR, ASSET_DIR
|
from utils.vars import Colors, LOGO_DIR, ASSET_DIR
|
||||||
|
|
||||||
|
# modes
|
||||||
import modes.score as score_mode
|
import modes.score as score_mode
|
||||||
|
import modes.fantasy as fantasy_mode
|
||||||
|
|
||||||
# --- Load environment vars ---
|
# --- Load environment vars ---
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -182,7 +185,11 @@ def run():
|
|||||||
if supported_modes[current_mode] == "score":
|
if supported_modes[current_mode] == "score":
|
||||||
canvas_ref = score_mode.draw_frame(canvas)
|
canvas_ref = score_mode.draw_frame(canvas)
|
||||||
canvas = matrix.SwapOnVSync(canvas_ref)
|
canvas = matrix.SwapOnVSync(canvas_ref)
|
||||||
++times_ran
|
elif supported_modes[current_mode] == "fantasy":
|
||||||
|
canvas_ref = fantasy_mode.draw_frame(canvas)
|
||||||
|
canvas = matrix.SwapOnVSync(canvas_ref)
|
||||||
|
|
||||||
|
++times_ran
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run()
|
run()
|
||||||
Reference in New Issue
Block a user