]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/elo.py
Proper fix for big alivetimes.
[xonotic/xonstat.git] / xonstat / elo.py
1 import logging
2 import math
3 import random
4 import sys
5 from xonstat.models import *
6
7
8 log = logging.getLogger(__name__)
9
10
11 class EloParms:
12     def __init__(self, global_K = 15, initial = 100, floor = 100, logdistancefactor = math.log(10)/float(400), maxlogdistance = math.log(10)):
13         self.global_K = global_K
14         self.initial = initial
15         self.floor = floor
16         self.logdistancefactor = logdistancefactor
17         self.maxlogdistance = maxlogdistance
18
19
20 class KReduction:
21     def __init__(self, fulltime, mintime, minratio, games_min, games_max, games_factor):
22         self.fulltime = fulltime
23         self.mintime = mintime
24         self.minratio = minratio
25         self.games_min = games_min
26         self.games_max = games_max
27         self.games_factor = games_factor
28
29     def eval(self, mygames, mytime, matchtime):
30         if mytime < self.mintime:
31             return 0
32         if mytime < self.minratio * matchtime:
33             return 0
34         if mytime < self.fulltime:
35             k = mytime / float(self.fulltime)
36         else:
37             k = 1.0
38         if mygames >= self.games_max:
39             k *= self.games_factor
40         elif mygames > self.games_min:
41             k *= 1.0 - (1.0 - self.games_factor) * (mygames - self.games_min) / float(self.games_max - self.games_min)
42         return k
43
44
45 def process_elos(game, session, game_type_cd=None):
46     if game_type_cd is None:
47         game_type_cd = game.game_type_cd
48
49     # we do not have the actual duration of the game, so use the 
50     # maximum alivetime of the players instead
51     duration = 0
52     for d in session.query(sfunc.max(PlayerGameStat.alivetime)).\
53                 filter(PlayerGameStat.game_id==game.game_id).\
54                 one():
55         duration = d.seconds
56
57     scores = {}
58     alivetimes = {}
59     for (p,s,a) in session.query(PlayerGameStat.player_id, 
60             PlayerGameStat.score, PlayerGameStat.alivetime).\
61             filter(PlayerGameStat.game_id==game.game_id).\
62             filter(PlayerGameStat.alivetime > timedelta(seconds=0)).\
63             filter(PlayerGameStat.player_id > 2).\
64             all():
65                 # scores are per second
66                 # with a short circuit to handle alivetimes > game
67                 # durations, which can happen due to warmup being
68                 # included (most often in duels)
69                 if game.duration is not None and a.seconds > game.duration.seconds:
70                     scores[p] = s/float(game.duration.seconds)
71                     alivetimes[p] = game.duration.seconds
72                 else:
73                     scores[p] = s/float(a.seconds)
74                     alivetimes[p] = a.seconds
75
76     player_ids = scores.keys()
77
78     elos = {}
79     for e in session.query(PlayerElo).\
80             filter(PlayerElo.player_id.in_(player_ids)).\
81             filter(PlayerElo.game_type_cd==game_type_cd).all():
82                 elos[e.player_id] = e
83
84     # ensure that all player_ids have an elo record
85     for pid in player_ids:
86         if pid not in elos.keys():
87             elos[pid] = PlayerElo(pid, game_type_cd, ELOPARMS.initial)
88
89     for pid in player_ids:
90         elos[pid].k = KREDUCTION.eval(elos[pid].games, alivetimes[pid],
91                 duration)
92         if elos[pid].k == 0:
93             del(elos[pid])
94             del(scores[pid])
95             del(alivetimes[pid])
96
97     elos = update_elos(game, session, elos, scores, ELOPARMS)
98
99     # add the elos to the session for committing
100     for e in elos:
101         session.add(elos[e])
102
103
104 def update_elos(game, session, elos, scores, ep):
105     if len(elos) < 2:
106         return elos
107
108     pids = elos.keys()
109
110     eloadjust = {}
111     for pid in pids:
112         eloadjust[pid] = 0.0
113
114     for i in xrange(0, len(pids)):
115         ei = elos[pids[i]]
116         for j in xrange(i+1, len(pids)):
117             ej = elos[pids[j]]
118             si = scores[ei.player_id]
119             sj = scores[ej.player_id]
120
121             # normalize scores
122             ofs = min(0, si, sj)
123             si -= ofs
124             sj -= ofs
125             if si + sj == 0:
126                 si, sj = 1, 1 # a draw
127
128             # real score factor
129             scorefactor_real = si / float(si + sj)
130
131             # duels are done traditionally - a win nets
132             # full points, not the score factor
133             if game.game_type_cd == 'duel':
134                 # player i won
135                 if scorefactor_real > 0.5:
136                     scorefactor_real = 1.0
137                 # player j won
138                 elif scorefactor_real < 0.5:
139                     scorefactor_real = 0.0
140                 # nothing to do here for draws
141
142             # expected score factor by elo
143             elodiff = min(ep.maxlogdistance, max(-ep.maxlogdistance,
144                 (float(ei.elo) - float(ej.elo)) * ep.logdistancefactor))
145             scorefactor_elo = 1 / (1 + math.exp(-elodiff))
146
147             # initial adjustment values, which we may modify with additional rules
148             adjustmenti = scorefactor_real - scorefactor_elo
149             adjustmentj = scorefactor_elo - scorefactor_real
150
151             # log.debug("Player i: {0}".format(ei.player_id))
152             # log.debug("Player i's K: {0}".format(ei.k))
153             # log.debug("Player j: {0}".format(ej.player_id))
154             # log.debug("Player j's K: {0}".format(ej.k))
155             # log.debug("Scorefactor real: {0}".format(scorefactor_real))
156             # log.debug("Scorefactor elo: {0}".format(scorefactor_elo))
157             # log.debug("adjustment i: {0}".format(adjustmenti))
158             # log.debug("adjustment j: {0}".format(adjustmentj))
159
160             if scorefactor_elo > 0.5:
161             # player i is expected to win
162                 if scorefactor_real > 0.5:
163                 # he DID win, so he should never lose points.
164                     adjustmenti = max(0, adjustmenti)
165                 else:
166                 # he lost, but let's make it continuous (making him lose less points in the result)
167                     adjustmenti = (2 * scorefactor_real - 1) * scorefactor_elo
168             else:
169             # player j is expected to win
170                 if scorefactor_real > 0.5:
171                 # he lost, but let's make it continuous (making him lose less points in the result)
172                     adjustmentj = (1 - 2 * scorefactor_real) * (1 - scorefactor_elo)
173                 else:
174                 # he DID win, so he should never lose points.
175                     adjustmentj = max(0, adjustmentj)
176
177             eloadjust[ei.player_id] += adjustmenti
178             eloadjust[ej.player_id] += adjustmentj
179
180     elo_deltas = {}
181     for pid in pids:
182         old_elo = float(elos[pid].elo)
183         new_elo = max(float(elos[pid].elo) + eloadjust[pid] * elos[pid].k * ep.global_K / float(len(elos) - 1), ep.floor)
184         elo_deltas[pid] = new_elo - old_elo
185
186         elos[pid].elo = new_elo
187         elos[pid].games += 1
188
189         log.debug("Setting Player {0}'s Elo delta to {1}. Elo is now {2} (was {3}).".format(pid, elo_deltas[pid], new_elo, old_elo))
190
191     save_elo_deltas(game, session, elo_deltas)
192
193     return elos
194
195
196 def save_elo_deltas(game, session, elo_deltas):
197     """
198     Saves the amount by which each player's Elo goes up or down
199     in a given game in the PlayerGameStat row, allowing for scoreboard display.
200
201     elo_deltas is a dictionary such that elo_deltas[player_id] is the elo_delta
202     for that player_id.
203     """
204     pgstats = {}
205     for pgstat in session.query(PlayerGameStat).\
206             filter(PlayerGameStat.game_id == game.game_id).\
207             all():
208                 pgstats[pgstat.player_id] = pgstat
209
210     for pid in elo_deltas.keys():
211         try:
212             pgstats[pid].elo_delta = elo_deltas[pid]
213             session.add(pgstats[pid])
214         except:
215             log.debug("Unable to save Elo delta value for player_id {0}".format(pid))
216
217
218 # parameters for K reduction
219 # this may be touched even if the DB already exists
220 KREDUCTION = KReduction(600, 120, 0.5, 0, 32, 0.2)
221
222 # parameters for chess elo
223 # only global_K may be touched even if the DB already exists
224 # we start at K=200, and fall to K=40 over the first 20 games
225 ELOPARMS = EloParms(global_K = 200)