author Ant Zucaro Wed, 1 Aug 2012 11:52:19 +0000 (07:52 -0400) committer Ant Zucaro Wed, 1 Aug 2012 11:52:19 +0000 (07:52 -0400)
Scale S according to the real scorefactor in non-duel games.
This eliminates the effects of people switching to the winning
or losing team at the very last second. This also avoids a
"winner take all" in DM mode, where most people would lose points.

Additionally, all players are compared to everyone else, not just
those on their own team. This makes it more fair anyway - no
averages are done over the opponent team only. You now have to
perform better overall to get more points.

 xonstat/elo.py patch | blob | history xonstat/models.py patch | blob | history xonstat/views/submission.py patch | blob | history

@@ -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:
+
+
+def update_elos(game, session, elos, scores, ep):
+    if len(elos) < 2:
+        return elos
+
+    pids = elos.keys()
+
+    for pid in pids:
+
+    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))
+
+            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.
+                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.
+
+
+    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]
+        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)
index 466bfa06e9f53276ee341d51c7cd22833ac9bc60..84a64ea9a665fc6c9d49e52f12c041d8d48bca17 100644 (file)
@@ -8,7 +8,6 @@ from sqlalchemy.orm import mapper
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
-from xonstat.elo import ELOPARMS, KREDUCTION
from xonstat.util import strip_colors, html_colors, pretty_date

log = logging.getLogger(__name__)
@@ -99,148 +98,6 @@ class Game(object):
def fuzzy_date(self):
return pretty_date(self.start_dt)

-    def process_elos(self, session, game_type_cd=None):
-        if game_type_cd is None:
-            game_type_cd = self.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==self.game_id).\
-                    one():
-            duration = d.seconds
-
-        scores = {}
-        alivetimes = {}
-        winners = []
-        losers = []
-        for (p,s,a,r,t) in session.query(PlayerGameStat.player_id,
-                PlayerGameStat.score, PlayerGameStat.alivetime,
-                PlayerGameStat.rank, PlayerGameStat.team).\
-                filter(PlayerGameStat.game_id==self.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
-
-                    # winners are either rank 1 or on the winning team
-                    # team games are where the team is set (duh)
-                    if r == 1 or (t == self.winner and t is not None):
-                        winners.append(p)
-                    else:
-                        losers.append(p)
-
-        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)
-
-        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])
-
-                if pid in winners:
-                    winners.remove(pid)
-                else:
-                    losers.remove(pid)
-
-        elos = self.update_elos(session, elos, scores, winners, losers, ELOPARMS)
-
-        # add the elos to the session for committing
-        for e in elos:
-
-    def update_elos(self, session, elos, scores, winners, losers, ep):
-        if len(elos) < 2 or len(winners) == 0 or len(losers) == 0:
-            return elos
-
-        pids = elos.keys()
-
-        elo_deltas = {}
-        for w_pid in winners:
-            w_elo = elos[w_pid]
-            for l_pid in losers:
-                l_elo = elos[l_pid]
-
-                w_q = math.pow(10, float(w_elo.elo)/400.0)
-                l_q = math.pow(10, float(l_elo.elo)/400.0)
-
-                w_delta = w_elo.k * ELOPARMS.global_K * (1 - w_q/(w_q + l_q))
-                l_delta = l_elo.k * ELOPARMS.global_K * (0 - l_q/(l_q + w_q))
-
-                elo_deltas[w_pid] = (elo_deltas.get(w_pid, 0.0) + w_delta)
-                elo_deltas[l_pid] = (elo_deltas.get(l_pid, 0.0) + l_delta)
-
-                log.debug("Winner {0}'s elo_delta vs Loser {1}: {2}".format(w_pid,
-                    l_pid, w_delta))
-
-                log.debug("Loser {0}'s elo_delta vs Winner {1}: {2}".format(l_pid,
-                    w_pid, l_delta))
-
-                log.debug("w_elo: {0}, w_k: {1}, w_q: {2}, l_elo: {3}, l_k: {4}, l_q: {5}".\
-                        format(w_elo.elo, w_elo.k, l_q, l_elo.elo, l_elo.k, l_q))
-
-        for pid in pids:
-            # average the elo gain for team games
-            if pid in winners:
-                elo_deltas[pid] = elo_deltas.get(pid, 0.0) / len(losers)
-            else:
-                elo_deltas[pid] = elo_deltas.get(pid, 0.0) / len(winners)
-
-            old_elo = float(elos[pid].elo)
-            new_elo = max(float(elos[pid].elo) + elo_deltas[pid], ep.floor)
-
-            # in case we've set a different delta from the above
-            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))
-
-        self.save_elo_deltas(session, elo_deltas)
-
-        return elos
-
-
-    def save_elo_deltas(self, 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 == self.game_id).\
-                all():
-                    pgstats[pgstat.player_id] = pgstat
-
-        for pid in elo_deltas.keys():
-            try:
-                pgstats[pid].elo_delta = elo_deltas[pid]
-            except:
-                log.debug("Unable to save Elo delta value for player_id {0}".format(pid))
-
-
-

class PlayerGameStat(object):
def __init__(self, player_game_stat_id=None, create_dt=None):
@@ -323,13 +180,13 @@ class PlayerNick(object):

class PlayerElo(object):
-    def __init__(self, player_id=None, game_type_cd=None):
+    def __init__(self, player_id=None, game_type_cd=None, elo=None):

self.player_id = player_id
self.game_type_cd = game_type_cd
+        self.elo = elo
self.score = 0
self.games = 0
-        self.elo = ELOPARMS.initial

def __repr__(self):
return "<PlayerElo(pid=%s, gametype=%s, elo=%s)>" % (self.player_id, self.game_type_cd, self.elo)
index d584915193706fa0bd47e9f1789fd750a1f72e11..78f02c94512301948da85053458c4d563f1726a8 100644 (file)
@@ -8,6 +8,7 @@ from pyramid.response import Response
from sqlalchemy import Sequence
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
from xonstat.d0_blind_id import d0_blind_id_verify
+from xonstat.elo import process_elos
from xonstat.models import *
from xonstat.util import strip_colors, qfont_decode

@@ -612,7 +613,7 @@ def stats_submit(request):

# update elos
try:
-            game.process_elos(session)
+            process_elos(game, session)
except Exception as e:
log.debug('Error (non-fatal): elo processing failed.')