From b749c45a9ad8c45e924f659855241bd842fdec50 Mon Sep 17 00:00:00 2001 From: Tyler Porter Date: Mon, 11 Mar 2024 01:41:02 -0400 Subject: [PATCH] Game screens, loading config and colors, rendering game states --- rewrite/colors/README.md | 8 + rewrite/colors/scoreboard.json.example | 306 ++++++++++++++++ rewrite/colors/teams.json.example | 473 +++++++++++++++++++++++++ rewrite/config/__init__.py | 19 +- rewrite/config/colors.py | 33 ++ rewrite/config/layout.py | 43 ++- rewrite/data/__init__.py | 4 +- rewrite/data/game.py | 111 +++++- rewrite/data/schedule.py | 33 +- rewrite/data/screen_request.py | 4 +- rewrite/data/status.py | 389 +++++++++++++++++++- rewrite/data/update_status.py | 15 + rewrite/screens/__init__.py | 4 +- rewrite/screens/base.py | 6 +- rewrite/screens/clock.py | 4 +- rewrite/screens/game.py | 17 - rewrite/screens/games/base.py | 14 + rewrite/screens/games/live_game.py | 14 + rewrite/screens/games/postgame.py | 46 +++ rewrite/screens/games/pregame.py | 20 ++ rewrite/screens/screen_manager.py | 16 +- rewrite/screens/static.py | 8 +- rewrite/screens/weather.py | 7 +- rewrite/utils/__init__.py | 9 + 24 files changed, 1530 insertions(+), 73 deletions(-) create mode 100644 rewrite/colors/README.md create mode 100644 rewrite/colors/scoreboard.json.example create mode 100644 rewrite/colors/teams.json.example create mode 100644 rewrite/config/colors.py create mode 100644 rewrite/data/update_status.py delete mode 100644 rewrite/screens/game.py create mode 100644 rewrite/screens/games/base.py create mode 100644 rewrite/screens/games/live_game.py create mode 100644 rewrite/screens/games/postgame.py create mode 100644 rewrite/screens/games/pregame.py diff --git a/rewrite/colors/README.md b/rewrite/colors/README.md new file mode 100644 index 00000000..20d111c1 --- /dev/null +++ b/rewrite/colors/README.md @@ -0,0 +1,8 @@ +These JSON files are used to determine the colors for pretty much every element that's renderered. + +# Custom Colors + +You can edit these colors to display parts of the scoreboard in any way you choose. Simply copy the file corresponding to the colors you wish to customize to the same filename without the `.example` extension. These JSON files only need to contain the parts you wish to override but it's often easier to just make a copy of the full example file and edit the values you want to change. + +## Examples +If you want to edit the color of some of the teams, copy `teams.json.example` to a new file called `teams.json`, then edit the `"r"`, `"g"` and `"b"` values for the colors you wish to change in that new file. If you want to customize the color of the scrolling text on the final screen, copy `scoreboard.json.example` to `scoreboard.json` and edit the `"final"->"scrolling_text"` keys to your liking. Your customized colors will always take precedence. diff --git a/rewrite/colors/scoreboard.json.example b/rewrite/colors/scoreboard.json.example new file mode 100644 index 00000000..b016193b --- /dev/null +++ b/rewrite/colors/scoreboard.json.example @@ -0,0 +1,306 @@ +{ + "bases": { + "1B": { + "r": 255, + "g": 235, + "b": 59 + }, + "2B": { + "r": 255, + "g": 235, + "b": 59 + }, + "3B": { + "r": 255, + "g": 235, + "b": 59 + } + }, + "final": { + "inning": { + "r": 255, + "g": 235, + "b": 59 + }, + "scrolling_text": { + "r": 255, + "g": 235, + "b": 59 + }, + "nohit_text": { + "r": 255, + "g": 50, + "b": 50 + } + }, + "inning": { + "break": { + "number": { + "r": 255, + "g": 235, + "b": 59 + }, + "text": { + "r": 255, + "g": 235, + "b": 59 + }, + "due_up": { + "r": 255, + "g": 235, + "b": 59 + }, + "due_up_divider": { + "r": 255, + "g": 235, + "b": 59 + }, + "due_up_names": { + "r": 255, + "g": 235, + "b": 59 + } + }, + "number": { + "r": 255, + "g": 235, + "b": 59 + }, + "arrow": { + "up": { + "r": 255, + "g": 235, + "b": 59 + }, + "down": { + "r": 255, + "g": 235, + "b": 59 + } + } + }, + "outs": { + "fill": { + "1": { + "r": 255, + "g": 235, + "b": 59 + } + }, + "1": { + "r": 255, + "g": 235, + "b": 59 + }, + "2": { + "r": 255, + "g": 235, + "b": 59 + }, + "3": { + "r": 255, + "g": 235, + "b": 59 + } + }, + "atbat": { + "batter": { + "r": 255, + "g": 235, + "b": 59 + }, + "pitcher": { + "r": 255, + "g": 235, + "b": 59 + }, + "pitch": { + "r": 255, + "g": 255, + "b": 255 + }, + "pitch_count": { + "r": 255, + "g": 255, + "b": 255 + }, + "play_result": { + "r": 255, + "g": 235, + "b": 59 + }, + "strikeout": { + "r": 255, + "g": 0, + "b": 0 + } + }, + "batter_count": { + "r": 255, + "g": 235, + "b": 59 + }, + "nohit_text": { + "r": 255, + "g": 110, + "b": 110 + }, + "perfect_game_text": { + "r": 255, + "g": 110, + "b": 110 + }, + "pregame": { + "matchup": { + "r": 255, + "g": 235, + "b": 59 + }, + "scrolling_text": { + "r": 255, + "g": 235, + "b": 59 + }, + "start_time": { + "r": 255, + "g": 235, + "b": 59 + }, + "warmup_text": { + "r": 255, + "g": 235, + "b": 59 + } + }, + "status": { + "text": { + "r": 255, + "g": 235, + "b": 59 + }, + "scrolling_text": { + "r": 255, + "g": 235, + "b": 59 + } + }, + "standings": { + "nl": { + "divider": { + "r": 0, + "g": 0, + "b": 255 + } + }, + "al": { + "divider": { + "r": 255, + "g": 0, + "b": 0 + } + }, + "background": { + "r": 37, + "g": 102, + "b": 30 + }, + "divider": { + "r": 13, + "g": 35, + "b": 11 + }, + "stat": { + "r": 171, + "g": 181, + "b": 170 + }, + "team": { + "name": { + "r": 171, + "g": 181, + "b": 170 + }, + "elim": { + "r": 190, + "g": 180, + "b": 160 + }, + "clinched": { + "r": 180, + "g": 210, + "b": 175 + }, + "stat": { + "r": 171, + "g": 181, + "b": 170 + } + } + }, + "offday": { + "scrolling_text": { + "r": 255, + "g": 235, + "b": 59 + }, + "time": { + "r": 255, + "g": 235, + "b": 59 + }, + "conditions": { + "r": 255, + "g": 235, + "b": 59 + }, + "temperature": { + "r": 255, + "g": 235, + "b": 59 + }, + "wind_speed": { + "r": 255, + "g": 235, + "b": 59 + }, + "wind_dir": { + "r": 255, + "g": 235, + "b": 59 + }, + "wind": { + "r": 255, + "g": 235, + "b": 59 + }, + "weather_icon": { + "r": 255, + "g": 235, + "b": 59 + } + }, + "network": { + "background": { + "r": 255, + "g": 0, + "b": 0 + }, + "text": { + "r": 255, + "g": 255, + "b": 255 + } + }, + "default": { + "background": { + "r": 7, + "g": 14, + "b": 25 + }, + "text": { + "r": 255, + "g": 235, + "b": 59 + } + } +} diff --git a/rewrite/colors/teams.json.example b/rewrite/colors/teams.json.example new file mode 100644 index 00000000..8ed4648c --- /dev/null +++ b/rewrite/colors/teams.json.example @@ -0,0 +1,473 @@ +{ + "az": { + "home": { + "r": 166, + "g": 25, + "b": 46 + }, + "text": { + "r": 217, + "g": 200, + "b": 157 + }, + "accent": { + "r": 0, + "g": 0, + "b": 0 + } + }, + "atl": { + "home": { + "r": 12, + "g": 35, + "b": 64 + }, + "accent": { + "r": 186, + "g": 12, + "b": 47 + } + }, + "bal": { + "home": { + "r": 252, + "g": 76, + "b": 2 + }, + "text": { + "r": 0, + "g": 0, + "b": 0 + }, + "accent": { + "r": 255, + "g": 255, + "b": 255 + } + }, + "bos": { + "home": { + "r": 200, + "g": 16, + "b": 46 + }, + "accent": { + "r": 12, + "g": 35, + "b": 64 + } + }, + "chc": { + "home": { + "r": 0, + "g": 47, + "b": 108 + }, + "accent": { + "r": 200, + "g": 16, + "b": 46 + } + }, + "cws": { + "home": { + "r": 0, + "g": 0, + "b": 0 + }, + "text": { + "r": 141, + "g": 144, + "b": 147 + }, + "accent": { + "r": 255, + "g": 255, + "b": 255 + } + }, + "cin": { + "home": { + "r": 186, + "g": 12, + "b": 47 + }, + "accent": { + "r":0, + "g":0, + "b":0 + } + }, + "cle": { + "home": { + "r": 15, + "g": 34, + "b": 62 + }, + "accent": { + "r": 227, + "g": 25, + "b": 55 + } + }, + "col": { + "home": { + "r": 51, + "g": 0, + "b": 114 + }, + "text": { + "r": 141, + "g": 144, + "b": 147 + }, + "accent": { + "r": 0, + "g": 0, + "b": 0 + } + }, + "det": { + "home": { + "r": 12, + "g": 35, + "b": 64 + }, + "text": { + "r": 250, + "g": 70, + "b": 22 + }, + "accent": { + "r": 250, + "g": 70, + "b": 22 + } + }, + "hou": { + "home": { + "r": 4, + "g": 30, + "b": 66 + }, + "text": { + "r": 207, + "g": 69, + "b": 32 + }, + "accent": { + "r": 229, + "g": 114, + "b": 0 + } + }, + "kc": { + "home": { + "r": 0, + "g": 45, + "b": 114 + }, + "accent": { + "r": 137, + "g": 115, + "b": 76 + } + }, + "laa": { + "home": { + "r": 186, + "g": 12, + "b": 47 + }, + "accent": { + "r": 12, + "g": 35, + "b": 64 + } + }, + "lad": { + "home": { + "r": 0, + "g": 47, + "b": 108 + }, + "text": { + "r": 255, + "g": 255, + "b": 255 + }, + "accent": { + "r": 145, + "g": 157, + "b": 157 + } + }, + "mia": { + "home": { + "r": 0, + "g": 0, + "b": 0 + }, + "text": { + "r": 0, + "g": 163, + "b": 224 + }, + "accent": { + "r": 239, + "g": 51, + "b": 64 + } + }, + "mil": { + "home": { + "r": 19, + "g": 41, + "b": 75 + }, + "text": { + "r": 255, + "g": 199, + "b": 44 + }, + "accent": { + "r": 0, + "g": 61, + "b": 165 + } + }, + "min": { + "home": { + "r": 12, + "g": 35, + "b": 64 + }, + "accent": { + "r": 186, + "g": 12, + "b": 47 + } + }, + "nym": { + "home": { + "r": 0, + "g": 45, + "b": 114 + }, + "text": { + "r": 252, + "g": 76, + "b": 2 + }, + "accent": { + "r": 255, + "g": 255, + "b": 255 + } + }, + "nyy": { + "home": { + "r": 12, + "g": 35, + "b": 64 + } + }, + "oak": { + "home": { + "r": 2, + "g": 70, + "b": 56 + }, + "text": { + "r": 255, + "g": 184, + "b": 28 + }, + "accent": { + "r": 255, + "g": 184, + "b": 28 + } + }, + "phi": { + "home": { + "r": 186, + "g": 12, + "b": 47 + }, + "accent": { + "r": 0, + "g": 45, + "b": 114 + } + }, + "pit": { + "home": { + "r": 0, + "g": 0, + "b": 0 + }, + "text": { + "r": 255, + "g": 199, + "b": 44 + }, + "accent": { + "r": 255, + "g": 199, + "b": 44 + } + }, + "sd": { + "home": { + "r": 62, + "g": 52, + "b": 47 + }, + "text": { + "r": 255, + "g": 199, + "b": 44 + }, + "accent": { + "r": 183, + "g": 169, + "b": 154 + } + }, + "sf": { + "home": { + "r": 250, + "g": 70, + "b": 22 + }, + "text": { + "r": 0, + "g": 0, + "b": 0 + }, + "accent": { + "r": 239, + "g": 209, + "b": 159 + } + }, + "sea": { + "home": { + "r": 12, + "g": 44, + "b": 86 + }, + "text": { + "r": 141, + "g": 144, + "b": 147 + }, + "accent": { + "r": 0, + "g": 104, + "b": 94 + } + }, + "stl": { + "home": { + "r": 186, + "g": 12, + "b": 47 + }, + "accent": { + "r": 12, + "g": 35, + "b": 64 + } + }, + "tb": { + "home": { + "r": 4, + "g": 30, + "b": 66 + }, + "accent": { + "r": 105, + "g": 179, + "b": 231 + } + }, + "tex": { + "home": { + "r": 0, + "g": 45, + "b": 114 + }, + "accent": { + "r": 186, + "g": 12, + "b": 47 + } + }, + "tor": { + "home": { + "r": 0, + "g": 61, + "b": 165 + }, + "accent": { + "r": 108, + "g": 172, + "b": 228 + } + }, + "wsh": { + "home": { + "r": 186, + "g": 12, + "b": 47 + }, + "accent": { + "r": 4, + "g": 30, + "b": 66 + } + }, + "nl": { + "home": { + "r": 158, + "g": 31, + "b": 22 + }, + "accent": { + "r": 20, + "g": 34, + "b": 88 + } + }, + "al": { + "home": { + "r": 20, + "g": 34, + "b": 88 + }, + "accent": { + "r": 158, + "g": 31, + "b": 22 + } + }, + "default": { + "home": { + "r": 7, + "g": 14, + "b": 25 + }, + "accent": { + "r": 255, + "g": 255, + "b": 255 + }, + "text": { + "r": 255, + "g": 255, + "b": 255 + } + } +} diff --git a/rewrite/config/__init__.py b/rewrite/config/__init__.py index 42d6fd69..27ae9cfe 100644 --- a/rewrite/config/__init__.py +++ b/rewrite/config/__init__.py @@ -1,12 +1,15 @@ -import json, os +import logging from config.layout import Layout +from config.colors import Colors from utils import logger as ScoreboardLogger from utils import deep_update, read_json class Config: + REFERENCE_FILENAME = "config.json.example" + def __init__(self, config_filename, width, height): self.width = width self.height = height @@ -15,17 +18,19 @@ def __init__(self, config_filename, width, height): config = self.__fetch_config(config_filename) self.__parse_config(config) + self.__set_log_level() + self.layout = Layout(width, height) + self.colors = Colors() def __fetch_config(self, name): """ Loads a config (JSON-formatted) with a custom filename. Falls back to a default if not found. """ filename = f"{name}.json" - reference_filename = f"config.json.example" custom_config = read_json(filename) - reference_config = read_json(reference_filename) + reference_config = read_json(Config.REFERENCE_FILENAME) if custom_config: # Retain only the values that are valid. @@ -66,3 +71,11 @@ def __parse_config(self, config): setattr(self, f"{key}_{value}", config[key][value]) else: setattr(self, key, config[key]) + + def __set_log_level(self): + # As early as possible, set the log level for the custom logger. + logger = logging.getLogger("mlbled") + if self.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.WARNING) diff --git a/rewrite/config/colors.py b/rewrite/config/colors.py new file mode 100644 index 00000000..851e448d --- /dev/null +++ b/rewrite/config/colors.py @@ -0,0 +1,33 @@ +import os, sys + +from utils import logger as ScoreboardLogger +from utils import deep_update, read_json + + +class Colors: + COLORS_DIRECTORY = os.path.abspath(os.path.join(__file__, "../../colors")) + + TEAM_COLORS_REFERENCE_FILENAME = "teams.json.example" + SCOREBOARD_COLORS_REFERENCE_FILENAME = "scoreboard.json.example" + + def __init__(self): + self._team_json = self.__fetch_colors(Colors.TEAM_COLORS_REFERENCE_FILENAME) + self._scoreboard_json = self.__fetch_colors(Colors.SCOREBOARD_COLORS_REFERENCE_FILENAME) + + def __fetch_colors(self, reference_filename): + filename = reference_filename.strip(".example") + reference_colors = read_json(os.path.join(Colors.COLORS_DIRECTORY, reference_filename)) + if not reference_colors: + ScoreboardLogger.error( + "Invalid {} reference color file. Make sure {} exists in colors/".format(filename, filename) + ) + sys.exit(1) + + custom_colors = read_json(os.path.join(Colors.COLORS_DIRECTORY, filename)) + if custom_colors: + ScoreboardLogger.info("Custom '%s.json' colors found. Merging with default reference colors.", filename) + colors = deep_update(reference_colors, custom_colors) + + return colors + + return reference_colors diff --git a/rewrite/config/layout.py b/rewrite/config/layout.py index 1e1564a0..46d8eef9 100644 --- a/rewrite/config/layout.py +++ b/rewrite/config/layout.py @@ -2,12 +2,18 @@ from utils import logger as ScoreboardLogger from utils.font import FontCache -from utils import deep_update, read_json +from utils import deep_update, read_json, value_at_keypath + +from collections import namedtuple class Layout: - LAYOUT_DIRECTORY = os.path.abspath(os.path.join("../../coordinates")) + class LayoutNotFound(Exception): + pass + + LAYOUT_DIRECTORY = os.path.abspath(os.path.join(__file__, "../../coordinates")) FONTNAME_DEFAULT = "4x6" + FONTNAME_KEY = "font_name" def __init__(self, width, height): self.width = width @@ -17,22 +23,41 @@ def __init__(self, width, height): self._json = self.__fetch_layout() + def coords(self, keypath): + try: + coord_dict = value_at_keypath(self._json, keypath) + except KeyError as e: + raise e + + if not isinstance(coord_dict, dict): # or not self.state in Layout.AVAILABLE_OPTIONAL_KEYS: + return coord_dict + + # TODO: Handle state + # if self.state in coord_dict: + # return coord_dict[self.state] + + coord_constructor = namedtuple("Coords", coord_dict.keys()) + + return coord_constructor(**coord_dict) + def font(self, font_name): """ Fetches a font from the font cache. """ - return self.font_cache.fetch_font(font_name) + return (self.font_cache.fetch_font(font_name), self.font_size(font_name)) + + def font_for(self, keypath): + font = value_at_keypath(self._json, keypath) + font_name = font.get(Layout.FONTNAME_KEY, Layout.FONTNAME_DEFAULT) + + return self.font(font_name) def font_size(self, font_name): return self.font_cache.font_size(font_name) - @property - def default_font(self): - return self.font_cache.default_font - def __fetch_layout(self): - filename = "coordinates/w{}h{}.json".format(self.width, self.height) - reference_filename = "{}.example".format(filename) + filename = os.path.join(Layout.LAYOUT_DIRECTORY, f"w{self.width}h{self.height}.json") + reference_filename = os.path.join(Layout.LAYOUT_DIRECTORY, f"{filename}.example") reference_layout = read_json(reference_filename) if not reference_layout: # Unsupported coordinates diff --git a/rewrite/data/__init__.py b/rewrite/data/__init__.py index 731ebb96..e73bb754 100644 --- a/rewrite/data/__init__.py +++ b/rewrite/data/__init__.py @@ -7,5 +7,5 @@ def __init__(self, screen_manager): self.schedule = Schedule(self) - def request_next_screen(self, screen): - self.screen_manager.request_next_screen(screen) + def request_next_screen(self, screen, **kwargs): + self.screen_manager.request_next_screen(screen, **kwargs) diff --git a/rewrite/data/game.py b/rewrite/data/game.py index f8bfbef2..9c0e0c6f 100644 --- a/rewrite/data/game.py +++ b/rewrite/data/game.py @@ -1,20 +1,119 @@ -from data.status import Status +import statsapi, time + +from datetime import datetime as dt + +from data.update_status import UpdateStatus +from data import status as GameState + +from utils import logger as ScoreboardLogger class Game: + API_FIELDS = ( + "gameData,game,id,datetime,dateTime,officialDate,flags,noHitter,perfectGame,status,detailedState,abstractGameState," + + "reason,probablePitchers,teams,home,away,abbreviation,teamName,record,wins,losses,players,id,boxscoreName,fullName,liveData,plays," + + "currentPlay,result,eventType,playEvents,isPitch,pitchData,startSpeed,details,type,code,description,decisions," + + "winner,loser,save,id,linescore,outs,balls,strikes,note,inningState,currentInning,currentInningOrdinal,offense," + + "batter,inHole,onDeck,first,second,third,defense,pitcher,boxscore,teams,runs,players,seasonStats,pitching,wins," + + "losses,saves,era,hits,errors,stats,pitching,numberOfPitches,weather,condition,temp,wind" + ) + + SCHEDULE_API_FIELDS = "dates,date,games,status,detailedState,abstractGameState,reason" + + REFRESH_RATE = 10 # seconds + @staticmethod - def from_schedule(game_data): + def from_schedule(game_data, update=False): game = Game(game_data) - if game.update() == Status.SUCCESS: + if not update or game.update(True) == UpdateStatus.SUCCESS: return game return None def __init__(self, data): - self._data = data + self.data = data self.id = data["game_id"] + self.date = data["game_date"] + self.status = data["status"] + self.updated_at = time.time() + + self._status = None + + def update(self, force=False): + if force or self.__should_update(): + self.updated_at = time.time() + try: + ScoreboardLogger.log("Fetching data for game %s", str(self.id)) + live_data = statsapi.get("game", {"gamePk": self.id, "fields": Game.API_FIELDS}) + # TODO: Re-implement the delay buffer + # we add a delay to avoid spoilers. During construction, this will still yield live data, but then + # it will recycle that data until the queue is full. + # self._data_wait_queue.push(live_data) + # self._api_data = self._data_wait_queue.peek() + self.data = live_data + self._status = self.data["gameData"]["status"] + + ScoreboardLogger.log(self._status) + + if live_data["gameData"]["datetime"]["officialDate"] > self.date: + # this is odd, but if a game is postponed then the 'game' endpoint gets the rescheduled game + ScoreboardLogger.log("Getting game status from schedule for game with strange date!") + try: + scheduled = statsapi.get( + "schedule", {"gamePk": self.id, "sportId": 1, "fields": Game.SCHEDULE_API_FIELDS} + ) + self._status = next( + game["games"][0]["status"] for game in scheduled["dates"] if game["date"] == self.date + ) + + except Exception as exception: + ScoreboardLogger.error(exception) + ScoreboardLogger.error("Failed to get game status from schedule") + + if self._status is None: + self.status = GameState.UNKNOWN + else: + self.status = self._status.get("detailedState", GameState.UNKNOWN) + + return UpdateStatus.SUCCESS + except Exception as exception: + ScoreboardLogger.exception(exception) + ScoreboardLogger.exception("Networking Error while refreshing the current game data.") + + return UpdateStatus.FAIL + + return UpdateStatus.DEFERRED + + def is_pregame(self): + """Returns whether the game is in a pregame state""" + return self.status in GameState.GAME_STATE_PREGAME + + def is_complete(self): + """Returns whether the game has been completed""" + return self.status in GameState.GAME_STATE_COMPLETE + + def is_live(self): + """Returns whether the game is currently live""" + return self.status in GameState.GAME_STATE_LIVE + + def is_irregular(self): + """Returns whether game is in an irregular state, such as delayed, postponed, cancelled, + or in a challenge.""" + return self.status in GameState.GAME_STATE_IRREGULAR + + def is_fresh(self): + """Returns whether the game is in progress or is very recently complete. Game Over + comes between In Progress and Final and allows a few minutes to see the final outcome before + the rotation kicks in.""" + return self.status in GameState.GAME_STATE_FRESH + + def is_inning_break(inning_state): + """Returns whether a game is in an inning break (mid/end). Pass in the inning state.""" + return inning_state not in GameState.GAME_STATE_INNING_LIVE + + def datetime(self): + time_utc = self.data["gameData"]["datetime"]["dateTime"] - def update(self): - return Status.SUCCESS + return dt.fromisoformat(time_utc.replace("Z", "+00:00")) diff --git a/rewrite/data/schedule.py b/rewrite/data/schedule.py index 339d67e8..f360758f 100644 --- a/rewrite/data/schedule.py +++ b/rewrite/data/schedule.py @@ -1,8 +1,10 @@ -import datetime, statsapi, time +import statsapi, time + +from datetime import datetime as dt from utils import logger as ScoreboardLogger -from data.status import Status +from data.update_status import UpdateStatus from data.game import Game from screens import Screen @@ -24,21 +26,38 @@ def __init__(self, data): def update(self): self.last_update = time.time() - ScoreboardLogger.log(f"Updating schedule for {datetime.datetime.today()}") + ScoreboardLogger.log(f"Updating schedule for {dt.today()}") try: - self.__fetch_updated_schedule(datetime.datetime.today()) + self.__fetch_updated_schedule(dt.today()) except Exception as exception: ScoreboardLogger.exception("Networking error while refreshing schedule!") ScoreboardLogger.exception(exception) - return Status.FAIL + return UpdateStatus.FAIL + + # TODO: Filter to target game + game = self.games[0] - self.data.request_next_screen(Screen.GAME) + self.__request_transition_to_game(game) - return Status.SUCCESS + return UpdateStatus.SUCCESS def __fetch_updated_schedule(self, date): self._games = statsapi.schedule(date.strftime("%Y-%m-%d")) self.games = [Game.from_schedule(game) for game in self._games] + + def __request_transition_to_game(self, game): + next_screen = None + + if game.is_complete(): + next_screen = Screen.POSTGAME + elif game.is_pregame(): + next_screen = Screen.PREGAME + elif game.is_live(): + next_screen = Screen.LIVE_GAME + + if next_screen is not None: + game.update(True) + self.data.request_next_screen(next_screen, game=game) diff --git a/rewrite/data/screen_request.py b/rewrite/data/screen_request.py index 8f853524..9795c323 100644 --- a/rewrite/data/screen_request.py +++ b/rewrite/data/screen_request.py @@ -5,13 +5,13 @@ class ScreenRequest: class InvalidRequest(Exception): pass - def __init__(self, type, manager, *args): + def __init__(self, type, manager, **kwargs): # Required arguments self.type = type self.manager = manager # Screen-specific arguments - self.args = args + self.kwargs = kwargs if self.type not in Screen: raise ScreenRequest.InvalidRequest(f"Screen type {self.type} is not valid") diff --git a/rewrite/data/status.py b/rewrite/data/status.py index c636ff51..7cb14489 100644 --- a/rewrite/data/status.py +++ b/rewrite/data/status.py @@ -1,15 +1,386 @@ -from enum import Enum +"""This will/should eventually download/read the actual json where +the status data comes from. https://statsapi.mlb.com/api/v1/gameStatus/ statsapi.meta('gameStatus')""" +CANCELLED = "Cancelled" # Final +CANCELLED_COLD = "Cancelled: Cold" # Final +CANCELLED_FOG = "Cancelled: Fog" # Final +CANCELLED_COVID19 = "Cancelled: COVID-19" # Final +CANCELLED_INCLEMENT_WEATHER = "Cancelled: Inclement Weather" # Final +CANCELLED_LIGHTNING = "Cancelled: Lightning" # Final +CANCELLED_AIR_QUALITY = "Cancelled: Air Quality" # Final +CANCELLED_POWER = "Cancelled: Power" # Final +CANCELLED_RAIN = "Cancelled: Rain" # Final +CANCELLED_SNOW = "Cancelled: Snow" # Final +CANCELLED_TRAGEDY = "Cancelled: Tragedy" # Final +CANCELLED_VENUE = "Cancelled: Venue" # Final +CANCELLED_WET_GROUNDS = "Cancelled: Wet Grounds" # Final +CANCELLED_WIND = "Cancelled: Wind" # Final +COMPLETED_EARLY = "Completed Early" # Final +COMPLETED_EARLY_COVID19 = "Completed Early: COVID-19" # Final +COMPLETED_EARLY_COLD = "Completed Early: Cold" # Final +COMPLETED_EARLY_FOG = "Completed Early: Fog" # Final +COMPLETED_EARLY_INCLEMENT_WEATHER = "Completed Early: Inclement Weather" # Final +COMPLETED_EARLY_LIGHTNING = "Completed Early: Lightning" # Final +COMPLETED_EARLY_AIR_QUALITY = "Completed Early: Air Quality" # Final +COMPLETED_EARLY_MERCY_RULE = "Completed Early: Mercy Rule" # Final +COMPLETED_EARLY_POWER = "Completed Early: Power" # Final +COMPLETED_EARLY_RAIN = "Completed Early: Rain" # Final +COMPLETED_EARLY_SNOW = "Completed Early: Snow" # Final +COMPLETED_EARLY_TRAGEDY = "Completed Early: Tragedy" # Final +COMPLETED_EARLY_VENUE = "Completed Early: Venue" # Final +COMPLETED_EARLY_WET_GROUNDS = "Completed Early: Wet Grounds" # Final +COMPLETED_EARLY_WIND = "Completed Early: Wind" # Final +DELAYED = "Delayed" # Live +DELAYED_ABOUT_TO_RESUME = "Delayed: About to Resume" # Live +DELAYED_CEREMONY = "Delayed: Ceremony" # Live +DELAYED_COLD = "Delayed: Cold" # Live +DELAYED_COVID19 = "Delayed: COVID-19" # Live +DELAYED_FOG = "Delayed: Fog" # Live +DELAYED_INCLEMENT_WEATHER = "Delayed: Inclement Weather" # Live +DELAYED_LIGHTNING = "Delayed: Lightning" # Live +DELAYED_AIR_QUALITY = "Delayed: Air Quality" # Live +DELAYED_POWER = "Delayed: Power" # Live +DELAYED_RAIN = "Delayed: Rain" # Live +DELAYED_SNOW = "Delayed: Snow" # Live +DELAYED_START = "Delayed Start" # Preview +DELAYED_START_CEREMONY = "Delayed Start: Ceremony" # Preview +DELAYED_START_COLD = "Delayed Start: Cold" # Preview +DELAYED_START_COVID19 = "Delayed Start: COVID-19" # Preview +DELAYED_START_FOG = "Delayed Start: Fog" # Preview +DELAYED_START_INCLEMENT_WEATHER = "Delayed Start: Inclement Weather" # Preview +DELAYED_START_LIGHTNING = "Delayed Start: Lightning" # Preview +DELAYED_START_POWER = "Delayed Start: Power" # Preview +DELAYED_START_RAIN = "Delayed Start: Rain" # Preview +DELAYED_START_SNOW = "Delayed Start: Snow" # Preview +DELAYED_START_TRAGEDY = "Delayed Start: Tragedy" # Preview +DELAYED_START_VENUE = "Delayed Start: Venue" # Preview +DELAYED_START_AIR_QUALITY = "Delayed Start: Air Quality" # Preview +DELAYED_START_WET_GROUNDS = "Delayed Start: Wet Grounds" # Preview +DELAYED_START_WIND = "Delayed Start: Wind" # Preview +DELAYED_TRAGEDY = "Delayed: Tragedy" # Live +DELAYED_VENUE = "Delayed: Venue" # Live +DELAYED_WET_GROUNDS = "Delayed: Wet Grounds" # Live +DELAYED_WIND = "Delayed: Wind" # Live +FINAL = "Final" # Final +FINAL_TIED = "Final: Tied" # Final +FINAL_TIE_DECISION_BY_TIEBREAKER = "Final: Tie, decision by tiebreaker" # Final +FORFEIT = "Forfeit" # Final +FORFEIT_DELAY_OF_GAME = "Forfeit: Delay of game " # Final +FORFEIT_FAILURE_TO_APPEAR = "Forfeit: Failure to appear " # Final +FORFEIT_FAILURE_TO_FIELD_LINEUP = "Forfeit: Failure to field lineup " # Final +FORFEIT_FINAL = "Forfeit: Final" # Final +FORFEIT_GAME_OVER = "Forfeit: Game Over" # Final +FORFEIT_IGNORING_EJECTION = "Forfeit: Ignoring ejection " # Final +FORFEIT_INELIGIBLE_PLAYER = "Forfeit: Ineligible player " # Final +FORFEIT_REFUSES_TO_PLAY = "Forfeit: Refuses to play " # Final +FORFEIT_UNPLAYABLE_FIELD = "Forfeit: Unplayable field " # Final +FORFEIT_WILLFUL_RULE_VIOLATION = "Forfeit: Willful rule violation" # Final +GAME_OVER = "Game Over" # Final +GAME_OVER_TIED = "Game Over: Tied" # Final +GAME_OVER_TIE_DECISION_BY_TIEBREAKER = "Game Over: Tie, decision by tiebreaker" # Final +IN_PROGRESS = "In Progress" # Live +INSTANT_REPLAY = "Instant Replay" # Live +MANAGER_CHALLENGE = "Manager challenge" # Live +MANAGER_CHALLENGE_CATCHDROP_IN_OUTFIELD = "Manager challenge: Catch/drop in outfield" # Live +MANAGER_CHALLENGE_CATCHERS_INTERFERENCE = "Manager challenge: Catchers Interference" # Live +MANAGER_CHALLENGE_CLOSE_PLAY_AT_1ST = "Manager challenge: Close play at 1st" # Live +MANAGER_CHALLENGE_FAIRFOUL_IN_OUTFIELD = "Manager challenge: Fair/foul in outfield" # Live +MANAGER_CHALLENGE_FAN_INTERFERENCE = "Manager challenge: Fan interference" # Live +MANAGER_CHALLENGE_FORCE_PLAY = "Manager challenge: Force play" # Live +MANAGER_CHALLENGE_GROUNDS_RULE = "Manager challenge: Grounds rule" # Live +MANAGER_CHALLENGE_HIT_BY_PITCH = "Manager challenge: Hit by pitch" # Live +MANAGER_CHALLENGE_HOME_RUN = "Manager challenge: Home run" # Live +MANAGER_CHALLENGE_HOMEPLATE_COLLISION = "Manager challenge: Home-plate collision" # Live +MANAGER_CHALLENGE_MULTIPLE_ISSUES = "Manager challenge: Multiple issues" # Live +MANAGER_CHALLENGE_PASSING_RUNNERS = "Manager challenge: Passing runners" # Live +MANAGER_CHALLENGE_RECORD_KEEPING = "Manager challenge: Record keeping" # Live +MANAGER_CHALLENGE_RULES_CHECK = "Manager challenge: Rules check" # Live +MANAGER_CHALLENGE_SLIDE_INTERFERENCE = "Manager challenge: Slide interference" # Live +MANAGER_CHALLENGE_STADIUM_BOUNDARY_CALL = "Manager challenge: Stadium boundary call" # Live +MANAGER_CHALLENGE_TAG_PLAY = "Manager challenge: Tag play" # Live +MANAGER_CHALLENGE_TAGUP_PLAY = "Manager challenge: Tag-up play" # Live +MANAGER_CHALLENGE_TIMING_PLAY = "Manager challenge: Timing play" # Live +MANAGER_CHALLENGE_TOUCHING_A_BASE = "Manager challenge: Touching a base" # Live +MANAGER_CHALLENGE_TRAP_PLAY_IN_OUTFIELD = "Manager challenge: Trap play in outfield" # Live +POSTPONED = "Postponed" # Final +POSTPONED_COLD = "Postponed: Cold" # Final +POSTPONED_COVID19 = "Postponed: COVID-19" # Final +POSTPONED_FOG = "Postponed: Fog" # Final +POSTPONED_INCLEMENT_WEATHER = "Postponed: Inclement Weather" # Final +POSTPONED_LIGHTNING = "Postponed: Lightning" # Final +POSTPONED_AIR_QUALITY = "Postponed: Air Quality" # Final +POSTPONED_POWER = "Postponed: Power" # Final +POSTPONED_RAIN = "Postponed: Rain" # Final +POSTPONED_SNOW = "Postponed: Snow" # Final +POSTPONED_TRAGEDY = "Postponed: Tragedy" # Final +POSTPONED_VENUE = "Postponed: Venue" # Final +POSTPONED_WET_GROUNDS = "Postponed: Wet Grounds" # Final +POSTPONED_WIND = "Postponed: Wind" # Final +PREGAME = "Pre-Game" # Preview +SCHEDULED = "Scheduled" # Preview +SUSPENDED = "Suspended" # Live +SUSPENDED_ABOUT_TO_RESUME = "Suspended: About to Resume" # Live +SUSPENDED_APPEAL_UPHELD = "Suspended: Appeal Upheld" # Live +SUSPENDED_COLD = "Suspended: Cold" # Live +SUSPENDED_COVID19 = "Suspended: COVID-19" # Live +SUSPENDED_FOG = "Suspended: Fog" # Live +SUSPENDED_INCLEMENT_WEATHER = "Suspended: Inclement Weather" # Live +SUSPENDED_LIGHTNING = "Suspended: Lightning" # Live +SUSPENDED_AIR_QUALITY = "Suspended: Air Quality" # Live +SUSPENDED_POWER = "Suspended: Power" # Live +SUSPENDED_RAIN = "Suspended: Rain" # Live +SUSPENDED_SNOW = "Suspended: Snow" # Live +SUSPENDED_TRAGEDY = "Suspended: Tragedy" # Live +SUSPENDED_VENUE = "Suspended: Venue" # Live +SUSPENDED_WET_GROUNDS = "Suspended: Wet Grounds" # Live +SUSPENDED_WIND = "Suspended: Wind" # Live +UMPIRE_REVIEW = "Umpire review" # Live +UMPIRE_REVIEW_CATCHDROP_IN_OUTFIELD = "Umpire review: Catch/drop in outfield" # Live +UMPIRE_REVIEW_CLOSE_PLAY_AT_1ST = "Umpire review: Close play at 1st" # Live +UMPIRE_REVIEW_FAIRFOUL_IN_OUTFIELD = "Umpire review: Fair/foul in outfield" # Live +UMPIRE_REVIEW_FAN_INTERFERENCE = "Umpire review: Fan interference" # Live +UMPIRE_REVIEW_FORCE_PLAY = "Umpire review: Force play" # Live +UMPIRE_REVIEW_GROUNDS_RULE = "Umpire review: Grounds rule" # Live +UMPIRE_REVIEW_HIT_BY_PITCH = "Umpire review: Hit by pitch" # Live +UMPIRE_REVIEW_HOME_RUN = "Umpire review: Home run" # Live +UMPIRE_REVIEW_HOMEPLATE_COLLISION = "Umpire review: Home-plate collision" # Live +UMPIRE_REVIEW_MULTIPLE_ISSUES = "Umpire review: Multiple issues" # Live +UMPIRE_REVIEW_PASSING_RUNNERS = "Umpire review: Passing runners" # Live +UMPIRE_REVIEW_RECORD_KEEPING = "Umpire review: Record keeping" # Live +UMPIRE_REVIEW_RULES_CHECK = "Umpire review: Rules check" # Live +UMPIRE_REVIEW_SLIDE_INTERFERENCE = "Umpire review: Slide interference" # Live +UMPIRE_REVIEW_STADIUM_BOUNDARY_CALL = "Umpire review: Stadium boundary call" # Live +UMPIRE_REVIEW_TAG_PLAY = "Umpire review: Tag play" # Live +UMPIRE_REVIEW_TAGUP_PLAY = "Umpire review: Tag-up play" # Live +UMPIRE_REVIEW_TIMING_PLAY = "Umpire review: Timing play" # Live +UMPIRE_REVIEW_TOUCHING_A_BASE = "Umpire review: Touching a base" # Live +UMPIRE_REVIEW_TRAP_PLAY_IN_OUTFIELD = "Umpire review: Trap play in outfield" # Live +UMPIRE_REVIEW_SHIFT_VIOLATION = "Umpire review: Def Shift Violation" # Live +UMPIRE_CHALLENGE_PITCH_RESULT = "Umpire Challenge: Pitch Result" # Live +PLAYER_CHALLENGE_PITCH_RESULT = "Player challenge: Pitch Result" # Live +UNKNOWN = "Unknown" # Other +WARMUP = "Warmup" # Live +WRITING = "Writing" # Other +REVIEW = "Review" # Not in json -class Status(Enum): - SUCCESS = 2 - DEFERRED = 1 - FAIL = 0 +# TODO: Make sure this works in re-write +INNING_TOP = "Top" # Live +INNING_BOTTOM = "Bottom" # Live +GAME_STATE_INNING_LIVE = [INNING_TOP, INNING_BOTTOM] +GAME_STATE_LIVE = [ + IN_PROGRESS, + WARMUP, + INSTANT_REPLAY, + MANAGER_CHALLENGE, + MANAGER_CHALLENGE_CATCHDROP_IN_OUTFIELD, + MANAGER_CHALLENGE_CATCHERS_INTERFERENCE, + MANAGER_CHALLENGE_CLOSE_PLAY_AT_1ST, + MANAGER_CHALLENGE_FAIRFOUL_IN_OUTFIELD, + MANAGER_CHALLENGE_FAN_INTERFERENCE, + MANAGER_CHALLENGE_FORCE_PLAY, + MANAGER_CHALLENGE_GROUNDS_RULE, + MANAGER_CHALLENGE_HIT_BY_PITCH, + MANAGER_CHALLENGE_HOME_RUN, + MANAGER_CHALLENGE_HOMEPLATE_COLLISION, + MANAGER_CHALLENGE_MULTIPLE_ISSUES, + MANAGER_CHALLENGE_PASSING_RUNNERS, + MANAGER_CHALLENGE_RECORD_KEEPING, + MANAGER_CHALLENGE_RULES_CHECK, + MANAGER_CHALLENGE_SLIDE_INTERFERENCE, + MANAGER_CHALLENGE_STADIUM_BOUNDARY_CALL, + MANAGER_CHALLENGE_TAG_PLAY, + MANAGER_CHALLENGE_TAGUP_PLAY, + MANAGER_CHALLENGE_TIMING_PLAY, + MANAGER_CHALLENGE_TOUCHING_A_BASE, + MANAGER_CHALLENGE_TRAP_PLAY_IN_OUTFIELD, + UMPIRE_REVIEW, + UMPIRE_REVIEW_CATCHDROP_IN_OUTFIELD, + UMPIRE_REVIEW_CLOSE_PLAY_AT_1ST, + UMPIRE_REVIEW_FAIRFOUL_IN_OUTFIELD, + UMPIRE_REVIEW_FAN_INTERFERENCE, + UMPIRE_REVIEW_FORCE_PLAY, + UMPIRE_REVIEW_GROUNDS_RULE, + UMPIRE_REVIEW_HIT_BY_PITCH, + UMPIRE_REVIEW_HOME_RUN, + UMPIRE_REVIEW_HOMEPLATE_COLLISION, + UMPIRE_REVIEW_MULTIPLE_ISSUES, + UMPIRE_REVIEW_PASSING_RUNNERS, + UMPIRE_REVIEW_RECORD_KEEPING, + UMPIRE_REVIEW_RULES_CHECK, + UMPIRE_REVIEW_SLIDE_INTERFERENCE, + UMPIRE_REVIEW_STADIUM_BOUNDARY_CALL, + UMPIRE_REVIEW_TAG_PLAY, + UMPIRE_REVIEW_TAGUP_PLAY, + UMPIRE_REVIEW_TIMING_PLAY, + UMPIRE_REVIEW_TOUCHING_A_BASE, + UMPIRE_REVIEW_TRAP_PLAY_IN_OUTFIELD, + UMPIRE_REVIEW_SHIFT_VIOLATION, + UMPIRE_CHALLENGE_PITCH_RESULT, + PLAYER_CHALLENGE_PITCH_RESULT, +] -def ok(status): - return status in [Status.SUCCESS, Status.DEFERRED] +GAME_STATE_PREGAME = [SCHEDULED, PREGAME, WARMUP] +GAME_STATE_COMPLETE = [ + COMPLETED_EARLY, + COMPLETED_EARLY_COLD, + COMPLETED_EARLY_FOG, + COMPLETED_EARLY_INCLEMENT_WEATHER, + COMPLETED_EARLY_LIGHTNING, + COMPLETED_EARLY_AIR_QUALITY, + COMPLETED_EARLY_MERCY_RULE, + COMPLETED_EARLY_COVID19, + COMPLETED_EARLY_POWER, + COMPLETED_EARLY_RAIN, + COMPLETED_EARLY_SNOW, + COMPLETED_EARLY_TRAGEDY, + COMPLETED_EARLY_VENUE, + COMPLETED_EARLY_WET_GROUNDS, + COMPLETED_EARLY_WIND, + FINAL, + FINAL_TIE_DECISION_BY_TIEBREAKER, + FINAL_TIED, + GAME_OVER, + GAME_OVER_TIE_DECISION_BY_TIEBREAKER, + GAME_OVER_TIED, +] -def fail(status): - return status in [Status.FAIL] +GAME_STATE_FRESH = [IN_PROGRESS, GAME_OVER, GAME_OVER_TIED, GAME_OVER_TIE_DECISION_BY_TIEBREAKER] + +GAME_STATE_IRREGULAR = [ + CANCELLED, + CANCELLED_COLD, + CANCELLED_COVID19, + CANCELLED_FOG, + CANCELLED_INCLEMENT_WEATHER, + CANCELLED_LIGHTNING, + CANCELLED_AIR_QUALITY, + CANCELLED_POWER, + CANCELLED_RAIN, + CANCELLED_SNOW, + CANCELLED_TRAGEDY, + CANCELLED_VENUE, + CANCELLED_WET_GROUNDS, + CANCELLED_WIND, + DELAYED, + DELAYED_ABOUT_TO_RESUME, + DELAYED_CEREMONY, + DELAYED_COLD, + DELAYED_COVID19, + DELAYED_FOG, + DELAYED_INCLEMENT_WEATHER, + DELAYED_LIGHTNING, + DELAYED_AIR_QUALITY, + DELAYED_POWER, + DELAYED_RAIN, + DELAYED_SNOW, + DELAYED_START, + DELAYED_START_CEREMONY, + DELAYED_START_COLD, + DELAYED_START_COVID19, + DELAYED_START_AIR_QUALITY, + DELAYED_START_FOG, + DELAYED_START_INCLEMENT_WEATHER, + DELAYED_START_LIGHTNING, + DELAYED_START_POWER, + DELAYED_START_RAIN, + DELAYED_START_SNOW, + DELAYED_START_TRAGEDY, + DELAYED_START_VENUE, + DELAYED_START_WET_GROUNDS, + DELAYED_START_WIND, + DELAYED_TRAGEDY, + DELAYED_VENUE, + DELAYED_WET_GROUNDS, + DELAYED_WIND, + FORFEIT, + FORFEIT_DELAY_OF_GAME, + FORFEIT_FAILURE_TO_APPEAR, + FORFEIT_FAILURE_TO_FIELD_LINEUP, + FORFEIT_FINAL, + FORFEIT_GAME_OVER, + FORFEIT_IGNORING_EJECTION, + FORFEIT_INELIGIBLE_PLAYER, + FORFEIT_REFUSES_TO_PLAY, + FORFEIT_UNPLAYABLE_FIELD, + FORFEIT_WILLFUL_RULE_VIOLATION, + MANAGER_CHALLENGE, + MANAGER_CHALLENGE_CATCHDROP_IN_OUTFIELD, + MANAGER_CHALLENGE_CATCHERS_INTERFERENCE, + MANAGER_CHALLENGE_CLOSE_PLAY_AT_1ST, + MANAGER_CHALLENGE_FAIRFOUL_IN_OUTFIELD, + MANAGER_CHALLENGE_FAN_INTERFERENCE, + MANAGER_CHALLENGE_FORCE_PLAY, + MANAGER_CHALLENGE_GROUNDS_RULE, + MANAGER_CHALLENGE_HIT_BY_PITCH, + MANAGER_CHALLENGE_HOME_RUN, + MANAGER_CHALLENGE_HOMEPLATE_COLLISION, + MANAGER_CHALLENGE_MULTIPLE_ISSUES, + MANAGER_CHALLENGE_PASSING_RUNNERS, + MANAGER_CHALLENGE_RECORD_KEEPING, + MANAGER_CHALLENGE_RULES_CHECK, + MANAGER_CHALLENGE_SLIDE_INTERFERENCE, + MANAGER_CHALLENGE_STADIUM_BOUNDARY_CALL, + MANAGER_CHALLENGE_TAG_PLAY, + MANAGER_CHALLENGE_TAGUP_PLAY, + MANAGER_CHALLENGE_TIMING_PLAY, + MANAGER_CHALLENGE_TOUCHING_A_BASE, + MANAGER_CHALLENGE_TRAP_PLAY_IN_OUTFIELD, + POSTPONED, + POSTPONED_COLD, + POSTPONED_COVID19, + POSTPONED_FOG, + POSTPONED_INCLEMENT_WEATHER, + POSTPONED_LIGHTNING, + POSTPONED_AIR_QUALITY, + POSTPONED_POWER, + POSTPONED_RAIN, + POSTPONED_SNOW, + POSTPONED_TRAGEDY, + POSTPONED_VENUE, + POSTPONED_WET_GROUNDS, + POSTPONED_WIND, + SUSPENDED, + SUSPENDED_ABOUT_TO_RESUME, + SUSPENDED_APPEAL_UPHELD, + SUSPENDED_COLD, + SUSPENDED_COVID19, + SUSPENDED_FOG, + SUSPENDED_INCLEMENT_WEATHER, + SUSPENDED_LIGHTNING, + SUSPENDED_AIR_QUALITY, + SUSPENDED_POWER, + SUSPENDED_RAIN, + SUSPENDED_SNOW, + SUSPENDED_TRAGEDY, + SUSPENDED_VENUE, + SUSPENDED_WET_GROUNDS, + SUSPENDED_WIND, + UMPIRE_REVIEW, + UMPIRE_REVIEW_CATCHDROP_IN_OUTFIELD, + UMPIRE_REVIEW_CLOSE_PLAY_AT_1ST, + UMPIRE_REVIEW_FAIRFOUL_IN_OUTFIELD, + UMPIRE_REVIEW_FAN_INTERFERENCE, + UMPIRE_REVIEW_FORCE_PLAY, + UMPIRE_REVIEW_GROUNDS_RULE, + UMPIRE_REVIEW_HIT_BY_PITCH, + UMPIRE_REVIEW_HOME_RUN, + UMPIRE_REVIEW_HOMEPLATE_COLLISION, + UMPIRE_REVIEW_MULTIPLE_ISSUES, + UMPIRE_REVIEW_PASSING_RUNNERS, + UMPIRE_REVIEW_RECORD_KEEPING, + UMPIRE_REVIEW_RULES_CHECK, + UMPIRE_REVIEW_SLIDE_INTERFERENCE, + UMPIRE_REVIEW_STADIUM_BOUNDARY_CALL, + UMPIRE_REVIEW_TAG_PLAY, + UMPIRE_REVIEW_TAGUP_PLAY, + UMPIRE_REVIEW_TIMING_PLAY, + UMPIRE_REVIEW_TOUCHING_A_BASE, + UMPIRE_REVIEW_TRAP_PLAY_IN_OUTFIELD, + UMPIRE_REVIEW_SHIFT_VIOLATION, + UMPIRE_CHALLENGE_PITCH_RESULT, + PLAYER_CHALLENGE_PITCH_RESULT, + WRITING, + UNKNOWN, +] diff --git a/rewrite/data/update_status.py b/rewrite/data/update_status.py new file mode 100644 index 00000000..d13e6714 --- /dev/null +++ b/rewrite/data/update_status.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class UpdateStatus(Enum): + SUCCESS = 2 + DEFERRED = 1 + FAIL = 0 + + +def ok(status): + return status in [UpdateStatus.SUCCESS, UpdateStatus.DEFERRED] + + +def fail(status): + return status in [UpdateStatus.FAIL] diff --git a/rewrite/screens/__init__.py b/rewrite/screens/__init__.py index 9b57b62f..306abb2a 100644 --- a/rewrite/screens/__init__.py +++ b/rewrite/screens/__init__.py @@ -5,4 +5,6 @@ class Screen(Enum): STATIC = 0 CLOCK = 1 WEATHER = 2 - GAME = 3 + PREGAME = 3 + LIVE_GAME = 4 + POSTGAME = 5 diff --git a/rewrite/screens/base.py b/rewrite/screens/base.py index 742e4251..8fd6b44f 100644 --- a/rewrite/screens/base.py +++ b/rewrite/screens/base.py @@ -2,7 +2,7 @@ class ScreenBase: - def __init__(self, manager): + def __init__(self, manager, **_kwargs): self.manager = manager self.start_time = None @@ -51,3 +51,7 @@ def data(self): @property def config(self): return self.manager.config + + @property + def layout(self): + return self.config.layout diff --git a/rewrite/screens/clock.py b/rewrite/screens/clock.py index 59412568..4a9253a5 100644 --- a/rewrite/screens/clock.py +++ b/rewrite/screens/clock.py @@ -13,9 +13,9 @@ def render(self): time_format_str = "%H:%M%p" time_text = time.strftime(time_format_str) - font = self.config.layout.font("4x6") + font, font_size = self.config.layout.font("4x6") graphics.DrawText(self.canvas, font, 5, 5, (255, 255, 255), time_text) - if self.duration > self.MAX_DURATION_SECONDS * 1000: + if self.duration > ClockScreen.MAX_DURATION_SECONDS * 1000: self.manager.request_next_screen(Screen.WEATHER) diff --git a/rewrite/screens/game.py b/rewrite/screens/game.py deleted file mode 100644 index 1a995361..00000000 --- a/rewrite/screens/game.py +++ /dev/null @@ -1,17 +0,0 @@ -import os, time - -from driver import graphics - -from screens import Screen -from screens.base import ScreenBase - - -class GameScreen(ScreenBase): - MAX_DURATION_SECONDS = 3 - - def render(self): - game_text = "It's a game!" - - font = self.config.layout.font("4x6") - - graphics.DrawText(self.canvas, font, 0, 10, (255, 255, 255), game_text) diff --git a/rewrite/screens/games/base.py b/rewrite/screens/games/base.py new file mode 100644 index 00000000..a10a2e8f --- /dev/null +++ b/rewrite/screens/games/base.py @@ -0,0 +1,14 @@ +from screens.base import ScreenBase + + +class GameScreen(ScreenBase): + class MissingGame(Exception): + pass + + def __init__(self, *args, game=None, **kwargs): + super().__init__(*args, **kwargs) + + self.game = game + + if self.game is None: + raise GameScreen.MissingGame("Game screens cannot be instantiated without a game object!") diff --git a/rewrite/screens/games/live_game.py b/rewrite/screens/games/live_game.py new file mode 100644 index 00000000..f557c449 --- /dev/null +++ b/rewrite/screens/games/live_game.py @@ -0,0 +1,14 @@ +from driver import graphics + +from screens.games.base import GameScreen + + +class LiveGameScreen(GameScreen): + MAX_DURATION_SECONDS = 5 + + def render(self): + game_text = "It's a game!" + + font, font_size = self.config.layout.font("4x6") + + graphics.DrawText(self.canvas, font, 0, 10, (255, 255, 255), game_text) diff --git a/rewrite/screens/games/postgame.py b/rewrite/screens/games/postgame.py new file mode 100644 index 00000000..f6d22239 --- /dev/null +++ b/rewrite/screens/games/postgame.py @@ -0,0 +1,46 @@ +import os, time + +from driver import graphics + +from screens.games.base import GameScreen + + +class PostGameScreen(GameScreen): + MAX_DURATION_SECONDS = 5 + + def render(self): + game_text = "It's a post-game!" + + font, font_size = self.config.layout.font("4x6") + + graphics.DrawText(self.canvas, font, 0, 10, (255, 255, 255), game_text) + + def _render_decision_scroll(self): + coords = self.manager.layout.coords("final.scrolling_text") + font, font_size = self.manager.layout.font("final.scrolling_text") + + # color = colors.graphics_color("final.scrolling_text") + # bgcolor = colors.graphics_color("default.background") + + color = (255, 255, 255) + bgcolor = (0, 0, 0) + + scroll_text = "W: {} {}-{} L: {} {}-{}".format( + self.game.winning_pitcher, + self.game.winning_pitcher_wins, + self.game.winning_pitcher_losses, + self.game.losing_pitcher, + self.game.losing_pitcher_wins, + self.game.losing_pitcher_losses, + ) + + if False and self.game.save_pitcher: + scroll_text += " SV: {} ({})".format(self.game.save_pitcher, self.game.save_pitcher_saves) + + # TODO: Playoffs + # if is_playoffs: + # scroll_text += " " + self.game.series_status + + # return scrollingtext.render_text( + # canvas, coords["x"], coords["y"], coords["width"], font, color, bgcolor, scroll_text, text_pos + # ) diff --git a/rewrite/screens/games/pregame.py b/rewrite/screens/games/pregame.py new file mode 100644 index 00000000..fcf1a108 --- /dev/null +++ b/rewrite/screens/games/pregame.py @@ -0,0 +1,20 @@ +from driver import graphics + +from screens.games.base import GameScreen + + +class PregameScreen(GameScreen): + MAX_DURATION_SECONDS = 5 + + def render(self): + self.__render_start_time() + + def __render_start_time(self): + time_text = str(self.game.datetime()) + coords = self.layout.coords("pregame.start_time") + font, font_size = self.layout.font_for("pregame.start_time") + # color = colors.graphics_color("pregame.start_time") + # time_x = center_text_position(time_text, coords["x"], font["size"]["width"]) + + color = (255, 255, 255) + graphics.DrawText(self.canvas, font, 0, coords.y, color, time_text) diff --git a/rewrite/screens/screen_manager.py b/rewrite/screens/screen_manager.py index 4e9ad634..f614cf2d 100644 --- a/rewrite/screens/screen_manager.py +++ b/rewrite/screens/screen_manager.py @@ -6,7 +6,9 @@ from screens.static import StaticScreen from screens.clock import ClockScreen from screens.weather import WeatherScreen -from screens.game import GameScreen +from screens.games.pregame import PregameScreen +from screens.games.live_game import LiveGameScreen +from screens.games.postgame import PostGameScreen class ScreenManager: @@ -14,7 +16,9 @@ class ScreenManager: Screen.STATIC: StaticScreen, Screen.CLOCK: ClockScreen, Screen.WEATHER: WeatherScreen, - Screen.GAME: GameScreen, + Screen.PREGAME: PregameScreen, + Screen.LIVE_GAME: LiveGameScreen, + Screen.POSTGAME: PostGameScreen, } def __init__(self, matrix, canvas, config, queue): @@ -34,8 +38,8 @@ def start(self): self.canvas = self.matrix.SwapOnVSync(self.canvas) - def request_next_screen(self, screen, *args): - request = ScreenRequest(screen, self, *args) + def request_next_screen(self, screen, **kwargs): + request = ScreenRequest(screen, self, **kwargs) self.queue.put(request) @@ -52,7 +56,7 @@ def __handle_screen_request(self): return False - screen_class = self.SCREENS.get(request.type, None) + screen_class = ScreenManager.SCREENS.get(request.type, None) if not screen_class: ScoreboardLogger.warning( @@ -62,7 +66,7 @@ def __handle_screen_request(self): return False try: - screen = screen_class(request.manager, *request.args) + screen = screen_class(request.manager, **request.kwargs) except Exception as exception: ScoreboardLogger.exception(exception) ScoreboardLogger.exception("Screen manager failed to process screen transition!") diff --git a/rewrite/screens/static.py b/rewrite/screens/static.py index a6dc2717..f9ee2ff5 100644 --- a/rewrite/screens/static.py +++ b/rewrite/screens/static.py @@ -9,12 +9,14 @@ class StaticScreen(ScreenBase): """ This screen is used to display a static image on startup before real data is loaded. + + Outside data sources must request a transition to move away from this screen. """ MIN_DURATION_SECONDS = 3 - def __init__(self, *args): - super(self.__class__, self).__init__(*args) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) dimensions = self.manager.config.dimensions logo_path = os.path.abspath( @@ -31,4 +33,4 @@ def ready_to_transition(self): """ If the static load screen is displayed, then leave it on screen for a few seconds to avoid flashing. """ - return self.duration >= self.MIN_DURATION_SECONDS * 1000 + return self.duration >= StaticScreen.MIN_DURATION_SECONDS * 1000 diff --git a/rewrite/screens/weather.py b/rewrite/screens/weather.py index 4cc3309a..8461d833 100644 --- a/rewrite/screens/weather.py +++ b/rewrite/screens/weather.py @@ -9,15 +9,12 @@ class WeatherScreen(ScreenBase): MAX_DURATION_SECONDS = 3 - def __init__(self, *args): - super(self.__class__, self).__init__(*args) - def render(self): weather_text = "It's weathery" - font = self.config.layout.font("4x6") + font, font_size = self.config.layout.font("4x6") graphics.DrawText(self.canvas, font, 0, 10, (255, 255, 255), weather_text) - if self.duration > self.MAX_DURATION_SECONDS * 1000: + if self.duration > WeatherScreen.MAX_DURATION_SECONDS * 1000: self.manager.request_next_screen(Screen.CLOCK) diff --git a/rewrite/utils/__init__.py b/rewrite/utils/__init__.py index 153327aa..d8a67359 100644 --- a/rewrite/utils/__init__.py +++ b/rewrite/utils/__init__.py @@ -201,3 +201,12 @@ def deep_update(reference, overrides): reference[key] = overrides[key] return reference + + +def value_at_keypath(current, keypath): + keys = keypath.split(".") + + for key in keys: + current = current.get(key, {}) + + return current