From: Ant Zucaro Date: Wed, 19 Sep 2012 02:47:43 +0000 (-0400) Subject: Add the logic behind the player_info view. Template changes coming. X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=f83816c955dda1f6e020318c163ccefb03c0d454;p=xonotic%2Fxonstat.git Add the logic behind the player_info view. Template changes coming. --- diff --git a/xonstat/views/player.py b/xonstat/views/player.py index b1fbd5d..b33e931 100644 --- a/xonstat/views/player.py +++ b/xonstat/views/player.py @@ -5,6 +5,7 @@ import re import sqlalchemy as sa import sqlalchemy.sql.functions as func import time +from collections import namedtuple from pyramid.response import Response from pyramid.url import current_route_url from sqlalchemy import desc, distinct @@ -52,6 +53,352 @@ def player_index_json(request): return [{'status':'not implemented'}] +def get_games_played(player_id): + """ + Provides a breakdown by gametype of the games played by player_id. + + Returns a list of namedtuples with the following members: + - game_type_cd + - games + - wins + - losses + - win_pct + + The list itself is ordered by the number of games played + """ + GamesPlayed = namedtuple('GamesPlayed', ['game_type_cd', 'games', 'wins', + 'losses', 'win_pct']) + + raw_games_played = DBSession.query('game_type_cd', 'wins', 'losses').\ + from_statement( + "SELECT game_type_cd, " + "SUM(win) wins, " + "SUM(loss) losses " + "FROM (SELECT g.game_id, " + "g.game_type_cd, " + "CASE " + "WHEN g.winner = pgs.team THEN 1 " + "WHEN pgs.rank = 1 THEN 1 " + "ELSE 0 " + "END win, " + "CASE " + "WHEN g.winner = pgs.team THEN 0 " + "WHEN pgs.rank = 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 " + "GROUP BY game_type_cd " + ).params(player_id=player_id).all() + + games_played = [] + overall_games = 0 + overall_wins = 0 + overall_losses = 0 + for row in raw_games_played: + games = row.wins + row.losses + overall_games += games + overall_wins += row.wins + overall_losses += row.losses + win_pct = float(row.wins)/games + + games_played.append(GamesPlayed(row.game_type_cd, games, row.wins, + row.losses, win_pct)) + + try: + overall_win_pct = float(overall_wins)/overall_games + except: + overall_win_pct = 0.0 + + games_played.append(GamesPlayed('overall', overall_games, overall_wins, + overall_losses, overall_win_pct)) + + # sort the resulting list by # of games played + games_played = sorted(games_played, key=lambda x:x.games) + games_played.reverse() + return games_played + + +def get_overall_stats(player_id): + """ + Provides a breakdown of stats by gametype played by player_id. + + Returns a dictionary of namedtuples with the following members: + - total_kills + - total_deaths + - k_d_ratio + - last_played (last time the player played the game type) + - total_playing_time (total amount of time played the game type) + - total_pickups (ctf only) + - total_captures (ctf only) + - cap_ratio (ctf only) + - total_carrier_frags (ctf only) + - game_type_cd + + 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', 'total_playing_time', 'total_pickups', + 'total_captures', 'cap_ratio', 'total_carrier_frags', 'game_type_cd']) + + raw_stats = DBSession.query('game_type_cd', 'total_kills', + 'total_deaths', 'last_played', 'total_playing_time', + 'total_pickups', 'total_captures', 'total_carrier_frags').\ + from_statement( + "SELECT g.game_type_cd, " + "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 games g, " + "player_game_stats pgs " + "WHERE g.game_id = pgs.game_id " + "AND pgs.player_id = :player_id " + "GROUP BY g.game_type_cd " + ).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 + except: + k_d_ratio = None + + try: + cap_ratio = float(row.total_pickups)/row.total_captures + 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, + k_d_ratio=k_d_ratio, + last_played=row.last_played, + total_playing_time=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) + + 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, + 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 + + return overall_stats + + +def get_fav_maps(player_id, game_type_cd=None): + """ + Provides a breakdown of favorite maps by gametype. + + Returns a dictionary of namedtuples with the following members: + - game_type_cd + - map_name (map name) + - map_id + - times_played + + The favorite map is defined as the map you've played the most + for the given game_type_cd. + + The key to the dictionary is the game type code. There is also an + "overall" game_type_cd which is the overall favorite map. This is + defined as the favorite map of the game type you've played the + most. The input parameter game_type_cd is for this. + """ + raw_favs = DBSession.query('game_type_cd', 'map_name', + 'map_id', 'times_played').\ + from_statement( + "SELECT game_type_cd, " + "name map_name, " + "map_id, " + "times_played " + "FROM (SELECT g.game_type_cd, " + "m.name, " + "m.map_id, " + "Count(*) times_played, " + "Row_number() " + "OVER ( " + "partition BY g.game_type_cd " + "ORDER BY Count(*) DESC, m.map_id ASC) rank " + "FROM games g, " + "player_game_stats pgs, " + "maps m " + "WHERE g.game_id = pgs.game_id " + "AND g.map_id = m.map_id " + "AND pgs.player_id = :player_id " + "GROUP BY g.game_type_cd, " + "m.map_id, " + "m.name) most_played " + "WHERE rank = 1 " + "ORDER BY times_played desc " + ).params(player_id=player_id).all() + + fav_maps = {} + overall_fav = None + for row in raw_favs: + # if we aren't given a favorite game_type_cd + # then the overall favorite is the one we've + # played the most + if overall_fav is None: + fav_maps['overall'] = row + overall_fav = row.game_type_cd + + # otherwise it is the favorite map from the + # favorite game_type_cd (provided as a param) + # and we'll overwrite the first dict entry + if game_type_cd == row.game_type_cd: + fav_maps['overall'] = row + + fav_maps[row.game_type_cd] = row + + return fav_maps + + +def get_ranks(player_id): + """ + Provides a breakdown of the player's ranks by game type. + + Returns a dictionary of namedtuples with the following members: + - game_type_cd + - rank + - max_rank + + The key to the dictionary is the game type code. There is also an + "overall" game_type_cd which is the overall best rank. + """ + raw_ranks = DBSession.query("game_type_cd", "rank", "max_rank").\ + from_statement( + "select pr.game_type_cd, pr.rank, overall.max_rank " + "from player_ranks pr, " + "(select game_type_cd, max(rank) max_rank " + "from player_ranks " + "group by game_type_cd) overall " + "where pr.game_type_cd = overall.game_type_cd " + "and player_id = :player_id " + "order by rank").\ + params(player_id=player_id).all() + + ranks = {} + found_top_rank = False + for row in raw_ranks: + if not found_top_rank: + ranks['overall'] = row + found_top_rank = True + + ranks[row.game_type_cd] = row + + return ranks; + + +def get_elos(player_id): + """ + Provides a breakdown of the player's elos by game type. + + Returns a dictionary of namedtuples with the following members: + - player_id + - game_type_cd + - games + - elo + + The key to the dictionary is the game type code. There is also an + "overall" game_type_cd which is the overall best rank. + """ + raw_elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\ + order_by(PlayerElo.elo.desc()).all() + + elos = {} + found_max_elo = False + for row in raw_elos: + if not found_max_elo: + elos['overall'] = row + found_max_elo = True + + elos[row.game_type_cd] = row + + return elos + + +def get_recent_games(player_id): + """ + Provides a list of recent games. + + Returns the full PlayerGameStat, Game, Server, Map + objects for all recent games. + """ + # 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] + + return 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 + + def _get_games_played(player_id): """ Provides a breakdown by gametype of the games played by player_id. @@ -115,31 +462,69 @@ def _get_total_stats(player_id): return total_stats -def _get_fav_map(player_id): +def _get_fav_maps(player_id): """ Get the player's favorite map. The favorite map is defined - as the map that he or she has played the most in the past - 90 days. - - Returns a dictionary with keys for the map's name and id. + as the map that he or she has played the most with game + types considered separate. This is to say that if a person + plays dm and duel on stormkeep with 25 games in each mode, + final_rage could still be the favorite map overall if it has + 26 dm games. + + Returns a dictionary with entries for each played game type. + Each game type ditionary value contained a nested dictionary + with the following keys: + id = the favorite map id + name = the favorite map's name + times_played = the number of times the map was played in that mode + + Note also that there's a superficial "overall" game type that is + meant to hold the top map overall. It'll be a dupe of one of the + other game types' nested dictionary. """ - # 90 day window - back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90) - - raw_fav_map = DBSession.query(Map.name, Map.map_id).\ - filter(Game.game_id == PlayerGameStat.game_id).\ - filter(Game.map_id == Map.map_id).\ - filter(PlayerGameStat.player_id == player_id).\ - filter(PlayerGameStat.create_dt > back_then).\ - group_by(Map.name, Map.map_id).\ - order_by(func.count().desc()).\ - limit(1).one() - - fav_map = {} - fav_map['name'] = raw_fav_map[0] - fav_map['id'] = raw_fav_map[1] - - return fav_map + fav_maps = {} + for (game_type_cd, name, map_id, times_played) in DBSession.\ + query("game_type_cd", "name", "map_id", "times_played").\ + from_statement( + "SELECT game_type_cd, " + "name, " + "map_id, " + "times_played " + "FROM (SELECT g.game_type_cd, " + "m.name, " + "m.map_id, " + "count(*) times_played, " + "row_number() " + "over ( " + "PARTITION BY g.game_type_cd " + "ORDER BY Count(*) DESC, m.map_id ASC) rank " + "FROM games g, " + "player_game_stats pgs, " + "maps m " + "WHERE g.game_id = pgs.game_id " + "AND g.map_id = m.map_id " + "AND pgs.player_id = :player_id " + "GROUP BY g.game_type_cd, " + "m.map_id, " + "m.name) most_played " + "WHERE rank = 1" + ).params(player_id=player_id).all(): + fav_map_detail = {} + fav_map_detail['name'] = name + fav_map_detail['map_id'] = map_id + fav_map_detail['times_played'] = times_played + fav_maps[game_type_cd] = fav_map_detail + + max_played = 0 + overall = {} + for fav_map_detail in fav_maps.values(): + if fav_map_detail['times_played'] > max_played: + max_played = fav_map_detail['times_played'] + overall = fav_map_detail + + fav_maps['overall'] = overall + + return fav_maps def _get_rank(player_id): @@ -251,79 +636,32 @@ def player_info_data(request): player = DBSession.query(Player).filter_by(player_id=player_id).\ filter(Player.active_ind == True).one() - # games played, alivetime, wins, kills, deaths - total_stats = _get_total_stats(player.player_id) - - # games breakdown - N games played (X ctf, Y dm) etc - (total_games, games_breakdown) = _get_games_played(player.player_id) - - # favorite map from the past 90 days - try: - fav_map = _get_fav_map(player.player_id) - except: - fav_map = None - - # friendly display of elo information and preliminary status - elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\ - filter(PlayerElo.game_type_cd.in_(['ctf','duel','dm'])).\ - order_by(PlayerElo.elo.desc()).all() - - elos_display = [] - for elo in elos: - if elo.games > 32: - str = "{0} ({1})" - else: - str = "{0}* ({1})" - - elos_display.append(str.format(round(elo.elo, 3), - elo.game_type_cd)) - - # get current rank information - ranks = _get_rank(player_id) - ranks_display = ', '.join(["{1} of {2} ({0})".format(gtc, rank, - max_rank) for gtc, rank, max_rank in ranks]) - - - # which weapons have been used in the past 90 days - # and also, used in 5 games or more? - back_then = 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 > back_then).\ - group_by(PlayerWeaponStat.weapon_cd).\ - having(func.count() > 4).\ - all(): - recent_weapons.append(weapon[0]) - - # 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] + games_played = get_games_played(player_id) + overall_stats = get_overall_stats(player_id) + fav_maps = get_fav_maps(player_id) + elos = get_elos(player_id) + ranks = get_ranks(player_id) + recent_games = get_recent_games(player_id) + recent_weapons = get_recent_weapons(player_id) except Exception as e: - player = None - elos_display = None - total_stats = None - recent_games = None - total_games = None - games_breakdown = None + player = None + games_played = None + overall_stats = None + fav_maps = None + elos = None + ranks = None + recent_games = None recent_weapons = [] - fav_map = None - ranks_display = None; return {'player':player, - 'elos_display':elos_display, + 'games_played':games_played, + 'overall_stats':overall_stats, + 'fav_maps':fav_maps, + 'elos':elos, + 'ranks':ranks, 'recent_games':recent_games, - 'total_stats':total_stats, - 'total_games':total_games, - 'games_breakdown':games_breakdown, - 'recent_weapons':recent_weapons, - 'fav_map':fav_map, - 'ranks_display':ranks_display, + 'recent_weapons':recent_weapons } @@ -331,7 +669,63 @@ def player_info(request): """ Provides detailed information on a specific player """ - return player_info_data(request) + player_info = player_info_data(request) + + player = player_info['player'] + games_played = player_info['games_played'] + overall_stats = player_info['overall_stats'] + fav_maps = player_info['fav_maps'] + elos = player_info['elos'] + ranks = player_info['ranks'] + recent_games = player_info['recent_games'] + recent_weapons = player_info['recent_weapons'] + + # holds all of the tab content data + stat_strings = {} + for g in games_played: + stat_strings[g.game_type_cd] = [] + + # last seen, playing time + for k,v in overall_stats.iteritems(): + stat_strings[k].append({'Last Played':v.last_played}) + stat_strings[k].\ + append({'Playing Time':v.total_playing_time}) + + # games played, win ratio + for g in games_played: + stat_strings[g.game_type_cd].append({'Games Played':g.games}) + stat_strings[g.game_type_cd].\ + append({'Win Pct':'{0} ({1} wins, {2} losses'.\ + format(g.win_pct, g.wins, g.losses)}) + + # kill ratio + for k,v in overall_stats.iteritems(): + stat_strings[k].\ + append({'Kill Ratio':'{0} ({1} kills, {2} deaths)'.\ + format(v.k_d_ratio, v.total_kills, v.total_deaths)}) + + # favorite maps + for k,v in fav_maps.iteritems(): + stat_strings[k].append({'Favorite Map':v.map_name}) + + # elos + for k,v in elos.iteritems(): + try: + stat_strings[k].append({'Elo':v.elo}) + except: + pass + + # ranks + for k,v in ranks.iteritems(): + try: + stat_strings[k].append({'Rank':'{0} of {1}'.\ + format(v.rank, v.max_rank)}) + except: + pass + + print stat_strings + + return player_info def player_info_json(request):