3 from calendar import timegm
4 from collections import namedtuple
5 from urllib import unquote
7 import pyramid.httpexceptions
8 import sqlalchemy as sa
9 import sqlalchemy.sql.expression as expr
10 import sqlalchemy.sql.functions as func
11 from webhelpers.paginate import Page
12 from xonstat.models import DBSession, Server, Map, Game, PlayerWeaponStat, Player, Hashkey
13 from xonstat.models import PlayerElo, PlayerCaptime, PlayerMedal, GameType
14 from xonstat.models.player import PlayerCapTime
15 from xonstat.util import is_cake_day, verify_request
16 from xonstat.util import page_url, to_json, pretty_date, datetime_seconds
17 from xonstat.views.helpers import RecentGame, recent_games_q
19 log = logging.getLogger(__name__)
22 def player_index_data(request):
23 if request.params.has_key('page'):
24 current_page = request.params['page']
29 player_q = DBSession.query(Player).\
30 filter(Player.player_id > 2).\
31 filter(Player.active_ind == True).\
32 filter(sa.not_(Player.nick.like('Anonymous Player%'))).\
33 order_by(Player.player_id.desc())
35 players = Page(player_q, current_page, items_per_page=25, url=page_url)
37 except Exception as e:
41 return {'players':players
45 def player_index(request):
47 Provides a list of all the current players.
49 return player_index_data(request)
52 def player_index_json(request):
54 Provides a list of all the current players. JSON.
56 return [{'status':'not implemented'}]
59 def get_games_played(player_id):
61 Provides a breakdown by gametype of the games played by player_id.
63 Returns a list of namedtuples with the following members:
70 The list itself is ordered by the number of games played
72 GamesPlayed = namedtuple('GamesPlayed', ['game_type_cd', 'games', 'wins',
75 raw_games_played = DBSession.query('game_type_cd', 'wins', 'losses').\
77 "SELECT game_type_cd, "
80 "FROM (SELECT g.game_id, "
83 "WHEN g.winner = pgs.team THEN 1 "
84 "WHEN pgs.scoreboardpos = 1 THEN 1 "
88 "WHEN g.winner = pgs.team THEN 0 "
89 "WHEN pgs.scoreboardpos = 1 THEN 0 "
93 "player_game_stats pgs "
94 "WHERE g.game_id = pgs.game_id "
95 "AND pgs.player_id = :player_id "
96 "AND g.players @> ARRAY[:player_id]) win_loss "
97 "GROUP BY game_type_cd "
98 ).params(player_id=player_id).all()
104 for row in raw_games_played:
105 games = row.wins + row.losses
106 overall_games += games
107 overall_wins += row.wins
108 overall_losses += row.losses
109 win_pct = float(row.wins)/games * 100
111 games_played.append(GamesPlayed(row.game_type_cd, games, row.wins,
112 row.losses, win_pct))
115 overall_win_pct = float(overall_wins)/overall_games * 100
117 overall_win_pct = 0.0
119 games_played.append(GamesPlayed('overall', overall_games, overall_wins,
120 overall_losses, overall_win_pct))
122 # sort the resulting list by # of games played
123 games_played = sorted(games_played, key=lambda x:x.games)
124 games_played.reverse()
129 def get_overall_stats(player_id):
131 Provides a breakdown of stats by gametype played by player_id.
133 Returns a dictionary of namedtuples with the following members:
137 - last_played (last time the player played the game type)
138 - last_played_epoch (same as above, but in seconds since epoch)
139 - last_played_fuzzy (same as above, but in relative date)
140 - total_playing_time (total amount of time played the game type)
141 - total_playing_time_secs (same as the above, but in seconds)
142 - total_pickups (ctf only)
143 - total_captures (ctf only)
144 - cap_ratio (ctf only)
145 - total_carrier_frags (ctf only)
149 The key to the dictionary is the game type code. There is also an
150 "overall" game_type_cd which sums the totals and computes the total ratios.
152 OverallStats = namedtuple('OverallStats', ['total_kills', 'total_deaths',
153 'k_d_ratio', 'last_played', 'last_played_epoch', 'last_played_fuzzy',
154 'total_playing_time', 'total_playing_time_secs', 'total_pickups', 'total_captures', 'cap_ratio',
155 'total_carrier_frags', 'game_type_cd', 'game_type_descr'])
157 raw_stats = DBSession.query('game_type_cd', 'game_type_descr',
158 'total_kills', 'total_deaths', 'last_played', 'total_playing_time',
159 'total_pickups', 'total_captures', 'total_carrier_frags').\
161 "SELECT g.game_type_cd, "
162 "gt.descr game_type_descr, "
163 "Sum(pgs.kills) total_kills, "
164 "Sum(pgs.deaths) total_deaths, "
165 "Max(pgs.create_dt) last_played, "
166 "Sum(pgs.alivetime) total_playing_time, "
167 "Sum(pgs.pickups) total_pickups, "
168 "Sum(pgs.captures) total_captures, "
169 "Sum(pgs.carrier_frags) total_carrier_frags "
172 "player_game_stats pgs "
173 "WHERE g.game_id = pgs.game_id "
174 "AND g.game_type_cd = gt.game_type_cd "
175 "AND g.players @> ARRAY[:player_id] "
176 "AND pgs.player_id = :player_id "
177 "GROUP BY g.game_type_cd, game_type_descr "
179 "SELECT 'overall' game_type_cd, "
180 "'Overall' game_type_descr, "
181 "Sum(pgs.kills) total_kills, "
182 "Sum(pgs.deaths) total_deaths, "
183 "Max(pgs.create_dt) last_played, "
184 "Sum(pgs.alivetime) total_playing_time, "
185 "Sum(pgs.pickups) total_pickups, "
186 "Sum(pgs.captures) total_captures, "
187 "Sum(pgs.carrier_frags) total_carrier_frags "
188 "FROM player_game_stats pgs "
189 "WHERE pgs.player_id = :player_id "
190 ).params(player_id=player_id).all()
192 # to be indexed by game_type_cd
195 for row in raw_stats:
196 # individual gametype ratio calculations
198 k_d_ratio = float(row.total_kills)/row.total_deaths
203 cap_ratio = float(row.total_captures)/row.total_pickups
207 # everything else is untouched or "raw"
208 os = OverallStats(total_kills=row.total_kills,
209 total_deaths=row.total_deaths,
211 last_played=row.last_played,
212 last_played_epoch=timegm(row.last_played.timetuple()),
213 last_played_fuzzy=pretty_date(row.last_played),
214 total_playing_time=row.total_playing_time,
215 total_playing_time_secs=int(datetime_seconds(row.total_playing_time)),
216 total_pickups=row.total_pickups,
217 total_captures=row.total_captures,
219 total_carrier_frags=row.total_carrier_frags,
220 game_type_cd=row.game_type_cd,
221 game_type_descr=row.game_type_descr)
223 overall_stats[row.game_type_cd] = os
225 # We have to edit "overall" stats to exclude deaths in CTS.
226 # Although we still want to record deaths, they shouldn't
227 # count towards the overall K:D ratio.
228 if 'cts' in overall_stats:
229 os = overall_stats['overall']
232 k_d_ratio = float(os.total_kills)/(os.total_deaths - overall_stats['cts'].total_deaths)
236 non_cts_deaths = os.total_deaths - overall_stats['cts'].total_deaths
239 overall_stats['overall'] = OverallStats(
240 total_kills = os.total_kills,
241 total_deaths = non_cts_deaths,
242 k_d_ratio = k_d_ratio,
243 last_played = os.last_played,
244 last_played_epoch = os.last_played_epoch,
245 last_played_fuzzy = os.last_played_fuzzy,
246 total_playing_time = os.total_playing_time,
247 total_playing_time_secs = os.total_playing_time_secs,
248 total_pickups = os.total_pickups,
249 total_captures = os.total_captures,
250 cap_ratio = os.cap_ratio,
251 total_carrier_frags = os.total_carrier_frags,
252 game_type_cd = os.game_type_cd,
253 game_type_descr = os.game_type_descr)
258 def get_fav_maps(player_id, game_type_cd=None):
260 Provides a breakdown of favorite maps by gametype.
262 Returns a dictionary of namedtuples with the following members:
264 - map_name (map name)
268 The favorite map is defined as the map you've played the most
269 for the given game_type_cd.
271 The key to the dictionary is the game type code. There is also an
272 "overall" game_type_cd which is the overall favorite map. This is
273 defined as the favorite map of the game type you've played the
274 most. The input parameter game_type_cd is for this.
276 FavMap = namedtuple('FavMap', ['map_name', 'map_id', 'times_played', 'game_type_cd'])
278 raw_favs = DBSession.query('game_type_cd', 'map_name',
279 'map_id', 'times_played').\
281 "SELECT game_type_cd, "
285 "FROM (SELECT g.game_type_cd, "
288 "Count(*) times_played, "
291 "partition BY g.game_type_cd "
292 "ORDER BY Count(*) DESC, m.map_id ASC) rank "
294 "player_game_stats pgs, "
296 "WHERE g.game_id = pgs.game_id "
297 "AND g.map_id = m.map_id "
298 "AND g.players @> ARRAY[:player_id]"
299 "AND pgs.player_id = :player_id "
300 "GROUP BY g.game_type_cd, "
302 "m.name) most_played "
304 "ORDER BY times_played desc "
305 ).params(player_id=player_id).all()
310 fv = FavMap(map_name=row.map_name,
312 times_played=row.times_played,
313 game_type_cd=row.game_type_cd)
315 # if we aren't given a favorite game_type_cd
316 # then the overall favorite is the one we've
318 if overall_fav is None:
319 fav_maps['overall'] = fv
320 overall_fav = fv.game_type_cd
322 # otherwise it is the favorite map from the
323 # favorite game_type_cd (provided as a param)
324 # and we'll overwrite the first dict entry
325 if game_type_cd == fv.game_type_cd:
326 fav_maps['overall'] = fv
328 fav_maps[row.game_type_cd] = fv
333 def get_ranks(player_id):
335 Provides a breakdown of the player's ranks by game type.
337 Returns a dictionary of namedtuples with the following members:
342 The key to the dictionary is the game type code. There is also an
343 "overall" game_type_cd which is the overall best rank.
345 Rank = namedtuple('Rank', ['rank', 'max_rank', 'percentile', 'game_type_cd'])
347 raw_ranks = DBSession.query("game_type_cd", "rank", "max_rank").\
349 "select pr.game_type_cd, pr.rank, overall.max_rank "
350 "from player_ranks pr, "
351 "(select game_type_cd, max(rank) max_rank "
353 "group by game_type_cd) overall "
354 "where pr.game_type_cd = overall.game_type_cd "
356 "and player_id = :player_id "
358 params(player_id=player_id).all()
361 found_top_rank = False
362 for row in raw_ranks:
363 rank = Rank(rank=row.rank,
364 max_rank=row.max_rank,
365 percentile=100 - 100*float(row.rank-1)/(row.max_rank-1),
366 game_type_cd=row.game_type_cd)
369 if not found_top_rank:
370 ranks['overall'] = rank
371 found_top_rank = True
372 elif rank.percentile > ranks['overall'].percentile:
373 ranks['overall'] = rank
375 ranks[row.game_type_cd] = rank
380 def get_elos(player_id):
382 Provides a breakdown of the player's elos by game type.
384 Returns a dictionary of namedtuples with the following members:
390 The key to the dictionary is the game type code. There is also an
391 "overall" game_type_cd which is the overall best rank.
393 raw_elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\
394 order_by(PlayerElo.elo.desc()).all()
397 found_max_elo = False
399 if not found_max_elo:
400 elos['overall'] = row
403 elos[row.game_type_cd] = row
408 def get_recent_games(player_id, limit=10):
410 Provides a list of recent games for a player. Uses the recent_games_q helper.
412 # recent games played in descending order
413 rgs = recent_games_q(player_id=player_id, force_player_id=True).limit(limit).all()
414 recent_games = [RecentGame(row) for row in rgs]
419 def get_accuracy_stats(player_id, weapon_cd, games):
421 Provides accuracy for weapon_cd by player_id for the past N games.
423 # Reaching back 90 days should give us an accurate enough average
424 # We then multiply this out for the number of data points (games) to
425 # create parameters for a flot graph
427 raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),
428 func.sum(PlayerWeaponStat.fired)).\
429 filter(PlayerWeaponStat.player_id == player_id).\
430 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
433 avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)
435 # Determine the raw accuracy (hit, fired) numbers for $games games
436 # This is then enumerated to create parameters for a flot graph
437 raw_accs = DBSession.query(PlayerWeaponStat.game_id,
438 PlayerWeaponStat.hit, PlayerWeaponStat.fired).\
439 filter(PlayerWeaponStat.player_id == player_id).\
440 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
441 order_by(PlayerWeaponStat.game_id.desc()).\
445 # they come out in opposite order, so flip them in the right direction
449 for i in range(len(raw_accs)):
450 accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))
458 def get_damage_stats(player_id, weapon_cd, games):
460 Provides damage info for weapon_cd by player_id for the past N games.
463 raw_avg = DBSession.query(func.sum(PlayerWeaponStat.actual),
464 func.sum(PlayerWeaponStat.hit)).\
465 filter(PlayerWeaponStat.player_id == player_id).\
466 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
469 avg = round(float(raw_avg[0])/raw_avg[1], 2)
471 # Determine the damage efficiency (hit, fired) numbers for $games games
472 # This is then enumerated to create parameters for a flot graph
473 raw_dmgs = DBSession.query(PlayerWeaponStat.game_id,
474 PlayerWeaponStat.actual, PlayerWeaponStat.hit).\
475 filter(PlayerWeaponStat.player_id == player_id).\
476 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
477 order_by(PlayerWeaponStat.game_id.desc()).\
481 # they come out in opposite order, so flip them in the right direction
485 for i in range(len(raw_dmgs)):
486 # try to derive, unless we've hit nothing then set to 0!
488 dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)
492 dmgs.append((raw_dmgs[i][0], dmg))
493 except Exception as e:
500 def get_player_medals(player_id):
501 """Retrieves the list of medals the player has received from tournaments or
504 medals = DBSession.query(PlayerMedal)\
505 .filter(PlayerMedal.player_id==player_id)\
506 .order_by(PlayerMedal.place)\
507 .order_by(PlayerMedal.create_dt)\
512 except Exception as e:
517 def player_info_data(request):
518 player_id = int(request.matchdict['id'])
523 player = DBSession.query(Player).filter_by(player_id=player_id).\
524 filter(Player.active_ind == True).one()
526 games_played = get_games_played(player_id)
527 overall_stats = get_overall_stats(player_id)
528 fav_maps = get_fav_maps(player_id)
529 elos = get_elos(player_id)
530 ranks = get_ranks(player_id)
531 medals = get_player_medals(player_id)
532 recent_games = get_recent_games(player_id)
533 cake_day = is_cake_day(player.create_dt)
535 except Exception as e:
536 raise pyramid.httpexceptions.HTTPNotFound
538 ## do not raise application exceptions here (only for debugging)
541 return {'player':player,
542 'games_played':games_played,
543 'overall_stats':overall_stats,
548 'recent_games':recent_games,
553 def player_info(request):
555 Provides detailed information on a specific player
557 return player_info_data(request)
560 def player_info_json(request):
562 Provides detailed information on a specific player. JSON.
565 # All player_info fields are converted into JSON-formattable dictionaries
566 player_info = player_info_data(request)
568 player = player_info['player'].to_dict()
571 for game in player_info['games_played']:
572 games_played[game.game_type_cd] = to_json(game)
575 for gt,stats in player_info['overall_stats'].items():
576 overall_stats[gt] = to_json(stats)
579 for gt,elo in player_info['elos'].items():
580 elos[gt] = to_json(elo.to_dict())
583 for gt,rank in player_info['ranks'].items():
584 ranks[gt] = to_json(rank)
587 for gt,mapinfo in player_info['fav_maps'].items():
588 fav_maps[gt] = to_json(mapinfo)
590 recent_games = [g.to_dict() for g in player_info['recent_games']]
594 'games_played': games_played,
595 'overall_stats': overall_stats,
596 'fav_maps': fav_maps,
599 'recent_games': recent_games,
603 def player_game_index_data(request):
605 player_id = int(request.matchdict['player_id'])
610 game_type_descr = None
612 if request.params.has_key('type'):
613 game_type_cd = request.params['type']
615 game_type_descr = DBSession.query(GameType.descr).\
616 filter(GameType.game_type_cd == game_type_cd).\
618 except Exception as e:
623 game_type_descr = None
625 if request.params.has_key('page'):
626 current_page = request.params['page']
631 player = DBSession.query(Player).\
632 filter_by(player_id=player_id).\
633 filter(Player.active_ind == True).\
636 rgs_q = recent_games_q(player_id=player.player_id,
637 force_player_id=True, game_type_cd=game_type_cd)
639 games = Page(rgs_q, current_page, items_per_page=20, url=page_url)
641 # replace the items in the canned pagination class with more rich ones
642 games.items = [RecentGame(row) for row in games.items]
644 games_played = get_games_played(player_id)
646 except Exception as e:
651 game_type_descr = None
655 'player_id':player.player_id,
658 'game_type_cd':game_type_cd,
659 'game_type_descr':game_type_descr,
660 'games_played':games_played,
664 def player_game_index(request):
666 Provides an index of the games in which a particular
667 player was involved. This is ordered by game_id, with
668 the most recent game_ids first. Paginated.
670 return player_game_index_data(request)
673 def player_game_index_json(request):
675 Provides an index of the games in which a particular
676 player was involved. This is ordered by game_id, with
677 the most recent game_ids first. Paginated. JSON.
679 return [{'status':'not implemented'}]
682 def player_accuracy_data(request):
683 player_id = request.matchdict['id']
684 allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']
688 if request.params.has_key('weapon'):
689 if request.params['weapon'] in allowed_weapons:
690 weapon_cd = request.params['weapon']
692 if request.params.has_key('games'):
694 games = request.params['games']
703 (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)
705 # if we don't have enough data for the given weapon
706 if len(accs) < games:
710 'player_id':player_id,
711 'player_url':request.route_url('player_info', id=player_id),
719 def player_accuracy(request):
721 Provides the accuracy for the given weapon. (JSON only)
723 return player_accuracy_data(request)
726 def player_accuracy_json(request):
728 Provides a JSON response representing the accuracy for the given weapon.
731 weapon = which weapon to display accuracy for. Valid values are 'nex',
732 'shotgun', 'uzi', and 'minstanex'.
733 games = over how many games to display accuracy. Can be up to 50.
735 return player_accuracy_data(request)
738 def player_damage_data(request):
739 player_id = request.matchdict['id']
740 allowed_weapons = ['grenadelauncher', 'electro', 'crylink', 'hagar',
741 'rocketlauncher', 'laser']
742 weapon_cd = 'rocketlauncher'
745 if request.params.has_key('weapon'):
746 if request.params['weapon'] in allowed_weapons:
747 weapon_cd = request.params['weapon']
749 if request.params.has_key('games'):
751 games = request.params['games']
760 (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)
762 # if we don't have enough data for the given weapon
763 if len(dmgs) < games:
767 'player_id':player_id,
768 'player_url':request.route_url('player_info', id=player_id),
776 def player_damage_json(request):
778 Provides a JSON response representing the damage for the given weapon.
781 weapon = which weapon to display damage for. Valid values are
782 'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',
784 games = over how many games to display damage. Can be up to 50.
786 return player_damage_data(request)
789 def player_hashkey_info_data(request):
790 # hashkey = request.matchdict['hashkey']
792 # the incoming hashkey is double quoted, and WSGI unquotes once...
793 # hashkey = unquote(hashkey)
795 # if using request verification to obtain the hashkey
796 (idfp, status) = verify_request(request)
797 log.debug("d0_blind_id verification: idfp={0} status={1}\n".format(idfp, status))
799 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
800 "----- END REQUEST BODY -----\n\n")
802 # if config is to *not* verify requests and we get nothing back, this
803 # query will return nothing and we'll 404.
805 player = DBSession.query(Player).\
806 filter(Player.player_id == Hashkey.player_id).\
807 filter(Player.active_ind == True).\
808 filter(Hashkey.hashkey == idfp).one()
810 games_played = get_games_played(player.player_id)
811 overall_stats = get_overall_stats(player.player_id)
812 fav_maps = get_fav_maps(player.player_id)
813 elos = get_elos(player.player_id)
814 ranks = get_ranks(player.player_id)
815 most_recent_game = get_recent_games(player.player_id, 1)[0]
817 except Exception as e:
818 raise pyramid.httpexceptions.HTTPNotFound
820 return {'player':player,
822 'games_played':games_played,
823 'overall_stats':overall_stats,
827 'most_recent_game':most_recent_game,
831 def player_hashkey_info_json(request):
833 Provides detailed information on a specific player. JSON.
836 # All player_info fields are converted into JSON-formattable dictionaries
837 player_info = player_hashkey_info_data(request)
839 player = player_info['player'].to_dict()
842 for game in player_info['games_played']:
843 games_played[game.game_type_cd] = to_json(game)
846 for gt,stats in player_info['overall_stats'].items():
847 overall_stats[gt] = to_json(stats)
850 for gt,elo in player_info['elos'].items():
851 elos[gt] = to_json(elo.to_dict())
854 for gt,rank in player_info['ranks'].items():
855 ranks[gt] = to_json(rank)
858 for gt,mapinfo in player_info['fav_maps'].items():
859 fav_maps[gt] = to_json(mapinfo)
861 most_recent_game = to_json(player_info['most_recent_game'])
866 'games_played': games_played,
867 'overall_stats': overall_stats,
868 'fav_maps': fav_maps,
871 'most_recent_game': most_recent_game,
875 def player_hashkey_info_text(request):
877 Provides detailed information on a specific player. Plain text.
880 now = timegm(datetime.datetime.utcnow().timetuple())
882 # All player_info fields are converted into JSON-formattable dictionaries
883 player_info = player_hashkey_info_data(request)
885 # gather all of the data up into aggregate structures
886 player = player_info['player']
887 games_played = player_info['games_played']
888 overall_stats = player_info['overall_stats']
889 elos = player_info['elos']
890 ranks = player_info['ranks']
891 fav_maps = player_info['fav_maps']
892 most_recent_game = player_info['most_recent_game']
894 # one-offs for things needing conversion for text/plain
895 player_joined = timegm(player.create_dt.timetuple())
896 player_joined_dt = player.create_dt
897 alivetime = int(datetime_seconds(overall_stats['overall'].total_playing_time))
899 # this is a plain text response, if we don't do this here then
900 # Pyramid will assume html
901 request.response.content_type = 'text/plain'
907 'hashkey': player_info['hashkey'],
908 'player_joined': player_joined,
909 'player_joined_dt': player_joined_dt,
910 'games_played': games_played,
911 'overall_stats': overall_stats,
912 'alivetime': alivetime,
913 'fav_maps': fav_maps,
916 'most_recent_game': most_recent_game,
920 def player_elo_info_data(request):
922 Provides elo information on a specific player. Raw data is returned.
924 (idfp, status) = verify_request(request)
925 log.debug("d0_blind_id verification: idfp={0} status={1}\n".format(idfp, status))
927 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
928 "----- END REQUEST BODY -----\n\n")
930 hashkey = request.matchdict['hashkey']
932 # the incoming hashkey is double quoted, and WSGI unquotes once...
933 hashkey = unquote(hashkey)
936 player = DBSession.query(Player).\
937 filter(Player.player_id == Hashkey.player_id).\
938 filter(Player.active_ind == True).\
939 filter(Hashkey.hashkey == hashkey).one()
941 elos = get_elos(player.player_id)
943 except Exception as e:
945 raise pyramid.httpexceptions.HTTPNotFound
954 def player_elo_info_json(request):
956 Provides elo information on a specific player. JSON.
958 elo_info = player_elo_info_data(request)
960 player = player_info['player'].to_dict()
963 for gt, elo in elo_info['elos'].items():
964 elos[gt] = to_json(elo.to_dict())
973 def player_elo_info_text(request):
975 Provides elo information on a specific player. Plain text.
978 now = timegm(datetime.datetime.utcnow().timetuple())
980 # All player_info fields are converted into JSON-formattable dictionaries
981 elo_info = player_elo_info_data(request)
983 # this is a plain text response, if we don't do this here then
984 # Pyramid will assume html
985 request.response.content_type = 'text/plain'
990 'hashkey': elo_info['hashkey'],
991 'player': elo_info['player'],
992 'elos': elo_info['elos'],
996 def player_captimes_data(request):
997 player_id = int(request.matchdict['player_id'])
1001 page = request.params.get("page", 1)
1003 sort = request.params.get("sort", "create_dt")
1006 player = DBSession.query(Player).filter_by(player_id=player_id).one()
1008 pct_q = DBSession.query(PlayerCaptime.fastest_cap, PlayerCaptime.create_dt,
1009 PlayerCaptime.player_id, PlayerCaptime.game_id, PlayerCaptime.map_id,
1010 Map.name.label('map_name'), Game.server_id, Server.name.label('server_name')).\
1011 filter(PlayerCaptime.player_id==player_id).\
1012 filter(PlayerCaptime.game_id==Game.game_id).\
1013 filter(PlayerCaptime.map_id==Map.map_id).\
1014 filter(Game.server_id==Server.server_id)
1016 if sort == "fastest":
1017 pct_q = pct_q.order_by(PlayerCaptime.fastest_cap)
1020 pct_q = pct_q.order_by(expr.desc(PlayerCaptime.create_dt))
1022 except Exception as e:
1023 raise pyramid.httpexceptions.HTTPNotFound
1025 captimes = Page(pct_q, page, items_per_page=20, url=page_url)
1027 # replace the items in the canned pagination class with more rich ones
1028 captimes.items = [PlayerCapTime(row) for row in captimes.items]
1031 "player_id" : player_id,
1033 "captimes" : captimes,
1039 def player_captimes(request):
1040 return player_captimes_data(request)
1043 def player_captimes_json(request):
1044 data = player_captimes_data(request)
1045 page = request.params.get("page", 1)
1047 # perform any necessary JSON conversions
1048 player_id = data["player_id"]
1049 player = data["player"].to_dict()
1050 captimes = [ct.to_dict() for ct in data["captimes"].items]
1054 "captimes" : captimes,
1059 def player_weaponstats_data_json(request):
1060 player_id = int(request.matchdict["id"])
1064 game_type_cd = request.params.get("game_type", None)
1065 if game_type_cd == "overall":
1069 if request.params.has_key("limit"):
1070 limit = int(request.params["limit"])
1078 # the game_ids of the most recently played ones
1079 # of the given game type is used for a subquery
1080 games_list = DBSession.query(Game.game_id).\
1081 filter(Game.players.contains([player_id]))
1083 if game_type_cd is not None:
1084 games_list = games_list.filter(Game.game_type_cd == game_type_cd)
1086 games_list = games_list.order_by(Game.game_id.desc()).limit(limit)
1088 weapon_stats_raw = DBSession.query(PlayerWeaponStat).\
1089 filter(PlayerWeaponStat.player_id == player_id).\
1090 filter(PlayerWeaponStat.game_id.in_(games_list)).\
1093 games_to_weapons = {}
1096 for ws in weapon_stats_raw:
1097 if ws.game_id not in games_to_weapons:
1098 games_to_weapons[ws.game_id] = [ws.weapon_cd]
1100 games_to_weapons[ws.game_id].append(ws.weapon_cd)
1102 weapons_used[ws.weapon_cd] = weapons_used.get(ws.weapon_cd, 0) + 1
1103 sum_avgs[ws.weapon_cd] = sum_avgs.get(ws.weapon_cd, 0) + float(ws.hit)/float(ws.fired)
1105 # Creating zero-valued weapon stat entries for games where a weapon was not
1106 # used in that game, but was used in another game for the set. This makes
1107 # the charts look smoother
1108 for game_id in games_to_weapons.keys():
1109 for weapon_cd in set(weapons_used.keys()) - set(games_to_weapons[game_id]):
1110 weapon_stats_raw.append(PlayerWeaponStat(player_id=player_id,
1111 game_id=game_id, weapon_cd=weapon_cd))
1113 # averages for the weapons used in the range
1115 for w in weapons_used.keys():
1116 avgs[w] = round(sum_avgs[w]/float(weapons_used[w])*100, 2)
1118 weapon_stats_raw = sorted(weapon_stats_raw, key = lambda x: x.game_id)
1119 games = sorted(games_to_weapons.keys())
1120 weapon_stats = [ws.to_dict() for ws in weapon_stats_raw]
1123 "weapon_stats": weapon_stats,
1124 "weapons_used": weapons_used.keys(),
1130 def player_versus_data(request):
1132 p1_id = int(request.params.get("p1", None))
1133 p2_id = int(request.params.get("p2", None))
1138 players = DBSession.query(Player).filter(sa.or_(Player.player_id ==
1139 p1_id, Player.player_id == p2_id)).order_by(Player.player_id).all()
1142 if len(players) < 2:
1143 raise Exception("Not enough players found.")
1145 # assign the players from the array retrieved above
1146 if players[0].player_id == p1_id:
1153 # note that wins and losses are from p1's perspective
1154 win_loss_sql = """select win_loss, count(1)
1157 when pgsp1.score >= pgsp2.score then 'win'
1160 from games g join player_game_stats pgsp1
1161 on g.game_id = pgsp1.game_id and pgsp1.player_id = :p1
1162 join player_game_stats pgsp2
1163 on g.game_id = pgsp2.game_id and pgsp2.player_id = :p2
1164 where g.players @> ARRAY[:p1,:p2]
1165 and g.game_type_cd = 'duel'
1166 and pgsp1.create_dt between g.create_dt - interval '1 hour'
1167 and g.create_dt + interval '1 hour'
1168 and pgsp2.create_dt between g.create_dt - interval '1 hour'
1169 and g.create_dt + interval '1 hour'
1174 wins_losses = DBSession.query("win_loss", "count").\
1175 from_statement(win_loss_sql).\
1176 params(p1=p1_id, p2=p2_id).all()
1178 for row in wins_losses:
1179 if row.win_loss == "win":
1181 elif row.win_loss == "loss":
1184 # grab the 20 most recent games between the two
1185 rgs_raw = recent_games_q(player_id=p1_id, player_id_2=p2_id,
1186 game_type_cd="duel").limit(20).all()
1188 rgs = [RecentGame(row) for row in rgs_raw]
1190 except Exception as e:
1206 "p1_wins" : p1_wins,
1207 "p2_wins" : p2_wins,
1208 "recent_games" : rgs,
1212 def player_versus(request):
1213 return player_versus_data(request)