Package nflgame :: Module game
[frames] | no frames]

Source Code for Module nflgame.game

  1  from collections import namedtuple 
  2  import os 
  3  import os.path as path 
  4  import gzip 
  5  import json 
  6  import sys 
  7  import urllib2 
  8   
  9  from nflgame import OrderedDict 
 10  import nflgame.player 
 11  import nflgame.seq 
 12  import nflgame.statmap 
 13   
 14  _jsonf = path.join(path.split(__file__)[0], 'gamecenter-json', '%s.json.gz') 
 15  _json_base_url = "http://www.nfl.com/liveupdate/game-center/%s/%s_gtd.json" 
 16   
 17  GameDiff = namedtuple('GameDiff', ['plays', 'players']) 
 18  """ 
 19  Represents the difference between two points in time of the same game 
 20  in terms of plays and player statistics. 
 21  """ 
 22   
 23  TeamStats = namedtuple('TeamStats', 
 24                         ['first_downs', 'total_yds', 'passing_yds', 
 25                          'rushing_yds', 'penalty_cnt', 'penalty_yds', 
 26                          'turnovers', 'punt_cnt', 'punt_yds', 'punt_avg', 
 27                          'pos_time']) 
 28  """A collection of team statistics for an entire game.""" 
 29   
 30   
