start.sh, shell script to launch the main code

        #!/bin/bash
        # Tyler Bisk, tjb274
        # Final Project
        # 12/5/23
        sleep $1
        sudo python /home/$2/scoreboard/main.py -u $2 $3 $4 $5 $6 $7 $8 $9 ${10}
        

matrix.py, code containing LED Matrix object

  # Tyler Bisk, tjb274
  # Final Project
  # 12/5/23
  from PIL import Image, ImageOps
  
  from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics
  
  
  class Matrix(object):
      def __init__(self, rows, cols, username, gpio_slowdown, brightness):
          super(Matrix, self).__init__()
  
          self.username = username
  
          options = RGBMatrixOptions()
          options.rows = rows
          options.cols = cols
          options.chain_length = 1
          options.parallel = 1
          options.row_address_type = 0
          options.multiplexing = 0
          options.pwm_bits = 11
          options.brightness = brightness
          options.pwm_lsb_nanoseconds = 130
          options.led_rgb_sequence = "RGB"
          options.pixel_mapper_config = ""
          options.panel_type = ""
          options.gpio_slowdown = gpio_slowdown
          options.drop_privileges = True
  
          self.matrix = RGBMatrix(options=options)
          self.screen = self.matrix.CreateFrameCanvas()
  
          self.white = graphics.Color(255, 255, 255)
          self.small_font = graphics.Font()
          self.small_font.LoadFont(
              f"/home/{self.username}/scoreboard/fonts/tom-thumb.bdf"
          )
  
          self.score_font = graphics.Font()
          self.score_font.LoadFont(f"/home/{self.username}/scoreboard/fonts/6x13B.bdf")
          self.score_font_width = 6
  
          self.small_score_font = graphics.Font()
          self.small_score_font.LoadFont(
              f"/home/{self.username}/scoreboard/fonts/5x8.bdf"
          )
          self.small_score_font_width = 5
  
          self.medium_font = graphics.Font()
          self.medium_font.LoadFont(f"/home/{self.username}/scoreboard/fonts/5x7.bdf")
  
      def DrawBox(self, x0, y0, x1, y1, color):
          assert x0 <= x1
          assert y0 <= y1
  
          for i in range(y1 - y0 + 1):
              graphics.DrawLine(self.screen, x0, y0 + i, x1, y0 + i, color)
  
      def resize_logo(self, logo, new_height):
          logo.thumbnail((new_height, new_height), Image.BICUBIC)
          return logo
  
      def invert_logo(self, logo):
          if logo.mode == "RGBA":
              r, g, b, a = logo.split()
              rgb_image = Image.merge("RGB", (r, g, b))
              inverted_image = ImageOps.invert(rgb_image)
              r2, g2, b2 = inverted_image.split()
              logo = Image.merge("RGBA", (r2, g2, b2, a))
          else:
              logo = ImageOps.invert(logo)
          return logo
  
      def hex_to_rgb(self, hex):
          return tuple(int(hex[i : i + 2], 16) for i in (0, 2, 4))
  
      def edit_color(self, color_a, color_b, t):
          return tuple(int(a + (b - a) * t) for a, b in zip(color_a, color_b))
  
      def ordinal(self, num):
          if num == 1:
              return "1st"
          elif num == 2:
              return "2nd"
          elif num == 3:
              return "3rd"
          else:
              return str(num) + "th"
  

