]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/player.py
Initial checkin of tabs for the player_info view.
[xonotic/xonstat.git] / xonstat / views / player.py
1 import datetime
2 import json
3 import logging
4 import re
5 import sqlalchemy as sa
6 import sqlalchemy.sql.functions as func
7 import time
8 from collections import namedtuple
9 from pyramid.response import Response
10 from pyramid.url import current_route_url
11 from sqlalchemy import desc, distinct
12 from webhelpers.paginate import Page, PageURL
13 from xonstat.models import *
14 from xonstat.util import page_url
15
16 log = logging.getLogger(__name__)
17
18
19 def player_index_data(request):
20     if request.params.has_key('page'):
21         current_page = request.params['page']
22     else:
23         current_page = 1
24
25     try:
26         player_q = DBSession.query(Player).\
27                 filter(Player.player_id > 2).\
28                 filter(Player.active_ind == True).\
29                 filter(sa.not_(Player.nick.like('Anonymous Player%'))).\
30                 order_by(Player.player_id.desc())
31
32         players = Page(player_q, current_page, items_per_page=10, url=page_url)
33
34     except Exception as e:
35         players = None
36         raise e
37
38     return {'players':players
39            }
40
41
42 def player_index(request):
43     """
44     Provides a list of all the current players.
45     """
46     return player_index_data(request)
47
48
49 def player_index_json(request):
50     """
51     Provides a list of all the current players. JSON.
52     """
53     return [{'status':'not implemented'}]
54
55
56 def get_games_played(player_id):
57     """
58     Provides a breakdown by gametype of the games played by player_id.
59
60     Returns a list of namedtuples with the following members:
61         - game_type_cd
62         - games
63         - wins
64         - losses
65         - win_pct
66
67     The list itself is ordered by the number of games played
68     """
69     GamesPlayed = namedtuple('GamesPlayed', ['game_type_cd', 'games', 'wins',
70         'losses', 'win_pct'])
71
72     raw_games_played = DBSession.query('game_type_cd', 'wins', 'losses').\
73             from_statement(
74                 "SELECT game_type_cd, "
75                        "SUM(win) wins, "
76                        "SUM(loss) losses "
77                 "FROM   (SELECT g.game_id, "
78                                "g.game_type_cd, "
79                                "CASE "
80                                  "WHEN g.winner = pgs.team THEN 1 "
81                                  "WHEN pgs.rank = 1 THEN 1 "
82                                  "ELSE 0 "
83                                "END win, "
84                                "CASE "
85                                  "WHEN g.winner = pgs.team THEN 0 "
86                                  "WHEN pgs.rank = 1 THEN 0 "
87                                  "ELSE 1 "
88                                "END loss "
89                         "FROM   games g, "
90                                "player_game_stats pgs "
91                         "WHERE  g.game_id = pgs.game_id "
92                         "AND pgs.player_id = :player_id) win_loss "
93                 "GROUP  BY game_type_cd "
94             ).params(player_id=player_id).all()
95
96     games_played = []
97     overall_games = 0
98     overall_wins = 0
99     overall_losses = 0
100     for row in raw_games_played:
101         games = row.wins + row.losses
102         overall_games += games
103         overall_wins += row.wins
104         overall_losses += row.losses
105         win_pct = float(row.wins)/games * 100
106
107         games_played.append(GamesPlayed(row.game_type_cd, games, row.wins,
108             row.losses, win_pct))
109
110     try:
111         overall_win_pct = float(overall_wins)/overall_games * 100
112     except:
113         overall_win_pct = 0.0
114
115     games_played.append(GamesPlayed('overall', overall_games, overall_wins,
116         overall_losses, overall_win_pct))
117
118     # sort the resulting list by # of games played
119     games_played = sorted(games_played, key=lambda x:x.games)
120     games_played.reverse()
121     return games_played
122
123
124 def get_overall_stats(player_id):
125     """
126     Provides a breakdown of stats by gametype played by player_id.
127
128     Returns a dictionary of namedtuples with the following members:
129         - total_kills
130         - total_deaths
131         - k_d_ratio
132         - last_played (last time the player played the game type)
133         - total_playing_time (total amount of time played the game type)
134         - total_pickups (ctf only)
135         - total_captures (ctf only)
136         - cap_ratio (ctf only)
137         - total_carrier_frags (ctf only)
138         - game_type_cd
139
140     The key to the dictionary is the game type code. There is also an
141     "overall" game_type_cd which sums the totals and computes the total ratios.
142     """
143     OverallStats = namedtuple('OverallStats', ['total_kills', 'total_deaths',
144         'k_d_ratio', 'last_played', 'total_playing_time', 'total_pickups',
145         'total_captures', 'cap_ratio', 'total_carrier_frags', 'game_type_cd'])
146
147     raw_stats = DBSession.query('game_type_cd', 'total_kills',
148             'total_deaths', 'last_played', 'total_playing_time',
149             'total_pickups', 'total_captures', 'total_carrier_frags').\
150             from_statement(
151                 "SELECT g.game_type_cd, "
152                        "Sum(pgs.kills)         total_kills, "
153                        "Sum(pgs.deaths)        total_deaths, "
154                        "Max(pgs.create_dt)     last_played, "
155                        "Sum(pgs.alivetime)     total_playing_time, "
156                        "Sum(pgs.pickups)       total_pickups, "
157                        "Sum(pgs.captures)      total_captures, "
158                        "Sum(pgs.carrier_frags) total_carrier_frags "
159                 "FROM   games g, "
160                        "player_game_stats pgs "
161                 "WHERE  g.game_id = pgs.game_id "
162                   "AND  pgs.player_id = :player_id "
163                 "GROUP  BY g.game_type_cd "
164             ).params(player_id=player_id).all()
165
166     # to be indexed by game_type_cd
167     overall_stats = {}
168
169     # sums for the "overall" game type (which is fake)
170     overall_kills = 0
171     overall_deaths = 0
172     overall_last_played = None
173     overall_playing_time = datetime.timedelta(seconds=0)
174     overall_carrier_frags = 0
175
176     for row in raw_stats:
177         # running totals or mins
178         overall_kills += row.total_kills or 0
179         overall_deaths += row.total_deaths or 0
180
181         if overall_last_played is None or row.last_played > overall_last_played:
182             overall_last_played = row.last_played
183
184         overall_playing_time += row.total_playing_time
185
186         # individual gametype ratio calculations
187         try:
188             k_d_ratio = float(row.total_kills)/row.total_deaths
189         except:
190             k_d_ratio = None
191
192         try:
193             cap_ratio = float(row.total_pickups)/row.total_captures
194         except:
195             cap_ratio = None
196
197         overall_carrier_frags += row.total_carrier_frags or 0
198
199         # everything else is untouched or "raw"
200         os = OverallStats(total_kills=row.total_kills,
201                 total_deaths=row.total_deaths,
202                 k_d_ratio=k_d_ratio,
203                 last_played=row.last_played,
204                 total_playing_time=row.total_playing_time,
205                 total_pickups=row.total_pickups,
206                 total_captures=row.total_captures,
207                 cap_ratio=cap_ratio,
208                 total_carrier_frags=row.total_carrier_frags,
209                 game_type_cd=row.game_type_cd)
210
211         overall_stats[row.game_type_cd] = os
212
213     # and lastly, the overall stuff
214     try:
215         overall_k_d_ratio = float(overall_kills)/overall_deaths
216     except:
217         overall_k_d_ratio = None
218
219     os = OverallStats(total_kills=overall_kills,
220             total_deaths=overall_deaths,
221             k_d_ratio=overall_k_d_ratio,
222             last_played=overall_last_played,
223             total_playing_time=overall_playing_time,
224             total_pickups=None,
225             total_captures=None,
226             cap_ratio=None,
227             total_carrier_frags=overall_carrier_frags,
228             game_type_cd='overall')
229
230     overall_stats['overall'] = os
231
232     return overall_stats
233
234
235 def get_fav_maps(player_id, game_type_cd=None):
236     """
237     Provides a breakdown of favorite maps by gametype.
238
239     Returns a dictionary of namedtuples with the following members:
240         - game_type_cd
241         - map_name (map name)
242         - map_id
243         - times_played
244
245     The favorite map is defined as the map you've played the most
246     for the given game_type_cd.
247
248     The key to the dictionary is the game type code. There is also an
249     "overall" game_type_cd which is the overall favorite map. This is
250     defined as the favorite map of the game type you've played the
251     most. The input parameter game_type_cd is for this.
252     """
253     raw_favs = DBSession.query('game_type_cd', 'map_name',
254             'map_id', 'times_played').\
255             from_statement(
256                 "SELECT game_type_cd, "
257                        "name map_name, "
258                        "map_id, "
259                        "times_played "
260                 "FROM   (SELECT g.game_type_cd, "
261                                "m.name, "
262                                "m.map_id, "
263                                "Count(*) times_played, "
264                                "Row_number() "
265                                  "OVER ( "
266                                    "partition BY g.game_type_cd "
267                                    "ORDER BY Count(*) DESC, m.map_id ASC) rank "
268                         "FROM   games g, "
269                                "player_game_stats pgs, "
270                                "maps m "
271                         "WHERE  g.game_id = pgs.game_id "
272                                "AND g.map_id = m.map_id "
273                                "AND pgs.player_id = :player_id "
274                         "GROUP  BY g.game_type_cd, "
275                                   "m.map_id, "
276                                   "m.name) most_played "
277                 "WHERE  rank = 1 "
278                 "ORDER BY  times_played desc "
279             ).params(player_id=player_id).all()
280
281     fav_maps = {}
282     overall_fav = None
283     for row in raw_favs:
284         # if we aren't given a favorite game_type_cd
285         # then the overall favorite is the one we've
286         # played the most
287         if overall_fav is None:
288             fav_maps['overall'] = row
289             overall_fav = row.game_type_cd
290
291         # otherwise it is the favorite map from the
292         # favorite game_type_cd (provided as a param)
293         # and we'll overwrite the first dict entry
294         if game_type_cd == row.game_type_cd:
295             fav_maps['overall'] = row
296
297         fav_maps[row.game_type_cd] = row
298
299     return fav_maps
300
301
302 def get_ranks(player_id):
303     """
304     Provides a breakdown of the player's ranks by game type.
305
306     Returns a dictionary of namedtuples with the following members:
307         - game_type_cd
308         - rank
309         - max_rank
310
311     The key to the dictionary is the game type code. There is also an
312     "overall" game_type_cd which is the overall best rank.
313     """
314     raw_ranks = DBSession.query("game_type_cd", "rank", "max_rank").\
315             from_statement(
316                 "select pr.game_type_cd, pr.rank, overall.max_rank "
317                 "from player_ranks pr,  "
318                    "(select game_type_cd, max(rank) max_rank "
319                     "from player_ranks  "
320                     "group by game_type_cd) overall "
321                 "where pr.game_type_cd = overall.game_type_cd  "
322                 "and player_id = :player_id "
323                 "order by rank").\
324             params(player_id=player_id).all()
325
326     ranks = {}
327     found_top_rank = False
328     for row in raw_ranks:
329         if not found_top_rank:
330             ranks['overall'] = row
331             found_top_rank = True
332
333         ranks[row.game_type_cd] = row
334
335     return ranks;
336
337
338 def get_elos(player_id):
339     """
340     Provides a breakdown of the player's elos by game type.
341
342     Returns a dictionary of namedtuples with the following members:
343         - player_id
344         - game_type_cd
345         - games
346         - elo
347
348     The key to the dictionary is the game type code. There is also an
349     "overall" game_type_cd which is the overall best rank.
350     """
351     raw_elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\
352             order_by(PlayerElo.elo.desc()).all()
353
354     elos = {}
355     found_max_elo = False
356     for row in raw_elos:
357         if not found_max_elo:
358             elos['overall'] = row
359             found_max_elo = True
360
361         elos[row.game_type_cd] = row
362
363     return elos
364
365
366 def get_recent_games(player_id):
367     """
368     Provides a list of recent games.
369
370     Returns the full PlayerGameStat, Game, Server, Map
371     objects for all recent games.
372     """
373     # recent games table, all data
374     recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\
375             filter(PlayerGameStat.player_id == player_id).\
376             filter(PlayerGameStat.game_id == Game.game_id).\
377             filter(Game.server_id == Server.server_id).\
378             filter(Game.map_id == Map.map_id).\
379             order_by(Game.game_id.desc())[0:10]
380
381     return recent_games
382
383
384 def get_recent_weapons(player_id):
385     """
386     Returns the weapons that have been used in the past 90 days
387     and also used in 5 games or more.
388     """
389     cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=90)
390     recent_weapons = []
391     for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\
392             filter(PlayerWeaponStat.player_id == player_id).\
393             filter(PlayerWeaponStat.create_dt > cutoff).\
394             group_by(PlayerWeaponStat.weapon_cd).\
395             having(func.count() > 4).\
396             all():
397                 recent_weapons.append(weapon[0])
398
399     return recent_weapons
400
401
402 def _get_games_played(player_id):
403     """
404     Provides a breakdown by gametype of the games played by player_id.
405
406     Returns a tuple containing (total_games, games_breakdown), where
407     total_games is the absolute number of games played by player_id
408     and games_breakdown is an array containing (game_type_cd, # games)
409     """
410     games_played = DBSession.query(Game.game_type_cd, func.count()).\
411             filter(Game.game_id == PlayerGameStat.game_id).\
412             filter(PlayerGameStat.player_id == player_id).\
413             group_by(Game.game_type_cd).\
414             order_by(func.count().desc()).all()
415
416     total = 0
417     for (game_type_cd, games) in games_played:
418         total += games
419
420     return (total, games_played)
421
422
423 # TODO: should probably factor the above function into this one such that
424 # total_stats['ctf_games'] is the count of CTF games and so on...
425 def _get_total_stats(player_id):
426     """
427     Provides aggregated stats by player_id.
428
429     Returns a dict with the keys 'kills', 'deaths', 'alivetime'.
430
431     kills = how many kills a player has over all games
432     deaths = how many deaths a player has over all games
433     alivetime = how long a player has played over all games
434
435     If any of the above are None, they are set to 0.
436     """
437     total_stats = {}
438     (total_stats['kills'], total_stats['deaths'], total_stats['alivetime']) = DBSession.\
439             query("total_kills", "total_deaths", "total_alivetime").\
440             from_statement(
441                 "select sum(kills) total_kills, "
442                 "sum(deaths) total_deaths, "
443                 "sum(alivetime) total_alivetime "
444                 "from player_game_stats "
445                 "where player_id=:player_id"
446             ).params(player_id=player_id).one()
447
448     (total_stats['wins'],) = DBSession.\
449             query("total_wins").\
450             from_statement(
451                 "select count(*) total_wins "
452                 "from games g, player_game_stats pgs "
453                 "where g.game_id = pgs.game_id "
454                 "and player_id=:player_id "
455                 "and (g.winner = pgs.team or pgs.rank = 1)"
456             ).params(player_id=player_id).one()
457
458     for (key,value) in total_stats.items():
459         if value == None:
460             total_stats[key] = 0
461
462     return total_stats
463
464
465 def _get_fav_maps(player_id):
466     """
467     Get the player's favorite map. The favorite map is defined
468     as the map that he or she has played the most with game
469     types considered separate. This is to say that if a person
470     plays dm and duel on stormkeep with 25 games in each mode, 
471     final_rage could still be the favorite map overall if it has
472     26 dm games.
473
474     Returns a dictionary with entries for each played game type.
475     Each game type ditionary value contained a nested dictionary
476     with the following keys:
477         id = the favorite map id
478         name = the favorite map's name
479         times_played = the number of times the map was played in that mode
480
481     Note also that there's a superficial "overall" game type that is
482     meant to hold the top map overall. It'll be a dupe of one of the
483     other game types' nested dictionary.
484     """
485     fav_maps = {}
486     for (game_type_cd, name, map_id, times_played) in DBSession.\
487             query("game_type_cd", "name", "map_id", "times_played").\
488             from_statement(
489                 "SELECT game_type_cd, "
490                        "name, "
491                        "map_id, "
492                        "times_played "
493                 "FROM   (SELECT g.game_type_cd, "
494                                "m.name, "
495                                "m.map_id, "
496                                "count(*) times_played, "
497                                "row_number() "
498                                  "over ( "
499                                    "PARTITION BY g.game_type_cd "
500                                    "ORDER BY Count(*) DESC, m.map_id ASC) rank "
501                         "FROM   games g, "
502                                "player_game_stats pgs, "
503                                "maps m "
504                         "WHERE  g.game_id = pgs.game_id "
505                                "AND g.map_id = m.map_id "
506                                "AND pgs.player_id = :player_id "
507                         "GROUP  BY g.game_type_cd, "
508                                   "m.map_id, "
509                                   "m.name) most_played "
510                 "WHERE  rank = 1"
511             ).params(player_id=player_id).all():
512                 fav_map_detail = {}
513                 fav_map_detail['name'] = name
514                 fav_map_detail['map_id'] = map_id
515                 fav_map_detail['times_played'] = times_played
516                 fav_maps[game_type_cd] = fav_map_detail
517
518     max_played = 0
519     overall = {}
520     for fav_map_detail in fav_maps.values():
521         if fav_map_detail['times_played'] > max_played:
522             max_played = fav_map_detail['times_played']
523             overall = fav_map_detail
524
525     fav_maps['overall'] = overall
526
527     return fav_maps
528
529
530 def _get_rank(player_id):
531     """
532     Get the player's rank as well as the total number of ranks.
533     """
534     rank = DBSession.query("game_type_cd", "rank", "max_rank").\
535             from_statement(
536                 "select pr.game_type_cd, pr.rank, overall.max_rank "
537                 "from player_ranks pr,  "
538                    "(select game_type_cd, max(rank) max_rank "
539                     "from player_ranks  "
540                     "group by game_type_cd) overall "
541                 "where pr.game_type_cd = overall.game_type_cd  "
542                 "and player_id = :player_id "
543                 "order by rank").\
544             params(player_id=player_id).all()
545
546     return rank;
547
548
549 def get_accuracy_stats(player_id, weapon_cd, games):
550     """
551     Provides accuracy for weapon_cd by player_id for the past N games.
552     """
553     # Reaching back 90 days should give us an accurate enough average
554     # We then multiply this out for the number of data points (games) to
555     # create parameters for a flot graph
556     try:
557         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),
558                 func.sum(PlayerWeaponStat.fired)).\
559                 filter(PlayerWeaponStat.player_id == player_id).\
560                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
561                 one()
562
563         avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)
564
565         # Determine the raw accuracy (hit, fired) numbers for $games games
566         # This is then enumerated to create parameters for a flot graph
567         raw_accs = DBSession.query(PlayerWeaponStat.game_id, 
568             PlayerWeaponStat.hit, PlayerWeaponStat.fired).\
569                 filter(PlayerWeaponStat.player_id == player_id).\
570                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
571                 order_by(PlayerWeaponStat.game_id.desc()).\
572                 limit(games).\
573                 all()
574
575         # they come out in opposite order, so flip them in the right direction
576         raw_accs.reverse()
577
578         accs = []
579         for i in range(len(raw_accs)):
580             accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))
581     except:
582         accs = []
583         avg = 0.0
584
585     return (avg, accs)
586
587
588 def get_damage_stats(player_id, weapon_cd, games):
589     """
590     Provides damage info for weapon_cd by player_id for the past N games.
591     """
592     try:
593         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.actual),
594                 func.sum(PlayerWeaponStat.hit)).\
595                 filter(PlayerWeaponStat.player_id == player_id).\
596                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
597                 one()
598
599         avg = round(float(raw_avg[0])/raw_avg[1], 2)
600
601         # Determine the damage efficiency (hit, fired) numbers for $games games
602         # This is then enumerated to create parameters for a flot graph
603         raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, 
604             PlayerWeaponStat.actual, PlayerWeaponStat.hit).\
605                 filter(PlayerWeaponStat.player_id == player_id).\
606                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
607                 order_by(PlayerWeaponStat.game_id.desc()).\
608                 limit(games).\
609                 all()
610
611         # they come out in opposite order, so flip them in the right direction
612         raw_dmgs.reverse()
613
614         dmgs = []
615         for i in range(len(raw_dmgs)):
616             # try to derive, unless we've hit nothing then set to 0!
617             try:
618                 dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)
619             except:
620                 dmg = 0.0
621
622             dmgs.append((raw_dmgs[i][0], dmg))
623     except Exception as e:
624         dmgs = []
625         avg = 0.0
626
627     return (avg, dmgs)
628
629
630 def player_info_data(request):
631     player_id = int(request.matchdict['id'])
632     if player_id <= 2:
633         player_id = -1;
634
635     try:
636         player = DBSession.query(Player).filter_by(player_id=player_id).\
637                 filter(Player.active_ind == True).one()
638
639         games_played   = get_games_played(player_id)
640         overall_stats  = get_overall_stats(player_id)
641         fav_maps       = get_fav_maps(player_id)
642         elos           = get_elos(player_id)
643         ranks          = get_ranks(player_id)
644         recent_games   = get_recent_games(player_id)
645         recent_weapons = get_recent_weapons(player_id)
646
647     except Exception as e:
648         player         = None
649         games_played   = None
650         overall_stats  = None
651         fav_maps       = None
652         elos           = None
653         ranks          = None
654         recent_games   = None
655         recent_weapons = []
656
657     return {'player':player,
658             'games_played':games_played,
659             'overall_stats':overall_stats,
660             'fav_maps':fav_maps,
661             'elos':elos,
662             'ranks':ranks,
663             'recent_games':recent_games,
664             'recent_weapons':recent_weapons
665             }
666
667
668 def player_info(request):
669     """
670     Provides detailed information on a specific player
671     """
672     return player_info_data(request)
673
674
675 def player_info_json(request):
676     """
677     Provides detailed information on a specific player. JSON.
678     """
679     return [{'status':'not implemented'}]
680
681
682 def player_game_index_data(request):
683     player_id = request.matchdict['player_id']
684
685     if request.params.has_key('page'):
686         current_page = request.params['page']
687     else:
688         current_page = 1
689
690     try:
691         games_q = DBSession.query(Game, Server, Map).\
692             filter(PlayerGameStat.game_id == Game.game_id).\
693             filter(PlayerGameStat.player_id == player_id).\
694             filter(Game.server_id == Server.server_id).\
695             filter(Game.map_id == Map.map_id).\
696             order_by(Game.game_id.desc())
697
698         games = Page(games_q, current_page, items_per_page=10, url=page_url)
699
700         pgstats = {}
701         for (game, server, map) in games:
702             pgstats[game.game_id] = DBSession.query(PlayerGameStat).\
703                     filter(PlayerGameStat.game_id == game.game_id).\
704                     order_by(PlayerGameStat.rank).\
705                     order_by(PlayerGameStat.score).all()
706
707     except Exception as e:
708         player = None
709         games = None
710
711     return {'player_id':player_id,
712             'games':games,
713             'pgstats':pgstats}
714
715
716 def player_game_index(request):
717     """
718     Provides an index of the games in which a particular
719     player was involved. This is ordered by game_id, with
720     the most recent game_ids first. Paginated.
721     """
722     return player_game_index_data(request)
723
724
725 def player_game_index_json(request):
726     """
727     Provides an index of the games in which a particular
728     player was involved. This is ordered by game_id, with
729     the most recent game_ids first. Paginated. JSON.
730     """
731     return [{'status':'not implemented'}]
732
733
734 def player_accuracy_data(request):
735     player_id = request.matchdict['id']
736     allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']
737     weapon_cd = 'nex'
738     games = 20
739
740     if request.params.has_key('weapon'):
741         if request.params['weapon'] in allowed_weapons:
742             weapon_cd = request.params['weapon']
743
744     if request.params.has_key('games'):
745         try:
746             games = request.params['games']
747
748             if games < 0:
749                 games = 20
750             if games > 50:
751                 games = 50
752         except:
753             games = 20
754
755     (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)
756
757     # if we don't have enough data for the given weapon
758     if len(accs) < games:
759         games = len(accs)
760
761     return {
762             'player_id':player_id, 
763             'player_url':request.route_url('player_info', id=player_id), 
764             'weapon':weapon_cd, 
765             'games':games, 
766             'avg':avg, 
767             'accs':accs
768             }
769
770
771 def player_accuracy(request):
772     """
773     Provides the accuracy for the given weapon. (JSON only)
774     """
775     return player_accuracy_data(request)
776
777
778 def player_accuracy_json(request):
779     """
780     Provides a JSON response representing the accuracy for the given weapon.
781
782     Parameters:
783        weapon = which weapon to display accuracy for. Valid values are 'nex',
784                 'shotgun', 'uzi', and 'minstanex'.
785        games = over how many games to display accuracy. Can be up to 50.
786     """
787     return player_accuracy_data(request)
788
789
790 def player_damage_data(request):
791     player_id = request.matchdict['id']
792     allowed_weapons = ['grenadelauncher', 'electro', 'crylink', 'hagar',
793             'rocketlauncher', 'laser']
794     weapon_cd = 'rocketlauncher'
795     games = 20
796
797     if request.params.has_key('weapon'):
798         if request.params['weapon'] in allowed_weapons:
799             weapon_cd = request.params['weapon']
800
801     if request.params.has_key('games'):
802         try:
803             games = request.params['games']
804
805             if games < 0:
806                 games = 20
807             if games > 50:
808                 games = 50
809         except:
810             games = 20
811
812     (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)
813
814     # if we don't have enough data for the given weapon
815     if len(dmgs) < games:
816         games = len(dmgs)
817
818     return {
819             'player_id':player_id, 
820             'player_url':request.route_url('player_info', id=player_id), 
821             'weapon':weapon_cd, 
822             'games':games, 
823             'avg':avg, 
824             'dmgs':dmgs
825             }
826
827
828 def player_damage_json(request):
829     """
830     Provides a JSON response representing the damage for the given weapon.
831
832     Parameters:
833        weapon = which weapon to display damage for. Valid values are
834          'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',
835          'laser'.
836        games = over how many games to display damage. Can be up to 50.
837     """
838     return player_damage_data(request)