Closed 5StringThunder closed 2 years ago
This is a good feature for 64x32.
The team score banners won't have enough room for anything else on a 32x32 and I wouldn't want to confuse the jersey numbers with the number of runs.
I'm not sure about number, but you can get and parse the fullName
of the batter and pitcher in the new API.
I have it in the code I've forked. I set up a separate data file called atbat
to handle it. I pull directly from MLB-StatsAPI.
I parse out first initial and last name. It's inelegant, but it works. I think number might be more difficult, though. You might be able to get to it by trying to link to their player database, but I found that to be a little too much for the Pi to handle without slowing down the whole process prohibitively. Remember that the player database, if I'm not mistaken, involves looking up a guy from every player who's ever played.
@pfightingpolish I was looking how to incorporate the batter and pitcher that is playing at that moment. Do you have the .py code or everything to make that work? i can dig on it but not that experienced. Thanks!
I make it work in my customized install, but I've also done a lot to differentiate my install from the base code in this project.
I'm not going to tell you it's easy, but I'll try and walk through it below.
I also can't promise you a be-all, end-all solution because mine tears apart a lot of what's in the 128x64 code and doesn't necessarily even try to respect other matrix sizes.
Here are some pieces that might help, though:
atbat.py
in the data
directory that uses the newer statsapi
functionality. This class gives functions to access a name for the current batter, pitcher, and on-deck and in-the-hole batters from the statsapi
linescore for a given game. It also formats those names to have just have first initials and the last name, unless the last name is particularly long, then it just shows last name. There's not a lot to it, honestly:
import statsapi
class AtBat: def init(self, linescore): self.batter = str(linescore.get('offense').get('batter',{}).get('fullName',{})) self.pitcher = str(linescore.get('defense').get('pitcher',{}).get('fullName',{})) self.onDeck = str(linescore.get('offense').get('onDeck',{}).get('fullName',{})) self.inHole = str(linescore.get('offense').get('inHole',{}).get('fullName',{}))
def get_batter(self): return format_statsapi_name(self, self.batter)
def get_pitcher(self): return format_statsapi_name(self, self.pitcher)
def get_onDeck(self): return format_statsapi_name(self, self.onDeck)
def get_inHole(self): return format_statsapi_name(self, self.inHole)
def format_statsapi_name(self, name): SpcPos = name.find(" ") + 1 playerLast = name[SpcPos:len(name)]
if name:
if len(playerLast) >= 9:
return playerLast
elif name[1] == ".":
return name
elif name == "{}":
return " "
else:
return name[0] + ". " + playerLast
else:
return " "
- I then have an "AtBatRenderer" class in its own `atbat.py` file in the `renderers` directory that utilizes the AtBat class functions and renders that data to the board on the in-game scoreboard. This draws the current batter with "AB: " in front of their name and pitcher with "P:" in front of their name.
from rgbmatrix import graphics from utils import get_font, get_file import json
class AtBatRenderer: """Renders the batter and pitcher."""
def init(self, canvas, atbat, data): self.canvas = canvas self.batter = atbat.get_batter() self.pitcher = atbat.get_pitcher() self.data = data self.colors = data.config.scoreboard_colors self.default_colors = self.data.config.team_colors.color("default") self.bgcolor = self.colors.graphics_color("default.background")
def render(self):
self.batter_coords = self.data.config.layout.coords("atbat.batter")
self.pitcher_coords = self.data.config.layout.coords("atbat.pitcher")
self.__render_batter_text(self.batter, self.default_colors, self.batter_coords["x"], self.batter_coords["y"])
self.__render_pitcher_text(self.pitcher, self.default_colors, self.pitcher_coords["x"], self.pitcher_coords["y"])
def __render_batter_text(self, batter, colors, x, y): text_color = self.default_colors['text'] text_color_graphic = graphics.Color(text_color['r'], text_color['g'], text_color['b']) font = self.data.config.layout.font("atbat.batter") batter_text = "AB: " + batter graphics.DrawText(self.canvas, font["font"], x, y, text_color_graphic, batter_text)
def __render_pitcher_text(self, pitcher, colors, x, y): text_color = self.default_colors['text'] text_color_graphic = graphics.Color(text_color['r'], text_color['g'], text_color['b']) font = self.data.config.layout.font("atbat.pitcher") pitcher_text = "P: " + pitcher graphics.DrawText(self.canvas, font["font"], x, y, text_color_graphic, pitcher_text)
- This does require the addition of an "atbat" section to the `ledcoords/w128h64.json` file:
"atbat": { "batter": { "font_name": "10x11", "x": 2, "y": 54 }, "pitcher": { "font_name": "10x11", "x": 8, "y": 63 } },
Adjust your font and where you want to put the current batter/pitcher accordingly. Mine looks like this:
![image](https://user-images.githubusercontent.com/68492369/115507238-7b4ab680-a241-11eb-9488-b2791a627ed8.png)
- This also will require an import and function call in the `renderers/scoreboard.py` file. You will need to duplicate the
`from renderers.bases import BasesRenderer` and `BasesRenderer(self.canvas, self.scoreboard.bases, self.data).render()` lines and replace "bases" or "Bases" with "atbat" or "AtBat" where appropriate.
- But what about the on-deck and in-the-hole batters? They have a place, notably on the between-innings screen. In the `renderers/inning.py` file, I've modified the `__render_inning_break` function to use the "current" batter, on-deck batter and in-the-hole batter to create a "due up" screen.
def __render_inning_break(self): text_font = self.layout.font("inning.break.text") num_font = self.layout.font("inning.break.number") text_coords = self.layout.coords("inning.break.text") num_coords = self.layout.coords("inning.break.number") color = self.colors.graphics_color("inning.break.text") text = self.inning.state num = self.inning.ordinal() if text == "Middle": num_x = 19 printtext = "Mid" elif text == "End": num_x = 24 printtext = "End" graphics.DrawText(self.canvas, text_font["font"], 2, num_coords["y"], color, printtext) graphics.DrawText(self.canvas, num_font["font"], num_x, num_coords["y"], color, num) graphics.DrawText(self.canvas, num_font["font"], 2, num_coords["y"] + 13, color, "Due Up:") graphics.DrawText(self.canvas, text_font["font"], 55, num_coords["y"] -3, color, self.atbat.get_batter()) graphics.DrawText(self.canvas, text_font["font"], 55, num_coords["y"] + 8, color, self.atbat.get_onDeck()) graphics.DrawText(self.canvas, text_font["font"], 55, num_coords["y"] + 19, color, self.atbat.get_inHole())
![image](https://user-images.githubusercontent.com/68492369/115508096-8eaa5180-a242-11eb-8cc1-e0c7a5339d45.png)
Granted, I got a little lazy and hard-coded some coordinates. I mentioned I tore the code apart pretty badly without mind for making it work for different board sizes. But it's another plausible use of that data.
This is just one way to get to & use the current hitter & pitcher data. There are others. But, like I said before, I'm not sure you can get to more than just `fullName`. As nice as it would be to go out and get stats or the hitter's number or whatever, that's not in the linescore info, which means you have to go out and get it from a different, larger, slower table, like the player table, if you want it, and that's going to really drag your board's operation to a crawl, maybe making it sit on one game for upwards of a minute. With mine, it also flickered like the dickens while it tried to figure out what had to happen. Again, that player table is every player who's ever played in the bigs. That's a lot of data to load compared to a single game's linescore.
Anyhow, that's how I made it work. Apologies for using a sledgehammer to kill a fly, but that fly is also secretly more like a grizzly bear when you actually start to figure out how to kill it.
WOW thank you sooo muchhh!!!! I was working on my own way to get the atbat and was missing some things you have!! Obviously i will make it work on a 64x32 board i have but getting the batter and pitcher info is huge! Thank you again for this and your time!!!! I will tinker and understand what you did
Granted, my stuff above also assumes you already have some way of either translating between mlbgame
and statsapi
, or have fully converted all your data to statsapi
, and neither is what I'd call an "easy" feat.
I think much of this project's code still runs on mlbgame
, and I know my function to convert between the two is almost messy enough to make me not want to post it here. It includes what amounts to a lookup table to convert between team abbreviations from mlbgame
to team IDs from statsapi
. It really messes with data/data.py
.
So yeah, like I say, the above are hints, but there might still be a lot of steps on your journey.
Ahhh so your not using mlbgame. I am using mlbgame. I dont mind using statsapi for just the hitter and pitcher. I did see thag the mlbgame does have the atbat class i can call
Also, for your class atbat, where you have return_statsapi_name, should i just switch that to the mlbgame return? If that makes sense? Or should i just run statsapi for just this instance of atbat and run mlbgame for everything else
... sooooooo the big issue with trying to go back and forth between mlbgame
and statsapi
is they work completely differently. And when I say completely differently, I mean completely differently.
It's been nine months since I worked on this, but I'm pretty sure I remember that getting current batter through mlbgame
was either difficult or impossible. I remember trying to come up with some odd combination of adding batters faced by the pitching data in the boxscore to iterate my way through the lineup and eventually just giving up on it. Maybe there's something you found that I didn't, but yeah, it wasn't fun.
So that left me with trying to get the info from statsapi
, the newer API for MLB data.
Here's the problem, though: You have to connect the mlbgame
overview structure with the statsapi
linescore structure. That's not easy, because you essentially have to connect a game in mlbgame
, which uses an ID format of 2021_04_21_milmlb_sdnmlb_1
, with a game in statsapi
, which uses a numerical ID, like 634476
.
Now, again, it was nine months ago, but I think I remember seeing after the fact that there's actually an mlbgame
-style ID field endpoint somewhere in statsapi
, but not before I went about a more convoluted way of piecing the two together in data/data.py
:
mlbgame
ID. mlbgame
road team abbreviation to a corresponding statsapi
team ID. mlbgame
ID format. statsapi
road team ID and doubleheader game number and using that in the statsapi "schedule" function to get the appropriate corresponding numeric game ID in statsapi
. statsapi
. Here's my modification of refresh_overview()
from data/data.py
, along with the helper functions I added, notably the team abbreviation translation table (I love how mlbgame
still refers to the Angels as "ANA"). Like I say ... not pretty, but it works.
def refresh_overview(self):
urllib.urlcleanup()
attempts_remaining = 5
while attempts_remaining > 0:
try:
self.overview = mlbgame.overview(self.current_game().game_id)
self.RoadTeam = self.get_road_abbrev()
self.RoadID = self.get_team_id(self.RoadTeam)
self.GameNum = self.get_game_num()
self.FunctionDate = datetime(self.year, self.month, self.day)
debug.log("Game ID: {}".format(self.current_game().game_id))
debug.log("Road Team: {}".format(self.RoadTeam))
debug.log("Road ID: {}".format(self.RoadID))
debug.log("Doubleheader Game Number: {}".format(self.GameNum))
debug.log("Date: {}".format(self.FunctionDate))
self.Stats_Game_ID = statsapi.schedule(datetime.strftime(self.FunctionDate, '%Y-%m-%d'), team=self.RoadID)[self.GameNum].get('game_id')
self.linescore = statsapi.get('game_linescore', {'gamePk': self.Stats_Game_ID})
self.__update_layout_state()
self.needs_refresh = False
self.print_overview_debug()
self.network_issues = False
break
except URLError, e:
self.network_issues = True
debug.error("Networking Error while refreshing the current overview. {} retries remaining.".format(attempts_remaining))
debug.error("URLError: {}".format(e.reason))
attempts_remaining -= 1
time.sleep(NETWORK_RETRY_SLEEP_TIME)
except ValueError:
self.network_issues = True
debug.error("Value Error while refreshing current overview. {} retries remaining.".format(attempts_remaining))
debug.error("ValueError: Failed to refresh overview for {}".format(self.current_game().game_id))
attempts_remaining -= 1
time.sleep(NETWORK_RETRY_SLEEP_TIME)
# If we run out of retries, just move on to the next game
if attempts_remaining <= 0 and self.config.rotation_enabled:
self.advance_to_next_game()
def get_game_num(self):
id_to_slice = self.current_game().game_id
TextNum = id_to_slice[-1:]
return int(TextNum) - 1
def get_road_abbrev(self):
id_to_slice = self.current_game().game_id
return id_to_slice[11:-12]
def get_team_id(self, team_abbrev):
print team_abbrev
if team_abbrev == "ari":
return 109
elif team_abbrev == "atl":
return 144
elif team_abbrev == "bal":
return 110
elif team_abbrev == "bos":
return 111
elif team_abbrev == "cha":
return 145
elif team_abbrev == "chn":
return 112
elif team_abbrev == "cin":
return 113
elif team_abbrev == "cle":
return 114
elif team_abbrev == "col":
return 115
elif team_abbrev == "det":
return 116
elif team_abbrev == "hou":
return 117
elif team_abbrev == "kca":
return 118
elif team_abbrev == "ana":
return 108
elif team_abbrev == "lan":
return 119
elif team_abbrev == "mia":
return 146
elif team_abbrev == "mil":
return 158
elif team_abbrev == "min":
return 142
elif team_abbrev == "nya":
return 147
elif team_abbrev == "nyn":
return 121
elif team_abbrev == "oak":
return 133
elif team_abbrev == "phi":
return 143
elif team_abbrev == "pit":
return 134
elif team_abbrev == "sdn":
return 135
elif team_abbrev == "sfn":
return 137
elif team_abbrev == "sea":
return 136
elif team_abbrev == "sln":
return 138
elif team_abbrev == "tba":
return 139
elif team_abbrev == "tex":
return 140
elif team_abbrev == "tor":
return 141
elif team_abbrev == "was":
return 120
elif team_abbrev == "nas":
return 160
elif team_abbrev == "aas":
return 159
else:
return 0
Ahh okay i didnt know they were completely different! All this stuff helps!! I will try to tackle it this week and see what i end up with. All of this help is fantastic!!! Here is what i found on the mlbgame atBat Its a class in the game events def. they have a list of what you can retrieve but not all the code. Just for pitches
http://panz.io/mlbgame/events.m.html#mlbgame.events.AtBat
def game_events(game_id)
class AtBat(object): """Class that holds information about at bats in games.
Properties:
away_team_runs
b
b1
b2
b3
batter
des
des_es
event
event_es
event_num
home_team_runs
num
o
pitcher
pitches
play_guid
s
start_tfs
start_tfs_zulu
"""
def __init__(self, data):
"""Creates an at bat object that matches the corresponding
info in `data`.
`data` should be a dictionary of values.
"""
# loop through data
for x in data:
# create pitches list if attribute name is pitches
if x == 'pitches':
self.pitches = []
for y in data[x]:
self.pitches.append(Pitch(y))
else:
# set information as correct data type
mlbgame.object.setobjattr(self, x, data[x])
def nice_output(self):
"""Prints basic at bat info in a nice way."""
return self.des
def __str__(self):
return self.nice_output()
@pfightingpolish If you've already solved this issue, can you just push a branch up to your forked copy and link us all the code?
@spaghettir101 If you want to do this in mlbgame
:
games = mlbgame.day(2021, 4, 21)
for game in games:
events = mlbgame.events.game_events(game.game_id)
Each of these would return a dictionary of innings shaped like this:
events = {
'1': {
'top': [
{
'tag': 'action',
'away_team_runs': '0',
'home_team_runs': '0',
'event_es': 'Game Advisory',
'event': 'Game Advisory',
'event_num': '1',
'des_es': 'Status Change - Pre-Game',
'des': 'Status Change - Pre-Game',
'o': '0',
's': '0',
'b': '0',
'tfs_zulu': '2021-04-21T17:24:57.577Z',
'tfs': '172457',
'pitch': '1',
'player': '624428'
},
{
'tag': 'atbat',
'num': '1',
'away_team_runs': '0',
'home_team_runs': '0',
'event_es': 'Game Advisory',
'event': 'Game Advisory',
'event_num': '2',
'des_es': 'Status Change - Pre-Game',
'des': 'Status Change - Pre-Game',
'p_throws': 'R',
'pitcher': '605242',
'b_height': '5\' 10"',
'stand': 'L',
'batter': '624428',
'end_tfs_zulu': '2021-04-21T17:24:57.577Z',
'start_tfs_zulu': '2021-04-21T17:24:57.577Z',
'start_tfs': '172457',
'o': '0',
's': '0',
'b': '0',
'pitches': []
}
],
'bottom': []
}
}
All those innings have a bunch of events in them, so you'd need to iterate through them all til you get to the last instance of an AtBat
. This is very expensive. Getting all the events for all the games takes about 10 seconds on my machine. There's also no player numbers here, I'm sure there's another mlbgame
call you could use.
@ty-porter Sure thing: Here's my fork.
Because solving the issue means altering much more than just a few lines of codes, and involves multiple files, and because my code has a lot of "other stuff" going on, I didn't think a simple link to the fork would do the trick. But yeah, it's mostly the ID translation in data/data.py
and adding the data/atbat.py
and render/atbat.py
files.
I was poking around with the game_events function when you posted, Ty, and I was coming to a similar conclusion that going through game_events was going to be problematic.
The big thing I get out of game_events is those events don't give you a clean batter name. Batter and pitcher come out as IDs, which means not only would you have to make that expensive iterative call, you'd need to make some other call to get player name data from the ID on top of that. It just doesn't work real well. Furthermore, as you can see from the 'des' attributes above, the events aren't even all at bats, as there are also "actions," such as the game status changing, mound visits, etc. It looks like it could get ugly fast.
Going through statsapi
and the linescore there seems easier, though still not "easy," per se. Nonetheless, linescore produces a lot of useful stuff. This is a snapshot from the Orioles/Marlins game going on as I type, with the Fish leading the Birds 3-1 in the top of the 3rd and Austin Hays having just ended the inning against Bruce Zimmerman:
statsapi.get('game_linescore', {'gamePk': 634476})
{
u"innings": [
{
u"home": {u"leftOnBase": 2, u"runs": 0, u"errors": 0, u"hits": 1},
u"away": {u"leftOnBase": 0, u"runs": 0, u"errors": 0, u"hits": 0},
u"num": 1,
u"ordinalNum": u"1st",
},
{
u"home": {u"leftOnBase": 1, u"runs": 0, u"errors": 0, u"hits": 1},
u"away": {u"leftOnBase": 1, u"runs": 0, u"errors": 0, u"hits": 1},
u"num": 2,
u"ordinalNum": u"2nd",
},
{
u"home": {u"leftOnBase": 0, u"hits": 0, u"errors": 0},
u"away": {u"leftOnBase": 0, u"runs": 0, u"errors": 0, u"hits": 1},
u"num": 3,
u"ordinalNum": u"3rd",
},
],
u"balls": 2,
u"currentInning": 3,
u"inningHalf": u"Top",
u"inningState": u"Top",
u"currentInningOrdinal": u"3rd",
u"scheduledInnings": 9,
u"strikes": 2,
u"teams": {
u"home": {u"leftOnBase": 3, u"runs": 0, u"errors": 0, u"hits": 2},
u"away": {u"leftOnBase": 1, u"runs": 0, u"errors": 0, u"hits": 2},
},
u"defense": {
u"onDeck": {
u"fullName": u"Miguel Rojas",
u"link": u"/api/v1/people/500743",
u"id": 500743,
},
u"catcher": {
u"fullName": u"Sandy Leon",
u"link": u"/api/v1/people/506702",
u"id": 506702,
},
u"center": {
u"fullName": u"Lewis Brinson",
u"link": u"/api/v1/people/621446",
u"id": 621446,
},
u"third": {
u"fullName": u"Jon Berti",
u"link": u"/api/v1/people/542932",
u"id": 542932,
},
u"shortstop": {
u"fullName": u"Miguel Rojas",
u"link": u"/api/v1/people/500743",
u"id": 500743,
},
u"pitcher": {
u"fullName": u"Trevor Rogers",
u"link": u"/api/v1/people/669432",
u"id": 669432,
},
u"second": {
u"fullName": u"Jazz Chisholm Jr.",
u"link": u"/api/v1/people/665862",
u"id": 665862,
},
u"right": {
u"fullName": u"Adam Duvall",
u"link": u"/api/v1/people/594807",
u"id": 594807,
},
u"batter": {
u"fullName": u"Jazz Chisholm Jr.",
u"link": u"/api/v1/people/665862",
u"id": 665862,
},
u"inHole": {
u"fullName": u"Jesus Aguilar",
u"link": u"/api/v1/people/542583",
u"id": 542583,
},
u"team": {u"link": u"/api/v1/teams/146", u"id": 146, u"name": u"Miami Marlins"},
u"left": {
u"fullName": u"Corey Dickerson",
u"link": u"/api/v1/people/572816",
u"id": 572816,
},
u"battingOrder": 10,
u"first": {
u"fullName": u"Jesus Aguilar",
u"link": u"/api/v1/people/542583",
u"id": 542583,
},
},
u"outs": 3,
u"offense": {
u"onDeck": {
u"fullName": u"Trey Mancini",
u"link": u"/api/v1/people/641820",
u"id": 641820,
},
u"battingOrder": 1,
u"pitcher": {
u"fullName": u"Bruce Zimmermann",
u"link": u"/api/v1/people/669145",
u"id": 669145,
},
u"batter": {
u"fullName": u"Austin Hays",
u"link": u"/api/v1/people/669720",
u"id": 669720,
},
u"inHole": {
u"fullName": u"Ryan Mountcastle",
u"link": u"/api/v1/people/663624",
u"id": 663624,
},
u"team": {
u"link": u"/api/v1/teams/110",
u"id": 110,
u"name": u"Baltimore Orioles",
},
},
u"isTopInning": True,
u"copyright": u"Copyright 2021 MLB Advanced Media, L.P. Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt",
}
@pfightingpolish thanks for everything! i installed MLB-StatsAPI (sudo install MLB-StatsAPI) and it installed, but when I run a .py with import statsapi, it says the module cannot be found. any reason why?
If you install under sudo
you will need to run your script under sudo
.
Hopefully also you ran sudo pip install MLB-StatsAPI
.
sorry, i did use sudo pip to install. Well i added the atbat.py and ran just that and it gave me that error. I then added stuff in the scoreboard.py and same error. Do i have to run the whole service for it to work?
Would it be possible to include the jersey number of the pitcher and batter for each team? If you're only using a single 32x32 matrix then there's not enough room for the pitcher and hitter. But if it was possible to put the jersey number or the pitcher and hitter at the end of the team's color block (far right behind the score) then it would still be visible who was up and who was pitching. Similar to how some ballparks do the out of town scoreboard