5 import sqlalchemy as sa
6 import sqlalchemy.sql.functions as func
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
16 log = logging.getLogger(__name__)
19 def player_index_data(request):
20 if request.params.has_key('page'):
21 current_page = request.params['page']
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())
32 players = Page(player_q, current_page, items_per_page=10, url=page_url)
34 except Exception as e:
38 return {'players':players
42 def player_index(request):
44 Provides a list of all the current players.
46 return player_index_data(request)
49 def player_index_json(request):
51 Provides a list of all the current players. JSON.
53 return [{'status':'not implemented'}]
56 def get_games_played(player_id):
58 Provides a breakdown by gametype of the games played by player_id.
60 Returns a list of namedtuples with the following members:
67 The list itself is ordered by the number of games played
69 GamesPlayed = namedtuple('GamesPlayed', ['game_type_cd', 'games', 'wins',
72 raw_games_played = DBSession.query('game_type_cd', 'wins', 'losses').\
74 "SELECT game_type_cd, "
77 "FROM (SELECT g.game_id, "
80 "WHEN g.winner = pgs.team THEN 1 "
81 "WHEN pgs.rank = 1 THEN 1 "
85 "WHEN g.winner = pgs.team THEN 0 "
86 "WHEN pgs.rank = 1 THEN 0 "
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()
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
107 games_played.append(GamesPlayed(row.game_type_cd, games, row.wins,
108 row.losses, win_pct))
111 overall_win_pct = float(overall_wins)/overall_games * 100
113 overall_win_pct = 0.0
115 games_played.append(GamesPlayed('overall', overall_games, overall_wins,
116 overall_losses, overall_win_pct))
118 # sort the resulting list by # of games played
119 games_played = sorted(games_played, key=lambda x:x.games)
120 games_played.reverse()
124 def get_overall_stats(player_id):
126 Provides a breakdown of stats by gametype played by player_id.
128 Returns a dictionary of namedtuples with the following members:
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)
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.
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'])
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').\
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 "
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()
166 # to be indexed by game_type_cd
169 # sums for the "overall" game type (which is fake)
172 overall_last_played = None
173 overall_playing_time = datetime.timedelta(seconds=0)
174 overall_carrier_frags = 0
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
181 if overall_last_played is None or row.last_played > overall_last_played:
182 overall_last_played = row.last_played
184 overall_playing_time += row.total_playing_time
186 # individual gametype ratio calculations
188 k_d_ratio = float(row.total_kills)/row.total_deaths
193 cap_ratio = float(row.total_pickups)/row.total_captures
197 overall_carrier_frags += row.total_carrier_frags or 0
199 # everything else is untouched or "raw"
200 os = OverallStats(total_kills=row.total_kills,
201 total_deaths=row.total_deaths,
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,
208 total_carrier_frags=row.total_carrier_frags,
209 game_type_cd=row.game_type_cd)
211 overall_stats[row.game_type_cd] = os
213 # and lastly, the overall stuff
215 overall_k_d_ratio = float(overall_kills)/overall_deaths
217 overall_k_d_ratio = None
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,
227 total_carrier_frags=overall_carrier_frags,
228 game_type_cd='overall')
230 overall_stats['overall'] = os
235 def get_fav_maps(player_id, game_type_cd=None):
237 Provides a breakdown of favorite maps by gametype.
239 Returns a dictionary of namedtuples with the following members:
241 - map_name (map name)
245 The favorite map is defined as the map you've played the most
246 for the given game_type_cd.
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.
253 raw_favs = DBSession.query('game_type_cd', 'map_name',
254 'map_id', 'times_played').\
256 "SELECT game_type_cd, "
260 "FROM (SELECT g.game_type_cd, "
263 "Count(*) times_played, "
266 "partition BY g.game_type_cd "
267 "ORDER BY Count(*) DESC, m.map_id ASC) rank "
269 "player_game_stats pgs, "
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, "
276 "m.name) most_played "
278 "ORDER BY times_played desc "
279 ).params(player_id=player_id).all()
284 # if we aren't given a favorite game_type_cd
285 # then the overall favorite is the one we've
287 if overall_fav is None:
288 fav_maps['overall'] = row
289 overall_fav = row.game_type_cd
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
297 fav_maps[row.game_type_cd] = row
302 def get_ranks(player_id):
304 Provides a breakdown of the player's ranks by game type.
306 Returns a dictionary of namedtuples with the following members:
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.
314 raw_ranks = DBSession.query("game_type_cd", "rank", "max_rank").\
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 "
320 "group by game_type_cd) overall "
321 "where pr.game_type_cd = overall.game_type_cd "
322 "and player_id = :player_id "
324 params(player_id=player_id).all()
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
333 ranks[row.game_type_cd] = row
338 def get_elos(player_id):
340 Provides a breakdown of the player's elos by game type.
342 Returns a dictionary of namedtuples with the following members:
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.
351 raw_elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\
352 order_by(PlayerElo.elo.desc()).all()
355 found_max_elo = False
357 if not found_max_elo:
358 elos['overall'] = row
361 elos[row.game_type_cd] = row
366 def get_recent_games(player_id):
368 Provides a list of recent games.
370 Returns the full PlayerGameStat, Game, Server, Map
371 objects for all recent games.
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]
384 def get_recent_weapons(player_id):
386 Returns the weapons that have been used in the past 90 days
387 and also used in 5 games or more.
389 cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=90)
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).\
397 recent_weapons.append(weapon[0])
399 return recent_weapons
402 def _get_games_played(player_id):
404 Provides a breakdown by gametype of the games played by player_id.
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)
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()
417 for (game_type_cd, games) in games_played:
420 return (total, games_played)
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):
427 Provides aggregated stats by player_id.
429 Returns a dict with the keys 'kills', 'deaths', 'alivetime'.
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
435 If any of the above are None, they are set to 0.
438 (total_stats['kills'], total_stats['deaths'], total_stats['alivetime']) = DBSession.\
439 query("total_kills", "total_deaths", "total_alivetime").\
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()
448 (total_stats['wins'],) = DBSession.\
449 query("total_wins").\
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()
458 for (key,value) in total_stats.items():
465 def _get_fav_maps(player_id):
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
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
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.
486 for (game_type_cd, name, map_id, times_played) in DBSession.\
487 query("game_type_cd", "name", "map_id", "times_played").\
489 "SELECT game_type_cd, "
493 "FROM (SELECT g.game_type_cd, "
496 "count(*) times_played, "
499 "PARTITION BY g.game_type_cd "
500 "ORDER BY Count(*) DESC, m.map_id ASC) rank "
502 "player_game_stats pgs, "
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, "
509 "m.name) most_played "
511 ).params(player_id=player_id).all():
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
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
525 fav_maps['overall'] = overall
530 def _get_rank(player_id):
532 Get the player's rank as well as the total number of ranks.
534 rank = DBSession.query("game_type_cd", "rank", "max_rank").\
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 "
540 "group by game_type_cd) overall "
541 "where pr.game_type_cd = overall.game_type_cd "
542 "and player_id = :player_id "
544 params(player_id=player_id).all()
549 def get_accuracy_stats(player_id, weapon_cd, games):
551 Provides accuracy for weapon_cd by player_id for the past N games.
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
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).\
563 avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)
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()).\
575 # they come out in opposite order, so flip them in the right direction
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)))
588 def get_damage_stats(player_id, weapon_cd, games):
590 Provides damage info for weapon_cd by player_id for the past N games.
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).\
599 avg = round(float(raw_avg[0])/raw_avg[1], 2)
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()).\
611 # they come out in opposite order, so flip them in the right direction
615 for i in range(len(raw_dmgs)):
616 # try to derive, unless we've hit nothing then set to 0!
618 dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)
622 dmgs.append((raw_dmgs[i][0], dmg))
623 except Exception as e:
630 def player_info_data(request):
631 player_id = int(request.matchdict['id'])
636 player = DBSession.query(Player).filter_by(player_id=player_id).\
637 filter(Player.active_ind == True).one()
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)
647 except Exception as e:
657 return {'player':player,
658 'games_played':games_played,
659 'overall_stats':overall_stats,
663 'recent_games':recent_games,
664 'recent_weapons':recent_weapons
668 def player_info(request):
670 Provides detailed information on a specific player
672 return player_info_data(request)
675 def player_info_json(request):
677 Provides detailed information on a specific player. JSON.
679 return [{'status':'not implemented'}]
682 def player_game_index_data(request):
683 player_id = request.matchdict['player_id']
685 if request.params.has_key('page'):
686 current_page = request.params['page']
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())
698 games = Page(games_q, current_page, items_per_page=10, url=page_url)
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()
707 except Exception as e:
711 return {'player_id':player_id,
716 def player_game_index(request):
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.
722 return player_game_index_data(request)
725 def player_game_index_json(request):
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.
731 return [{'status':'not implemented'}]
734 def player_accuracy_data(request):
735 player_id = request.matchdict['id']
736 allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']
740 if request.params.has_key('weapon'):
741 if request.params['weapon'] in allowed_weapons:
742 weapon_cd = request.params['weapon']
744 if request.params.has_key('games'):
746 games = request.params['games']
755 (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)
757 # if we don't have enough data for the given weapon
758 if len(accs) < games:
762 'player_id':player_id,
763 'player_url':request.route_url('player_info', id=player_id),
771 def player_accuracy(request):
773 Provides the accuracy for the given weapon. (JSON only)
775 return player_accuracy_data(request)
778 def player_accuracy_json(request):
780 Provides a JSON response representing the accuracy for the given weapon.
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.
787 return player_accuracy_data(request)
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'
797 if request.params.has_key('weapon'):
798 if request.params['weapon'] in allowed_weapons:
799 weapon_cd = request.params['weapon']
801 if request.params.has_key('games'):
803 games = request.params['games']
812 (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)
814 # if we don't have enough data for the given weapon
815 if len(dmgs) < games:
819 'player_id':player_id,
820 'player_url':request.route_url('player_info', id=player_id),
828 def player_damage_json(request):
830 Provides a JSON response representing the damage for the given weapon.
833 weapon = which weapon to display damage for. Valid values are
834 'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',
836 games = over how many games to display damage. Can be up to 50.
838 return player_damage_data(request)