]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/player.py
clean dangling commit on actually merged branch
[xonotic/xonstat.git] / xonstat / views / player.py
1 import datetime\r
2 import json\r
3 import logging\r
4 import re\r
5 import sqlalchemy as sa\r
6 import sqlalchemy.sql.functions as func\r
7 import time\r
8 from pyramid.response import Response\r
9 from pyramid.url import current_route_url\r
10 from sqlalchemy import desc, distinct\r
11 from webhelpers.paginate import Page, PageURL\r
12 from xonstat.models import *\r
13 from xonstat.util import page_url\r
14 \r
15 log = logging.getLogger(__name__)\r
16 \r
17 \r
18 def player_index(request):\r
19     """\r
20     Provides a list of all the current players. \r
21     """\r
22     if 'page' in request.matchdict:\r
23         current_page = int(request.matchdict['page'])\r
24     else:\r
25         current_page = 1\r
26 \r
27     try:\r
28         player_q = DBSession.query(Player).\\r
29                 filter(Player.player_id > 2).\\r
30                 filter(Player.active_ind == True).\\r
31                 filter(sa.not_(Player.nick.like('Anonymous Player%'))).\\r
32                 order_by(Player.player_id.desc())\r
33 \r
34         players = Page(player_q, current_page, items_per_page=10, url=page_url)\r
35 \r
36         last_linked_page = current_page + 4\r
37         if last_linked_page > players.last_page:\r
38             last_linked_page = players.last_page\r
39 \r
40         pages_to_link = range(current_page+1, last_linked_page+1)\r
41 \r
42     except Exception as e:\r
43         players = None\r
44         raise e\r
45 \r
46     return {'players':players,\r
47             'pages_to_link':pages_to_link,\r
48             }\r
49 \r
50 \r
51 def get_games_played(player_id):\r
52     """\r
53     Provides a breakdown by gametype of the games played by player_id.\r
54 \r
55     Returns a tuple containing (total_games, games_breakdown), where\r
56     total_games is the absolute number of games played by player_id\r
57     and games_breakdown is an array containing (game_type_cd, # games)\r
58     """\r
59     games_played = DBSession.query(Game.game_type_cd, func.count()).\\r
60             filter(Game.game_id == PlayerGameStat.game_id).\\r
61             filter(PlayerGameStat.player_id == player_id).\\r
62             group_by(Game.game_type_cd).\\r
63             order_by(func.count().desc()).all()\r
64 \r
65     total = 0\r
66     for (game_type_cd, games) in games_played:\r
67         total += games\r
68 \r
69     return (total, games_played)\r
70 \r
71 \r
72 # TODO: should probably factor the above function into this one such that\r
73 # total_stats['ctf_games'] is the count of CTF games and so on...\r
74 def get_total_stats(player_id):\r
75     """\r
76     Provides aggregated stats by player_id.\r
77 \r
78     Returns a dict with the keys 'kills', 'deaths', 'alivetime'.\r
79 \r
80     kills = how many kills a player has over all games\r
81     deaths = how many deaths a player has over all games\r
82     alivetime = how long a player has played over all games\r
83 \r
84     If any of the above are None, they are set to 0.\r
85     """\r
86     total_stats = {}\r
87     (total_stats['kills'], total_stats['deaths'], total_stats['alivetime']) = DBSession.\\r
88             query("total_kills", "total_deaths", "total_alivetime").\\r
89             from_statement(\r
90                 "select sum(kills) total_kills, "\r
91                 "sum(deaths) total_deaths, "\r
92                 "sum(alivetime) total_alivetime "\r
93                 "from player_game_stats "\r
94                 "where player_id=:player_id"\r
95             ).params(player_id=player_id).one()\r
96 \r
97     (total_stats['wins'],) = DBSession.\\r
98             query("total_wins").\\r
99             from_statement(\r
100                 "select count(*) total_wins "\r
101                 "from games g, player_game_stats pgs "\r
102                 "where g.game_id = pgs.game_id "\r
103                 "and player_id=:player_id "\r
104                 "and (g.winner = pgs.team or pgs.rank = 1)"\r
105             ).params(player_id=player_id).one()\r
106 \r
107     for (key,value) in total_stats.items():\r
108         if value == None:\r
109             total_stats[key] = 0\r
110 \r
111     return total_stats\r
112 \r
113 \r
114 def get_accuracy_stats(player_id, weapon_cd, games):\r
115     """\r
116     Provides accuracy for weapon_cd by player_id for the past N games.\r
117     """\r
118     # Reaching back 90 days should give us an accurate enough average\r
119     # We then multiply this out for the number of data points (games) to\r
120     # create parameters for a flot graph\r
121     try:\r
122         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),\r
123                 func.sum(PlayerWeaponStat.fired)).\\r
124                 filter(PlayerWeaponStat.player_id == player_id).\\r
125                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\\r
126                 one()\r
127 \r
128         avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)\r
129 \r
130         # Determine the raw accuracy (hit, fired) numbers for $games games\r
131         # This is then enumerated to create parameters for a flot graph\r
132         raw_accs = DBSession.query(PlayerWeaponStat.game_id, \r
133             PlayerWeaponStat.hit, PlayerWeaponStat.fired).\\r
134                 filter(PlayerWeaponStat.player_id == player_id).\\r
135                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\\r
136                 order_by(PlayerWeaponStat.game_id.desc()).\\r
137                 limit(games).\\r
138                 all()\r
139 \r
140         # they come out in opposite order, so flip them in the right direction\r
141         raw_accs.reverse()\r
142 \r
143         accs = []\r
144         for i in range(len(raw_accs)):\r
145             accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))\r
146     except:\r
147         accs = []\r
148         avg = 0.0\r
149 \r
150     return (avg, accs)\r
151 \r
152 \r
153 def player_info(request):\r
154     """\r
155     Provides detailed information on a specific player\r
156     """\r
157     player_id = int(request.matchdict['id'])\r
158     if player_id <= 2:\r
159         player_id = -1;\r
160 \r
161     try:\r
162         player = DBSession.query(Player).filter_by(player_id=player_id).\\r
163                 filter(Player.active_ind == True).one()\r
164 \r
165         # games played, alivetime, wins, kills, deaths\r
166         total_stats = get_total_stats(player.player_id)\r
167 \r
168         # games breakdown - N games played (X ctf, Y dm) etc\r
169         (total_games, games_breakdown) = get_games_played(player.player_id)\r
170 \r
171 \r
172         # friendly display of elo information and preliminary status\r
173         elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\\r
174                 filter(PlayerElo.game_type_cd.in_(['ctf','duel','dm'])).\\r
175                 order_by(PlayerElo.elo.desc()).all()\r
176 \r
177         elos_display = []\r
178         for elo in elos:\r
179             if elo.games > 32:\r
180                 str = "{0} ({1})"\r
181             else:\r
182                 str = "{0}* ({1})"\r
183 \r
184             elos_display.append(str.format(round(elo.elo, 3),\r
185                 elo.game_type_cd))\r
186 \r
187         # which weapons have been used in the past 90 days\r
188         # and also, used in 5 games or more?\r
189         back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)\r
190         recent_weapons = []\r
191         for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\\r
192                 filter(PlayerWeaponStat.player_id == player_id).\\r
193                 filter(PlayerWeaponStat.create_dt > back_then).\\r
194                 group_by(PlayerWeaponStat.weapon_cd).\\r
195                 having(func.count() > 4).\\r
196                 all():\r
197                     recent_weapons.append(weapon[0])\r
198 \r
199         # recent games table, all data\r
200         recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\\r
201                 filter(PlayerGameStat.player_id == player_id).\\r
202                 filter(PlayerGameStat.game_id == Game.game_id).\\r
203                 filter(Game.server_id == Server.server_id).\\r
204                 filter(Game.map_id == Map.map_id).\\r
205                 order_by(Game.game_id.desc())[0:10]\r
206 \r
207     except Exception as e:\r
208         player = None\r
209         elos_display = None\r
210         total_stats = None\r
211         recent_games = None\r
212         total_games = None\r
213         games_breakdown = None\r
214         recent_weapons = []\r
215 \r
216     return {'player':player,\r
217             'elos_display':elos_display,\r
218             'recent_games':recent_games,\r
219             'total_stats':total_stats,\r
220             'total_games':total_games,\r
221             'games_breakdown':games_breakdown,\r
222             'recent_weapons':recent_weapons,\r
223             }\r
224 \r
225 \r
226 def player_game_index(request):\r
227     """\r
228     Provides an index of the games in which a particular\r
229     player was involved. This is ordered by game_id, with\r
230     the most recent game_ids first. Paginated.\r
231     """\r
232     player_id = request.matchdict['player_id']\r
233 \r
234     if 'page' in request.matchdict:\r
235         current_page = request.matchdict['page']\r
236     else:\r
237         current_page = 1\r
238 \r
239     try:\r
240         games_q = DBSession.query(Game, Server, Map).\\r
241             filter(PlayerGameStat.game_id == Game.game_id).\\r
242             filter(PlayerGameStat.player_id == player_id).\\r
243             filter(Game.server_id == Server.server_id).\\r
244             filter(Game.map_id == Map.map_id).\\r
245             order_by(Game.game_id.desc())\r
246 \r
247         games = Page(games_q, current_page, url=page_url)\r
248 \r
249         pgstats = {}\r
250         for (game, server, map) in games:\r
251             pgstats[game.game_id] = DBSession.query(PlayerGameStat).\\r
252                     filter(PlayerGameStat.game_id == game.game_id).\\r
253                     order_by(PlayerGameStat.rank).\\r
254                     order_by(PlayerGameStat.score).all()\r
255 \r
256     except Exception as e:\r
257         player = None\r
258         games = None\r
259 \r
260     return {'player_id':player_id,\r
261             'games':games,\r
262             'pgstats':pgstats}\r
263 \r
264 def player_accuracy(request):\r
265     """\r
266     Provides a JSON response representing the accuracy for the given weapon.\r
267 \r
268     Parameters:\r
269        weapon = which weapon to display accuracy for. Valid values are 'nex',\r
270                 'shotgun', 'uzi', and 'minstanex'.\r
271        games = over how many games to display accuracy. Can be up to 50.\r
272     """\r
273     player_id = request.matchdict['id']\r
274     allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']\r
275     weapon_cd = 'nex'\r
276     games = 20\r
277 \r
278     if request.params.has_key('weapon'):\r
279         if request.params['weapon'] in allowed_weapons:\r
280             weapon_cd = request.params['weapon']\r
281 \r
282     if request.params.has_key('games'):\r
283         try:\r
284             games = request.params['games']\r
285 \r
286             if games < 0:\r
287                 games = 20\r
288             if games > 50:\r
289                 games = 50\r
290         except:\r
291             games = 20\r
292 \r
293     (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)\r
294 \r
295     # if we don't have enough data for the given weapon\r
296     if len(accs) < games:\r
297         games = len(accs)\r
298 \r
299     return {\r
300             'player_id':player_id, \r
301             'player_url':request.route_url('player_info', id=player_id), \r
302             'weapon':weapon_cd, \r
303             'games':games, \r
304             'avg':avg, \r
305             'accs':accs\r
306             }\r
307 \r