]> de.git.xonotic.org Git - xonotic/xonstat.git/blobdiff - xonstat/elo.py
Initial debug version of elo ranking.
[xonotic/xonstat.git] / xonstat / elo.py
diff --git a/xonstat/elo.py b/xonstat/elo.py
new file mode 100755 (executable)
index 0000000..4ba3553
--- /dev/null
@@ -0,0 +1,99 @@
+import sys\r
+import math\r
+import random\r
+\r
+class EloParms:\r
+    def __init__(self, global_K = 15, initial = 100, floor = 100, logdistancefactor = math.log(10)/float(400), maxlogdistance = math.log(10)):\r
+        self.global_K = global_K\r
+        self.initial = initial\r
+        self.floor = floor\r
+        self.logdistancefactor = logdistancefactor\r
+        self.maxlogdistance = maxlogdistance\r
+\r
+\r
+class KReduction:\r
+    def __init__(self, fulltime, mintime, minratio, games_min, games_max, games_factor):\r
+        self.fulltime = fulltime\r
+        self.mintime = mintime\r
+        self.minratio = minratio\r
+        self.games_min = games_min\r
+        self.games_max = games_max\r
+        self.games_factor = games_factor\r
+\r
+    def eval(self, mygames, mytime, matchtime):\r
+        if mytime < self.mintime:\r
+            return 0\r
+        if mytime < self.minratio * matchtime:\r
+            return 0\r
+        if mytime < self.fulltime:\r
+            k = mytime / float(self.fulltime)\r
+        else:\r
+            k = 1.0\r
+        if mygames >= self.games_max:\r
+            k *= self.games_factor\r
+        elif mygames > self.games_min:\r
+            k *= 1.0 - (1.0 - self.games_factor) * (mygames - self.games_min) / float(self.games_max - self.games_min)\r
+        return k\r
+\r
+\r
+# For team games where multiple scores and elos are at play, the elos\r
+# must be adjusted according to their strength relative to the player\r
+# in the next-lowest scoreboard position.\r
+def update(elos, ep):\r
+    for x in elos:\r
+        if x.elo == None:\r
+            x.elo = ep.initial\r
+        x.eloadjust = 0\r
+    if len(elos) < 2:\r
+        return elos\r
+    for i in xrange(0, len(elos)):\r
+        ei = elos[i]\r
+        for j in xrange(i+1, len(elos)):\r
+            ej = elos[j]\r
+            si = ei.score\r
+            sj = ej.score\r
+\r
+            # normalize scores\r
+            ofs = min(0, si, sj)\r
+            si -= ofs\r
+            sj -= ofs\r
+            if si + sj == 0:\r
+                si, sj = 1, 1 # a draw\r
+\r
+            # real score factor\r
+            scorefactor_real = si / float(si + sj)\r
+\r
+            # estimated score factor by elo\r
+            elodiff = min(ep.maxlogdistance, max(-ep.maxlogdistance, (ei.elo - ej.elo) * ep.logdistancefactor))\r
+            scorefactor_elo = 1 / (1 + math.exp(-elodiff))\r
+\r
+            # how much adjustment is good?\r
+            # scorefactor(elodiff) = 1 / (1 + e^(-elodiff * logdistancefactor))\r
+            # elodiff(scorefactor) = -ln(1/scorefactor - 1) / logdistancefactor\r
+            # elodiff'(scorefactor) = 1 / ((scorefactor) (1 - scorefactor) logdistancefactor)\r
+            # elodiff'(scorefactor) >= 4 / logdistancefactor\r
+\r
+            # adjust'(scorefactor) = K1 + K2\r
+\r
+            # so we want:\r
+            # K1 + K2 <= 4 / logdistancefactor <= elodiff'(scorefactor)\r
+            # as we then don't overcompensate\r
+\r
+            adjustment = scorefactor_real - scorefactor_elo\r
+            ei.eloadjust += adjustment\r
+            ej.eloadjust -= adjustment\r
+    for x in elos:\r
+        x.elo = max(x.elo + x.eloadjust * x.k * ep.global_K / float(len(elos) - 1), ep.floor)\r
+        x.games += 1\r
+    return elos\r
+\r
+\r
+# parameters for K reduction\r
+# this may be touched even if the DB already exists\r
+KREDUCTION = KReduction(600, 120, 0.5, 0, 32, 0.2)\r
+\r
+# parameters for chess elo\r
+# only global_K may be touched even if the DB already exists\r
+# we start at K=200, and fall to K=40 over the first 20 games\r
+ELOPARMS = EloParms(global_K = 200)\r
+\r