]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/player.py
Add favorite map support.
[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 player_index_json(request):
49     """
50     Provides a list of all the current players. JSON.
51     """
52     return [{'status':'not implemented'}]
53
54
55 def _get_games_played(player_id):
56     """
57     Provides a breakdown by gametype of the games played by player_id.
58
59     Returns a tuple containing (total_games, games_breakdown), where
60     total_games is the absolute number of games played by player_id
61     and games_breakdown is an array containing (game_type_cd, # games)
62     """
63     games_played = DBSession.query(Game.game_type_cd, func.count()).\
64             filter(Game.game_id == PlayerGameStat.game_id).\
65             filter(PlayerGameStat.player_id == player_id).\
66             group_by(Game.game_type_cd).\
67             order_by(func.count().desc()).all()
68
69     total = 0
70     for (game_type_cd, games) in games_played:
71         total += games
72
73     return (total, games_played)
74
75
76 # TODO: should probably factor the above function into this one such that
77 # total_stats['ctf_games'] is the count of CTF games and so on...
78 def _get_total_stats(player_id):
79     """
80     Provides aggregated stats by player_id.
81
82     Returns a dict with the keys 'kills', 'deaths', 'alivetime'.
83
84     kills = how many kills a player has over all games
85     deaths = how many deaths a player has over all games
86     alivetime = how long a player has played over all games
87
88     If any of the above are None, they are set to 0.
89     """
90     total_stats = {}
91     (total_stats['kills'], total_stats['deaths'], total_stats['alivetime']) = DBSession.\
92             query("total_kills", "total_deaths", "total_alivetime").\
93             from_statement(
94                 "select sum(kills) total_kills, "
95                 "sum(deaths) total_deaths, "
96                 "sum(alivetime) total_alivetime "
97                 "from player_game_stats "
98                 "where player_id=:player_id"
99             ).params(player_id=player_id).one()
100
101     (total_stats['wins'],) = DBSession.\
102             query("total_wins").\
103             from_statement(
104                 "select count(*) total_wins "
105                 "from games g, player_game_stats pgs "
106                 "where g.game_id = pgs.game_id "
107                 "and player_id=:player_id "
108                 "and (g.winner = pgs.team or pgs.rank = 1)"
109             ).params(player_id=player_id).one()
110
111     for (key,value) in total_stats.items():
112         if value == None:
113             total_stats[key] = 0
114
115     return total_stats
116
117
118 def _get_fav_map(player_id):
119     """
120     Get the player's favorite map. The favorite map is defined
121     as the map that he or she has played the most in the past 
122     90 days.
123
124     Returns a map object.
125     """
126     # 90 day window
127     back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
128
129     fav_map = DBSession.query(Map).\
130             filter(Game.game_id == PlayerGameStat.game_id).\
131             filter(Game.map_id == Map.map_id).\
132             filter(PlayerGameStat.player_id == player_id).\
133             filter(PlayerGameStat.create_dt > back_then).\
134             group_by(Map.map_id).\
135             order_by(func.count().desc()).\
136             limit(1).one()
137
138     return fav_map
139
140
141 def get_accuracy_stats(player_id, weapon_cd, games):
142     """
143     Provides accuracy for weapon_cd by player_id for the past N games.
144     """
145     # Reaching back 90 days should give us an accurate enough average
146     # We then multiply this out for the number of data points (games) to
147     # create parameters for a flot graph
148     try:
149         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),
150                 func.sum(PlayerWeaponStat.fired)).\
151                 filter(PlayerWeaponStat.player_id == player_id).\
152                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
153                 one()
154
155         avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)
156
157         # Determine the raw accuracy (hit, fired) numbers for $games games
158         # This is then enumerated to create parameters for a flot graph
159         raw_accs = DBSession.query(PlayerWeaponStat.game_id, 
160             PlayerWeaponStat.hit, PlayerWeaponStat.fired).\
161                 filter(PlayerWeaponStat.player_id == player_id).\
162                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
163                 order_by(PlayerWeaponStat.game_id.desc()).\
164                 limit(games).\
165                 all()
166
167         # they come out in opposite order, so flip them in the right direction
168         raw_accs.reverse()
169
170         accs = []
171         for i in range(len(raw_accs)):
172             accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))
173     except:
174         accs = []
175         avg = 0.0
176
177     return (avg, accs)
178
179
180 def get_damage_stats(player_id, weapon_cd, games):
181     """
182     Provides damage info for weapon_cd by player_id for the past N games.
183     """
184     try:
185         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.actual),
186                 func.sum(PlayerWeaponStat.hit)).\
187                 filter(PlayerWeaponStat.player_id == player_id).\
188                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
189                 one()
190
191         avg = round(float(raw_avg[0])/raw_avg[1], 2)
192
193         # Determine the damage efficiency (hit, fired) numbers for $games games
194         # This is then enumerated to create parameters for a flot graph
195         raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, 
196             PlayerWeaponStat.actual, PlayerWeaponStat.hit).\
197                 filter(PlayerWeaponStat.player_id == player_id).\
198                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
199                 order_by(PlayerWeaponStat.game_id.desc()).\
200                 limit(games).\
201                 all()
202
203         # they come out in opposite order, so flip them in the right direction
204         raw_dmgs.reverse()
205
206         dmgs = []
207         for i in range(len(raw_dmgs)):
208             # try to derive, unless we've hit nothing then set to 0!
209             try:
210                 dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)
211             except:
212                 dmg = 0.0
213
214             dmgs.append((raw_dmgs[i][0], dmg))
215     except Exception as e:
216         dmgs = []
217         avg = 0.0
218
219     return (avg, dmgs)
220
221
222 def _player_info_data(request):
223     player_id = int(request.matchdict['id'])
224     if player_id <= 2:
225         player_id = -1;
226
227     try:
228         player = DBSession.query(Player).filter_by(player_id=player_id).\
229                 filter(Player.active_ind == True).one()
230
231         # games played, alivetime, wins, kills, deaths
232         total_stats = _get_total_stats(player.player_id)
233
234         # games breakdown - N games played (X ctf, Y dm) etc
235         (total_games, games_breakdown) = _get_games_played(player.player_id)
236
237         # favorite map from the past 90 days
238         fav_map = _get_fav_map(player.player_id)
239
240         # friendly display of elo information and preliminary status
241         elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\
242                 filter(PlayerElo.game_type_cd.in_(['ctf','duel','dm'])).\
243                 order_by(PlayerElo.elo.desc()).all()
244
245         elos_display = []
246         for elo in elos:
247             if elo.games > 32:
248                 str = "{0} ({1})"
249             else:
250                 str = "{0}* ({1})"
251
252             elos_display.append(str.format(round(elo.elo, 3),
253                 elo.game_type_cd))
254
255         # which weapons have been used in the past 90 days
256         # and also, used in 5 games or more?
257         back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
258         recent_weapons = []
259         for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\
260                 filter(PlayerWeaponStat.player_id == player_id).\
261                 filter(PlayerWeaponStat.create_dt > back_then).\
262                 group_by(PlayerWeaponStat.weapon_cd).\
263                 having(func.count() > 4).\
264                 all():
265                     recent_weapons.append(weapon[0])
266
267         # recent games table, all data
268         recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\
269                 filter(PlayerGameStat.player_id == player_id).\
270                 filter(PlayerGameStat.game_id == Game.game_id).\
271                 filter(Game.server_id == Server.server_id).\
272                 filter(Game.map_id == Map.map_id).\
273                 order_by(Game.game_id.desc())[0:10]
274
275     except Exception as e:
276         player = None
277         elos_display = None
278         total_stats = None
279         recent_games = None
280         total_games = None
281         games_breakdown = None
282         recent_weapons = []
283         fav_map = None
284
285     return {'player':player,
286             'elos_display':elos_display,
287             'recent_games':recent_games,
288             'total_stats':total_stats,
289             'total_games':total_games,
290             'games_breakdown':games_breakdown,
291             'recent_weapons':recent_weapons,
292             'fav_map':fav_map,
293             }
294
295
296 def player_info(request):
297     """
298     Provides detailed information on a specific player
299     """
300     return _player_info_data(request)
301
302
303 def player_info_json(request):
304     """
305     Provides detailed information on a specific player. JSON.
306     """
307     return [{'status':'not implemented'}]
308
309
310 def _player_game_index_data(request):
311     player_id = request.matchdict['player_id']
312
313     if request.params.has_key('page'):
314         current_page = request.params['page']
315     else:
316         current_page = 1
317
318     try:
319         games_q = DBSession.query(Game, Server, Map).\
320             filter(PlayerGameStat.game_id == Game.game_id).\
321             filter(PlayerGameStat.player_id == player_id).\
322             filter(Game.server_id == Server.server_id).\
323             filter(Game.map_id == Map.map_id).\
324             order_by(Game.game_id.desc())
325
326         games = Page(games_q, current_page, items_per_page=10, url=page_url)
327
328         pgstats = {}
329         for (game, server, map) in games:
330             pgstats[game.game_id] = DBSession.query(PlayerGameStat).\
331                     filter(PlayerGameStat.game_id == game.game_id).\
332                     order_by(PlayerGameStat.rank).\
333                     order_by(PlayerGameStat.score).all()
334
335     except Exception as e:
336         player = None
337         games = None
338
339     return {'player_id':player_id,
340             'games':games,
341             'pgstats':pgstats}
342
343
344 def player_game_index(request):
345     """
346     Provides an index of the games in which a particular
347     player was involved. This is ordered by game_id, with
348     the most recent game_ids first. Paginated.
349     """
350     return _player_game_index_data(request)
351
352
353 def player_game_index_json(request):
354     """
355     Provides an index of the games in which a particular
356     player was involved. This is ordered by game_id, with
357     the most recent game_ids first. Paginated. JSON.
358     """
359     return [{'status':'not implemented'}]
360
361
362 def _player_accuracy_data(request):
363     player_id = request.matchdict['id']
364     allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']
365     weapon_cd = 'nex'
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, accs) = get_accuracy_stats(player_id, weapon_cd, games)
384
385     # if we don't have enough data for the given weapon
386     if len(accs) < games:
387         games = len(accs)
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             'accs':accs
396             }
397
398
399 def player_accuracy(request):
400     """
401     Provides the accuracy for the given weapon. (JSON only)
402     """
403     return _player_accuracy_data(request)
404
405
406 def player_accuracy_json(request):
407     """
408     Provides a JSON response representing the accuracy for the given weapon.
409
410     Parameters:
411        weapon = which weapon to display accuracy for. Valid values are 'nex',
412                 'shotgun', 'uzi', and 'minstanex'.
413        games = over how many games to display accuracy. Can be up to 50.
414     """
415     return _player_accuracy_data(request)
416
417
418 def _player_damage_data(request):
419     player_id = request.matchdict['id']
420     allowed_weapons = ['grenadelauncher', 'electro', 'crylink', 'hagar',
421             'rocketlauncher', 'laser']
422     weapon_cd = 'rocketlauncher'
423     games = 20
424
425     if request.params.has_key('weapon'):
426         if request.params['weapon'] in allowed_weapons:
427             weapon_cd = request.params['weapon']
428
429     if request.params.has_key('games'):
430         try:
431             games = request.params['games']
432
433             if games < 0:
434                 games = 20
435             if games > 50:
436                 games = 50
437         except:
438             games = 20
439
440     (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)
441
442     # if we don't have enough data for the given weapon
443     if len(dmgs) < games:
444         games = len(dmgs)
445
446     return {
447             'player_id':player_id, 
448             'player_url':request.route_url('player_info', id=player_id), 
449             'weapon':weapon_cd, 
450             'games':games, 
451             'avg':avg, 
452             'dmgs':dmgs
453             }
454
455
456 def player_damage_json(request):
457     """
458     Provides a JSON response representing the damage for the given weapon.
459
460     Parameters:
461        weapon = which weapon to display damage for. Valid values are
462          'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',
463          'laser'.
464        games = over how many games to display damage. Can be up to 50.
465     """
466     return _player_damage_data(request)