diff --git a/README.md b/README.md index e2fb9182..4d77072d 100755 --- a/README.md +++ b/README.md @@ -259,7 +259,7 @@ A default `config.json.example` file is included for reference. Copy this file t ### Additional Features * Runs/Hits/Errors - Runs are always shown on the games screen, but you can enable or adjust spacing of a "runs, hits, errors" display. Take a look at the [coordinates readme file](/coordinates/README.md) for details. -* Pitch Data - Pitch data can be shown on the game screen, See the [coordinates readme file](/coordinates/README.md) for details. In addition, the Short and Long pitch description can be changed in data/pitches.py +* Pitch Data - Pitch data can be shown on the game screen, See the [coordinates readme file](/coordinates/README.md) for details. In addition, the `short` and `long` pitch description can be changed in data/pitches.py ### Flags diff --git a/colors/scoreboard.json.example b/colors/scoreboard.json.example index ddff96ab..b016193b 100644 --- a/colors/scoreboard.json.example +++ b/colors/scoreboard.json.example @@ -124,10 +124,15 @@ "g": 255, "b": 255 }, - "strikeout": { + "play_result": { "r": 255, "g": 235, "b": 59 + }, + "strikeout": { + "r": 255, + "g": 0, + "b": 0 } }, "batter_count": { diff --git a/coordinates/README.md b/coordinates/README.md index a86d8872..301bd590 100644 --- a/coordinates/README.md +++ b/coordinates/README.md @@ -21,7 +21,7 @@ The layout can have a couple of different states where things are rendered diffe ## Pitch Data * `enabled` (true/false) turn feature on/off * `mph` (true/false) When rendering pitch speed add mph after (99 mph) -* `desc_length` (Short/Long) The short or long pitch type description, you can change both the short and long description to your liking in data/pitches as long as you do not change the index value. +* `desc_length` (short/long) The short or long pitch type description, you can change both the short and long description to your liking in data/pitches as long as you do not change the index value. ## Updates The software develops and releases features with full support for the default layouts, so custom layouts may look unsatisfactory if you update to later versions of the scoreboard. If you as a user decide to create a custom layout file, you are responsible for tweaking the coordinates to your liking with each update. diff --git a/coordinates/w128h32.json.example b/coordinates/w128h32.json.example index 75e49010..c181acdf 100644 --- a/coordinates/w128h32.json.example +++ b/coordinates/w128h32.json.example @@ -173,7 +173,7 @@ "y": 50, "enabled": false, "mph": false, - "desc_length": "Short" + "desc_length": "short" }, "pitch_count": { "font_name": "4x6", @@ -184,8 +184,16 @@ }, "loop": 64, "strikeout": { - "x": 84, - "y": 30 + "x": 60, + "y": 30, + "desc_length": "short", + "enabled": true + }, + "play_result": { + "x": 60, + "y": 30, + "desc_length": "short", + "enabled": true } }, "pregame": { diff --git a/coordinates/w128h64.json.example b/coordinates/w128h64.json.example index 92c0feb2..184a7e0c 100644 --- a/coordinates/w128h64.json.example +++ b/coordinates/w128h64.json.example @@ -135,7 +135,7 @@ "y": 50, "enabled": true, "mph": true, - "desc_length": "Long" + "desc_length": "long" }, "pitch_count": { "font_name": "4x6", @@ -146,8 +146,16 @@ }, "loop": 68, "strikeout": { - "x": 32, - "y": 60 + "x": 16, + "y": 60, + "desc_length": "long", + "enabled": true + }, + "play_result": { + "x": 16, + "y": 60, + "desc_length": "long", + "enabled": true } }, "batter_count": { diff --git a/coordinates/w192h64.json.example b/coordinates/w192h64.json.example index 87cdd9dc..6f010530 100644 --- a/coordinates/w192h64.json.example +++ b/coordinates/w192h64.json.example @@ -135,7 +135,7 @@ "y": 50, "enabled": true, "mph": true, - "desc_length": "Long" + "desc_length": "long" }, "pitch_count": { "font_name": "4x6", @@ -146,8 +146,16 @@ }, "loop": 68, "strikeout": { - "x": 32, - "y": 60 + "x": 16, + "y": 60, + "desc_length": "long", + "enabled": true + }, + "play_result": { + "x": 16, + "y": 60, + "desc_length": "long", + "enabled": true } }, "batter_count": { diff --git a/coordinates/w32h32.json.example b/coordinates/w32h32.json.example index 7a71fad6..31a8dd0a 100644 --- a/coordinates/w32h32.json.example +++ b/coordinates/w32h32.json.example @@ -153,7 +153,7 @@ "y": 50, "enabled": false, "mph": false, - "desc_length": "Short" + "desc_length": "short" }, "pitch_count": { "font_name": "4x6", @@ -165,7 +165,15 @@ "loop": 16, "strikeout": { "x": 33, - "y": 33 + "y": 33, + "desc_length": "short", + "enabled": false + }, + "play_result": { + "x": 33, + "y": 33, + "desc_length": "short", + "enabled": false } }, "batter_count": { diff --git a/coordinates/w64h32.json.example b/coordinates/w64h32.json.example index 6c98341a..a9ee194b 100644 --- a/coordinates/w64h32.json.example +++ b/coordinates/w64h32.json.example @@ -130,7 +130,7 @@ "y": 50, "enabled": false, "mph": false, - "desc_length": "Short" + "desc_length": "short" }, "pitch_count": { "x": 1, @@ -142,8 +142,18 @@ "strikeout": { "x": 15, "y": 29, - "font_name": "5x7" + "font_name": "5x7", + "desc_length": "short", + "enabled": true + }, + "play_result": { + "x": 15, + "y": 29, + "font_name": "5x7", + "desc_length": "short", + "enabled": true } + }, "batter_count": { "x": 34, diff --git a/coordinates/w64h64.json.example b/coordinates/w64h64.json.example index 36c5ef64..5ec273bb 100644 --- a/coordinates/w64h64.json.example +++ b/coordinates/w64h64.json.example @@ -129,7 +129,7 @@ "y": 50, "enabled": false, "mph": false, - "desc_length": "Short" + "desc_length": "short" }, "pitch_count": { "font_name": "4x6", @@ -141,7 +141,15 @@ "loop": 64, "strikeout": { "x": 31, - "y": 36 + "y": 36, + "desc_length": "short", + "enabled": true + }, + "play_result": { + "x": 31, + "y": 36, + "desc_length": "short", + "enabled": true } }, "pregame": { diff --git a/data/plays.py b/data/plays.py new file mode 100644 index 00000000..c1701093 --- /dev/null +++ b/data/plays.py @@ -0,0 +1,37 @@ +SINGLE = "single" +DOUBLE = "double" +TRIPLE = "triple" +HOME_RUN = "home_run" + +WALK = "walk" +INTENTIONAL_WALK = "intent_walk" +HIT_BY_PITCH = "hit_by_pitch" + +STRIKEOUT = "strikeout" +STRIKEOUT_ALT = "strike_out" +STRIKEOUT_LOOKING = "strikeout_looking" + +ERROR = "error" +FIELDERS_CHOICE = "fielders_choice" + +HITS = [SINGLE, DOUBLE, TRIPLE] + +WALKS = [WALK, INTENTIONAL_WALK, HIT_BY_PITCH] + +OTHERS = [ERROR, FIELDERS_CHOICE] + +STRIKEOUTS = [STRIKEOUT, STRIKEOUT_ALT, STRIKEOUT_LOOKING] + +PLAY_RESULTS = { + SINGLE: {"short": "1B", "long": "Single"}, + DOUBLE: {"short": "2B", "long": "Double"}, + TRIPLE: {"short": "3B", "long": "Triple"}, + WALK: {"short": "BB", "long": "Walk"}, + INTENTIONAL_WALK: {"short": "IBB", "long": "Int. Walk"}, + STRIKEOUT: {"short": "K", "long": "K"}, + STRIKEOUT_ALT: {"short": "K", "long": "K"}, + STRIKEOUT_LOOKING: {"short": "ꓘ", "long": "ꓘ"}, + HIT_BY_PITCH: {"short": "HBP", "long": "Hit Bttr"}, + ERROR: {"short": "E", "long": "Error"}, + FIELDERS_CHOICE: {"short": "FC", "long": "Fielder's Chc"}, +} diff --git a/data/scoreboard/__init__.py b/data/scoreboard/__init__.py index 93ff5eb6..649d328a 100644 --- a/data/scoreboard/__init__.py +++ b/data/scoreboard/__init__.py @@ -5,6 +5,8 @@ from data.scoreboard.outs import Outs from data.scoreboard.pitches import Pitches from data.scoreboard.team import Team +from data import plays + class Scoreboard: @@ -41,6 +43,12 @@ def strikeout(self): def strikeout_looking(self): return self.play_result == "strikeout_looking" + + def hit(self): + return self.play_result in plays.HITS + + def walk(self): + return self.play_result in plays.WALKS def get_text_for_reason(self): if self.note: diff --git a/renderers/games/game.py b/renderers/games/game.py index cac05611..5c2e1960 100644 --- a/renderers/games/game.py +++ b/renderers/games/game.py @@ -6,26 +6,25 @@ from data.scoreboard.bases import Bases from data.scoreboard.inning import Inning from data.scoreboard.pitches import Pitches +from data.plays import PLAY_RESULTS + from renderers import scrollingtext from renderers.games import nohitter def render_live_game(canvas, layout: Layout, colors: Color, scoreboard: Scoreboard, text_pos, animation_time): pos = 0 - if scoreboard.inning.state == Inning.TOP or scoreboard.inning.state == Inning.BOTTOM: - pos = _render_at_bat( - canvas, - layout, - colors, - scoreboard.atbat, - text_pos, - scoreboard.strikeout(), - scoreboard.strikeout_looking(), - (animation_time // 6) % 2, - scoreboard.pitches, - ) + canvas, + layout, + colors, + scoreboard.atbat, + text_pos, + scoreboard.play_result, + (animation_time // 6) % 2, + scoreboard.pitches + ) # Check if we're deep enough into a game and it's a no hitter or perfect game should_display_nohitter = layout.coords("nohitter")["innings_until_display"] @@ -38,6 +37,7 @@ def render_live_game(canvas, layout: Layout, colors: Color, scoreboard: Scoreboa _render_bases(canvas, layout, colors, scoreboard.bases, scoreboard.homerun(), (animation_time % 16) // 5) _render_inning_display(canvas, layout, colors, scoreboard.inning) + else: _render_inning_break(canvas, layout, colors, scoreboard.inning) _render_due_up(canvas, layout, colors, scoreboard.atbat) @@ -46,24 +46,41 @@ def render_live_game(canvas, layout: Layout, colors: Color, scoreboard: Scoreboa # --------------- at-bat --------------- -def _render_at_bat(canvas, layout, colors, atbat: AtBat, text_pos, strikeout, looking, animation, pitches: Pitches): +def _render_at_bat(canvas, layout, colors, atbat: AtBat, text_pos, play_result, animation, pitches: Pitches): plength = __render_pitcher_text(canvas, layout, colors, atbat.pitcher, pitches, text_pos) __render_pitch_text(canvas, layout, colors, pitches) __render_pitch_count(canvas, layout, colors, pitches) - if strikeout: + results = list(PLAY_RESULTS.keys()) + if play_result in results and __should_render_play_result(play_result, layout): if animation: - __render_strikeout(canvas, layout, colors, looking) + __render_play_result(canvas, layout, colors, play_result) return plength else: blength = __render_batter_text(canvas, layout, colors, atbat.batter, text_pos) return max(plength, blength) -def __render_strikeout(canvas, layout, colors, looking): - coords = layout.coords("atbat.strikeout") - color = colors.graphics_color("atbat.strikeout") - font = layout.font("atbat.strikeout") - text = "ꓘ" if looking else "K" +def __should_render_play_result(play_result, layout): + if "strikeout" in play_result: + coords = layout.coords("atbat.strikeout") + else: + coords = layout.coords("atbat.play_result") + return coords["enabled"] + + +def __render_play_result(canvas, layout, colors, play_result): + if "strikeout" in play_result: + color = colors.graphics_color("atbat.strikeout") + coords = layout.coords("atbat.strikeout") + font = layout.font("atbat.strikeout") + else: + color = colors.graphics_color("atbat.play_result") + coords = layout.coords("atbat.play_result") + font = layout.font("atbat.play_result") + try: + text = PLAY_RESULTS[play_result][coords["desc_length"].lower()] + except KeyError: + return graphics.DrawText(canvas, font["font"], coords["x"], coords["y"], color, text) @@ -123,9 +140,9 @@ def __render_pitch_text(canvas, layout, colors, pitches: Pitches): mph = " " if coords["mph"]: mph = "mph " - if coords["desc_length"] == "Long": + if coords["desc_length"].lower() == "long": pitch_text = str(pitches.last_pitch_speed) + mph + pitches.last_pitch_type_long - elif coords["desc_length"] == "Short": + elif coords["desc_length"].lower() == "short": pitch_text = str(pitches.last_pitch_speed) + mph + pitches.last_pitch_type else: pitch_text = "" @@ -242,6 +259,7 @@ def __fill_out_circle(canvas, out, color): # --------------- inning information --------------- def _render_inning_break(canvas, layout, colors, inning: Inning): + text_font = layout.font("inning.break.text") num_font = layout.font("inning.break.number") text_coords = layout.coords("inning.break.text") diff --git a/renderers/main.py b/renderers/main.py index c98ef020..bf7eb5ac 100644 --- a/renderers/main.py +++ b/renderers/main.py @@ -153,7 +153,7 @@ def __draw_game(self): self.data.scrolling_finished = True else: # draw a live game - if scoreboard.homerun() or scoreboard.strikeout(): + if scoreboard.homerun() or scoreboard.strikeout() or scoreboard.hit() or scoreboard.walk(): self.animation_time += 1 else: self.animation_time = 0 diff --git a/tests/test_data_up_to_date.py b/tests/test_data_up_to_date.py index ba7062e5..64e8b4c8 100644 --- a/tests/test_data_up_to_date.py +++ b/tests/test_data_up_to_date.py @@ -17,18 +17,22 @@ def test_status_complete(self): self.assertSetEqual(official_statuses, our_statuses) def test_teams_complete(self): - teams = statsapi.get('teams', {'sportIds':1})['teams'] + teams = statsapi.get("teams", {"sportIds": 1})["teams"] - abbr_to_full = {t['teamName']:t['name'] for t in teams} + abbr_to_full = {t["teamName"]: t["name"] for t in teams} self.assertEqual(abbr_to_full, data.teams.TEAM_FULL) - full_to_abbr = {t['name']:t['abbreviation'] for t in teams} + full_to_abbr = {t["name"]: t["abbreviation"] for t in teams} self.assertEqual(full_to_abbr, data.teams.TEAM_ABBR_LN) def test_pitches_complete(self): - pitches = set(p["code"] for p in statsapi.meta("pitchTypes")) + pitches = set(p["code"] for p in statsapi.meta("pitchTypes")) self.assertSetEqual(pitches, set(data.pitches.PITCH_SHORT.keys())) self.assertSetEqual(pitches, set(data.pitches.PITCH_LONG.keys())) + def test_results_exist(self): + events = set(e["code"] for e in statsapi.meta("eventTypes")) + events.add("strikeout_looking") # our custom event + self.assertTrue(events.issuperset(data.plays.PLAY_RESULTS.keys())) if __name__ == "__main__": diff --git a/version.py b/version.py index 6bb344b1..96825d08 100644 --- a/version.py +++ b/version.py @@ -1,5 +1,5 @@ SCRIPT_NAME = "MLB LED Scoreboard" -SCRIPT_VERSION = "6.4.1" +SCRIPT_VERSION = "6.5.0" if __name__ == "__main__":