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
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
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
65 return cmp(self.offset, other.offset)
66
68 return '%d' % self.offset
69
70
72 """
73 Represents the amount of time a drive lasted in (minutes, seconds).
74 """
76 self.clock = clock
77 self.minutes, self.seconds = map(int, self.clock.split(':'))
78
80 return self.seconds + self.minutes * 60
81
83 a, b = (self.minutes, self.seconds), (other.minutes, other.seconds)
84 return cmp(a, b)
85
93
102
105
106
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 """
114 self.qtr = qtr
115 self.clock = clock
116
117
118
119 self.__minutes, self.__seconds = map(int, self.clock.split(':'))
120
121
122 try:
123 self.__qtr = int(self.qtr)
124 if self.__qtr >= 3:
125 self.__qtr += 1
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
137 return self.qtr == 'Pregame'
138
140 return self.qtr == 'Halftime'
141
143 return self.qtr == 'Final' or self.qtr == 'final overtime'
144
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
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
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
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
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
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
248 if self.game_over() and not os.access(_jsonf % eid, os.R_OK):
249 self.save()
250
252 """Returns true if team (i.e., 'NE') is the home team."""
253 return team == self.home
254
256 """game_over returns true if the game is no longer being played."""
257 return self.time.is_final()
258
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
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
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
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
326
327
328
329
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
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
376
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
391
392
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
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
455
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
468 """Whether a player with id playerid participated in this play."""
469 return playerid in self.__players
470
473
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
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
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()
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
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
561
562
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
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
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