]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/player.py
Finalized "Game Breakdown" section (fixed implementation; added more data; improved...
[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     DEPRECATED: Now included in _get_total_stats()
58     
59     Provides a breakdown by gametype of the games played by player_id.
60
61     Returns a tuple containing (total_games, games_breakdown), where
62     total_games is the absolute number of games played by player_id
63     and games_breakdown is an array containing (game_type_cd, # games)
64     """
65     games_played = DBSession.query(Game.game_type_cd, func.count()).\
66             filter(Game.game_id == PlayerGameStat.game_id).\
67             filter(PlayerGameStat.player_id == player_id).\
68             group_by(Game.game_type_cd).\
69             order_by(func.count().desc()).all()
70
71     total = 0
72     for (game_type_cd, games) in games_played:
73         total += games
74
75     return (total, games_played)
76
77
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     games = how many games a player has played
85     games_breakdown = how many games of given type a player has played (dictionary)
86     games_alivetime = how many time a player has spent in a give game type (dictionary)
87     kills = how many kills a player has over all games
88     deaths = how many deaths a player has over all games
89     suicides = how many suicides a player has over all games
90     alivetime = how long a player has played over all games
91     alivetime_week = how long a player has played over all games in the last week
92     alivetime_month = how long a player has played over all games in the last month
93     wins = how many games a player has won
94
95     If any of the above are None, they are set to 0.
96     """
97     # 7 and 30 day windows
98     one_week_ago  = datetime.datetime.utcnow() - datetime.timedelta(days=7)
99     one_month_ago = datetime.datetime.utcnow() - datetime.timedelta(days=30)
100
101     total_stats = {}
102
103     games_played = DBSession.query(
104             Game.game_type_cd, func.count(), func.sum(PlayerGameStat.alivetime)).\
105             filter(Game.game_id == PlayerGameStat.game_id).\
106             filter(PlayerGameStat.player_id == player_id).\
107             group_by(Game.game_type_cd).\
108             order_by(func.count().desc()).\
109             all()
110
111     total_stats['games'] = 0
112     total_stats['games_breakdown'] = {}  # this is a dictionary inside a dictionary .. dictception?
113     total_stats['games_alivetime'] = {}
114     for (game_type_cd, games, alivetime) in games_played:
115         total_stats['games'] += games
116         total_stats['games_breakdown'][game_type_cd] = games
117         total_stats['games_alivetime'][game_type_cd] = alivetime
118
119      # more fields can be added here, e.g. 'collects' for kh games
120     (total_stats['kills'], total_stats['deaths'], total_stats['suicides'],
121      total_stats['alivetime'],) = DBSession.query(
122             func.sum(PlayerGameStat.kills),
123             func.sum(PlayerGameStat.deaths),
124             func.sum(PlayerGameStat.suicides),
125             func.sum(PlayerGameStat.alivetime)).\
126             filter(PlayerGameStat.player_id == player_id).\
127             one()
128
129     (total_stats['alivetime_week'],) = DBSession.query(
130             func.sum(PlayerGameStat.alivetime)).\
131             filter(PlayerGameStat.player_id == player_id).\
132             filter(PlayerGameStat.create_dt > one_week_ago).\
133             one()
134
135     (total_stats['alivetime_month'],) = DBSession.query(
136             func.sum(PlayerGameStat.alivetime)).\
137             filter(PlayerGameStat.player_id == player_id).\
138             filter(PlayerGameStat.create_dt > one_month_ago).\
139             one()
140
141     (total_stats['wins'],) = DBSession.query(
142             func.count("*")).\
143             filter(Game.game_id == PlayerGameStat.game_id).\
144             filter(PlayerGameStat.player_id == player_id).\
145             filter(Game.winner == PlayerGameStat.team or PlayerGameStat.rank == 1).\
146             one()
147
148     (total_stats['duel_wins'],) = DBSession.query(
149             func.count("*")).\
150             filter(Game.game_id == PlayerGameStat.game_id).\
151             filter(Game.game_type_cd == "duel").\
152             filter(PlayerGameStat.player_id == player_id).\
153             filter(PlayerGameStat.rank == 1).\
154             one()
155
156     (total_stats['duel_kills'], total_stats['duel_deaths'], total_stats['duel_suicides'],) = DBSession.query(
157             func.sum(PlayerGameStat.kills),
158             func.sum(PlayerGameStat.deaths),
159             func.sum(PlayerGameStat.suicides)).\
160             filter(Game.game_id == PlayerGameStat.game_id).\
161             filter(Game.game_type_cd == "duel").\
162             filter(PlayerGameStat.player_id == player_id).\
163             one()
164
165     (total_stats['dm_wins'],) = DBSession.query(
166             func.count("*")).\
167             filter(Game.game_id == PlayerGameStat.game_id).\
168             filter(Game.game_type_cd == "dm").\
169             filter(PlayerGameStat.player_id == player_id).\
170             filter(PlayerGameStat.rank == 1).\
171             one()
172
173     (total_stats['dm_kills'], total_stats['dm_deaths'], total_stats['dm_suicides'],) = DBSession.query(
174             func.sum(PlayerGameStat.kills),
175             func.sum(PlayerGameStat.deaths),
176             func.sum(PlayerGameStat.suicides)).\
177             filter(Game.game_id == PlayerGameStat.game_id).\
178             filter(Game.game_type_cd == "dm").\
179             filter(PlayerGameStat.player_id == player_id).\
180             one()
181
182     (total_stats['tdm_kills'], total_stats['tdm_deaths'], total_stats['tdm_suicides'],) = DBSession.query(
183             func.sum(PlayerGameStat.kills),
184             func.sum(PlayerGameStat.deaths),
185             func.sum(PlayerGameStat.suicides)).\
186             filter(Game.game_id == PlayerGameStat.game_id).\
187             filter(Game.game_type_cd == "tdm").\
188             filter(PlayerGameStat.player_id == player_id).\
189             one()
190
191     (total_stats['tdm_wins'],) = DBSession.query(
192             func.count("*")).\
193             filter(Game.game_id == PlayerGameStat.game_id).\
194             filter(Game.game_type_cd == "tdm").\
195             filter(PlayerGameStat.player_id == player_id).\
196             filter(PlayerGameStat.rank == 1).\
197             one()
198
199     (total_stats['ctf_wins'],) = DBSession.query(
200             func.count("*")).\
201             filter(Game.game_id == PlayerGameStat.game_id).\
202             filter(Game.game_type_cd == "ctf").\
203             filter(PlayerGameStat.player_id == player_id).\
204             filter(PlayerGameStat.rank == 1).\
205             one()
206
207     (total_stats['ctf_caps'], total_stats['ctf_pickups'], total_stats['ctf_drops'],
208      total_stats['ctf_returns'], total_stats['ctf_fckills'],) = DBSession.query(
209             func.sum(PlayerGameStat.captures),
210             func.sum(PlayerGameStat.pickups),
211             func.sum(PlayerGameStat.drops),
212             func.sum(PlayerGameStat.returns),
213             func.sum(PlayerGameStat.carrier_frags)).\
214             filter(Game.game_id == PlayerGameStat.game_id).\
215             filter(Game.game_type_cd == "ctf").\
216             filter(PlayerGameStat.player_id == player_id).\
217             one()
218
219     for (key,value) in total_stats.items():
220         if value == None:
221             total_stats[key] = 0
222
223     return total_stats
224
225
226 def _get_fav_map(player_id):
227     """
228     Get the player's favorite map. The favorite map is defined
229     as the map that he or she has played the most in the past 
230     90 days.
231
232     Returns a dictionary with keys for the map's name and id.
233     """
234     # 90 day window
235     back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
236
237     raw_fav_map = DBSession.query(Map.name, Map.map_id).\
238             filter(Game.game_id == PlayerGameStat.game_id).\
239             filter(Game.map_id == Map.map_id).\
240             filter(PlayerGameStat.player_id == player_id).\
241             filter(PlayerGameStat.create_dt > back_then).\
242             group_by(Map.name, Map.map_id).\
243             order_by(func.count().desc()).\
244             limit(5).all()
245
246     fav_map = []
247     for map_e in raw_fav_map:
248         entry = {}
249         entry['name'] = map_e[0]
250         entry['id']   = map_e[1]
251         fav_map.append(entry)
252
253     return fav_map
254
255
256 def _get_fav_weapon(player_id):
257     """
258     Get the player's favorite weapon. The favorite weapon is defined
259     as the weapon that he or she has employed the most in the past 
260     90 days.
261
262     Returns a sequence of dictionaries with keys for the weapon's name and id.
263     The sequence holds the most-used weapons in decreasing order.
264     """
265     # 90 day window
266     back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
267
268     raw_fav_weapon = DBSession.query(Weapon.descr, Weapon.weapon_cd).\
269             filter(Game.game_id == PlayerWeaponStat.game_id).\
270             filter(PlayerWeaponStat.player_id == player_id).\
271             filter(PlayerWeaponStat.weapon_cd == Weapon.weapon_cd).\
272             filter(PlayerWeaponStat.create_dt > back_then).\
273             group_by(Weapon.descr, Weapon.weapon_cd).\
274             order_by(func.count().desc()).\
275             limit(5).all()
276
277     fav_weapon = []
278     for wpn_e in raw_fav_weapon:
279         entry = {}
280         entry['name'] = wpn_e[0]
281         entry['id']   = wpn_e[1]
282         fav_weapon.append(entry)
283
284     return fav_weapon
285
286
287 def _get_fav_server(player_id):
288     """
289     Get the player's favorite server. The favorite server is defined
290     as the server that he or she has played on the most in the past 
291     90 days.
292
293     Returns a sequence of dictionaries with keys for the server's name and id.
294     The sequence holds the most-used servers in decreasing order.
295     """
296     # 90 day window
297     back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
298
299     raw_fav_server = DBSession.query(Server.name, Server.server_id).\
300             filter(Game.game_id == PlayerGameStat.game_id).\
301             filter(Game.server_id == Server.server_id).\
302             filter(PlayerGameStat.player_id == player_id).\
303             filter(PlayerGameStat.create_dt > back_then).\
304             group_by(Server.name, Server.server_id).\
305             order_by(func.count().desc()).\
306             limit(5).all()
307
308     fav_server = []
309     for srv_e in raw_fav_server:
310         entry = {}
311         entry['name'] = srv_e[0]
312         entry['id']   = srv_e[1]
313         fav_server.append(entry)
314
315     return fav_server
316
317
318 def _get_rank(player_id):
319     """
320     Get the player's rank as well as the total number of ranks.
321     """
322     rank = DBSession.query("game_type_cd", "rank", "max_rank").\
323             from_statement(
324                 "select pr.game_type_cd, pr.rank, overall.max_rank "
325                 "from player_ranks pr,  "
326                    "(select game_type_cd, max(rank) max_rank "
327                     "from player_ranks  "
328                     "group by game_type_cd) overall "
329                 "where pr.game_type_cd = overall.game_type_cd  "
330                 "and player_id = :player_id "
331                 "order by rank").\
332             params(player_id=player_id).all()
333
334     return rank;
335
336
337 def get_accuracy_stats(player_id, weapon_cd, games):
338     """
339     Provides accuracy for weapon_cd by player_id for the past N games.
340     """
341     # Reaching back 90 days should give us an accurate enough average
342     # We then multiply this out for the number of data points (games) to
343     # create parameters for a flot graph
344     try:
345         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),
346                 func.sum(PlayerWeaponStat.fired)).\
347                 filter(PlayerWeaponStat.player_id == player_id).\
348                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
349                 one()
350
351         avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)
352
353         # Determine the raw accuracy (hit, fired) numbers for $games games
354         # This is then enumerated to create parameters for a flot graph
355         raw_accs = DBSession.query(PlayerWeaponStat.game_id, 
356             PlayerWeaponStat.hit, PlayerWeaponStat.fired).\
357                 filter(PlayerWeaponStat.player_id == player_id).\
358                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
359                 order_by(PlayerWeaponStat.game_id.desc()).\
360                 limit(games).\
361                 all()
362
363         # they come out in opposite order, so flip them in the right direction
364         raw_accs.reverse()
365
366         accs = []
367         for i in range(len(raw_accs)):
368             accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))
369     except:
370         accs = []
371         avg = 0.0
372
373     return (avg, accs)
374
375
376 def get_damage_stats(player_id, weapon_cd, games):
377     """
378     Provides damage info for weapon_cd by player_id for the past N games.
379     """
380     try:
381         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.actual),
382                 func.sum(PlayerWeaponStat.hit)).\
383                 filter(PlayerWeaponStat.player_id == player_id).\
384                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
385                 one()
386
387         avg = round(float(raw_avg[0])/raw_avg[1], 2)
388
389         # Determine the damage efficiency (hit, fired) numbers for $games games
390         # This is then enumerated to create parameters for a flot graph
391         raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, 
392             PlayerWeaponStat.actual, PlayerWeaponStat.hit).\
393                 filter(PlayerWeaponStat.player_id == player_id).\
394                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
395                 order_by(PlayerWeaponStat.game_id.desc()).\
396                 limit(games).\
397                 all()
398
399         # they come out in opposite order, so flip them in the right direction
400         raw_dmgs.reverse()
401
402         dmgs = []
403         for i in range(len(raw_dmgs)):
404             # try to derive, unless we've hit nothing then set to 0!
405             try:
406                 dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)
407             except:
408                 dmg = 0.0
409
410             dmgs.append((raw_dmgs[i][0], dmg))
411     except Exception as e:
412         dmgs = []
413         avg = 0.0
414
415     return (avg, dmgs)
416
417
418 def player_info_data(request):
419     player_id = int(request.matchdict['id'])
420     if player_id <= 2:
421         player_id = -1;
422
423     try:
424         player = DBSession.query(Player).filter_by(player_id=player_id).\
425                 filter(Player.active_ind == True).one()
426
427         # games played, alivetime, wins, kills, deaths
428         total_stats = _get_total_stats(player.player_id)
429
430         # games breakdown - N games played (X ctf, Y dm) etc
431         # DEPRECATED: included in total_stats, see above
432         # (total_games, games_breakdown) = _get_games_played(player.player_id)
433
434         # favorite map from the past 90 days
435         try:
436             fav_map = _get_fav_map(player.player_id)
437         except:
438             fav_map = None
439
440         # favorite weapon from the past 90 days
441         try:
442             fav_weapon = _get_fav_weapon(player.player_id)
443         except:
444             fav_weapon = None
445
446         # favorite server from the past 90 days
447         try:
448             fav_server = _get_fav_server(player.player_id)
449         except:
450             fav_server = None
451
452         # friendly display of elo information and preliminary status
453         elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\
454                 filter(PlayerElo.game_type_cd.in_(['ctf','duel','dm'])).\
455                 order_by(PlayerElo.elo.desc()).all()
456
457         elos_display = []
458         elos_dict    = {}
459         for elo in elos:
460             if elo.games > 32:
461                 str = "{0} ({1})"
462             else:
463                 str = "{0}* ({1})"
464
465             elos_display.append(str.format(round(elo.elo, 3),
466                 elo.game_type_cd))
467             elos_dict[elo.game_type_cd] = round(elo.elo, 3)
468         elos_display = ', '.join(elos_display)
469
470         # get current rank information
471         ranks = _get_rank(player_id)
472         
473         ranks_display = []
474         ranks_dict    = {}
475         for gtc,rank,max_rank in ranks:
476             ranks_display.append("{1} of {2} ({0})".format(gtc, rank, max_rank))
477             ranks_dict[gtc] = (rank, max_rank)
478         ranks_display = ', '.join(ranks_display)
479
480
481         # which weapons have been used in the past 90 days
482         # and also, used in 5 games or more?
483         back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
484         recent_weapons = []
485         for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\
486                 filter(PlayerWeaponStat.player_id == player_id).\
487                 filter(PlayerWeaponStat.create_dt > back_then).\
488                 group_by(PlayerWeaponStat.weapon_cd).\
489                 having(func.count() > 4).\
490                 all():
491                     recent_weapons.append(weapon[0])
492
493         # recent games table, all data
494         recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\
495                 filter(PlayerGameStat.player_id == player_id).\
496                 filter(PlayerGameStat.game_id == Game.game_id).\
497                 filter(Game.server_id == Server.server_id).\
498                 filter(Game.map_id == Map.map_id).\
499                 order_by(Game.game_id.desc())[0:10]
500
501     except Exception as e:
502         player = None
503         elos = None
504         elos_display = None
505         total_stats = None
506         recent_games = None
507         # DEPRECATED: included in total_stats, see above
508         #total_games = None
509         #games_breakdown = None
510         recent_weapons = []
511         fav_map = None
512         fav_weapon = None
513         fav_server = None
514         ranks = None
515         ranks_display = None;
516
517     return {'player':player,
518             'elos':elos_dict,
519             'elos_display':elos_display,
520             'recent_games':recent_games,
521             'total_stats':total_stats,
522             # DEPRECATED: included in total_stats, see above
523             #'total_games':total_games,
524             #'games_breakdown':games_breakdown,
525             'recent_weapons':recent_weapons,
526             'fav_map':fav_map,
527             'fav_weapon':fav_weapon,
528             'fav_server':fav_server,
529             'ranks':ranks_dict,
530             'ranks_display':ranks_display,
531             }
532
533
534 def player_info(request):
535     """
536     Provides detailed information on a specific player
537     """
538     return player_info_data(request)
539
540
541 def player_info_json(request):
542     """
543     Provides detailed information on a specific player. JSON.
544     """
545     return [{'status':'not implemented'}]
546
547
548 def player_game_index_data(request):
549     player_id = request.matchdict['player_id']
550
551     if request.params.has_key('page'):
552         current_page = request.params['page']
553     else:
554         current_page = 1
555
556     try:
557         games_q = DBSession.query(Game, Server, Map).\
558             filter(PlayerGameStat.game_id == Game.game_id).\
559             filter(PlayerGameStat.player_id == player_id).\
560             filter(Game.server_id == Server.server_id).\
561             filter(Game.map_id == Map.map_id).\
562             order_by(Game.game_id.desc())
563
564         games = Page(games_q, current_page, items_per_page=10, url=page_url)
565
566         pgstats = {}
567         for (game, server, map) in games:
568             pgstats[game.game_id] = DBSession.query(PlayerGameStat).\
569                     filter(PlayerGameStat.game_id == game.game_id).\
570                     order_by(PlayerGameStat.rank).\
571                     order_by(PlayerGameStat.score).all()
572
573     except Exception as e:
574         player = None
575         games = None
576
577     return {'player_id':player_id,
578             'games':games,
579             'pgstats':pgstats}
580
581
582 def player_game_index(request):
583     """
584     Provides an index of the games in which a particular
585     player was involved. This is ordered by game_id, with
586     the most recent game_ids first. Paginated.
587     """
588     return player_game_index_data(request)
589
590
591 def player_game_index_json(request):
592     """
593     Provides an index of the games in which a particular
594     player was involved. This is ordered by game_id, with
595     the most recent game_ids first. Paginated. JSON.
596     """
597     return [{'status':'not implemented'}]
598
599
600 def player_accuracy_data(request):
601     player_id = request.matchdict['id']
602     allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']
603     weapon_cd = 'nex'
604     games = 20
605
606     if request.params.has_key('weapon'):
607         if request.params['weapon'] in allowed_weapons:
608             weapon_cd = request.params['weapon']
609
610     if request.params.has_key('games'):
611         try:
612             games = request.params['games']
613
614             if games < 0:
615                 games = 20
616             if games > 50:
617                 games = 50
618         except:
619             games = 20
620
621     (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)
622
623     # if we don't have enough data for the given weapon
624     if len(accs) < games:
625         games = len(accs)
626
627     return {
628             'player_id':player_id, 
629             'player_url':request.route_url('player_info', id=player_id), 
630             'weapon':weapon_cd, 
631             'games':games, 
632             'avg':avg, 
633             'accs':accs
634             }
635
636
637 def player_accuracy(request):
638     """
639     Provides the accuracy for the given weapon. (JSON only)
640     """
641     return player_accuracy_data(request)
642
643
644 def player_accuracy_json(request):
645     """
646     Provides a JSON response representing the accuracy for the given weapon.
647
648     Parameters:
649        weapon = which weapon to display accuracy for. Valid values are 'nex',
650                 'shotgun', 'uzi', and 'minstanex'.
651        games = over how many games to display accuracy. Can be up to 50.
652     """
653     return player_accuracy_data(request)
654
655
656 def player_damage_data(request):
657     player_id = request.matchdict['id']
658     allowed_weapons = ['grenadelauncher', 'electro', 'crylink', 'hagar',
659             'rocketlauncher', 'laser']
660     weapon_cd = 'rocketlauncher'
661     games = 20
662
663     if request.params.has_key('weapon'):
664         if request.params['weapon'] in allowed_weapons:
665             weapon_cd = request.params['weapon']
666
667     if request.params.has_key('games'):
668         try:
669             games = request.params['games']
670
671             if games < 0:
672                 games = 20
673             if games > 50:
674                 games = 50
675         except:
676             games = 20
677
678     (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)
679
680     # if we don't have enough data for the given weapon
681     if len(dmgs) < games:
682         games = len(dmgs)
683
684     return {
685             'player_id':player_id, 
686             'player_url':request.route_url('player_info', id=player_id), 
687             'weapon':weapon_cd, 
688             'games':games, 
689             'avg':avg, 
690             'dmgs':dmgs
691             }
692
693
694 def player_damage_json(request):
695     """
696     Provides a JSON response representing the damage for the given weapon.
697
698     Parameters:
699        weapon = which weapon to display damage for. Valid values are
700          'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',
701          'laser'.
702        games = over how many games to display damage. Can be up to 50.
703     """
704     return player_damage_data(request)