X-Git-Url: http://de.git.xonotic.org/?p=xonotic%2Fxonstat.git;a=blobdiff_plain;f=xonstat%2Fviews%2Fplayer.py;h=41af4f11e2d2b4a9bb177de4474c4ece67dfbaea;hp=0c4ce59bcfcfee888b6126dbe49722bdb7b8cd0d;hb=ccc6418a1dea2ac63e99cb9c3d57e87b943644fa;hpb=78e172fb9d7f3493605a851a4f5f49c0f34d42e3 diff --git a/xonstat/views/player.py b/xonstat/views/player.py index 0c4ce59..41af4f1 100644 --- a/xonstat/views/player.py +++ b/xonstat/views/player.py @@ -1,18 +1,20 @@ import datetime -import json import logging -import re -import sqlalchemy as sa -import sqlalchemy.sql.functions as func -import time from calendar import timegm from collections import namedtuple -from pyramid.response import Response -from pyramid.url import current_route_url -from sqlalchemy import desc, distinct -from webhelpers.paginate import Page, PageURL -from xonstat.models import * -from xonstat.util import page_url, to_json, pretty_date +from urllib import unquote + +import pyramid.httpexceptions +import sqlalchemy as sa +import sqlalchemy.sql.expression as expr +import sqlalchemy.sql.functions as func +from webhelpers.paginate import Page +from xonstat.models import DBSession, Server, Map, Game, PlayerWeaponStat, Player, Hashkey +from xonstat.models import PlayerElo, PlayerCaptime, PlayerMedal, GameType +from xonstat.models.player import PlayerCapTime +from xonstat.util import is_cake_day, verify_request +from xonstat.util import page_url, to_json, pretty_date, datetime_seconds +from xonstat.views.helpers import RecentGame, recent_games_q log = logging.getLogger(__name__) @@ -30,7 +32,7 @@ def player_index_data(request): filter(sa.not_(Player.nick.like('Anonymous Player%'))).\ order_by(Player.player_id.desc()) - players = Page(player_q, current_page, items_per_page=10, url=page_url) + players = Page(player_q, current_page, items_per_page=25, url=page_url) except Exception as e: players = None @@ -79,18 +81,19 @@ def get_games_played(player_id): "g.game_type_cd, " "CASE " "WHEN g.winner = pgs.team THEN 1 " - "WHEN pgs.rank = 1 THEN 1 " + "WHEN pgs.scoreboardpos = 1 THEN 1 " "ELSE 0 " "END win, " "CASE " "WHEN g.winner = pgs.team THEN 0 " - "WHEN pgs.rank = 1 THEN 0 " + "WHEN pgs.scoreboardpos = 1 THEN 0 " "ELSE 1 " "END loss " "FROM games g, " "player_game_stats pgs " "WHERE g.game_id = pgs.game_id " - "AND pgs.player_id = :player_id) win_loss " + "AND pgs.player_id = :player_id " + "AND g.players @> ARRAY[:player_id]) win_loss " "GROUP BY game_type_cd " ).params(player_id=player_id).all() @@ -119,6 +122,7 @@ def get_games_played(player_id): # sort the resulting list by # of games played games_played = sorted(games_played, key=lambda x:x.games) games_played.reverse() + return games_played @@ -134,25 +138,28 @@ def get_overall_stats(player_id): - last_played_epoch (same as above, but in seconds since epoch) - last_played_fuzzy (same as above, but in relative date) - total_playing_time (total amount of time played the game type) + - total_playing_time_secs (same as the above, but in seconds) - total_pickups (ctf only) - total_captures (ctf only) - cap_ratio (ctf only) - total_carrier_frags (ctf only) - game_type_cd + - game_type_descr The key to the dictionary is the game type code. There is also an "overall" game_type_cd which sums the totals and computes the total ratios. """ OverallStats = namedtuple('OverallStats', ['total_kills', 'total_deaths', 'k_d_ratio', 'last_played', 'last_played_epoch', 'last_played_fuzzy', - 'total_playing_time', 'total_pickups', 'total_captures', 'cap_ratio', - 'total_carrier_frags', 'game_type_cd']) + 'total_playing_time', 'total_playing_time_secs', 'total_pickups', 'total_captures', 'cap_ratio', + 'total_carrier_frags', 'game_type_cd', 'game_type_descr']) - raw_stats = DBSession.query('game_type_cd', 'total_kills', - 'total_deaths', 'last_played', 'total_playing_time', + raw_stats = DBSession.query('game_type_cd', 'game_type_descr', + 'total_kills', 'total_deaths', 'last_played', 'total_playing_time', 'total_pickups', 'total_captures', 'total_carrier_frags').\ from_statement( "SELECT g.game_type_cd, " + "gt.descr game_type_descr, " "Sum(pgs.kills) total_kills, " "Sum(pgs.deaths) total_deaths, " "Max(pgs.create_dt) last_played, " @@ -161,32 +168,31 @@ def get_overall_stats(player_id): "Sum(pgs.captures) total_captures, " "Sum(pgs.carrier_frags) total_carrier_frags " "FROM games g, " + "cd_game_type gt, " "player_game_stats pgs " "WHERE g.game_id = pgs.game_id " + "AND g.game_type_cd = gt.game_type_cd " + "AND g.players @> ARRAY[:player_id] " "AND pgs.player_id = :player_id " - "GROUP BY g.game_type_cd " + "GROUP BY g.game_type_cd, game_type_descr " + "UNION " + "SELECT 'overall' game_type_cd, " + "'Overall' game_type_descr, " + "Sum(pgs.kills) total_kills, " + "Sum(pgs.deaths) total_deaths, " + "Max(pgs.create_dt) last_played, " + "Sum(pgs.alivetime) total_playing_time, " + "Sum(pgs.pickups) total_pickups, " + "Sum(pgs.captures) total_captures, " + "Sum(pgs.carrier_frags) total_carrier_frags " + "FROM player_game_stats pgs " + "WHERE pgs.player_id = :player_id " ).params(player_id=player_id).all() # to be indexed by game_type_cd overall_stats = {} - # sums for the "overall" game type (which is fake) - overall_kills = 0 - overall_deaths = 0 - overall_last_played = None - overall_playing_time = datetime.timedelta(seconds=0) - overall_carrier_frags = 0 - for row in raw_stats: - # running totals or mins - overall_kills += row.total_kills or 0 - overall_deaths += row.total_deaths or 0 - - if overall_last_played is None or row.last_played > overall_last_played: - overall_last_played = row.last_played - - overall_playing_time += row.total_playing_time - # individual gametype ratio calculations try: k_d_ratio = float(row.total_kills)/row.total_deaths @@ -198,8 +204,6 @@ def get_overall_stats(player_id): except: cap_ratio = None - overall_carrier_frags += row.total_carrier_frags or 0 - # everything else is untouched or "raw" os = OverallStats(total_kills=row.total_kills, total_deaths=row.total_deaths, @@ -208,34 +212,45 @@ def get_overall_stats(player_id): last_played_epoch=timegm(row.last_played.timetuple()), last_played_fuzzy=pretty_date(row.last_played), total_playing_time=row.total_playing_time, + total_playing_time_secs=int(datetime_seconds(row.total_playing_time)), total_pickups=row.total_pickups, total_captures=row.total_captures, cap_ratio=cap_ratio, total_carrier_frags=row.total_carrier_frags, - game_type_cd=row.game_type_cd) + game_type_cd=row.game_type_cd, + game_type_descr=row.game_type_descr) overall_stats[row.game_type_cd] = os - # and lastly, the overall stuff - try: - overall_k_d_ratio = float(overall_kills)/overall_deaths - except: - overall_k_d_ratio = None - - os = OverallStats(total_kills=overall_kills, - total_deaths=overall_deaths, - k_d_ratio=overall_k_d_ratio, - last_played=overall_last_played, - last_played_epoch=timegm(overall_last_played.timetuple()), - last_played_fuzzy=pretty_date(overall_last_played), - total_playing_time=overall_playing_time, - total_pickups=None, - total_captures=None, - cap_ratio=None, - total_carrier_frags=overall_carrier_frags, - game_type_cd='overall') - - overall_stats['overall'] = os + # We have to edit "overall" stats to exclude deaths in CTS. + # Although we still want to record deaths, they shouldn't + # count towards the overall K:D ratio. + if 'cts' in overall_stats: + os = overall_stats['overall'] + + try: + k_d_ratio = float(os.total_kills)/(os.total_deaths - overall_stats['cts'].total_deaths) + except: + k_d_ratio = None + + non_cts_deaths = os.total_deaths - overall_stats['cts'].total_deaths + + + overall_stats['overall'] = OverallStats( + total_kills = os.total_kills, + total_deaths = non_cts_deaths, + k_d_ratio = k_d_ratio, + last_played = os.last_played, + last_played_epoch = os.last_played_epoch, + last_played_fuzzy = os.last_played_fuzzy, + total_playing_time = os.total_playing_time, + total_playing_time_secs = os.total_playing_time_secs, + total_pickups = os.total_pickups, + total_captures = os.total_captures, + cap_ratio = os.cap_ratio, + total_carrier_frags = os.total_carrier_frags, + game_type_cd = os.game_type_cd, + game_type_descr = os.game_type_descr) return overall_stats @@ -280,6 +295,7 @@ def get_fav_maps(player_id, game_type_cd=None): "maps m " "WHERE g.game_id = pgs.game_id " "AND g.map_id = m.map_id " + "AND g.players @> ARRAY[:player_id]" "AND pgs.player_id = :player_id " "GROUP BY g.game_type_cd, " "m.map_id, " @@ -295,7 +311,7 @@ def get_fav_maps(player_id, game_type_cd=None): map_id=row.map_id, times_played=row.times_played, game_type_cd=row.game_type_cd) - + # if we aren't given a favorite game_type_cd # then the overall favorite is the one we've # played the most @@ -325,8 +341,8 @@ def get_ranks(player_id): The key to the dictionary is the game type code. There is also an "overall" game_type_cd which is the overall best rank. - """ - Rank = namedtuple('Rank', ['rank', 'max_rank', 'game_type_cd']) + """ + Rank = namedtuple('Rank', ['rank', 'max_rank', 'percentile', 'game_type_cd']) raw_ranks = DBSession.query("game_type_cd", "rank", "max_rank").\ from_statement( @@ -336,6 +352,7 @@ def get_ranks(player_id): "from player_ranks " "group by game_type_cd) overall " "where pr.game_type_cd = overall.game_type_cd " + "and max_rank > 1 " "and player_id = :player_id " "order by rank").\ params(player_id=player_id).all() @@ -345,14 +362,14 @@ def get_ranks(player_id): for row in raw_ranks: rank = Rank(rank=row.rank, max_rank=row.max_rank, + percentile=100 - 100*float(row.rank-1)/(row.max_rank-1), game_type_cd=row.game_type_cd) - percentile = float(row.rank)/row.max_rank if not found_top_rank: ranks['overall'] = rank found_top_rank = True - elif percentile < float(ranks['overall'].rank)/ranks['overall'].max_rank: + elif rank.percentile > ranks['overall'].percentile: ranks['overall'] = rank ranks[row.game_type_cd] = rank @@ -388,47 +405,15 @@ def get_elos(player_id): return elos -def get_recent_games(player_id): +def get_recent_games(player_id, limit=10): """ - Provides a list of recent games. - - Returns the full PlayerGameStat, Game, Server, Map - objects for all recent games. + Provides a list of recent games for a player. Uses the recent_games_q helper. """ - RecentGame = namedtuple('RecentGame', ['player_stats', 'game', 'server', 'map']) - - # recent games table, all data - recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\ - filter(PlayerGameStat.player_id == player_id).\ - filter(PlayerGameStat.game_id == Game.game_id).\ - filter(Game.server_id == Server.server_id).\ - filter(Game.map_id == Map.map_id).\ - order_by(Game.game_id.desc())[0:10] + # recent games played in descending order + rgs = recent_games_q(player_id=player_id, force_player_id=True).limit(limit).all() + recent_games = [RecentGame(row) for row in rgs] - return [ - RecentGame(player_stats=row.PlayerGameStat, - game=row.Game, - server=row.Server, - map=row.Map) - for row in recent_games ] - - -def get_recent_weapons(player_id): - """ - Returns the weapons that have been used in the past 90 days - and also used in 5 games or more. - """ - cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=90) - recent_weapons = [] - for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\ - filter(PlayerWeaponStat.player_id == player_id).\ - filter(PlayerWeaponStat.create_dt > cutoff).\ - group_by(PlayerWeaponStat.weapon_cd).\ - having(func.count() > 4).\ - all(): - recent_weapons.append(weapon[0]) - - return recent_weapons + return recent_games def get_accuracy_stats(player_id, weapon_cd, games): @@ -449,7 +434,7 @@ def get_accuracy_stats(player_id, weapon_cd, games): # Determine the raw accuracy (hit, fired) numbers for $games games # This is then enumerated to create parameters for a flot graph - raw_accs = DBSession.query(PlayerWeaponStat.game_id, + raw_accs = DBSession.query(PlayerWeaponStat.game_id, PlayerWeaponStat.hit, PlayerWeaponStat.fired).\ filter(PlayerWeaponStat.player_id == player_id).\ filter(PlayerWeaponStat.weapon_cd == weapon_cd).\ @@ -485,7 +470,7 @@ def get_damage_stats(player_id, weapon_cd, games): # Determine the damage efficiency (hit, fired) numbers for $games games # This is then enumerated to create parameters for a flot graph - raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, + raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, PlayerWeaponStat.actual, PlayerWeaponStat.hit).\ filter(PlayerWeaponStat.player_id == player_id).\ filter(PlayerWeaponStat.weapon_cd == weapon_cd).\ @@ -512,6 +497,23 @@ def get_damage_stats(player_id, weapon_cd, games): return (avg, dmgs) +def get_player_medals(player_id): + """Retrieves the list of medals the player has received from tournaments or + other contests.""" + try: + medals = DBSession.query(PlayerMedal)\ + .filter(PlayerMedal.player_id==player_id)\ + .order_by(PlayerMedal.place)\ + .order_by(PlayerMedal.create_dt)\ + .all() + + return medals + + except Exception as e: + log.debug(e) + return [] + + def player_info_data(request): player_id = int(request.matchdict['id']) if player_id <= 2: @@ -526,18 +528,15 @@ def player_info_data(request): fav_maps = get_fav_maps(player_id) elos = get_elos(player_id) ranks = get_ranks(player_id) + medals = get_player_medals(player_id) recent_games = get_recent_games(player_id) - recent_weapons = get_recent_weapons(player_id) + cake_day = is_cake_day(player.create_dt) except Exception as e: - player = None - games_played = None - overall_stats = None - fav_maps = None - elos = None - ranks = None - recent_games = None - recent_weapons = [] + raise pyramid.httpexceptions.HTTPNotFound + + ## do not raise application exceptions here (only for debugging) + # raise e return {'player':player, 'games_played':games_played, @@ -545,8 +544,9 @@ def player_info_data(request): 'fav_maps':fav_maps, 'elos':elos, 'ranks':ranks, + 'medals':medals, 'recent_games':recent_games, - 'recent_weapons':recent_weapons + 'cake_day':cake_day, } @@ -561,38 +561,34 @@ def player_info_json(request): """ Provides detailed information on a specific player. JSON. """ - + # All player_info fields are converted into JSON-formattable dictionaries - player_info = player_info_data(request) - + player_info = player_info_data(request) + player = player_info['player'].to_dict() games_played = {} for game in player_info['games_played']: games_played[game.game_type_cd] = to_json(game) - + overall_stats = {} for gt,stats in player_info['overall_stats'].items(): overall_stats[gt] = to_json(stats) - + elos = {} for gt,elo in player_info['elos'].items(): elos[gt] = to_json(elo.to_dict()) - + ranks = {} for gt,rank in player_info['ranks'].items(): ranks[gt] = to_json(rank) - + fav_maps = {} for gt,mapinfo in player_info['fav_maps'].items(): fav_maps[gt] = to_json(mapinfo) - - recent_games = [] - for game in player_info['recent_games']: - recent_games.append(to_json(game)) - - #recent_weapons = player_info['recent_weapons'] - + + recent_games = [g.to_dict() for g in player_info['recent_games']] + return [{ 'player': player, 'games_played': games_played, @@ -601,14 +597,30 @@ def player_info_json(request): 'elos': elos, 'ranks': ranks, 'recent_games': recent_games, - # 'recent_weapons': recent_weapons, - 'recent_weapons': ['not implemented'], }] - #return [{'status':'not implemented'}] def player_game_index_data(request): - player_id = request.matchdict['player_id'] + try: + player_id = int(request.matchdict['player_id']) + except: + player_id = -1 + + game_type_cd = None + game_type_descr = None + + if request.params.has_key('type'): + game_type_cd = request.params['type'] + try: + game_type_descr = DBSession.query(GameType.descr).\ + filter(GameType.game_type_cd == game_type_cd).\ + one()[0] + except Exception as e: + pass + + else: + game_type_cd = None + game_type_descr = None if request.params.has_key('page'): current_page = request.params['page'] @@ -616,29 +628,37 @@ def player_game_index_data(request): current_page = 1 try: - games_q = DBSession.query(Game, Server, Map).\ - filter(PlayerGameStat.game_id == Game.game_id).\ - filter(PlayerGameStat.player_id == player_id).\ - filter(Game.server_id == Server.server_id).\ - filter(Game.map_id == Map.map_id).\ - order_by(Game.game_id.desc()) - - games = Page(games_q, current_page, items_per_page=10, url=page_url) - - pgstats = {} - for (game, server, map) in games: - pgstats[game.game_id] = DBSession.query(PlayerGameStat).\ - filter(PlayerGameStat.game_id == game.game_id).\ - order_by(PlayerGameStat.rank).\ - order_by(PlayerGameStat.score).all() + player = DBSession.query(Player).\ + filter_by(player_id=player_id).\ + filter(Player.active_ind == True).\ + one() + + rgs_q = recent_games_q(player_id=player.player_id, + force_player_id=True, game_type_cd=game_type_cd) + + games = Page(rgs_q, current_page, items_per_page=20, url=page_url) + + # replace the items in the canned pagination class with more rich ones + games.items = [RecentGame(row) for row in games.items] + + games_played = get_games_played(player_id) except Exception as e: + raise e player = None games = None + game_type_cd = None + game_type_descr = None + games_played = None - return {'player_id':player_id, + return { + 'player_id':player.player_id, + 'player':player, 'games':games, - 'pgstats':pgstats} + 'game_type_cd':game_type_cd, + 'game_type_descr':game_type_descr, + 'games_played':games_played, + } def player_game_index(request): @@ -687,11 +707,11 @@ def player_accuracy_data(request): games = len(accs) return { - 'player_id':player_id, - 'player_url':request.route_url('player_info', id=player_id), - 'weapon':weapon_cd, - 'games':games, - 'avg':avg, + 'player_id':player_id, + 'player_url':request.route_url('player_info', id=player_id), + 'weapon':weapon_cd, + 'games':games, + 'avg':avg, 'accs':accs } @@ -744,11 +764,11 @@ def player_damage_data(request): games = len(dmgs) return { - 'player_id':player_id, - 'player_url':request.route_url('player_info', id=player_id), - 'weapon':weapon_cd, - 'games':games, - 'avg':avg, + 'player_id':player_id, + 'player_url':request.route_url('player_info', id=player_id), + 'weapon':weapon_cd, + 'games':games, + 'avg':avg, 'dmgs':dmgs } @@ -764,3 +784,430 @@ def player_damage_json(request): games = over how many games to display damage. Can be up to 50. """ return player_damage_data(request) + + +def player_hashkey_info_data(request): + # hashkey = request.matchdict['hashkey'] + + # the incoming hashkey is double quoted, and WSGI unquotes once... + # hashkey = unquote(hashkey) + + # if using request verification to obtain the hashkey + (idfp, status) = verify_request(request) + log.debug("d0_blind_id verification: idfp={0} status={1}\n".format(idfp, status)) + + log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body + + "----- END REQUEST BODY -----\n\n") + + # if config is to *not* verify requests and we get nothing back, this + # query will return nothing and we'll 404. + try: + player = DBSession.query(Player).\ + filter(Player.player_id == Hashkey.player_id).\ + filter(Player.active_ind == True).\ + filter(Hashkey.hashkey == idfp).one() + + games_played = get_games_played(player.player_id) + overall_stats = get_overall_stats(player.player_id) + fav_maps = get_fav_maps(player.player_id) + elos = get_elos(player.player_id) + ranks = get_ranks(player.player_id) + most_recent_game = get_recent_games(player.player_id, 1)[0] + + except Exception as e: + raise pyramid.httpexceptions.HTTPNotFound + + return {'player':player, + 'hashkey':idfp, + 'games_played':games_played, + 'overall_stats':overall_stats, + 'fav_maps':fav_maps, + 'elos':elos, + 'ranks':ranks, + 'most_recent_game':most_recent_game, + } + + +def player_hashkey_info_json(request): + """ + Provides detailed information on a specific player. JSON. + """ + + # All player_info fields are converted into JSON-formattable dictionaries + player_info = player_hashkey_info_data(request) + + player = player_info['player'].to_dict() + + games_played = {} + for game in player_info['games_played']: + games_played[game.game_type_cd] = to_json(game) + + overall_stats = {} + for gt,stats in player_info['overall_stats'].items(): + overall_stats[gt] = to_json(stats) + + elos = {} + for gt,elo in player_info['elos'].items(): + elos[gt] = to_json(elo.to_dict()) + + ranks = {} + for gt,rank in player_info['ranks'].items(): + ranks[gt] = to_json(rank) + + fav_maps = {} + for gt,mapinfo in player_info['fav_maps'].items(): + fav_maps[gt] = to_json(mapinfo) + + most_recent_game = to_json(player_info['most_recent_game']) + + return [{ + 'version': 1, + 'player': player, + 'games_played': games_played, + 'overall_stats': overall_stats, + 'fav_maps': fav_maps, + 'elos': elos, + 'ranks': ranks, + 'most_recent_game': most_recent_game, + }] + + +def player_hashkey_info_text(request): + """ + Provides detailed information on a specific player. Plain text. + """ + # UTC epoch + now = timegm(datetime.datetime.utcnow().timetuple()) + + # All player_info fields are converted into JSON-formattable dictionaries + player_info = player_hashkey_info_data(request) + + # gather all of the data up into aggregate structures + player = player_info['player'] + games_played = player_info['games_played'] + overall_stats = player_info['overall_stats'] + elos = player_info['elos'] + ranks = player_info['ranks'] + fav_maps = player_info['fav_maps'] + most_recent_game = player_info['most_recent_game'] + + # one-offs for things needing conversion for text/plain + player_joined = timegm(player.create_dt.timetuple()) + player_joined_dt = player.create_dt + alivetime = int(datetime_seconds(overall_stats['overall'].total_playing_time)) + + # this is a plain text response, if we don't do this here then + # Pyramid will assume html + request.response.content_type = 'text/plain' + + return { + 'version': 1, + 'now': now, + 'player': player, + 'hashkey': player_info['hashkey'], + 'player_joined': player_joined, + 'player_joined_dt': player_joined_dt, + 'games_played': games_played, + 'overall_stats': overall_stats, + 'alivetime': alivetime, + 'fav_maps': fav_maps, + 'elos': elos, + 'ranks': ranks, + 'most_recent_game': most_recent_game, + } + + +def player_elo_info_data(request): + """ + Provides elo information on a specific player. Raw data is returned. + """ + (idfp, status) = verify_request(request) + log.debug("d0_blind_id verification: idfp={0} status={1}\n".format(idfp, status)) + + log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body + + "----- END REQUEST BODY -----\n\n") + + hashkey = request.matchdict['hashkey'] + + # the incoming hashkey is double quoted, and WSGI unquotes once... + hashkey = unquote(hashkey) + + try: + player = DBSession.query(Player).\ + filter(Player.player_id == Hashkey.player_id).\ + filter(Player.active_ind == True).\ + filter(Hashkey.hashkey == hashkey).one() + + elos = get_elos(player.player_id) + + except Exception as e: + log.debug(e) + raise pyramid.httpexceptions.HTTPNotFound + + return { + 'hashkey':hashkey, + 'player':player, + 'elos':elos, + } + + +def player_elo_info_json(request): + """ + Provides elo information on a specific player. JSON. + """ + elo_info = player_elo_info_data(request) + + player = player_info['player'].to_dict() + + elos = {} + for gt, elo in elo_info['elos'].items(): + elos[gt] = to_json(elo.to_dict()) + + return [{ + 'version': 1, + 'player': player, + 'elos': elos, + }] + + +def player_elo_info_text(request): + """ + Provides elo information on a specific player. Plain text. + """ + # UTC epoch + now = timegm(datetime.datetime.utcnow().timetuple()) + + # All player_info fields are converted into JSON-formattable dictionaries + elo_info = player_elo_info_data(request) + + # this is a plain text response, if we don't do this here then + # Pyramid will assume html + request.response.content_type = 'text/plain' + + return { + 'version': 1, + 'now': now, + 'hashkey': elo_info['hashkey'], + 'player': elo_info['player'], + 'elos': elo_info['elos'], + } + + +def player_captimes_data(request): + player_id = int(request.matchdict['player_id']) + if player_id <= 2: + player_id = -1; + + page = request.params.get("page", 1) + + sort = request.params.get("sort", "create_dt") + + try: + player = DBSession.query(Player).filter_by(player_id=player_id).one() + + pct_q = DBSession.query(PlayerCaptime.fastest_cap, PlayerCaptime.create_dt, + PlayerCaptime.player_id, PlayerCaptime.game_id, PlayerCaptime.map_id, + Map.name.label('map_name'), Game.server_id, Server.name.label('server_name')).\ + filter(PlayerCaptime.player_id==player_id).\ + filter(PlayerCaptime.game_id==Game.game_id).\ + filter(PlayerCaptime.map_id==Map.map_id).\ + filter(Game.server_id==Server.server_id) + + if sort == "fastest": + pct_q = pct_q.order_by(PlayerCaptime.fastest_cap) + else: + sort = "create_dt" + pct_q = pct_q.order_by(expr.desc(PlayerCaptime.create_dt)) + + except Exception as e: + raise pyramid.httpexceptions.HTTPNotFound + + captimes = Page(pct_q, page, items_per_page=20, url=page_url) + + # replace the items in the canned pagination class with more rich ones + captimes.items = [PlayerCapTime(row) for row in captimes.items] + + return { + "player_id" : player_id, + "player" : player, + "captimes" : captimes, + "page" : page, + "sort" : sort, + } + + +def player_captimes(request): + return player_captimes_data(request) + + +def player_captimes_json(request): + data = player_captimes_data(request) + page = request.params.get("page", 1) + + # perform any necessary JSON conversions + player_id = data["player_id"] + player = data["player"].to_dict() + captimes = [ct.to_dict() for ct in data["captimes"].items] + + return { + "player" : player, + "captimes" : captimes, + "page" : page, + } + + +def player_weaponstats_data_json(request): + player_id = int(request.matchdict["id"]) + if player_id <= 2: + player_id = -1; + + game_type_cd = request.params.get("game_type", None) + if game_type_cd == "overall": + game_type_cd = None + + limit = 20 + if request.params.has_key("limit"): + limit = int(request.params["limit"]) + + if limit < 0: + limit = 20 + if limit > 50: + limit = 50 + + + # the game_ids of the most recently played ones + # of the given game type is used for a subquery + games_list = DBSession.query(Game.game_id).\ + filter(Game.players.contains([player_id])) + + if game_type_cd is not None: + games_list = games_list.filter(Game.game_type_cd == game_type_cd) + + games_list = games_list.order_by(Game.game_id.desc()).limit(limit) + + weapon_stats_raw = DBSession.query(PlayerWeaponStat).\ + filter(PlayerWeaponStat.player_id == player_id).\ + filter(PlayerWeaponStat.game_id.in_(games_list)).\ + all() + + games_to_weapons = {} + weapons_used = {} + sum_avgs = {} + for ws in weapon_stats_raw: + if ws.game_id not in games_to_weapons: + games_to_weapons[ws.game_id] = [ws.weapon_cd] + else: + games_to_weapons[ws.game_id].append(ws.weapon_cd) + + weapons_used[ws.weapon_cd] = weapons_used.get(ws.weapon_cd, 0) + 1 + sum_avgs[ws.weapon_cd] = sum_avgs.get(ws.weapon_cd, 0) + float(ws.hit)/float(ws.fired) + + # Creating zero-valued weapon stat entries for games where a weapon was not + # used in that game, but was used in another game for the set. This makes + # the charts look smoother + for game_id in games_to_weapons.keys(): + for weapon_cd in set(weapons_used.keys()) - set(games_to_weapons[game_id]): + weapon_stats_raw.append(PlayerWeaponStat(player_id=player_id, + game_id=game_id, weapon_cd=weapon_cd)) + + # averages for the weapons used in the range + avgs = {} + for w in weapons_used.keys(): + avgs[w] = round(sum_avgs[w]/float(weapons_used[w])*100, 2) + + weapon_stats_raw = sorted(weapon_stats_raw, key = lambda x: x.game_id) + games = sorted(games_to_weapons.keys()) + weapon_stats = [ws.to_dict() for ws in weapon_stats_raw] + + return { + "weapon_stats": weapon_stats, + "weapons_used": weapons_used.keys(), + "games": games, + "averages": avgs, + } + + +def player_versus_data(request): + try: + p1_id = int(request.params.get("p1", None)) + p2_id = int(request.params.get("p2", None)) + + p1_wins = 0 + p2_wins = 0 + + players = DBSession.query(Player).filter(sa.or_(Player.player_id == + p1_id, Player.player_id == p2_id)).order_by(Player.player_id).all() + + + if len(players) < 2: + raise Exception("Not enough players found.") + + # assign the players from the array retrieved above + if players[0].player_id == p1_id: + p1 = players[0] + p2 = players[1] + else: + p1 = players[1] + p2 = players[0] + + # note that wins and losses are from p1's perspective + win_loss_sql = """select win_loss, count(1) + from ( + select case + when pgsp1.score >= pgsp2.score then 'win' + else 'loss' + end win_loss + from games g join player_game_stats pgsp1 + on g.game_id = pgsp1.game_id and pgsp1.player_id = :p1 + join player_game_stats pgsp2 + on g.game_id = pgsp2.game_id and pgsp2.player_id = :p2 + where g.players @> ARRAY[:p1,:p2] + and g.game_type_cd = 'duel' + and pgsp1.create_dt between g.create_dt - interval '1 hour' + and g.create_dt + interval '1 hour' + and pgsp2.create_dt between g.create_dt - interval '1 hour' + and g.create_dt + interval '1 hour' + ) wl + group by win_loss + """ + + wins_losses = DBSession.query("win_loss", "count").\ + from_statement(win_loss_sql).\ + params(p1=p1_id, p2=p2_id).all() + + for row in wins_losses: + if row.win_loss == "win": + p1_wins = row.count + elif row.win_loss == "loss": + p2_wins = row.count + + # grab the 20 most recent games between the two + rgs_raw = recent_games_q(player_id=p1_id, player_id_2=p2_id, + game_type_cd="duel").limit(20).all() + + rgs = [RecentGame(row) for row in rgs_raw] + + except Exception as e: + log.debug(e) + + p1_id = None + p2_id = None + p1 = None + p2 = None + p1_wins = None + p2_wins = None + rgs = None + + return { + "p1_id" : p1_id, + "p2_id" : p2_id, + "p1" : p1, + "p2" : p2, + "p1_wins" : p1_wins, + "p2_wins" : p2_wins, + "recent_games" : rgs, + } + + +def player_versus(request): + return player_versus_data(request)