return k
-def process_elos(game, session, game_type_cd=None):
- if game_type_cd is None:
- game_type_cd = game.game_type_cd
-
- # we do not have the actual duration of the game, so use the
- # maximum alivetime of the players instead
- duration = 0
- for d in session.query(sfunc.max(PlayerGameStat.alivetime)).\
- filter(PlayerGameStat.game_id==game.game_id).\
- one():
- duration = d.seconds
-
- scores = {}
- alivetimes = {}
- for (p,s,a) in session.query(PlayerGameStat.player_id,
- PlayerGameStat.score, PlayerGameStat.alivetime).\
- filter(PlayerGameStat.game_id==game.game_id).\
- filter(PlayerGameStat.alivetime > timedelta(seconds=0)).\
- filter(PlayerGameStat.player_id > 2).\
- all():
- # scores are per second
- # with a short circuit to handle alivetimes > game
- # durations, which can happen due to warmup being
- # included (most often in duels)
- if game.duration is not None and a.seconds > game.duration.seconds:
- scores[p] = s/float(game.duration.seconds)
- alivetimes[p] = game.duration.seconds
- else:
- scores[p] = s/float(a.seconds)
- alivetimes[p] = a.seconds
-
- player_ids = scores.keys()
-
- elos = {}
- for e in session.query(PlayerElo).\
- filter(PlayerElo.player_id.in_(player_ids)).\
- filter(PlayerElo.game_type_cd==game_type_cd).all():
- elos[e.player_id] = e
-
- # ensure that all player_ids have an elo record
- for pid in player_ids:
- if pid not in elos.keys():
- elos[pid] = PlayerElo(pid, game_type_cd, ELOPARMS.initial)
-
- for pid in player_ids:
- elos[pid].k = KREDUCTION.eval(elos[pid].games, alivetimes[pid],
- duration)
- if elos[pid].k == 0:
- del(elos[pid])
- del(scores[pid])
- del(alivetimes[pid])
-
- elos = update_elos(game, session, elos, scores, ELOPARMS)
-
- # add the elos to the session for committing
- for e in elos:
- session.add(elos[e])
-
-
-def update_elos(game, session, elos, scores, ep):
- if len(elos) < 2:
- return elos
-
- pids = elos.keys()
-
- eloadjust = {}
- for pid in pids:
- eloadjust[pid] = 0.0
-
- for i in xrange(0, len(pids)):
- ei = elos[pids[i]]
- for j in xrange(i+1, len(pids)):
- ej = elos[pids[j]]
- si = scores[ei.player_id]
- sj = scores[ej.player_id]
-
- # normalize scores
- ofs = min(0, si, sj)
- si -= ofs
- sj -= ofs
- if si + sj == 0:
- si, sj = 1, 1 # a draw
-
- # real score factor
- scorefactor_real = si / float(si + sj)
-
- # duels are done traditionally - a win nets
- # full points, not the score factor
- if game.game_type_cd == 'duel':
- # player i won
- if scorefactor_real > 0.5:
- scorefactor_real = 1.0
- # player j won
- elif scorefactor_real < 0.5:
- scorefactor_real = 0.0
- # nothing to do here for draws
-
- # expected score factor by elo
- elodiff = min(ep.maxlogdistance, max(-ep.maxlogdistance,
- (float(ei.elo) - float(ej.elo)) * ep.logdistancefactor))
- scorefactor_elo = 1 / (1 + math.exp(-elodiff))
-
- # initial adjustment values, which we may modify with additional rules
- adjustmenti = scorefactor_real - scorefactor_elo
- adjustmentj = scorefactor_elo - scorefactor_real
-
- # log.debug("Player i: {0}".format(ei.player_id))
- # log.debug("Player i's K: {0}".format(ei.k))
- # log.debug("Player j: {0}".format(ej.player_id))
- # log.debug("Player j's K: {0}".format(ej.k))
- # log.debug("Scorefactor real: {0}".format(scorefactor_real))
- # log.debug("Scorefactor elo: {0}".format(scorefactor_elo))
- # log.debug("adjustment i: {0}".format(adjustmenti))
- # log.debug("adjustment j: {0}".format(adjustmentj))
-
- if scorefactor_elo > 0.5:
- # player i is expected to win
- if scorefactor_real > 0.5:
- # he DID win, so he should never lose points.
- adjustmenti = max(0, adjustmenti)
- else:
- # he lost, but let's make it continuous (making him lose less points in the result)
- adjustmenti = (2 * scorefactor_real - 1) * scorefactor_elo
- else:
- # player j is expected to win
- if scorefactor_real > 0.5:
- # he lost, but let's make it continuous (making him lose less points in the result)
- adjustmentj = (1 - 2 * scorefactor_real) * (1 - scorefactor_elo)
- else:
- # he DID win, so he should never lose points.
- adjustmentj = max(0, adjustmentj)
+class EloWIP:
+ """EloWIP is a work-in-progress Elo value. It contains all of the
+ attributes necessary to calculate Elo deltas for a given game."""
+ def __init__(self, player_id, pgstat=None):
+ # player_id this belongs to
+ self.player_id = player_id
+
+ # score per second in the game
+ self.score_per_second = 0.0
+
+ # seconds alive during a given game
+ self.alivetime = 0
+
+ # current elo record
+ self.elo = None
- eloadjust[ei.player_id] += adjustmenti
- eloadjust[ej.player_id] += adjustmentj
+ # current player_game_stat record
+ self.pgstat = pgstat
- elo_deltas = {}
- for pid in pids:
- old_elo = float(elos[pid].elo)
- new_elo = max(float(elos[pid].elo) + eloadjust[pid] * elos[pid].k * ep.global_K / float(len(elos) - 1), ep.floor)
- elo_deltas[pid] = new_elo - old_elo
+ # Elo algorithm K-factor
+ self.k = 0.0
- elos[pid].elo = new_elo
- elos[pid].games += 1
- elos[pid].update_dt = datetime.datetime.utcnow()
+ # Elo points accumulator, which is not adjusted by the K-factor
+ self.adjustment = 0.0
- 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))
+ # elo points delta accumulator for the game, which IS adjusted
+ # by the K-factor
+ self.elo_delta = 0.0
- save_elo_deltas(game, session, elo_deltas)
+ def should_save(self):
+ """Determines if the elo and pgstat attributes of this instance should
+ be persisted to the database"""
+ return self.k > 0.0
- return elos
+ def __repr__(self):
+ return "<EloWIP(player_id={}, score_per_second={}, alivetime={}, \
+ elo={}, pgstat={}, k={}, adjustment={}, elo_delta={})>".\
+ format(self.player_id, self.score_per_second, self.alivetime, \
+ self.elo, self.pgstat, self.k, self.adjustment, self.elo_delta)
-def save_elo_deltas(game, session, elo_deltas):
- """
- Saves the amount by which each player's Elo goes up or down
- in a given game in the PlayerGameStat row, allowing for scoreboard display.
+class EloProcessor:
+ """EloProcessor is a container for holding all of the intermediary AND
+ final values used to calculate Elo deltas for all players in a given
+ game."""
+ def __init__(self, session, game, pgstats):
- elo_deltas is a dictionary such that elo_deltas[player_id] is the elo_delta
- for that player_id.
- """
- pgstats = {}
- for pgstat in session.query(PlayerGameStat).\
- filter(PlayerGameStat.game_id == game.game_id).\
- all():
- pgstats[pgstat.player_id] = pgstat
+ # game which we are processing
+ self.game = game
- for pid in elo_deltas.keys():
- try:
- pgstats[pid].elo_delta = elo_deltas[pid]
- session.add(pgstats[pid])
- except:
- log.debug("Unable to save Elo delta value for player_id {0}".format(pid))
+ # work-in-progress values, indexed by player
+ self.wip = {}
+
+ # used to determine if a pgstat record is elo-eligible
+ def elo_eligible(pgs):
+ return pgs.player_id > 2 and pgs.alivetime > timedelta(seconds=0)
+
+ # only process elos for elo-eligible players
+ for pgstat in filter(elo_eligible, pgstats):
+ self.wip[pgstat.player_id] = EloWIP(pgstat.player_id, pgstat)
+
+ # determine duration from the maximum alivetime
+ # of the players if the game doesn't have one
+ self.duration = 0
+ if game.duration is not None:
+ self.duration = game.duration.seconds
+ else:
+ self.duration = max(i.alivetime.seconds for i in elostats)
+
+ # Calculate the score_per_second and alivetime values for each player.
+ # Warmups may mess up the player alivetime values, so this is a
+ # failsafe to put the alivetime ceiling to be the game's duration.
+ for e in self.wip.values():
+ if e.pgstat.alivetime.seconds > self.duration:
+ e.score_per_second = e.pgstat.score/float(self.duration)
+ e.alivetime = self.duration
+ else:
+ e.score_per_second = e.pgstat.score/float(e.pgstat.alivetime.seconds)
+ e.alivetime = e.pgstat.alivetime.seconds
+
+ # Fetch current Elo values for all players. For players that don't yet
+ # have an Elo record, we'll give them a default one.
+ for e in session.query(PlayerElo).\
+ filter(PlayerElo.player_id.in_(self.wip.keys())).\
+ filter(PlayerElo.game_type_cd==game.game_type_cd).all():
+ self.wip[e.player_id].elo = e
+
+ for pid in self.wip.keys():
+ if self.wip[pid].elo is None:
+ self.wip[pid].elo = PlayerElo(pid, game.game_type_cd, ELOPARMS.initial)
+
+ # determine k reduction
+ self.wip[pid].k = KREDUCTION.eval(self.wip[pid].elo.games,
+ self.wip[pid].alivetime, self.duration)
+
+ # we don't process the players who have a zero K factor
+ self.wip = { e.player_id:e for e in self.wip.values() if e.k > 0.0}
+
+ # now actually process elos
+ self.process()
+
+ # DEBUG
+ # for w in self.wip.values():
+ # log.debug(w.player_id)
+ # log.debug(w)
+
+ def scorefactor(self, si, sj):
+ """Calculate the real scorefactor of the game. This is how players
+ actually performed, which is compared to their expected performance as
+ predicted by their Elo values."""
+ scorefactor_real = si / float(si + sj)
+
+ # duels are done traditionally - a win nets
+ # full points, not the score factor
+ if self.game.game_type_cd == 'duel':
+ # player i won
+ if scorefactor_real > 0.5:
+ scorefactor_real = 1.0
+ # player j won
+ elif scorefactor_real < 0.5:
+ scorefactor_real = 0.0
+ # nothing to do here for draws
+
+ return scorefactor_real
+
+ def process(self):
+ """Perform the core Elo calculation, storing the values in the "wip"
+ dict for passing upstream."""
+ if len(self.wip.keys()) < 2:
+ return
+
+ ep = ELOPARMS
+
+ pids = self.wip.keys()
+ for i in xrange(0, len(pids)):
+ ei = self.wip[pids[i]].elo
+ for j in xrange(i+1, len(pids)):
+ ej = self.wip[pids[j]].elo
+ si = self.wip[pids[i]].score_per_second
+ sj = self.wip[pids[j]].score_per_second
+
+ # normalize scores
+ ofs = min(0, si, sj)
+ si -= ofs
+ sj -= ofs
+ if si + sj == 0:
+ si, sj = 1, 1 # a draw
+
+ # real score factor
+ scorefactor_real = self.scorefactor(si, sj)
+
+ # expected score factor by elo
+ elodiff = min(ep.maxlogdistance, max(-ep.maxlogdistance,
+ (float(ei.elo) - float(ej.elo)) * ep.logdistancefactor))
+ scorefactor_elo = 1 / (1 + math.exp(-elodiff))
+
+ # initial adjustment values, which we may modify with additional rules
+ adjustmenti = scorefactor_real - scorefactor_elo
+ adjustmentj = scorefactor_elo - scorefactor_real
+
+ # DEBUG
+ # log.debug("(New) Player i: {0}".format(ei.player_id))
+ # log.debug("(New) Player i's K: {0}".format(self.wip[pids[i]].k))
+ # log.debug("(New) Player j: {0}".format(ej.player_id))
+ # log.debug("(New) Player j's K: {0}".format(self.wip[pids[j]].k))
+ # log.debug("(New) Scorefactor real: {0}".format(scorefactor_real))
+ # log.debug("(New) Scorefactor elo: {0}".format(scorefactor_elo))
+ # log.debug("(New) adjustment i: {0}".format(adjustmenti))
+ # log.debug("(New) adjustment j: {0}".format(adjustmentj))
+
+ if scorefactor_elo > 0.5:
+ # player i is expected to win
+ if scorefactor_real > 0.5:
+ # he DID win, so he should never lose points.
+ adjustmenti = max(0, adjustmenti)
+ else:
+ # he lost, but let's make it continuous (making him lose less points in the result)
+ adjustmenti = (2 * scorefactor_real - 1) * scorefactor_elo
+ else:
+ # player j is expected to win
+ if scorefactor_real > 0.5:
+ # he lost, but let's make it continuous (making him lose less points in the result)
+ adjustmentj = (1 - 2 * scorefactor_real) * (1 - scorefactor_elo)
+ else:
+ # he DID win, so he should never lose points.
+ adjustmentj = max(0, adjustmentj)
+
+ self.wip[pids[i]].adjustment += adjustmenti
+ self.wip[pids[j]].adjustment += adjustmentj
+
+ for pid in pids:
+ w = self.wip[pid]
+ old_elo = float(w.elo.elo)
+ new_elo = max(float(w.elo.elo) + w.adjustment * w.k * ep.global_K / float(len(pids) - 1), ep.floor)
+ w.elo_delta = new_elo - old_elo
+
+ w.elo.elo = new_elo
+ w.elo.games += 1
+ w.elo.update_dt = datetime.datetime.utcnow()
+
+ # log.debug("Setting Player {0}'s Elo delta to {1}. Elo is now {2}\
+ # (was {3}).".format(pid, w.elo_delta, new_elo, old_elo))
+
+ def save(self, session):
+ """Put all changed PlayerElo and PlayerGameStat instances into the
+ session to be updated or inserted upon commit."""
+ # first, save all of the player_elo values
+ for w in self.wip.values():
+ session.add(w.elo)
+
+ try:
+ w.pgstat.elo_delta = w.elo_delta
+ session.add(w.pgstat)
+ except:
+ log.debug("Unable to save Elo delta value for player_id {0}".format(w.player_id))
# parameters for K reduction