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