From bc0de0594bde37b0dc647d8b34b83218b13e4334 Mon Sep 17 00:00:00 2001 From: Tyler Porter Date: Wed, 13 Mar 2024 01:49:22 -0400 Subject: [PATCH] Postgame banner 75% finished --- rewrite/config/colors.py | 15 ++-- rewrite/data/game.py | 110 ++++++++++++++++++++----- rewrite/data/schedule.py | 3 +- rewrite/presenters/postgame.py | 30 +++---- rewrite/screens/components/__init__.py | 0 rewrite/screens/components/team.py | 82 ++++++++++++++++++ rewrite/screens/games/__init__.py | 0 rewrite/screens/games/base.py | 4 + rewrite/screens/games/postgame.py | 6 +- rewrite/utils/graphics.py | 15 ++++ 10 files changed, 223 insertions(+), 42 deletions(-) create mode 100644 rewrite/screens/components/__init__.py create mode 100644 rewrite/screens/components/team.py create mode 100644 rewrite/screens/games/__init__.py create mode 100644 rewrite/utils/graphics.py diff --git a/rewrite/config/colors.py b/rewrite/config/colors.py index 5f5de96f..0433f1d5 100644 --- a/rewrite/config/colors.py +++ b/rewrite/config/colors.py @@ -34,16 +34,19 @@ def __fetch_colors(self, reference_filename): return reference_colors - def team_graphics_color(self, keypath): - return self.__fetch_color(self._team_json, keypath) + def team_graphics_color(self, keypath, default=True): + return self.__fetch_color(self._team_json, keypath, default) - def graphics_color(self, keypath): - return self.__fetch_color(self._scoreboard_json, keypath) + def graphics_color(self, keypath, default=True): + return self.__fetch_color(self._scoreboard_json, keypath, default) - def __fetch_color(self, config, keypath): + def __fetch_color(self, config, keypath, default): color = value_at_keypath(config, keypath) if color: return (color["r"], color["g"], color["b"]) - return Colors.DEFAULT_COLOR + if default: + return Colors.DEFAULT_COLOR + + return None diff --git a/rewrite/data/game.py b/rewrite/data/game.py index 04d2858d..36dcba59 100644 --- a/rewrite/data/game.py +++ b/rewrite/data/game.py @@ -6,6 +6,7 @@ from data import status as GameState from utils import logger as ScoreboardLogger +from utils import value_at_keypath class Game: @@ -114,28 +115,10 @@ def is_inning_break(inning_state): return inning_state not in GameState.GAME_STATE_INNING_LIVE def datetime(self): - time_utc = self.data["gameData"]["datetime"]["dateTime"] + time_utc = value_at_keypath(self.data, "gameData.datetime.dateTime") return dt.fromisoformat(time_utc.replace("Z", "+00:00")) - def home_score(self): - return self.data["liveData"]["linescore"]["teams"]["home"].get("runs", 0) - - def away_score(self): - return self.data["liveData"]["linescore"]["teams"]["away"].get("runs", 0) - - def home_hits(self): - return self.data["liveData"]["linescore"]["teams"]["home"].get("hits", 0) - - def away_hits(self): - return self.data["liveData"]["linescore"]["teams"]["away"].get("hits", 0) - - def home_errors(self): - return self.data["liveData"]["linescore"]["teams"]["home"].get("errors", 0) - - def away_errors(self): - return self.data["liveData"]["linescore"]["teams"]["away"].get("errors", 0) - def winning_team(self): if self.status == GameState.FINAL: if self.home_score() > self.away_score(): @@ -150,6 +133,95 @@ def losing_team(self): return opposite.get(self.winning_team(), None) + def decision_pitcher_id(self, decision): + return value_at_keypath(self.data, f"liveData.decisions.{decision}").get("id", None) + + def full_name(self, player): + ID = Game.format_id(player) + + return value_at_keypath(self.data, f"gameData.players.{ID}").get("fullName", "") + + def pitcher_stat(self, player, stat, team=None): + ID = Game.format_id(player) + + keypath = lambda team, ID: value_at_keypath( + self.data, f"liveData.boxscore.teams.{team}.players.{ID}.seasonStats" + ) + + if team is not None: + stats = keypath(team, ID).get("pitching", {}) + else: + stats = keypath(team, ID).get("pitching", None) or keypath(team, ID).get("pitching", {}) + + return stats[stat] + def series_status(self): # TODO: Reimplement series status return "0-0" + + @staticmethod + def format_id(ID): + if "ID" in str(ID): + return ID + + return "ID" + str(ID) + + """ + Home / Away data accessors. + + TODO: Make this dynamic somehow? + """ + + def home_hits(self): + return self.__hits("home") + + def away_hits(self): + return self.__hits("away") + + def __hits(self, variant): + return value_at_keypath(self.data, f"liveData.linescore.teams.{variant}").get("hits", 0) + + def home_errors(self): + return self.__errors("home") + + def away_errors(self): + return self.__errors("away") + + def __errors(self, variant): + return value_at_keypath(self.data, f"liveData.linescore.teams.{variant}").get("errors", 0) + + def home_score(self): + return self.__score("home") + + def away_score(self): + return self.__score("away") + + def __score(self, variant): + return value_at_keypath(self.data, f"liveData.linescore.teams.{variant}").get("runs", 0) + + def home_name(self): + return self.__name("home") + + def away_name(self): + return self.__name("away") + + def __name(self, variant): + return value_at_keypath(self.data, f"gameData.teams.{variant}").get("teamName", "") + + def home_abbreviation(self): + return self.__abbreviation("home") + + def away_abbreviation(self): + return self.__abbreviation("away") + + def __abbreviation(self, variant): + return value_at_keypath(self.data, f"gameData.teams.{variant}").get("abbreviation", "") + + def home_record(self): + return self.__record("home") + + def away_record(self): + return self.__record("away") + + def __record(self, variant): + return value_at_keypath(self.data, f"gameData.teams.{variant}.record") diff --git a/rewrite/data/schedule.py b/rewrite/data/schedule.py index f360758f..c4cd6521 100644 --- a/rewrite/data/schedule.py +++ b/rewrite/data/schedule.py @@ -44,7 +44,8 @@ def update(self): return UpdateStatus.SUCCESS def __fetch_updated_schedule(self, date): - self._games = statsapi.schedule(date.strftime("%Y-%m-%d")) + # self._games = statsapi.schedule(date.strftime("%Y-%m-%d")) + self._games = statsapi.schedule(date.strftime("2024-03-12")) self.games = [Game.from_schedule(game) for game in self._games] diff --git a/rewrite/presenters/postgame.py b/rewrite/presenters/postgame.py index 61b7289b..f978816e 100644 --- a/rewrite/presenters/postgame.py +++ b/rewrite/presenters/postgame.py @@ -14,23 +14,23 @@ def __init__(self, game): self.save_pitcher = None self.save_pitcher_saves = None - # winner = game.decision_pitcher_id("winner") - # if winner is not None: - # self.winning_pitcher = game.full_name(winner) - # self.winning_pitcher_wins = game.pitcher_stat(winner, "wins", winner_side) - # self.winning_pitcher_losses = game.pitcher_stat(winner, "losses", winner_side) + winner = game.decision_pitcher_id("winner") + if winner is not None: + self.winning_pitcher = game.full_name(winner) + self.winning_pitcher_wins = game.pitcher_stat(winner, "wins", winner_side) + self.winning_pitcher_losses = game.pitcher_stat(winner, "losses", winner_side) - # save = game.decision_pitcher_id("save") - # if save is not None: - # self.save_pitcher = game.full_name(save) - # self.save_pitcher_saves = game.pitcher_stat(save, "saves", winner_side) + save = game.decision_pitcher_id("save") + if save is not None: + self.save_pitcher = game.full_name(save) + self.save_pitcher_saves = game.pitcher_stat(save, "saves", winner_side) - # loser = game.decision_pitcher_id("loser") - # if loser is not None: - # loser_side = game.losing_team() - # self.losing_pitcher = game.full_name(loser) - # self.losing_pitcher_wins = game.pitcher_stat(loser, "wins", loser_side) - # self.losing_pitcher_losses = game.pitcher_stat(loser, "losses", loser_side) + loser = game.decision_pitcher_id("loser") + if loser is not None: + loser_side = game.losing_team() + self.losing_pitcher = game.full_name(loser) + self.losing_pitcher_wins = game.pitcher_stat(loser, "wins", loser_side) + self.losing_pitcher_losses = game.pitcher_stat(loser, "losses", loser_side) self.series_status = game.series_status() diff --git a/rewrite/screens/components/__init__.py b/rewrite/screens/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rewrite/screens/components/team.py b/rewrite/screens/components/team.py new file mode 100644 index 00000000..90d407ff --- /dev/null +++ b/rewrite/screens/components/team.py @@ -0,0 +1,82 @@ +from driver import graphics + +from utils.graphics import DrawRect + + +class TeamBanner: + def __init__(self, kind, screen): + self.kind = kind + + # Can reach into the screen to access config for that screen + self.screen = screen + + # breakpoint() + self.team_name = self.game.home_name() if kind == "home" else self.game.away_name() + self.team_abbreviation = self.game.home_abbreviation() if kind == "home" else self.game.away_abbreviation() + + self.__load_colors() + + def render(self): + self.__render_background() + self.__render_accents() + self.__render_team_name() + self.__render_score() + + def __render_background(self): + coords = self.layout.coords(f"teams.background.{self.kind}") + + DrawRect(self.canvas, coords.x, coords.y, coords.width, coords.height, self.bg_color) + + def __render_accents(self): + coords = self.layout.coords(f"teams.accent.{self.kind}") + + DrawRect(self.canvas, coords.x, coords.y, coords.width, coords.height, self.accent_color) + + def __render_team_name(self): + keypath = f"teams.name.{self.kind}" + + coords = self.layout.coords(keypath) + font, font_size = self.layout.font_for(keypath) + + # TODO: Trunc on long RHE + if self.config.full_team_names: + text = "{:13s}".format(self.team_name) + else: + text = "{:3s}".format(self.team_abbreviation.upper()) + + graphics.DrawText(self.canvas, font, coords.x, coords.y, self.text_color, text) + + def __load_colors(self): + team_key = self.team_abbreviation.lower() + + self.bg_color = self.__color(f"{team_key}.home", "default.home") + self.text_color = self.__color(f"{team_key}.text", "default.text") + self.accent_color = self.__color(f"{team_key}.accent", "default.accent") + + def __color(self, keypath, default_keypath): + color = self.colors.team_graphics_color(keypath, default=False) + + if color: + return color + + return self.colors.team_graphics_color(default_keypath) + + @property + def game(self): + return self.screen.game + + @property + def canvas(self): + return self.screen.canvas + + @property + def config(self): + return self.screen.config + + @property + def colors(self): + return self.screen.colors + + @property + def layout(self): + return self.screen.layout diff --git a/rewrite/screens/games/__init__.py b/rewrite/screens/games/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rewrite/screens/games/base.py b/rewrite/screens/games/base.py index a10a2e8f..c928cfea 100644 --- a/rewrite/screens/games/base.py +++ b/rewrite/screens/games/base.py @@ -1,4 +1,5 @@ from screens.base import ScreenBase +from screens.components.team import TeamBanner class GameScreen(ScreenBase): @@ -12,3 +13,6 @@ def __init__(self, *args, game=None, **kwargs): if self.game is None: raise GameScreen.MissingGame("Game screens cannot be instantiated without a game object!") + + self.away_team_banner = TeamBanner("away", self) + self.home_team_banner = TeamBanner("home", self) diff --git a/rewrite/screens/games/postgame.py b/rewrite/screens/games/postgame.py index 5506cb99..4d90e131 100644 --- a/rewrite/screens/games/postgame.py +++ b/rewrite/screens/games/postgame.py @@ -16,6 +16,10 @@ def render(self): self.__render_final_inning(presenter) self.__render_decision_scroll(presenter) + # Overlay banners + self.away_team_banner.render() + self.home_team_banner.render() + def __render_final_inning(self, presenter): text = "FINAL" color = self.colors.graphics_color("final.inning") @@ -56,7 +60,7 @@ def __render_decision_scroll(self, presenter): ) if presenter.save_pitcher: - scroll_text += " SV: {} ({})".format(self.game.save_pitcher, self.game.save_pitcher_saves) + text += " SV: {} ({})".format(presenter.save_pitcher, presenter.save_pitcher_saves) # TODO: Playoffs # if is_playoffs: diff --git a/rewrite/utils/graphics.py b/rewrite/utils/graphics.py new file mode 100644 index 00000000..20f33dc4 --- /dev/null +++ b/rewrite/utils/graphics.py @@ -0,0 +1,15 @@ +from driver import graphics + + +def DrawRect(canvas, x, y, width, height, color): + """ + Draws a rectangle on screen with (X, Y) given as screen coordinates where (0, 0) is top left. + + Chooses the smallest dimension as the render direction to prevent extra draw calls. + """ + if width > height: + for offset in range(0, height): + graphics.DrawLine(canvas, x, y + offset, x + width - 1, y + offset, color) + else: + for offset in range(0, width): + graphics.DrawLine(canvas, x + offset, y, x + offset, y + height - 1, color)