5 from xonstat.models import PlayerElo
7 log = logging.getLogger(__name__)
11 def __init__(self, global_K = 15, initial = 100, floor = 100, logdistancefactor = math.log(10)/float(400), maxlogdistance = math.log(10)):
12 self.global_K = global_K
13 self.initial = initial
15 self.logdistancefactor = logdistancefactor
16 self.maxlogdistance = maxlogdistance
20 def __init__(self, fulltime, mintime, minratio, games_min, games_max, games_factor):
21 self.fulltime = fulltime
22 self.mintime = mintime
23 self.minratio = minratio
24 self.games_min = games_min
25 self.games_max = games_max
26 self.games_factor = games_factor
28 def eval(self, mygames, mytime, matchtime):
29 if mytime < self.mintime:
31 if mytime < self.minratio * matchtime:
33 if mytime < self.fulltime:
34 k = mytime / float(self.fulltime)
37 if mygames >= self.games_max:
38 k *= self.games_factor
39 elif mygames > self.games_min:
40 k *= 1.0 - (1.0 - self.games_factor) * (mygames - self.games_min) / float(self.games_max - self.games_min)
45 """EloWIP is a work-in-progress Elo value. It contains all of the
46 attributes necessary to calculate Elo deltas for a given game."""
47 def __init__(self, player_id, pgstat=None):
48 # player_id this belongs to
49 self.player_id = player_id
51 # score per second in the game
52 self.score_per_second = 0.0
54 # seconds alive during a given game
60 # current player_game_stat record
63 # Elo algorithm K-factor
66 # Elo points accumulator, which is not adjusted by the K-factor
69 # elo points delta accumulator for the game, which IS adjusted
73 def should_save(self):
74 """Determines if the elo and pgstat attributes of this instance should
75 be persisted to the database"""
79 return "<EloWIP(player_id={}, score_per_second={}, alivetime={}, \
80 elo={}, pgstat={}, k={}, adjustment={}, elo_delta={})>".\
81 format(self.player_id, self.score_per_second, self.alivetime, \
82 self.elo, self.pgstat, self.k, self.adjustment, self.elo_delta)
86 """EloProcessor is a container for holding all of the intermediary AND
87 final values used to calculate Elo deltas for all players in a given
89 def __init__(self, session, game, pgstats):
91 # game which we are processing
94 # work-in-progress values, indexed by player
97 # used to determine if a pgstat record is elo-eligible
98 def elo_eligible(pgs):
99 return pgs.player_id > 2 and pgs.alivetime > timedelta(seconds=0)
101 # only process elos for elo-eligible players
102 for pgstat in filter(elo_eligible, pgstats):
103 self.wip[pgstat.player_id] = EloWIP(pgstat.player_id, pgstat)
105 # determine duration from the maximum alivetime
106 # of the players if the game doesn't have one
108 if game.duration is not None:
109 self.duration = game.duration.seconds
111 self.duration = max(i.alivetime.seconds for i in elostats)
113 # Calculate the score_per_second and alivetime values for each player.
114 # Warmups may mess up the player alivetime values, so this is a
115 # failsafe to put the alivetime ceiling to be the game's duration.
116 for e in self.wip.values():
117 if e.pgstat.alivetime.seconds > self.duration:
118 e.score_per_second = e.pgstat.score/float(self.duration)
119 e.alivetime = self.duration
121 e.score_per_second = e.pgstat.score/float(e.pgstat.alivetime.seconds)
122 e.alivetime = e.pgstat.alivetime.seconds
124 # Fetch current Elo values for all players. For players that don't yet
125 # have an Elo record, we'll give them a default one.
126 for e in session.query(PlayerElo).\
127 filter(PlayerElo.player_id.in_(self.wip.keys())).\
128 filter(PlayerElo.game_type_cd==game.game_type_cd).all():
129 self.wip[e.player_id].elo = e
131 for pid in self.wip.keys():
132 if self.wip[pid].elo is None:
133 self.wip[pid].elo = PlayerElo(pid, game.game_type_cd, ELOPARMS.initial)
135 # determine k reduction
136 self.wip[pid].k = KREDUCTION.eval(self.wip[pid].elo.games,
137 self.wip[pid].alivetime, self.duration)
139 # we don't process the players who have a zero K factor
140 self.wip = { e.player_id:e for e in self.wip.values() if e.k > 0.0}
142 # now actually process elos
146 # for w in self.wip.values():
147 # log.debug(w.player_id)
150 def scorefactor(self, si, sj):
151 """Calculate the real scorefactor of the game. This is how players
152 actually performed, which is compared to their expected performance as
153 predicted by their Elo values."""
154 scorefactor_real = si / float(si + sj)
156 # duels are done traditionally - a win nets
157 # full points, not the score factor
158 if self.game.game_type_cd == 'duel':
160 if scorefactor_real > 0.5:
161 scorefactor_real = 1.0
163 elif scorefactor_real < 0.5:
164 scorefactor_real = 0.0
165 # nothing to do here for draws
167 return scorefactor_real
170 """Perform the core Elo calculation, storing the values in the "wip"
171 dict for passing upstream."""
172 if len(self.wip.keys()) < 2:
177 pids = self.wip.keys()
178 for i in xrange(0, len(pids)):
179 ei = self.wip[pids[i]].elo
180 for j in xrange(i+1, len(pids)):
181 ej = self.wip[pids[j]].elo
182 si = self.wip[pids[i]].score_per_second
183 sj = self.wip[pids[j]].score_per_second
190 si, sj = 1, 1 # a draw
193 scorefactor_real = self.scorefactor(si, sj)
195 # expected score factor by elo
196 elodiff = min(ep.maxlogdistance, max(-ep.maxlogdistance,
197 (float(ei.elo) - float(ej.elo)) * ep.logdistancefactor))
198 scorefactor_elo = 1 / (1 + math.exp(-elodiff))
200 # initial adjustment values, which we may modify with additional rules
201 adjustmenti = scorefactor_real - scorefactor_elo
202 adjustmentj = scorefactor_elo - scorefactor_real
205 # log.debug("(New) Player i: {0}".format(ei.player_id))
206 # log.debug("(New) Player i's K: {0}".format(self.wip[pids[i]].k))
207 # log.debug("(New) Player j: {0}".format(ej.player_id))
208 # log.debug("(New) Player j's K: {0}".format(self.wip[pids[j]].k))
209 # log.debug("(New) Scorefactor real: {0}".format(scorefactor_real))
210 # log.debug("(New) Scorefactor elo: {0}".format(scorefactor_elo))
211 # log.debug("(New) adjustment i: {0}".format(adjustmenti))
212 # log.debug("(New) adjustment j: {0}".format(adjustmentj))
214 if scorefactor_elo > 0.5:
215 # player i is expected to win
216 if scorefactor_real > 0.5:
217 # he DID win, so he should never lose points.
218 adjustmenti = max(0, adjustmenti)
220 # he lost, but let's make it continuous (making him lose less points in the result)
221 adjustmenti = (2 * scorefactor_real - 1) * scorefactor_elo
223 # player j is expected to win
224 if scorefactor_real > 0.5:
225 # he lost, but let's make it continuous (making him lose less points in the result)
226 adjustmentj = (1 - 2 * scorefactor_real) * (1 - scorefactor_elo)
228 # he DID win, so he should never lose points.
229 adjustmentj = max(0, adjustmentj)
231 self.wip[pids[i]].adjustment += adjustmenti
232 self.wip[pids[j]].adjustment += adjustmentj
236 old_elo = float(w.elo.elo)
237 new_elo = max(float(w.elo.elo) + w.adjustment * w.k * ep.global_K / float(len(pids) - 1), ep.floor)
238 w.elo_delta = new_elo - old_elo
242 w.elo.update_dt = datetime.datetime.utcnow()
244 # log.debug("Setting Player {0}'s Elo delta to {1}. Elo is now {2}\
245 # (was {3}).".format(pid, w.elo_delta, new_elo, old_elo))
247 def save(self, session):
248 """Put all changed PlayerElo and PlayerGameStat instances into the
249 session to be updated or inserted upon commit."""
250 # first, save all of the player_elo values
251 for w in self.wip.values():
255 w.pgstat.elo_delta = w.elo_delta
256 session.add(w.pgstat)
258 log.debug("Unable to save Elo delta value for player_id {0}".format(w.player_id))
261 # parameters for K reduction
262 # this may be touched even if the DB already exists
263 KREDUCTION = KReduction(600, 120, 0.5, 0, 32, 0.2)
265 # parameters for chess elo
266 # only global_K may be touched even if the DB already exists
267 # we start at K=200, and fall to K=40 over the first 20 games
268 ELOPARMS = EloParms(global_K = 200)