]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/player.py
Implemented feature #1228:
[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     suicides = how many suicides a player has over all games
87     alivetime = how long a player has played over all games
88     alivetime_week = how long a player has played over all games in the last week
89     alivetime_month = how long a player has played over all games in the last month
90     wins = how many games a player has won
91
92     If any of the above are None, they are set to 0.
93     """
94     # 7 and 30 day windows
95     one_week_ago  = datetime.datetime.utcnow() - datetime.timedelta(days=7)
96     one_month_ago = datetime.datetime.utcnow() - datetime.timedelta(days=30)
97
98     total_stats = {}
99     (total_stats['kills'], total_stats['deaths'], total_stats['suicides'], total_stats['alivetime'],) = DBSession.query(
100             func.sum(PlayerGameStat.kills),
101             func.sum(PlayerGameStat.deaths),
102             func.sum(PlayerGameStat.suicides),
103             func.sum(PlayerGameStat.alivetime)).\
104             filter(PlayerGameStat.player_id == player_id).\
105             one()
106
107     (total_stats['alivetime_week'],) = DBSession.query(
108             func.sum(PlayerGameStat.alivetime)).\
109             filter(PlayerGameStat.player_id == player_id).\
110             filter(PlayerGameStat.create_dt > one_week_ago).\
111             one()
112
113     (total_stats['alivetime_month'],) = DBSession.query(
114             func.sum(PlayerGameStat.alivetime)).\
115             filter(PlayerGameStat.player_id == player_id).\
116             filter(PlayerGameStat.create_dt > one_month_ago).\
117             one()
118
119     (total_stats['wins'],) = DBSession.query(
120             func.count("*")).\
121             filter(Game.game_id == PlayerGameStat.game_id).\
122             filter(PlayerGameStat.player_id == player_id).\
123             filter(Game.winner == PlayerGameStat.team or PlayerGameStat.rank == 1).\
124             one()
125
126     for (key,value) in total_stats.items():
127         if value == None:
128             total_stats[key] = 0
129
130     return total_stats
131
132
133 def _get_fav_map(player_id):
134     """
135     Get the player's favorite map. The favorite map is defined
136     as the map that he or she has played the most in the past 
137     90 days.
138
139     Returns a dictionary with keys for the map's name and id.
140     """
141     # 90 day window
142     back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
143
144     raw_fav_map = DBSession.query(Map.name, Map.map_id).\
145             filter(Game.game_id == PlayerGameStat.game_id).\
146             filter(Game.map_id == Map.map_id).\
147             filter(PlayerGameStat.player_id == player_id).\
148             filter(PlayerGameStat.create_dt > back_then).\
149             group_by(Map.name, Map.map_id).\
150             order_by(func.count().desc()).\
151             all()
152
153     fav_map = []
154     for map_e in raw_fav_map:
155         entry = {}
156         entry['name'] = map_e[0]
157         entry['id']   = map_e[1]
158         fav_map.append(entry)
159
160     return fav_map
161
162
163 def _get_fav_weapon(player_id):
164     """
165     Get the player's favorite weapon. The favorite weapon is defined
166     as the weapon that he or she has employed the most in the past 
167     90 days.
168
169     Returns a sequence of dictionaries with keys for the weapon's name and id.
170     The sequence holds the most-used weapons in decreasing order.
171     """
172     # 90 day window
173     back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
174
175     raw_fav_weapon = DBSession.query(Weapon.descr, Weapon.weapon_cd).\
176             filter(Game.game_id == PlayerGameStat.game_id).\
177             filter(PlayerWeaponStat.weapon_cd == Weapon.weapon_cd).\
178             filter(PlayerGameStat.player_id == player_id).\
179             filter(PlayerGameStat.create_dt > back_then).\
180             group_by(Weapon.descr, Weapon.weapon_cd).\
181             order_by(func.count().desc()).\
182             all()
183
184     fav_weapon = []
185     for wpn_e in raw_fav_weapon:
186         entry = {}
187         entry['name'] = wpn_e[0]
188         entry['id']   = wpn_e[1]
189         fav_weapon.append(entry)
190
191     return fav_weapon
192
193
194 def _get_rank(player_id):
195     """
196     Get the player's rank as well as the total number of ranks.
197     """
198     rank = DBSession.query("game_type_cd", "rank", "max_rank").\
199             from_statement(
200                 "select pr.game_type_cd, pr.rank, overall.max_rank "
201                 "from player_ranks pr,  "
202                    "(select game_type_cd, max(rank) max_rank "
203                     "from player_ranks  "
204                     "group by game_type_cd) overall "
205                 "where pr.game_type_cd = overall.game_type_cd  "
206                 "and player_id = :player_id "
207                 "order by rank").\
208             params(player_id=player_id).all()
209
210     return rank;
211
212
213 def get_accuracy_stats(player_id, weapon_cd, games):
214     """
215     Provides accuracy for weapon_cd by player_id for the past N games.
216     """
217     # Reaching back 90 days should give us an accurate enough average
218     # We then multiply this out for the number of data points (games) to
219     # create parameters for a flot graph
220     try:
221         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),
222                 func.sum(PlayerWeaponStat.fired)).\
223                 filter(PlayerWeaponStat.player_id == player_id).\
224                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
225                 one()
226
227         avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)
228
229         # Determine the raw accuracy (hit, fired) numbers for $games games
230         # This is then enumerated to create parameters for a flot graph
231         raw_accs = DBSession.query(PlayerWeaponStat.game_id, 
232             PlayerWeaponStat.hit, PlayerWeaponStat.fired).\
233                 filter(PlayerWeaponStat.player_id == player_id).\
234                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
235                 order_by(PlayerWeaponStat.game_id.desc()).\
236                 limit(games).\
237                 all()
238
239         # they come out in opposite order, so flip them in the right direction
240         raw_accs.reverse()
241
242         accs = []
243         for i in range(len(raw_accs)):
244             accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))
245     except:
246         accs = []
247         avg = 0.0
248
249     return (avg, accs)
250
251
252 def get_damage_stats(player_id, weapon_cd, games):
253     """
254     Provides damage info for weapon_cd by player_id for the past N games.
255     """
256     try:
257         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.actual),
258                 func.sum(PlayerWeaponStat.hit)).\
259                 filter(PlayerWeaponStat.player_id == player_id).\
260                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
261                 one()
262
263         avg = round(float(raw_avg[0])/raw_avg[1], 2)
264
265         # Determine the damage efficiency (hit, fired) numbers for $games games
266         # This is then enumerated to create parameters for a flot graph
267         raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, 
268             PlayerWeaponStat.actual, PlayerWeaponStat.hit).\
269                 filter(PlayerWeaponStat.player_id == player_id).\
270                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
271                 order_by(PlayerWeaponStat.game_id.desc()).\
272                 limit(games).\
273                 all()
274
275         # they come out in opposite order, so flip them in the right direction
276         raw_dmgs.reverse()
277
278         dmgs = []
279         for i in range(len(raw_dmgs)):
280             # try to derive, unless we've hit nothing then set to 0!
281             try:
282                 dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)
283             except:
284                 dmg = 0.0
285
286             dmgs.append((raw_dmgs[i][0], dmg))
287     except Exception as e:
288         dmgs = []
289         avg = 0.0
290
291     return (avg, dmgs)
292
293
294 def player_info_data(request):
295     player_id = int(request.matchdict['id'])
296     if player_id <= 2:
297         player_id = -1;
298
299     try:
300         player = DBSession.query(Player).filter_by(player_id=player_id).\
301                 filter(Player.active_ind == True).one()
302
303         # games played, alivetime, wins, kills, deaths
304         total_stats = _get_total_stats(player.player_id)
305
306         # games breakdown - N games played (X ctf, Y dm) etc
307         (total_games, games_breakdown) = _get_games_played(player.player_id)
308
309         # favorite map from the past 90 days
310         try:
311             fav_map = _get_fav_map(player.player_id)
312         except:
313             fav_map = None
314
315         # favorite weapon from the past 90 days
316         try:
317             fav_weapon = _get_fav_weapon(player.player_id)
318         except:
319             fav_weapon = None
320
321         # friendly display of elo information and preliminary status
322         elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\
323                 filter(PlayerElo.game_type_cd.in_(['ctf','duel','dm'])).\
324                 order_by(PlayerElo.elo.desc()).all()
325
326         elos_display = []
327         for elo in elos:
328             if elo.games > 32:
329                 str = "{0} ({1})"
330             else:
331                 str = "{0}* ({1})"
332
333             elos_display.append(str.format(round(elo.elo, 3),
334                 elo.game_type_cd))
335
336         # get current rank information
337         ranks = _get_rank(player_id)
338         ranks_display = ', '.join(["{1} of {2} ({0})".format(gtc, rank,
339             max_rank) for gtc, rank, max_rank in ranks])
340
341
342         # which weapons have been used in the past 90 days
343         # and also, used in 5 games or more?
344         back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
345         recent_weapons = []
346         for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\
347                 filter(PlayerWeaponStat.player_id == player_id).\
348                 filter(PlayerWeaponStat.create_dt > back_then).\
349                 group_by(PlayerWeaponStat.weapon_cd).\
350                 having(func.count() > 4).\
351                 all():
352                     recent_weapons.append(weapon[0])
353
354         # recent games table, all data
355         recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\
356                 filter(PlayerGameStat.player_id == player_id).\
357                 filter(PlayerGameStat.game_id == Game.game_id).\
358                 filter(Game.server_id == Server.server_id).\
359                 filter(Game.map_id == Map.map_id).\
360                 order_by(Game.game_id.desc())[0:10]
361
362     except Exception as e:
363         player = None
364         elos_display = None
365         total_stats = None
366         recent_games = None
367         total_games = None
368         games_breakdown = None
369         recent_weapons = []
370         fav_map = None
371         fav_weapon = None
372         ranks_display = None;
373
374     return {'player':player,
375             'elos_display':elos_display,
376             'recent_games':recent_games,
377             'total_stats':total_stats,
378             'total_games':total_games,
379             'games_breakdown':games_breakdown,
380             'recent_weapons':recent_weapons,
381             'fav_map':fav_map,
382             'fav_weapon':fav_weapon,
383             'ranks_display':ranks_display,
384             }
385
386
387 def player_info(request):
388     """
389     Provides detailed information on a specific player
390     """
391     return player_info_data(request)
392
393
394 def player_info_json(request):
395     """
396     Provides detailed information on a specific player. JSON.
397     """
398     return [{'status':'not implemented'}]
399
400
401 def player_game_index_data(request):
402     player_id = request.matchdict['player_id']
403
404     if request.params.has_key('page'):
405         current_page = request.params['page']
406     else:
407         current_page = 1
408
409     try:
410         games_q = DBSession.query(Game, Server, Map).\
411             filter(PlayerGameStat.game_id == Game.game_id).\
412             filter(PlayerGameStat.player_id == player_id).\
413             filter(Game.server_id == Server.server_id).\
414             filter(Game.map_id == Map.map_id).\
415             order_by(Game.game_id.desc())
416
417         games = Page(games_q, current_page, items_per_page=10, url=page_url)
418
419         pgstats = {}
420         for (game, server, map) in games:
421             pgstats[game.game_id] = DBSession.query(PlayerGameStat).\
422                     filter(PlayerGameStat.game_id == game.game_id).\
423                     order_by(PlayerGameStat.rank).\
424                     order_by(PlayerGameStat.score).all()
425
426     except Exception as e:
427         player = None
428         games = None
429
430     return {'player_id':player_id,
431             'games':games,
432             'pgstats':pgstats}
433
434
435 def player_game_index(request):
436     """
437     Provides an index of the games in which a particular
438     player was involved. This is ordered by game_id, with
439     the most recent game_ids first. Paginated.
440     """
441     return player_game_index_data(request)
442
443
444 def player_game_index_json(request):
445     """
446     Provides an index of the games in which a particular
447     player was involved. This is ordered by game_id, with
448     the most recent game_ids first. Paginated. JSON.
449     """
450     return [{'status':'not implemented'}]
451
452
453 def player_accuracy_data(request):
454     player_id = request.matchdict['id']
455     allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']
456     weapon_cd = 'nex'
457     games = 20
458
459     if request.params.has_key('weapon'):
460         if request.params['weapon'] in allowed_weapons:
461             weapon_cd = request.params['weapon']
462
463     if request.params.has_key('games'):
464         try:
465             games = request.params['games']
466
467             if games < 0:
468                 games = 20
469             if games > 50:
470                 games = 50
471         except:
472             games = 20
473
474     (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)
475
476     # if we don't have enough data for the given weapon
477     if len(accs) < games:
478         games = len(accs)
479
480     return {
481             'player_id':player_id, 
482             'player_url':request.route_url('player_info', id=player_id), 
483             'weapon':weapon_cd, 
484             'games':games, 
485             'avg':avg, 
486             'accs':accs
487             }
488
489
490 def player_accuracy(request):
491     """
492     Provides the accuracy for the given weapon. (JSON only)
493     """
494     return player_accuracy_data(request)
495
496
497 def player_accuracy_json(request):
498     """
499     Provides a JSON response representing the accuracy for the given weapon.
500
501     Parameters:
502        weapon = which weapon to display accuracy for. Valid values are 'nex',
503                 'shotgun', 'uzi', and 'minstanex'.
504        games = over how many games to display accuracy. Can be up to 50.
505     """
506     return player_accuracy_data(request)
507
508
509 def player_damage_data(request):
510     player_id = request.matchdict['id']
511     allowed_weapons = ['grenadelauncher', 'electro', 'crylink', 'hagar',
512             'rocketlauncher', 'laser']
513     weapon_cd = 'rocketlauncher'
514     games = 20
515
516     if request.params.has_key('weapon'):
517         if request.params['weapon'] in allowed_weapons:
518             weapon_cd = request.params['weapon']
519
520     if request.params.has_key('games'):
521         try:
522             games = request.params['games']
523
524             if games < 0:
525                 games = 20
526             if games > 50:
527                 games = 50
528         except:
529             games = 20
530
531     (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)
532
533     # if we don't have enough data for the given weapon
534     if len(dmgs) < games:
535         games = len(dmgs)
536
537     return {
538             'player_id':player_id, 
539             'player_url':request.route_url('player_info', id=player_id), 
540             'weapon':weapon_cd, 
541             'games':games, 
542             'avg':avg, 
543             'dmgs':dmgs
544             }
545
546
547 def player_damage_json(request):
548     """
549     Provides a JSON response representing the damage for the given weapon.
550
551     Parameters:
552        weapon = which weapon to display damage for. Valid values are
553          'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',
554          'laser'.
555        games = over how many games to display damage. Can be up to 50.
556     """
557     return player_damage_data(request)