main.py, main code for Pygame Scoreboard

  #!/usr/bin/env python
  # Tyler Bisk, tjb274
  # Final Project
  # 12/5/23
  import argparse
  import json
  import os
  import subprocess
  import time
  from datetime import datetime, timedelta
  from threading import Thread
  from urllib.request import urlopen
  
  import pygame
  import pytz
  import requests
  from PIL import Image
  from pygame.locals import *
  
  import matrix
  from consts import *
  from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics
  from utils import *
  
  global sport, league, team, group
  global iteration, in_progress_only
  global code_run, in_demo, demo_state
  
  
  class Score(matrix.Matrix):
      def __init__(self, rows, cols, username, gpio_slowdown, brightness):
          super(Score, self).__init__(rows, cols, username, gpio_slowdown, brightness)
  
          self.logos_to_invert = ["Iowa Hawkeyes", "Pittsburgh Pirates"]
          self.swap_colors = [
              "Missouri Tigers",
              "Princeton Tigers",
              "Carolina Panthers",
              "Syracuse Orange",
              "Tennessee Titans",
              "Tampa Bay Buccaneers",
              "Philadelphia Phillies",
              "Washington Wizards",
              "Nashville Predators",
              "Memphis Grizzlies",
              "Florida Panthers",
          ]
          self.white_font = [
              "New Orleans Saints",
              "Charlotte Hornets",
          ]
  
      def run(self):
          global iteration, team, code_run
          iteration = 0
  
          #### Display IP Address for 1 second ####
          ip = subprocess.run(
              "ifconfig | grep 'inet 10'", shell=True, stdout=subprocess.PIPE
          ).stdout.decode("utf-8")[13:]
          ip = ip[0 : ip.find(" ")]
          graphics.DrawText(
              self.screen, self.small_font, 4, self.matrix.height / 2 + 3, self.white, ip
          )
  
          self.screen = self.matrix.SwapOnVSync(self.screen)
  
          time.sleep(1)
  
          #############################################
          while code_run:
              self.screen.Clear()
              if iteration % 8 == 0:
                  try:
                      home, away, competition, date, logo = self.get_competition_data()
                  except Exception as e:
                      print("could not reach api")
                      print(e)
                      iteration = 0
                      time.sleep(8)
                      continue
  
                  home, away = self.get_team_data(home, away)
  
              self.display_team(home, away, competition, date, logo)
  
              self.screen = self.matrix.SwapOnVSync(self.screen)
              iteration = (iteration + 1) % 7200
              time.sleep(1)
  
      def more_than_one_in_progress(self, events):
          in_progress = 0
          for event in events:
              status = event["competitions"][0]["status"]["type"]["name"]
              if status not in ["STATUS_DELAYED", "STATUS_FINAL", "STATUS_SCHEDULED"]:
                  in_progress += 1
              if in_progress > 1:
                  return True
          return False
  
      def get_competition_data(self):
          global sport, league, team, group, iteration
          global in_demo, demo_state
  
          URL = "http://site.api.espn.com/apis/site/v2/sports/{}/{}/scoreboard?limit=500&{}".format(  # ?dates=20231027"
              sport, league, group
          )
          if in_demo:
              sport = "hockey"
              league = "mens-college-hockey"
              team = "Cornell Big Red"
              ds = demo_list[demo_state]
              if ds == "PRE GAME":
                  api = json.loads(
                      open(
                          f"/home/{self.username}/scoreboard/sample_jsons/cornellhockeyscheduled.json",
                          "r",
                      ).read()
                  )
              elif ds == "IN PROGRESS":
                  api = json.loads(
                      open(
                          f"/home/{self.username}/scoreboard/sample_jsons/cornellhockeyprogress.json",
                          "r",
                      ).read()
                  )
              elif ds == "GOAL":
                  api = json.loads(
                      open(
                          f"/home/{self.username}/scoreboard/sample_jsons/cornellhockeygoal.json",
                          "r",
                      ).read()
                  )
              elif ds == "POST GAME":
                  api = json.loads(
                      open(
                          f"/home/{self.username}/scoreboard/sample_jsons/cornellhockeyfinal.json",
                          "r",
                      ).read()
                  )
          else:
              api = json.loads(urlopen(URL).read())
          rotate = [
              "All",
              "Top 25",
              "Big Ten",
              "Ivy League",
              "ACC",
              "PAC-12",
              "SEC",
              "Big 12",
          ]
  
          if team in rotate:
              event = api["events"][iteration // 8 % len(api["events"])]
              home = event["competitions"][0]["competitors"][0]
              away = event["competitions"][0]["competitors"][1]
  
              if in_progress_only and self.more_than_one_in_progress(api["events"]):
                  print("more than one!")
                  status = event["competitions"][0]["status"]["type"]["name"]
                  tries = 0
                  while status in [
                      "STATUS_DELAYED",
                      "STATUS_FINAL",
                      "STATUS_SCHEDULED",
                  ] and tries < len(api["events"]):
                      iteration += 8
                      event = api["events"][iteration // 8 % len(api["events"])]
                      status = event["competitions"][0]["status"]["type"]["name"]
                      tries += 1
                  home = event["competitions"][0]["competitors"][0]
                  away = event["competitions"][0]["competitors"][1]
              # elif final_only # TODO:
  
          else:
              found = False
              days_ahead = 0
              # search in the next week
              max_days = 50
              while not found and days_ahead < max_days:
                  # check the current API response for the given team
                  for event in api["events"]:
                      home = event["competitions"][0]["competitors"][0]
                      away = event["competitions"][0]["competitors"][1]
                      if (
                          team in home["team"]["displayName"]
                          or team in away["team"]["displayName"]
                          or home["team"]["displayName"] in team
                          or away["team"]["displayName"] in team
                      ):
                          found = True
                          break
                  if not found:
                      # check the next day to see if team is playing
                      day = datetime.today() + timedelta(days=days_ahead)
                      date = day.strftime("%Y%m%d")
                      if league == "college-football":
                          # Check teams outside the top 25
                          group = "enable=groups&groups=80&dates=" + date
                      elif league == "mens-college-basketball":
                          group = "enable=groups&groups=50&dates=" + date
                      else:
                          group = "dates=" + date
                      URL = "http://site.api.espn.com/apis/site/v2/sports/{}/{}/scoreboard?limit=500&{}".format(  # ?dates=20231027"
                          sport, league, group
                      )
                      api = json.loads(urlopen(URL).read())
                      days_ahead = days_ahead + 1
  
              if not found:
                  group = ""
                  team = "All"
          competition = event["competitions"][0]
  
          if home["homeAway"] != "home":
              tmp = home
              home = away
              away = tmp
  
          date = (
              datetime.fromisoformat(event["date"][:-1])
              .replace(tzinfo=pytz.utc)
              .astimezone(pytz.timezone("US/Eastern"))
          )
  
          logo = Image.open((f"/home/{self.username}/scoreboard/images/{sport}.png"), "r")
  
          return home, away, competition, date, logo
  
      def get_team_data(self, home, away):
          home.setdefault("curatedRank", {"current": 99})
          home["curatedRank"].setdefault("current", 99)
          away.setdefault("curatedRank", {"current": 99})
          away["curatedRank"].setdefault("current", 99)
          home.setdefault("records", [{"summary": "0-0"}])
          home["records"][0].setdefault("summary", "0-0")
          away.setdefault("records", [{"summary": "0-0"}])
          away["records"][0].setdefault("summary", "0-0")
          home["team"].setdefault("color", "000000")
          home["team"].setdefault("alternateColor", "FFFFFF")
          home.setdefault("score", 0)
          away["team"].setdefault("color", "000000")
          away["team"].setdefault("alternateColor", "FFFFFF")
          away.setdefault("score", 0)
          home["team"].setdefault(
              "logo",
              "https://secure.espncdn.com/combiner/i?img=/i/teamlogos/default-team-logo-500.png",
          )
          away["team"].setdefault(
              "logo",
              "https://secure.espncdn.com/combiner/i?img=/i/teamlogos/default-team-logo-500.png",
          )
  
          # convert logo urls to Image
          home_logo = Image.open(requests.get(home["team"]["logo"], stream=True).raw)
          away_logo = Image.open(requests.get(away["team"]["logo"], stream=True).raw)
  
          # Rank applies for college only
  
          home = TeamData(
              home["id"],
              home["team"]["displayName"],
              home["team"]["shortDisplayName"],
              home["team"]["abbreviation"][0:4],
              home["curatedRank"]["current"],
              "(" + home["records"][0]["summary"] + ")",
              home_logo,
              home["team"]["color"],
              home["team"]["alternateColor"],
              home["score"],
          )
  
          away = TeamData(
              away["id"],
              away["team"]["displayName"],
              away["team"]["shortDisplayName"],
              away["team"]["abbreviation"][0:4],
              away["curatedRank"]["current"],
              "(" + away["records"][0]["summary"] + ")",
              away_logo,
              away["team"]["color"],
              away["team"]["alternateColor"],
              away["score"],
          )
  
          # remove parenthesis from records if too long
          away.record = away.record[1:-1] if len(away.record) > 7 else away.record
          home.record = home.record[1:-1] if len(home.record) > 7 else home.record
  
          if home.name in self.logos_to_invert:
              home.logo = self.invert_logo(home.logo)
          elif away.name in self.logos_to_invert:
              away.logo = self.invert_logo(away.logo)
  
          return home, away
  
      def display_pregame_team(self, home, away, date, logo):
          home.logo = self.resize_logo(home.logo, self.matrix.height * 0.625)
          away.logo = self.resize_logo(away.logo, self.matrix.height * 0.625)
          logo = self.resize_logo(logo, 9)
  
          self.screen.SetImage(
              away.logo.convert("RGB"),
              self.matrix.height * (5 / 32),
              self.matrix.height * (6 / 32),
              False,
          )
          self.screen.SetImage(
              home.logo.convert("RGB"),
              self.matrix.width - self.matrix.height * (0.625) - 5,
              self.matrix.height * (6 / 32),
              False,
          )
  
          self.screen.SetImage(
              logo.convert("RGB"),
              self.matrix.width / 2 - 4,
              self.matrix.height * 0.5 - 4,
              False,
          )
  
          if iteration % 8 <= 3 or date.strftime("%-I:%M %p") == "12:00 AM":
              date_human_readable = date.strftime("%a %b %-d")
              # records
          else:
              date_human_readable = date.strftime("%-I:%M %p")
              # rank
          home_detail = ""
          away_detail = ""
          if iteration % 8 <= 3 or "(0-0)" in home.record or "(0-0)" in away.record:
              if home.rank > 0 and home.rank < 26:
                  home_detail = str(home.rank) + " "
              home_detail += home.abbreviation
              if away.rank > 0 and away.rank < 26:
                  away_detail = str(away.rank) + " "
              away_detail += away.abbreviation
          else:
              home_detail = home.record
              away_detail = away.record
  
          date_length = len(date_human_readable) * 4
  
          graphics.DrawText(
              self.screen,
              self.small_font,
              self.matrix.width / 2 - date_length / 2 + 1,
              6,
              self.white,
              date_human_readable,
          )
  
          graphics.DrawText(
              self.screen,
              self.small_font,
              self.matrix.width * 0.75 - len(home_detail) * 4 / 2,
              self.matrix.height - 1,
              self.white,
              home_detail,
          )
  
          graphics.DrawText(
              self.screen,
              self.small_font,
              self.matrix.width * 0.25 - len(away_detail) * 4 / 2,
              self.matrix.height - 1,
              self.white,
              away_detail,
          )
  
          return
  
      def display_team_in_progress(self, home, away, competition, status):
          hc1 = self.hex_to_rgb(home.color1)
          hc2 = self.hex_to_rgb(home.color2)
          ac1 = self.hex_to_rgb(away.color1)
          ac2 = self.hex_to_rgb(away.color2)
  
          if home.name in self.swap_colors:
              tmp = hc1
              hc1 = hc2
              hc2 = tmp
          elif away.name in self.swap_colors:
              tmp = ac1
              ac1 = ac2
              ac2 = tmp
  
          if home.name in self.white_font:
              hc2 = self.hex_to_rgb("FFFFFF")
          elif away.name in self.white_font:
              ac2 = self.hex_to_rgb("FFFFFF")
  
          # change black to white if color 2
          if hc2 == (0, 0, 0):
              hc2 = (255, 255, 255)
          elif ac2 == (0, 0, 0):
              ac2 = (255, 255, 255)
  
          hc1 = self.edit_color(hc1, (0, 0, 0), 0.9)
          hc2 = self.edit_color(hc2, (255, 255, 255), 0.1)
          ac1 = self.edit_color(ac1, (0, 0, 0), 0.9)
          ac2 = self.edit_color(ac2, (255, 255, 255), 0.1)
  
          # set defaults for all sports
          competition["situation"].setdefault("down", 0)
          competition["situation"].setdefault("distance", "")
          competition["situation"].setdefault("possessionText", "")
          competition["situation"].setdefault("possession", "0")
          competition["situation"].setdefault("homeTimeouts", 0)
          competition["situation"].setdefault("awayTimeouts", 0)
          competition["situation"].setdefault("balls", 0)
          competition["situation"].setdefault("strikes", 0)
          competition["situation"].setdefault("outs", 0)
          competition["situation"].setdefault("onFirst", False)
          competition["situation"].setdefault("onSecond", False)
          competition["situation"].setdefault("onThird", False)
          competition["situation"].setdefault("lastPlay", {"type": {"text": ""}})
          competition["situation"].setdefault("lastPlay", {"team": {"id": "0"}})
          competition["situation"]["lastPlay"].setdefault("type", {"text": ""})
          competition["situation"]["lastPlay"].setdefault("team", {"id": "0"})
          competition["situation"]["lastPlay"]["type"].setdefault("text", "")
          competition["situation"]["lastPlay"]["team"].setdefault("id", "0")
  
          # Convert period numbers to periods for each sport and league
          # OT is sometimes "OT" and sometimes a "5th Quater"
          if sport != "baseball":
              period = self.ordinal(status["period"])
              if period[0] == "O":
                  period = "OT"
              elif sport == "hockey" and int(period[0]) > 3:
                  period = "OT"
              elif (
                  sport == "basketball"
                  and league == "mens-college-basketball"
                  and int(period[0]) > 2
              ):
                  period = "OT"
              elif int(period[0]) > 4:
                  period = "OT"
  
              if int(period[0]) > 4:
                  period = "OT"
          else:
              period = str(status["period"])
  
          situation = Situation(
              period,
              status["displayClock"],
              ""
              if competition["situation"]["down"] in [0, -1]
              else self.ordinal(competition["situation"]["down"])
              + "+"
              + str(competition["situation"]["distance"]),
              competition["situation"]["possessionText"],
              competition["situation"]["homeTimeouts"],
              competition["situation"]["awayTimeouts"],
              competition["situation"]["possession"],
              competition["situation"]["balls"],
              competition["situation"]["strikes"],
              competition["situation"]["outs"],
              competition["situation"]["onFirst"],
              competition["situation"]["onSecond"],
              competition["situation"]["onThird"],
              competition["situation"]["lastPlay"],
          )
  
          # Blink Team and logo if:
          # Touchdown, Field Goal, Safety, Extra Point
          # Home run, run scores
          # Goal scores
  
          if iteration % 16 < 11 and (
              (
                  sport == "football"
                  and situation.down_to_go == ""
                  and situation.last_play["team"]["id"] != "0"
              )
              or (
                  sport in ["baseball", "hockey"]
                  and situation.last_play["scoreValue"] != 0
              )
          ):
              if situation.last_play["team"]["id"] == home.id:
                  team = home
              else:
                  team = away
  
              # Blink
              if iteration % 2 == 1:
                  team.logo = self.resize_logo(team.logo, self.matrix.height * 18 / 32)
  
                  self.screen.SetImage(
                      team.logo.convert("RGB"),
                      self.matrix.width * 23 / 64,
                      self.matrix.height * 7 / 32,
                      False,
                  )
  
                  if sport == "hockey":
                      text = "GOAL"
                  elif sport == "baseball":
                      runs = situation.last_play["scoreValue"]
                      if runs == 4:
                          text = "GRAND SLAM"
                      else:
                          if "HOME" in situation.last_play["type"]["text"].upper():
                              text = "HOME RUN"
                          elif "WALK" in situation.last_play["type"]["text"].upper():
                              text = "WALK FOR RUN"
                          elif "STOLE" in situation.last_play["type"]["text"].upper():
                              text = "STOLE HOME"
                          else:
                              text = "RBI"
                          if runs > 1:
                              text = (
                                  str(situation.last_play["scoreValue"]) + "-RUN " + text
                              )
  
                  elif (
                      "TOUCHDOWN" in situation.last_play["type"]["text"].upper()
                  ) or str(situation.last_play["scoreValue"]) == "6":
                      text = "TOUCHDOWN"
                  # TODO: 2-Pt conversion succeeded, 2-pt failed, extra point missed, field goal missed,
                  # all dont fit and need to be abbreviated PAT GOOD, FG GOOD, etc
                  else:
                      text = situation.last_play["type"]["text"].upper()
  
                  graphics.DrawText(
                      self.screen,
                      self.small_font,
                      self.matrix.width / 2 - ((len(text) * 4) / 2 - 1),
                      6,
                      self.white,
                      text,
                  )
  
                  graphics.DrawText(
                      self.screen,
                      self.small_font,
                      self.matrix.width / 2 - ((len(team.shortName) * 4) / 2 - 1),
                      self.matrix.height - 1,
                      self.white,
                      team.shortName.upper(),
                  )
          # Just display the teams, scores, boxes, and situation
          else:
              home.logo = self.resize_logo(home.logo, self.matrix.height * 0.5)
              home_logo = Image.new("RGB", home.logo.size, hc1)
              home_logo.paste(home.logo, mask=home.logo.split()[3])
  
              away.logo = self.resize_logo(away.logo, self.matrix.height * 0.5)
              away_logo = Image.new("RGB", away.logo.size, ac1)
              away_logo.paste(away.logo, mask=away.logo.split()[3])
  
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  (self.matrix.width * 28 / 64 - len(situation.down_to_go) * 4) / 2,
                  self.matrix.height / 2 + 7,
                  self.white,
                  situation.down_to_go,
              )
  
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  (self.matrix.width * 28 / 64 - len(situation.location) * 4) / 2,
                  self.matrix.height / 2 + 14,
                  self.white,
                  situation.location,
              )
  
              # Draw boxes
              self.DrawBox(
                  self.matrix.width * 28 / 64,
                  0,
                  self.matrix.width,
                  self.matrix.height // 2 - 1,
                  graphics.Color(*ac1),
              )
              self.DrawBox(
                  self.matrix.width * 28 / 64,
                  self.matrix.height // 2,
                  self.matrix.width,
                  self.matrix.height,
                  graphics.Color(*hc1),
              )
  
              # Display Time Outs Remaining
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  self.matrix.width * 0.75,
                  self.matrix.height / 2 - 1,
                  self.white,
                  "_" * situation.away_timeouts,
              )
  
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  self.matrix.width * 0.75,
                  self.matrix.height - 1,
                  self.white,
                  "_" * situation.home_timeouts,
              )
  
              # Display logos
              self.screen.SetImage(
                  away_logo.convert("RGB"), self.matrix.width * 29 / 64, 0, False
              )
  
              self.screen.SetImage(
                  home_logo.convert("RGB"),
                  self.matrix.width * 29 / 64,
                  self.matrix.height / 2,
                  False,
              )
  
              # Display abbreviations
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  self.matrix.width * 0.75,
                  6,
                  graphics.Color(*ac2),
                  away.abbreviation,
              )
  
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  self.matrix.width * 0.75,
                  self.matrix.height / 2 + 6,
                  graphics.Color(*hc2),
                  home.abbreviation,
              )
  
              # Display scores
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  self.matrix.width * 0.75,
                  12,
                  graphics.Color(*ac2),
                  away.score,
              )
  
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  self.matrix.width * 0.75,
                  self.matrix.height / 2 + 12,
                  graphics.Color(*hc2),
                  home.score,
              )
  
              # Display possession (football only)
              if situation.possession == away.id:
                  graphics.DrawText(
                      self.screen,
                      self.medium_font,
                      self.matrix.width - 4,
                      self.matrix.height * 0.25 + 1,
                      self.white,
                      ".",
                  )
              elif situation.possession == home.id:
                  graphics.DrawText(
                      self.screen,
                      self.medium_font,
                      self.matrix.width - 4,
                      self.matrix.height / 2 + self.matrix.height * 0.25 + 1,
                      self.white,
                      ".",
                  )
  
              # move clock to the middle whenever there's no down information
              if situation.down_to_go == "":
                  # move clock down
                  h = 15
              else:
                  h = 7
  
              # Display clock and period for all sports except baseball
              if sport != "baseball":
                  graphics.DrawText(
                      self.screen, self.small_font, 8, h, self.white, situation.period
                  )
  
                  graphics.DrawText(
                      self.screen,
                      self.small_font,
                      (self.matrix.width * 28 / 64 - len(situation.clock) * 4) / 2,
                      h + 7,
                      self.white,
                      situation.clock,
                  )
              else:
                  if "Mid" in status["type"]["detail"]:
                      graphics.DrawText(
                          self.screen, self.small_font, 8, h, self.white, "MID"
                      )
                  elif "End" in status["type"]["detail"]:
                      graphics.DrawText(
                          self.screen, self.small_font, 8, h, self.white, "END"
                      )
  
                  graphics.DrawText(
                      self.screen,
                      self.small_font,
                      8,
                      h + 7,
                      self.white,
                      self.ordinal(int(situation.period)),
                  )
  
      def display_postgame_team(self, home, away, status, logo):
          # Game can be between periods or over. Display "status detail"
          home.logo = self.resize_logo(home.logo, self.matrix.height * 0.625)
          away.logo = self.resize_logo(away.logo, self.matrix.height * 0.625)
          logo = self.resize_logo(logo, 8)
  
          # Display logos
          self.screen.SetImage(
              away.logo.convert("RGB"),
              self.matrix.width * -(1 / 16),
              self.matrix.height * 6 / 32 + 1,
              False,
          )
          self.screen.SetImage(
              home.logo.convert("RGB"),
              self.matrix.width
              + self.matrix.width * (1 / 16)
              - self.matrix.height * 0.625,
              self.matrix.height * 6 / 32 + 1,
              False,
          )
  
          self.screen.SetImage(
              logo.convert("RGB"),
              self.matrix.width / 2 - 4,
              self.matrix.height - 9,
              False,
          )
  
          # Display scores
          scoreline = away.score + " " + home.score
          # Both scores in range [0, 99]
          if len(scoreline) < 6:
              length = len(scoreline) * self.score_font_width
              graphics.DrawText(
                  self.screen,
                  self.score_font,
                  self.matrix.width / 2 - length / 2,
                  self.matrix.height / 2 + 5,
                  self.white,
                  scoreline,
              )
          else:
              length = len(away.score) * self.small_score_font_width
              graphics.DrawText(
                  self.screen,
                  self.small_score_font,
                  self.matrix.width / 2 - length - 1,
                  self.matrix.height / 2 + 3,
                  self.white,
                  away.score,
              )
  
              length = len(home.score) * self.small_score_font_width
              graphics.DrawText(
                  self.screen,
                  self.small_score_font,
                  self.matrix.width / 2 + 1,
                  self.matrix.height / 2 + 3,
                  self.white,
                  home.score,
              )
          detail = status["type"]["detail"].upper()
          if "END OF 1ST" in detail:
              detail = "END Q1"
          elif "END OF 3RD" in detail:
              detail = "END Q3"
          elif "END OF 4TH" in detail:
              detail = "END Q4"
  
          if sport == "hockey":
              if "END OF 2ND" in detail:
                  detail = "END P2"
              detail = detail.replace("Q", "P")
  
          leng = len(detail)
          graphics.DrawText(
              self.screen,
              self.medium_font,
              self.matrix.width / 2 - ((leng * 5) / 2 - 1),
              7,
              self.white,
              detail,
          )
  
          if "FINAL" in detail and home.record != "(0-0)" and away.record != "(0-0)":
              home_detail = ""
              away_detail = ""
              if iteration % 8 <= 3 or "(0-0)" in home.record or "(0-0)" in away.record:
                  if home.rank > 0 and home.rank < 26:
                      home_detail = str(home.rank) + " "
                  home_detail += home.abbreviation
                  if away.rank > 0 and away.rank < 26:
                      away_detail = str(away.rank) + " "
                  away_detail += away.abbreviation
              else:
                  home_detail = home.record
                  away_detail = away.record
  
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  self.matrix.width * 0.8 + 1 - len(home_detail) * 4 / 2,
                  self.matrix.height - 1,
                  self.white,
                  home_detail,
              )
  
              graphics.DrawText(
                  self.screen,
                  self.small_font,
                  self.matrix.width * 0.2 - len(away_detail) * 4 / 2,
                  self.matrix.height - 1,
                  self.white,
                  away_detail,
              )
  
          return
  
      def display_team(self, home, away, competition, date, logo):
          status = competition["status"]
          if (
              status["type"]["name"] == "STATUS_SCHEDULED"
              or status["type"]["name"] == "STATUS_DELAYED"
          ):
              self.display_pregame_team(home, away, date, logo)
          elif status["type"]["name"] == "STATUS_IN_PROGRESS":
              self.display_team_in_progress(home, away, competition, status)
          else:
              self.display_postgame_team(home, away, status, logo)
          return
  
      def scroll(self):
          self.screen = self.matrix.CreateFrameCanvas()
          font = graphics.Font()
          font.LoadFont("fonts/6x13B.bdf")
          textColor = graphics.Color(255, 255, 0)
          pos = self.screen.width
          my_text = "HELLO JFS9"
  
          while True:
              self.screen.Clear()
              len = graphics.DrawText(self.screen, font, pos, 20, textColor, my_text)
              pos -= 1
              if pos + len < 0:
                  pos = self.screen.width
  
              time.sleep(0.05)
              self.screen = self.matrix.SwapOnVSync(self.screen)
  
  
  # parse arguments for given LED matrix
  parser = argparse.ArgumentParser(prog="scoreboard")
  parser.add_argument("-r", "--rows", default=32)
  parser.add_argument("-c", "--columns", default=64)
  parser.add_argument("-u", "--username", default="pi")
  parser.add_argument("-s", "--gpio_slowdown", default=4)
  parser.add_argument("-b", "--brightness", default=80)
  
  args = parser.parse_args()
  
  # Run on Pi TFT
  os.putenv("SDL_VIDEODRIVER", "fbcon")
  os.putenv("SDL_FBDEV", "/dev/fb0")
  
  # Use the touchscreen
  os.putenv("SDL_MOUSEDRV", "TSLIB")
  os.putenv("SDL_MOUSEDEV", "/dev/input/touchscreen")
  
  # init pygame
  pygame.init()
  
  # hide the mouse
  pygame.mouse.set_visible(False)
  tft = pygame.display.set_mode((320, 240))
  
  code_run = True
  
  score = Score(
      int(args.rows),
      int(args.columns),
      args.username,
      int(args.gpio_slowdown),
      int(args.brightness),
  )
  sport, league, team, in_progress_only, group = read_values(score.username)
  
  time.sleep(0.1)
  
  t1 = Thread(target=score.run)
  t1.start()
  
  # set some variables to use later
  whte = 255, 255, 255
  black = 0, 0, 0
  
  # init the font
  font = pygame.font.Font(None, 20)
  smal_font = pygame.font.Font(None, 17)
  big_font = pygame.font.Font(None, 40)
  
  # init the buttons
  start_demo_button = font.render("START DEMO", True, whte)
  start_demo_button_rect = start_demo_button.get_rect(center=(320 // 2, 220))
  quit_button = font.render("QUIT", True, whte)
  quit_button_rect = quit_button.get_rect(center=(int(320 * 0.8), 220))
  stop_demo_button = font.render("STOP DEMO", True, whte)
  stop_demo_button_rect = stop_demo_button.get_rect(center=(320 // 2, 220))
  left_button = big_font.render("<", True, whte)
  left_button_rect = left_button.get_rect(center=(20, 240 // 2))
  right_button = big_font.render(">", True, whte)
  right_button_rect = right_button.get_rect(center=(300, 240 // 2))
  up_button = pygame.transform.rotate(right_button, 90)
  down_button = pygame.transform.rotate(left_button, 90)
  send_parameters_button = font.render("SEND PARAMETERS", True, whte)
  send_parameters_button_rect = send_parameters_button.get_rect(center=(320 // 2, 20))
  sport_up_rect = up_button.get_rect(center=(int(320 * 0.15), 70))
  sport_down_rect = down_button.get_rect(center=(int(320 * 0.15), 170))
  league_up_rect = up_button.get_rect(center=(int(320 * 0.35), 70))
  league_down_rect = down_button.get_rect(center=(int(320 * 0.35), 170))
  team_up_rect = up_button.get_rect(center=(int(320 * 0.75), 70))
  team_down_rect = down_button.get_rect(center=(int(320 * 0.75), 170))
  
  # make an inflated rect for larger touch area
  start_demo_button_rect_inflated = start_demo_button_rect.inflate(20, 10)
  quit_button_rect_inflated = quit_button_rect.inflate(20, 10)
  right_button_rect_inflated = right_button_rect.inflate(30, 30)
  left_button_rect_inflated = left_button_rect.inflate(30, 30)
  sport_up_rect_inflated = sport_up_rect.inflate(30, 30)
  sport_down_rect_inflated = sport_down_rect.inflate(30, 30)
  league_up_rect_inflated = league_up_rect.inflate(30, 30)
  league_down_rect_inflated = league_down_rect.inflate(30, 30)
  team_up_rect_inflated = team_up_rect.inflate(30, 30)
  team_down_rect_inflated = team_down_rect.inflate(30, 30)
  send_parameters_button_rect_inflated = send_parameters_button_rect.inflate(20, 10)
  
  # initialize a clock to set constant fps
  clock = pygame.time.Clock()
  fps = 10
  
  # def run_pygame(fps):
  in_demo = False
  
  sport_state = 0
  league_state = 0
  league_list = []
  team_state = 0
  team_list = []
  demo_state = 0
  
  while code_run:
      tft.fill(black)
  
      # 1. Draw everything according to current state in FSM
      if in_demo:
          tft.blit(stop_demo_button, stop_demo_button_rect)
          tft.blit(left_button, left_button_rect)
          tft.blit(right_button, right_button_rect)
          demo_text = big_font.render(demo_list[demo_state], True, whte)
          tft.blit(demo_text, demo_text.get_rect(center=(int(320 / 2), int(240 / 2))))
      else:
          tft.blit(send_parameters_button, send_parameters_button_rect)
          tft.blit(start_demo_button, start_demo_button_rect)
          tft.blit(quit_button, quit_button_rect)
          tft.blit(up_button, sport_up_rect)
          tft.blit(down_button, sport_down_rect)
          tft.blit(up_button, league_up_rect)
          tft.blit(down_button, league_down_rect)
          tft.blit(up_button, team_up_rect)
          tft.blit(down_button, team_down_rect)
          sport_text = font.render(sport_list[sport_state], True, whte)
          tft.blit(sport_text, sport_text.get_rect(center=(int(320 * 0.15), 120)))
          if sport_list[sport_state] == "Hockey":
              league_list = hockey_league_list
          elif sport_list[sport_state] == "Football":
              league_list = football_league_list
          elif sport_list[sport_state] == "Basketball":
              league_list = basketball_league_list
          league_text = font.render(league_list[league_state], True, whte)
          tft.blit(league_text, league_text.get_rect(center=(int(320 * 0.35), 120)))
          if league_list[league_state] == "NHL":
              team_list = nhl_list
          elif league_list[league_state] == "NFL":
              team_list = nfl_list
          elif league_list[league_state] == "NBA":
              team_list = nba_list
          else:
              team_list = ncaa_list
          team_text = smal_font.render(team_list[team_state], True, whte)
          tft.blit(team_text, team_text.get_rect(center=(int(320 * 0.75), 120)))
      # 2. Check if anything was clicked
      for event in pygame.event.get():
          if event.type == MOUSEBUTTONUP:
              pos = pygame.mouse.get_pos()
              # see if the click collided with an active button depending on state
              if start_demo_button_rect_inflated.collidepoint(pos):
                  demo_state = 0
                  iteration = 0
                  in_demo = not in_demo
              if in_demo:
                  if left_button_rect_inflated.collidepoint(pos):
                      demo_state = (demo_state - 1) % len(demo_list)
                      iteration = 0
                  if right_button_rect_inflated.collidepoint(pos):
                      demo_state = (demo_state + 1) % len(demo_list)
                      iteration = 0
              else:
                  if quit_button_rect_inflated.collidepoint(pos):
                      code_run = False
                  if sport_up_rect_inflated.collidepoint(pos):
                      sport_state = (sport_state - 1) % len(sport_list)
                      league_state = 0
                      team_state = 0
                  if sport_down_rect_inflated.collidepoint(pos):
                      sport_state = (sport_state + 1) % len(sport_list)
                      league_state = 0
                      team_state = 0
                  if league_up_rect_inflated.collidepoint(pos):
                      league_state = (league_state - 1) % len(league_list)
                      team_state = 0
                  if league_down_rect_inflated.collidepoint(pos):
                      league_state = (league_state + 1) % len(league_list)
                      team_state = 0
                  if team_up_rect_inflated.collidepoint(pos):
                      team_state = (team_state - 1) % len(team_list)
                  if team_down_rect_inflated.collidepoint(pos):
                      team_state = (team_state + 1) % len(team_list)
                  if send_parameters_button_rect_inflated.collidepoint(pos):
                      sport = sport_list[sport_state].lower()
                      leaguee = league_list[league_state].lower()
                      if leaguee == "ncaa":
                          league = "college-" + sport
                      elif leaguee == "ncaam":
                          league = "mens-college-" + sport
                      elif leaguee == "ncaaw":
                          league = "womens-college-" + sport
                      else:
                          league = leaguee
                      team = team_list[team_state]
                      iteration = 0
                      in_progress_only = True
                      group = get_group(team, league)
  
      pygame.display.flip()
      clock.tick(fps)
  pygame.quit()
  

utils.py, helper functions for Scoreboard

        # Tyler Bisk, tjb274
        # Final Project
        # 12/5/23
        import yaml
        
        
        def get_group(team, league):
            if team == "Big Ten":
                if league == "college-football":
                    group = "enable=groups&groups=5"
                else:
                    group = "enable=groups&groups=7"
            elif team == "Ivy League":
                if league == "college-football":
                    group = "enable=groups&groups=22"
                else:
                    group = "enable=groups&groups=12"
            elif team == "ACC":
                if league == "college-football":
                    group = "enable=groups&groups=1"
                else:
                    group = "enable=groups&groups=2"
            elif team == "PAC-12":
                if league == "college-football":
                    group = "enable=groups&groups=9"
                else:
                    group = "enable=groups&groups=21"
            elif team == "SEC":
                if league == "college-football":
                    group = "enable=groups&groups=8"
                else:
                    group = "enable=groups&groups=23"
            elif team == "Big 12":
                if league == "college-football":
                    group = "enable=groups&groups=4"
                else:
                    group = "enable=groups&groups=8"
            else:
                group = ""
        
            return group
        
        
        def read_values(username):
            f = open(f"/home/{username}/scoreboard/config.yml", "r")
            config = yaml.safe_load(f)
            sport = config["sport"]
            league = config["league"]
            team = config["team"]
            in_progress_only = config["in-progress-only"]
            group = get_group(team, league)
            return sport, league, team, in_progress_only, group
        

consts.py, file containing constant lists and structs

        # Tyler Bisk, tjb274
        # Final Project
        # 12/5/23
        from dataclasses import dataclass
        
        
        @dataclass
        class TeamData:
            id: str
            name: str
            shortName: str
            abbreviation: str
            rank: int
            record: str
            logo: str
            color1: str
            color2: str
            score: str
        
        
        @dataclass
        class AthleteData:
            id: str
            name: str
            rank: str
            flag: str
            score: str
        
        
        @dataclass
        class Situation:
            period: int
            clock: str
            down_to_go: str
            location: str
            home_timeouts: int
            away_timeouts: int
            possession: str
            balls: int
            stikes: int
            outs: int
            onFirst: bool
            onSecond: bool
            onThird: bool
            last_play: str
        
        
        nfl_list = [
            "All",
            "Arizona Cardinals",
            "Atlanta Falcons",
            "Baltimore Ravens",
            "Buffalo Bills",
            "Carolina Panthers",
            "Chicago Bears",
            "Cincinnati Bengals",
            "Cleveland Browns",
            "Dallas Cowboys",
            "Denver Broncos",
            "Detroit Lions",
            "Green Bay Packers",
            "Houston Texans",
            "Indianapolis Colts",
            "Jacksonville Jaguars",
            "Kansas City Chiefs",
            "Las Vegas Raiders",
            "Los Angeles Chargers",
            "Los Angeles Rams",
            "Miami Dolphins",
            "Minnesota Vikings",
            "New England Patriots",
            "New Orleans Saints",
            "New York Giants",
            "New York Jets",
            "Philadelphia Eagles",
            "Pittsburgh Steelers",
            "San Francisco 49ers",
            "Seattle Seahawks",
            "Tampa Bay Buccaneers",
            "Tennessee Titans",
            "Washington Commanders",
        ]
        
        nhl_list = [
            "All",
            "Anaheim Ducks",
            "Arizona Coyotes",
            "Boston Bruins",
            "Buffalo Sabres",
            "Calgary Flames",
            "Carolina Hurricanes",
            "Chicago Blackhawks",
            "Colorado Avalanche",
            "Columbus Blue Jackets",
            "Dallas Stars",
            "Detroit Red Wings",
            "Edmonton Oilers",
            "Florida Panthers",
            "Los Angeles Kings",
            "Minnesota Wild",
            "Montreal Canadiens",
            "Nashville Predators",
            "New Jersey Devils",
            "New York Islanders",
            "New York Rangers",
            "Ottawa Senators",
            "Philadelphia Flyers",
            "Pittsburgh Penguins",
            "San Jose Sharks",
            "Seattle Kraken",
            "St. Louis Blues",
            "Tampa Bay Lightning",
            "Toronto Maple Leafs",
            "Vancouver Canucks",
            "Vegas Golden Knights",
            "Washington Capitals",
            "Winnipeg Jets",
        ]
        
        nba_list = [
            "All",
            "Atlanta Hawks",
            "Boston Celtics",
            "Brooklyn Nets",
            "Charlotte Hornets",
            "Chicago Bulls",
            "Cleveland Cavaliers",
            "Dallas Mavericks",
            "Denver Nuggets",
            "Detroit Pistons",
            "Golden State Warriors",
            "Houston Rockets",
            "Indiana Pacers",
            "LA Clippers",
            "Los Angeles Lakers",
            "Memphis Grizzlies",
            "Miami Heat",
            "Milwaukee Bucks",
            "Minnesota Timberwolves",
            "New Orleans Pelicans",
            "New York Knicks",
            "Oklahoma City Thunder",
            "Orlando Magic",
            "Philadelphia 76ers",
            "Phoenix Suns",
            "Portland Trail Blazers",
            "Sacramento Kings",
            "San Antonio Spurs",
            "Toronto Raptors",
            "Utah Jazz",
            "Washington Wizards",
        ]
        
        ncaa_list = [
            "Top 25",
            "ACC",
            "Big 12",
            "Big Ten",
            "Ivy League",
            "PAC-12",
            "SEC",
            "Alabama rgb(178, 0, 0) Tide",
            "Arkansas Razorbacks",
            "Auburn Tigers",
            "Baylor Bears",
            "Bentley Falcons",
            "Boston College Eagles",
            "Brown Bears",
            "California Golden Bears",
            "Clemson Tigers",
            "Colgate Raiders",
            "Colorado Buffaloes",
            "Columbia Lions",
            "Cornell Big Red",
            "Dartmouth Big Green",
            "Duke Blue Devils",
            "Florida Gators",
            "Florida State Seminoles",
            "Georgia Bulldogs",
            "Harvard rgb(178, 0, 0)",
            "Holy Cross Crusaders",
            "Illinois Fighting Illini",
            "Indiana Hoosiers",
            "Iowa Hawkeyes",
            "Iowa State Cyclones",
            "Kansas Jayhawks",
            "Kansas State Wildcats",
            "Kentucky Wildcats",
            "LSU Tigers",
            "Maryland Terrapins",
            "Merrimack Warriors",
            "Miami Hurricanes",
            "Michigan State Spartans",
            "Michigan Wolverines",
            "Minnesota Golden Gophers",
            "Mississippi State Bulldogs",
            "Missouri Tigers",
            "Nebraska Cornhuskers",
            "North Carolina Tar Heels",
            "Northwestern Wildcats",
            "Ohio State Buckeyes",
            "Oklahoma Sooners",
            "Oklahoma State Cowboys",
            "Oregon Ducks",
            "Oregon State Beavers",
            "Pennsylvania Quakers",
            "Penn State Nittany Lions",
            "Pittsburgh Panthers",
            "Princeton Tigers",
            "Quinnipiac Bobcats",
            "Rutgers Scarlet Knights",
            "Stanford Cardinal",
            "St. Lawrence Saints",
            "Syracuse Orange",
            "TCU Horned Frogs",
            "Tennessee Volunteers",
            "Texas A&M Aggies",
            "Texas Longhorns",
            "Texas Tech Red Raiders",
            "UCLA Bruins",
            "USC Trojans",
            "Utah Utes",
            "Vanderbilt Commodores",
            "Virginia Cavaliers",
            "Virginia Tech Hokies",
            "Washington Huskies",
            "Wisconsin Badgers",
            "Yale Bulldogs",
        ]
        
        sport_list = ["Hockey", "Football", "Basketball"]
        
        hockey_league_list = ["NHL", "NCAAM"]
        
        basketball_league_list = ["NBA", "NCAAM", "NCAAW"]
        
        football_league_list = ["NFL", "NCAA"]
        
        
        demo_list = ["PRE GAME", "IN PROGRESS", "GOAL", "POST GAME"]
        

config.yml, default values for display

          sport:
            basketball
          league:
            mens-college-basketball
          team:
            Ivy League
          in-progress-only:
            True