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