6 from xonstat.models import *
9 log = logging.getLogger(__name__)
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
17 self.logdistancefactor = logdistancefactor
18 self.maxlogdistance = maxlogdistance
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
30 def eval(self, mygames, mytime, matchtime):
31 if mytime < self.mintime:
33 if mytime < self.minratio * matchtime:
35 if mytime < self.fulltime:
36 k = mytime / float(self.fulltime)
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)
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
50 # we do not have the actual duration of the game, so use the
51 # maximum alivetime of the players instead
53 for d in session.query(sfunc.max(PlayerGameStat.alivetime)).\
54 filter(PlayerGameStat.game_id==game.game_id).\
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).\
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
74 scores[p] = s/float(a.seconds)
75 alivetimes[p] = a.seconds
77 player_ids = scores.keys()
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():
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)
90 for pid in player_ids:
91 elos[pid].k = KREDUCTION.eval(elos[pid].games, alivetimes[pid],
98 elos = update_elos(game, session, elos, scores, ELOPARMS)
100 # add the elos to the session for committing
105 def update_elos(game, session, elos, scores, ep):
115 for i in xrange(0, len(pids)):
117 for j in xrange(i+1, len(pids)):
119 si = scores[ei.player_id]
120 sj = scores[ej.player_id]
127 si, sj = 1, 1 # a draw
130 scorefactor_real = si / float(si + sj)
132 # duels are done traditionally - a win nets
133 # full points, not the score factor
134 if game.game_type_cd == 'duel':
136 if scorefactor_real > 0.5:
137 scorefactor_real = 1.0
139 elif scorefactor_real < 0.5:
140 scorefactor_real = 0.0
141 # nothing to do here for draws
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))
148 # initial adjustment values, which we may modify with additional rules
149 adjustmenti = scorefactor_real - scorefactor_elo
150 adjustmentj = scorefactor_elo - scorefactor_real
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))
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)
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
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)
175 # he DID win, so he should never lose points.
176 adjustmentj = max(0, adjustmentj)
178 eloadjust[ei.player_id] += adjustmenti
179 eloadjust[ej.player_id] += adjustmentj
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
187 elos[pid].elo = new_elo
189 elos[pid].update_dt = datetime.datetime.utcnow()
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))
193 save_elo_deltas(game, session, elo_deltas)
198 def save_elo_deltas(game, session, elo_deltas):
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.
203 elo_deltas is a dictionary such that elo_deltas[player_id] is the elo_delta
207 for pgstat in session.query(PlayerGameStat).\
208 filter(PlayerGameStat.game_id == game.game_id).\
210 pgstats[pgstat.player_id] = pgstat
212 for pid in elo_deltas.keys():
214 pgstats[pid].elo_delta = elo_deltas[pid]
215 session.add(pgstats[pid])
217 log.debug("Unable to save Elo delta value for player_id {0}".format(pid))
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)
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)