31 -class FieldPosition (object):
32 """ 33 Represents field position. 34 35 The representation here is an integer offset where the 50 yard line 36 corresponds to '0'. Being in the own territory corresponds to a negative 37 offset while being in the opponent's territory corresponds to a positive 38 offset. 39 40 e.g., NE has the ball on the NE 45, the offset is -5. 41 e.g., NE has the ball on the NYG 2, the offset is 48. 42 """
43 - def __new__(cls, pos_team, yardline):
44 if not yardline: 45 return None 46 return object.__new__(cls)
47
48 - def __init__(self, pos_team, yardline):
49 """ 50 pos_team is the team on offense, and yardline is a string formatted 51 like 'team-territory yard-line'. e.g., "NE 32". 52 """ 53 if yardline == '50': 54 self.offset = 0 55 return 56 57 territory, yd_str = yardline.split() 58 yd = int(yd_str) 59 if territory == pos_team: 60 self.offset = -(50 - yd) 61 else: 62 self.offset = 50 - yd
63
64 - def __cmp__(self, other):
65 return cmp(self.offset, other.offset)
66
67 - def __str__(self):
68 return '%d' % self.offset
69 70
71 -class PossessionTime (object):
72 """ 73 Represents the amount of time a drive lasted in (minutes, seconds). 74 """
75 - def __init__(self, clock):
76 self.clock = clock 77 self.minutes, self.seconds = map(int, self.clock.split(':'))
78
79 - def total_seconds(self):
80 return self.seconds + self.minutes * 60
81
82 - def __cmp__(self, other):
83 a, b = (self.minutes, self.seconds), (other.minutes, other.seconds) 84 return cmp(a, b)
85
86 - def __add__(self, other):
87 new_time = PossessionTime('0:00') 88 total_seconds = self.total_seconds() + other.total_seconds() 89 new_time.minutes = total_seconds / 60 90 new_time.seconds = total_seconds % 60 91 new_time.clock = '%.2d:%.2d' % (new_time.minutes, new_time.seconds) 92 return new_time
93
94 - def __sub__(self, other):
95 assert self >= other 96 new_time = PossessionTime('0:00') 97 total_seconds = self.total_seconds() - other.total_seconds() 98 new_time.minutes = total_seconds / 60 99 new_time.seconds = total_seconds % 60 100 new_time.clock = '%.2d:%.2d' % (new_time.minutes, new_time.seconds) 101 return new_time
102
103 - def __str__(self):
104 return self.clock
105 106
107 -class GameClock (object):
108 """ 109 Represents the current time in a game. Namely, it keeps track of the 110 quarter and clock time. Also, GameClock can represent whether 111 the game hasn't started yet, is half time or if it's over. 112 """
113 - def __init__(self, qtr, clock):
114 self.qtr = qtr 115 self.clock = clock 116 117 # Make it easy for comparison. 118 # try: 119 self.__minutes, self.__seconds = map(int, self.clock.split(':')) 120 # except ValueError: 121 # self.__minutes, self.__seconds = 0, 0 122 try: 123 self.__qtr = int(self.qtr) 124 if self.__qtr >= 3: 125 self.__qtr += 1 # Let halftime be quarter 3 126 except ValueError: 127 if self.is_pregame(): 128 self.__qtr = 0 129 elif self.is_halftime(): 130 self.__qtr = 3 131 elif self.is_final(): 132 self.__qtr = sys.maxint 133 else: 134 assert False, 'Unknown QTR value: "%s"' % self.qtr
135
136 - def is_pregame(self):
137 return self.qtr == 'Pregame'
138
139 - def is_halftime(self):
140 return self.qtr == 'Halftime'
141
142 - def is_final(self):
143 return self.qtr == 'Final' or self.qtr == 'final overtime'
144
145 - def __cmp__(self, other):
146 if self.__qtr != other.__qtr: 147 return cmp(self.__qtr, other.__qtr) 148 elif self.__minutes != other.__minutes: 149 return cmp(other.__minutes, self.__minutes) 150 return cmp(other.__seconds, self.__seconds)
151
152 - def __str__(self):
153 """ 154 Returns a nicely formatted string indicating the current time of the 155 game. Examples include "Q1 10:52", "Q4 1:25", "Pregame", "Halftime" 156 and "Final". 157 """ 158 try: 159 q = int(self.qtr) 160 return 'Q%d %s' % (q, self.clock) 161 except ValueError: 162 return self.qtr
163 164
165 -class Game (object):
166 """ 167 Game represents a single pre- or regular-season game. It provides a window 168 into the statistics of every player that played into the game, along with 169 the winner of the game, the score and a list of all the scoring plays. 170 """ 171
172 - def __new__(cls, eid=None, fpath=None):
173 # If we can't get a valid JSON data, exit out and return None. 174 rawData = _get_json_data(eid, fpath) 175 if rawData is None or rawData.strip() == '{}': 176 return None 177 game = object.__new__(cls) 178 game.rawData = rawData 179 return game
180
181 - def __init__(self, eid=None, fpath=None):
182 """ 183 Creates a new Game instance given a game identifier. 184 185 The game identifier is used by NFL.com's GameCenter live update web 186 pages. It is used to construct a URL to download JSON data for the 187 game. 188 189 If the game has been completed, the JSON data will be cached to disk 190 so that subsequent accesses will not re-download the data but instead 191 read it from disk. 192 193 When the JSON data is written to disk, it is compressed using gzip. 194 """ 195 196 if eid is not None: 197 self.eid = eid 198 self.data = json.loads(self.rawData)[self.eid] 199 else: 200 self.eid = None 201 self.data = json.loads(self.rawData) 202 for k, v in self.data.iteritems(): 203 if isinstance(v, dict): 204 self.eid = k 205 self.data = v 206 break 207 assert self.eid is not None 208 209 # Home and team cumulative statistics. 210 self.home = self.data['home']['abbr'] 211 self.away = self.data['away']['abbr'] 212 self.stats_home = _json_team_stats(self.data['home']['stats']['team']) 213 self.stats_away = _json_team_stats(self.data['away']['stats']['team']) 214 215 # Load up some simple static values. 216 self.time = GameClock(self.data['qtr'], self.data['clock']) 217 self.down = _tryint(self.data['down']) 218 self.togo = _tryint(self.data['togo']) 219 self.score_home = int(self.data['home']['score']['T']) 220 self.score_away = int(self.data['away']['score']['T']) 221 for q in (1, 2, 3, 4, 5): 222 for team in ('home', 'away'): 223 score = self.data[team]['score'][str(q)] 224 self.__dict__['score_%s_q%d' % (team, q)] = int(score) 225 226 if not self.game_over(): 227 self.winner = None 228 else: 229 if self.score_home > self.score_away: 230 self.winner = self.home 231 self.loser = self.away 232 elif self.score_away > self.score_home: 233 self.winner = self.away 234 self.loser = self.home 235 else: 236 self.winner = '%s/%s' % (self.home, self.away) 237 self.loser = '%s/%s' % (self.home, self.away) 238 239 # Load the scoring summary into a simple list of strings. 240 self.scores = [] 241 for k in sorted(map(int, self.data['scrsummary'])): 242 play = self.data['scrsummary'][str(k)] 243 s = '%s - Q%d - %s - %s' \ 244 % (play['team'], play['qtr'], play['type'], play['desc']) 245 self.scores.append(s) 246 247 # Check to see if the game is over, and if so, cache the data. 248 if self.game_over() and not os.access(_jsonf % eid, os.R_OK): 249 self.save()
250
251 - def is_home(self, team):
252 """Returns true if team (i.e., 'NE') is the home team.""" 253 return team == self.home
254
255 - def game_over(self):
256 """game_over returns true if the game is no longer being played.""" 257 return self.time.is_final()
258
259 - def playing(self):
260 """playing returns true if the game is currently being played.""" 261 return not self.time.is_pregame() and not self.time.is_final()
262
263 - def save(self, fpath=None):
264 """ 265 Save the JSON data to fpath. This is done automatically if the 266 game is over. 267 """ 268 if fpath is None: 269 fpath = _jsonf % self.eid 270 try: 271 print >> gzip.open(fpath, 'w+'), self.rawData, 272 except IOError: 273 print >> sys.stderr, "Could not cache JSON data. Please " \ 274 "make '%s' writable." \ 275 % os.path.dirname(fpath)
276
277 - def nice_score(self):
278 """ 279 Returns a string of the score of the game. 280 e.g., "NE (32) vs. NYG (0)". 281 """ 282 return '%s (%d) vs. %s (%d)' \ 283 % (self.home, self.score_home, self.away, self.score_away)
284
285 - def __getattr__(self, name):
286 if name == 'players': 287 self.__players = _json_game_player_stats(self.data) 288 self.players = nflgame.seq.GenPlayerStats(self.__players) 289 return self.players 290 if name == 'drives': 291 self.__drives = _json_drives(self, self.home, self.data['drives']) 292 self.drives = nflgame.seq.GenDrives(self.__drives) 293 return self.drives
294
295 - def __sub__(self, other):
296 return diff(other, self)
297 298
299 -def diff(before, after):
300 """ 301 Returns the difference between two points of time in a game in terms of 302 plays and player statistics. The return value is a GameDiff namedtuple 303 with two attributes: plays and players. Each contains *only* the data 304 that is in the after game but not in the before game. 305 306 This is useful for sending alerts where you're guaranteed to see each 307 play statistic only once (assuming NFL.com behaves itself). 308 309 XXX: There is an assertion that requires after's game clock be the same 310 or later than before's game clock. This may need to be removed if NFL.com 311 allows its game clock to be rolled back due to corrections from refs. 312 """ 313 assert after.time >= before.time, \ 314 'When diffing two games, "after" (%s) must be later or the ' \ 315 'same time as "before" (%s).' % (after.time, before.time) 316 assert after.eid == before.eid 317 318 plays = [] 319 after_plays = list(after.drives.plays()) 320 before_plays = list(before.drives.plays()) 321 for play in after_plays: 322 if play not in before_plays: 323 plays.append(play) 324 325 # You might think that updated play data is enough. You could scan 326 # it for statistics you're looking for (like touchdowns). 327 # But sometimes a play can sneak in twice if its description gets 328 # updated (late call? play review? etc.) 329 # Thus, we do a diff on the play statistics for player data too. 330 _players = OrderedDict() 331 after_players = list(after.drives.players()) 332 before_players = list(before.drives.players()) 333 for aplayer in after_players: 334 has_before = False 335 for bplayer in before_players: 336 if aplayer.playerid == bplayer.playerid: 337 has_before = True 338 pdiff = aplayer - bplayer 339 if pdiff is not None: 340 _players[aplayer.playerid] = pdiff 341 if not has_before: 342 _players[aplayer.playerid] = aplayer 343 players = nflgame.seq.GenPlayerStats(_players) 344 345 return GameDiff(plays=plays, players=players)
346 347
348 -class Drive (object):
349 """ 350 Drive represents a single drive in an NFL game. It contains a list 351 of all plays that happened in the drive, in chronological order. 352 It also contains meta information about the drive such as the start 353 and stop times and field position, length of possession, the number 354 of first downs and a short descriptive string of the result of the 355 drive. 356 357 """
358 - def __init__(self, game, drive_num, home_team, data):
359 if data is None: 360 return 361 self.game = game 362 self.drive_num = drive_num 363 self.team = data['posteam'] 364 self.home = self.team == home_team 365 self.first_downs = int(data['fds']) 366 self.result = data['result'] 367 self.penalty_yds = int(data['penyds']) 368 self.total_yds = int(data['ydsgained']) 369 self.pos_time = PossessionTime(data['postime']) 370 self.play_cnt = int(data['numplays']) 371 self.field_start = FieldPosition(self.team, data['start']['yrdln']) 372 self.time_start = GameClock(data['start']['qtr'], 373 data['start']['time']) 374 375 # When the game is over, the yardline isn't reported. So find the 376 # last play that does report a yardline. 377 if data['end']['yrdln'].strip(): 378 self.field_end = FieldPosition(self.team, data['end']['yrdln']) 379 else: 380 self.field_end = None 381 playids = sorted(map(int, data['plays'].keys()), reverse=True) 382 for pid in playids: 383 yrdln = data['plays'][str(pid)]['yrdln'].strip() 384 if yrdln: 385 self.field_end = FieldPosition(self.team, yrdln) 386 break 387 if self.field_end is None: 388 self.field_end = FieldPosition(self.team, '50') 389 390 # When a drive lasts from Q1 to Q2 or Q3 to Q4, the 'end' doesn't 391 # seem to change to the proper quarter. So look at the last play and 392 # use that quarter instead. 393 lastplayid = str(sorted(map(int, data['plays'].keys()))[-1]) 394 endqtr = data['plays'][lastplayid]['qtr'] 395 self.time_end = GameClock(endqtr, data['end']['time']) 396 397 self.__plays = _json_plays(self, data['plays']) 398 self.plays = nflgame.seq.GenPlays(self.__plays)
399
400 - def __add__(self, other):
401 """ 402 Adds the statistics of two drives together. 403 404 Note that once two drives are added, the following fields 405 automatically get None values: result, field_start, field_end, 406 time_start and time_end. 407 """ 408 assert self.team == other.team, \ 409 'Cannot add drives from different teams "%s" and "%s".' \ 410 % (self.team, other.team) 411 new_drive = Drive(None, 0, '', None) 412 new_drive.team = self.team 413 new_drive.home = self.home 414 new_drive.first_downs = self.first_downs + other.first_downs 415 new_drive.penalty_yds = self.penalty_yds + other.penalty_yds 416 new_drive.total_yds = self.total_yds + other.total_yds 417 new_drive.pos_time = self.pos_time + other.pos_time 418 new_drive.play_cnt = self.play_cnt + other.play_cnt 419 new_drive.__plays = self.__plays + other.__plays 420 new_drive.result = None 421 new_drive.field_start = None 422 new_drive.field_end = None 423 new_drive.time_start = None 424 new_drive.time_end = None 425 return new_drive
426 427
428 -class Play (object):
429 """ 430 Play represents a single play. It contains a list of all players 431 that participated in the play (including offense, defense and special 432 teams). The play also includes meta information about what down it 433 is, field position, clock time, etc. 434 435 Play objects also contain team-level statistics, such as whether the 436 play was a first down, a fourth down failure, etc. 437 """
438 - def __init__(self, drive, playid, data):
439 self.drive = drive 440 self.playid = playid 441 self.team = data['posteam'] 442 self.desc = data['desc'] 443 self.note = data['note'] 444 self.down = int(data['down']) 445 self.yards_togo = int(data['ydstogo']) 446 self.touchdown = 'touchdown' in self.desc.lower() 447 448 if not self.team: 449 self.time, self.yardline = None, None 450 else: 451 self.time = GameClock(data['qtr'], data['time']) 452 self.yardline = FieldPosition(self.team, data['yrdln']) 453 454 # Load team statistics directly into the Play instance. 455 # Things like third down attempts, first downs, etc. 456 if '0' in data['players']: 457 for info in data['players']['0']: 458 if info['statId'] not in nflgame.statmap.idmap: 459 continue 460 statvals = nflgame.statmap.values(info['statId'], 461 info['yards']) 462 for k, v in statvals.iteritems(): 463 self.__dict__[k] = self.__dict__.get(k, 0) + v 464 self.__players = _json_play_players(self, data['players']) 465 self.players = nflgame.seq.GenPlayerStats(self.__players)
466
467 - def has_player(self, playerid):
468 """Whether a player with id playerid participated in this play.""" 469 return playerid in self.__players
470
471 - def __str__(self):
472 return self.desc
473
474 - def __eq__(self, other):
475 """ 476 We use the play description to determine equality because the 477 play description can be changed. (Like when a play is reversed.) 478 """ 479 return self.playid == other.playid and self.desc == other.desc
480 481
482 -def _json_team_stats(data):
483 """ 484 Takes a team stats JSON entry and converts it to a TeamStats namedtuple. 485 """ 486 return TeamStats( 487 first_downs=int(data['totfd']), 488 total_yds=int(data['totyds']), 489 passing_yds=int(data['pyds']), 490 rushing_yds=int(data['ryds']), 491 penalty_cnt=int(data['pen']), 492 penalty_yds=int(data['penyds']), 493 turnovers=int(data['trnovr']), 494 punt_cnt=int(data['pt']), 495 punt_yds=int(data['ptyds']), 496 punt_avg=int(data['ptavg']), 497 pos_time=PossessionTime(data['top']))
498 499
500 -def _json_drives(game, home_team, data):
501 """ 502 Takes a home or away JSON entry and converts it to a list of Drive 503 objects. 504 """ 505 drive_nums = [] 506 for drive_num in data: 507 try: 508 drive_nums.append(int(drive_num)) 509 except: 510 pass 511 drives = [] 512 playids = set() # Plays can be repeated! Ah! 513 for i, drive_num in enumerate(sorted(drive_nums), 1): 514 repeat_drive = False 515 for playid in data[str(drive_num)]['plays']: 516 if playid in playids: 517 repeat_drive = True 518 break 519 playids.add(playid) 520 if repeat_drive: 521 continue 522 drives.append(Drive(game, i, home_team, data[str(drive_num)])) 523 return drives
524 525
526 -def _json_plays(drive, data):
527 """ 528 Takes a single JSON drive entry (data) and converts it to a list 529 of Play objects. 530 """ 531 plays = [] 532 for playid in map(str, sorted(map(int, data))): 533 plays.append(Play(drive, playid, data[playid])) 534 return plays
535 536
537 -def _json_play_players(play, data):
538 """ 539 Takes a single JSON play entry (data) and converts it to an OrderedDict 540 of player statistics. 541 542 play is the instance of Play that this data is part of. It is used 543 to determine whether the player belong to the home team or not. 544 """ 545 players = OrderedDict() 546 for playerid, statcats in data.iteritems(): 547 if playerid == '0': 548 continue 549 for info in statcats: 550 if info['statId'] not in nflgame.statmap.idmap: 551 continue 552 if playerid not in players: 553 home = play.drive.game.is_home(info['clubcode']) 554 stats = nflgame.player.PlayPlayerStats(playerid, 555 info['playerName'], 556 home) 557 players[playerid] = stats 558 statvals = nflgame.statmap.values(info['statId'], info['yards']) 559 players[playerid]._add_stats(statvals) 560 return players
561 562
563 -def _json_game_player_stats(data):
564 """ 565 Parses the 'home' and 'away' team stats and returns an OrderedDict 566 mapping player id to their total game statistics as instances of 567 nflgame.player.GamePlayerStats. 568 """ 569 players = OrderedDict() 570 for team in ('home', 'away'): 571 for category in nflgame.statmap.categories: 572 if category not in data[team]['stats']: 573 continue 574 for pid, raw in data[team]['stats'][category].iteritems(): 575 stats = {} 576 for k, v in raw.iteritems(): 577 if k == 'name': 578 continue 579 stats['%s_%s' % (category, k)] = v 580 if pid not in players: 581 home = team == 'home' 582 players[pid] = nflgame.player.GamePlayerStats(pid, 583 raw['name'], 584 home) 585 players[pid]._add_stats(stats) 586 return players
587 588
589 -def _get_json_data(eid=None, fpath=None):
590 """ 591 Returns the JSON data corresponding to the game represented by eid. 592 593 If the JSON data is already on disk, it is read, decompressed and returned. 594 595 Otherwise, the JSON data is downloaded from the NFL web site. If the data 596 doesn't exist yet or there was an error, _get_json_data returns None. 597 598 If eid is None, then the JSON data is read from the file at fpath. 599 """ 600 assert eid is not None or fpath is not None 601 602 if fpath is not None: 603 return gzip.open(fpath).read() 604 605 fpath = _jsonf % eid 606 if os.access(fpath, os.R_OK): 607 return gzip.open(fpath).read() 608 try: 609 return urllib2.urlopen(_json_base_url % (eid, eid)).read() 610 except urllib2.HTTPError: 611 pass 612 return None
613 614
615 -def _tryint(v):
616 """ 617 Tries to convert v to an integer. If it fails, return 0. 618 """ 619 try: 620 return int(v) 621 except: 622 return 0
623