diff --git a/rewrite/data/game.py b/rewrite/data/game.py index 36dcba59..77bb8d30 100644 --- a/rewrite/data/game.py +++ b/rewrite/data/game.py @@ -4,6 +4,7 @@ from data.update_status import UpdateStatus from data import status as GameState +from data.team import TeamType from utils import logger as ScoreboardLogger from utils import value_at_keypath @@ -122,14 +123,14 @@ def datetime(self): def winning_team(self): if self.status == GameState.FINAL: if self.home_score() > self.away_score(): - return "home" + return TeamType.HOME if self.home_score() < self.away_score(): - return "away" + return TeamType.AWAY return None def losing_team(self): - opposite = {"home": "away", "away": "home"} + opposite = {TeamType.HOME: TeamType.AWAY, TeamType.AWAY: TeamType.HOME} return opposite.get(self.winning_team(), None) @@ -172,56 +173,65 @@ def format_id(ID): TODO: Make this dynamic somehow? """ + def home_runs(self): + return self.__runs(TeamType.HOME) + + def away_runs(self): + return self.__runs(TeamType.AWAY) + + def __runs(self, variant): + return value_at_keypath(self.data, f"liveData.linescore.teams.{variant}").get("runs", 0) + def home_hits(self): - return self.__hits("home") + return self.__hits(TeamType.HOME) def away_hits(self): - return self.__hits("away") + return self.__hits(TeamType.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") + return self.__errors(TeamType.HOME) def away_errors(self): - return self.__errors("away") + return self.__errors(TeamType.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") + return self.__score(TeamType.HOME) def away_score(self): - return self.__score("away") + return self.__score(TeamType.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") + return self.__name(TeamType.HOME) def away_name(self): - return self.__name("away") + return self.__name(TeamType.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") + return self.__abbreviation(TeamType.HOME) def away_abbreviation(self): - return self.__abbreviation("away") + return self.__abbreviation(TeamType.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") + return self.__record(TeamType.HOME) def away_record(self): - return self.__record("away") + return self.__record(TeamType.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 f806ed6a..d961f5bc 100644 --- a/rewrite/data/schedule.py +++ b/rewrite/data/schedule.py @@ -38,8 +38,7 @@ def update(self): return UpdateStatus.FAIL - # TODO: Filter to target game - self.game = self.games[0] + self.game = self.games[self.__next_game_index()] self.__request_transition_to_game(self.game) @@ -71,6 +70,7 @@ def __request_transition_to_game(self, game): self.data.request_next_screen(next_screen, game=game) def __next_game_index(self): + # TODO: Some sort of intelligent decisioning here beyond i + 1. if len(self.games) > 0 and self.game in self.games: i = self.games.index(self.game) diff --git a/rewrite/data/team.py b/rewrite/data/team.py new file mode 100644 index 00000000..5598f4b9 --- /dev/null +++ b/rewrite/data/team.py @@ -0,0 +1,6 @@ +from enum import StrEnum + + +class TeamType(StrEnum): + AWAY = "away" + HOME = "home" diff --git a/rewrite/screens/components/team.py b/rewrite/screens/components/team.py index e3714265..b99b5590 100644 --- a/rewrite/screens/components/team.py +++ b/rewrite/screens/components/team.py @@ -1,7 +1,11 @@ from driver import graphics +from data.team import TeamType + from utils.graphics import DrawRect +from collections import namedtuple + class TeamBanner: def __init__(self, kind, screen): @@ -10,17 +14,21 @@ def __init__(self, kind, screen): # 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.team_name = self.game.home_name() if kind == TeamType.HOME else self.game.away_name() + self.team_abbreviation = ( + self.game.home_abbreviation() if kind == TeamType.HOME else self.game.away_abbreviation() + ) self.__load_colors() + # Constructor to build scoreline objects for both teams + self._scoreline = namedtuple("Scoreline", ("runs", "hits", "errors")) + def render(self): self.__render_background() self.__render_accents() self.__render_team_name() - self.__render_score() + self.__render_scoreline() def __render_background(self): coords = self.layout.coords(f"teams.background.{self.kind}") @@ -32,22 +40,76 @@ def __render_accents(self): DrawRect(self.canvas, coords.x, coords.y, coords.width, coords.height, self.accent_color) - def __render_score(self): - pass - def __render_team_name(self): keypath = f"teams.name.{self.kind}" coords = self.layout.coords(keypath) font, font_size = self.layout.font_for(keypath) + text = self.__team_name() + + graphics.DrawText(self.canvas, font, coords.x, coords.y, self.text_color, text) + + def __render_scoreline(self): + coords = self.layout.coords(f"teams.runs.{self.kind}") + font, font_size = self.layout.font_for(f"teams.runs.{self.kind}") + + for c, pos in self.__calculate_scoreline_positions(coords.x, font_size): + graphics.DrawText(self.canvas, font, pos, coords.y, self.text_color, c) - # TODO: Trunc on long RHE - if self.config.full_team_names: - text = "{:13s}".format(self.team_name) + def __calculate_scoreline_positions(self, start, font_size): + """ + Returns an array of tuples containing a character and position (c, p) for values in the scoreline. + + Character tuples will be arranged in format runs-hits-errors (RHE). + Each value is placed into a column with its value right-justified if required. + This class is responsible for rendering a single team's banner, but guarantees the column will align with the opposite team's columns. + + Order of the characters in the returned array is not guaranteed. They should be sorted on position if required. + """ + scoreline_options = self.layout.coords("teams.runs.runs_hits_errors") + + if scoreline_options.show: + # Assumes scoreline constructor will have fields in R, H, E order + home = [str(value) for value in self.scoreline[TeamType.HOME]] + away = [str(value) for value in self.scoreline[TeamType.AWAY]] else: - text = "{:3s}".format(self.team_abbreviation.upper()) + home = [str(self.scoreline[TeamType.HOME].runs)] + away = [str(self.scoreline[TeamType.AWAY].runs)] - graphics.DrawText(self.canvas, font, coords.x, coords.y, self.text_color, text) + pairs = list(zip(home, away)) + positions = [] + + # Need to manage the position pointer and iterator on our own. + x = start + i = 0 + + # Characters are drawn right to left + for pair in pairs[::-1]: + hv, av = pair + + # Ensure the algorithm operates on strings of equal length, otherwise add padding left. + hv = hv.rjust(max(len(hv), len(av)), " ") + av = av.rjust(max(len(hv), len(av)), " ") + + target = hv if self.kind == TeamType.HOME else av + + # Continue to draw right to left + for c in target[::-1]: + # If compression is set, shift pointer back to the left + if i > 0 and scoreline_options.compress_digits: + x += 1 + + # Append a pair (character, position) if not blank + if c != " ": + positions.append((c, x - font_size[0] * (i + 1))) + + i += 1 + + # Shift position pointer to the right by the spacing setting. + # BUG: Why is this a setting but is possibly off-by-one? + x -= scoreline_options.spacing - 1 + + return positions def __load_colors(self): team_key = self.team_abbreviation.lower() @@ -64,6 +126,47 @@ def __color(self, keypath, default_keypath): return self.colors.team_graphics_color(default_keypath) + def __team_name(self): + """ + Returns either the team's actual name or the abbreviated name based on several criteria. + + Long names are returned if: + 1. `full_team_names` config setting is enabled + 2. Matrix width is greater than 32px + 3. `short_team_names_for_runs_hits` config setting is + a. Disabled + -OR- + b. Enabled, but any team's runs and hits is less than 9 (two digits) + + Otherwise, short names are returned. + """ + short_name = "{:3s}".format(self.team_abbreviation.upper()) + long_name = "{:13s}".format(self.team_name) + + enabled = self.config.full_team_names + matrix_size_supported = self.canvas.width > 32 + overflow_prevention_enabled = self.config.short_team_names_for_runs_hits + + if not enabled or not matrix_size_supported: + return short_name + + if not overflow_prevention_enabled: + return long_name + + # Either team can possibly overflow + for kind in [TeamType.HOME, TeamType.AWAY]: + if self.scoreline[kind].hits > 9 or self.scoreline[kind].runs > 9: + return short_name + + return long_name + + @property + def scoreline(self): + return { + TeamType.HOME: self._scoreline(self.game.home_runs(), self.game.home_hits(), self.game.home_errors()), + TeamType.AWAY: self._scoreline(self.game.away_runs(), self.game.away_hits(), self.game.away_errors()), + } + @property def game(self): return self.screen.game diff --git a/rewrite/screens/games/base.py b/rewrite/screens/games/base.py index c928cfea..ecaac9a8 100644 --- a/rewrite/screens/games/base.py +++ b/rewrite/screens/games/base.py @@ -1,3 +1,5 @@ +from data.team import TeamType + from screens.base import ScreenBase from screens.components.team import TeamBanner @@ -14,5 +16,5 @@ 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) + self.away_team_banner = TeamBanner(TeamType.AWAY, self) + self.home_team_banner = TeamBanner(TeamType.HOME, self)