X-Git-Url: http://de.git.xonotic.org/?a=blobdiff_plain;f=xonstat%2Felo.py;h=48a24e831bfd1a6dddbec61130aeb2314d63dfc2;hb=607d5d84ee1427501276ab6df3c4fca3bdeac56a;hp=19bcfbada1ba62f117fe8c8128ec6377369ca7a3;hpb=ab01b511f02a52d5386be63f228f9237b0e4586f;p=xonotic%2Fxonstat.git diff --git a/xonstat/elo.py b/xonstat/elo.py index 19bcfba..48a24e8 100644 --- a/xonstat/elo.py +++ b/xonstat/elo.py @@ -1,6 +1,12 @@ -import sys +import logging import math import random +import sys +from xonstat.models import * + + +log = logging.getLogger(__name__) + class EloParms: def __init__(self, global_K = 15, initial = 100, floor = 100, logdistancefactor = math.log(10)/float(400), maxlogdistance = math.log(10)): @@ -36,6 +42,172 @@ class KReduction: 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 + 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) + + eloadjust[ei.player_id] += adjustmenti + eloadjust[ej.player_id] += adjustmentj + + 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 + + elos[pid].elo = new_elo + elos[pid].games += 1 + + 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)) + + save_elo_deltas(game, session, elo_deltas) + + return elos + + +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. + + 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 + + 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)) + + # parameters for K reduction # this may be touched even if the DB already exists KREDUCTION = KReduction(600, 120, 0.5, 0, 32, 0.2)