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