]> git.xonotic.org Git - xonotic/xonstat.git/blobdiff - xonstat/glicko.py
Count XPM as vanilla as well.
[xonotic/xonstat.git] / xonstat / glicko.py
index 6d185fb30ef649dcec9d5cac7ea6f658a8062cc4..167304392ce280e1a48d92e60528dc5bf6ef8994 100644 (file)
@@ -13,6 +13,9 @@ log = logging.getLogger(__name__)
 # the default system volatility constant
 TAU = 0.3
 
+# how much ping influences results
+LATENCY_TREND_FACTOR = 0.2
+
 
 def calc_g(phi):
     return 1 / math.sqrt(1 + (3 * phi ** 2) / (math.pi ** 2))
@@ -89,6 +92,9 @@ def rate(player, opponents, results):
     Calculate the ratings improvement for a given player, provided their opponents and
     corresponding results versus them.
     """
+    if len(opponents) == 0 or len(results) == 0:
+        return player
+
     p_g2 = player.to_glicko2()
 
     gs = []
@@ -130,7 +136,7 @@ def rate(player, opponents, results):
     return new_rating
 
 
-class KReduction:
+class KReduction(object):
     """
     Scale the points gained or lost for players based on time played in the given game.
     """
@@ -188,6 +194,10 @@ class GlickoWIP(object):
         # the list of results for those games in the ranking period
         self.results = []
 
+    def __repr__(self):
+        return ("<GlickoWIP({0.pg}, k={0.k_factors}, ping={0.ping_factors}, "
+                "opponents={0.opponents}, results={0.results})>".format(self))
+
 
 class GlickoProcessor(object):
     """
@@ -239,11 +249,20 @@ class GlickoProcessor(object):
                 .filter(PlayerGameStat.player_id > 2)\
                 .all()
 
+            return pgstats_raw
+
         except Exception as e:
             log.error("Error fetching player_game_stat records for game {}".format(game.game_id))
             log.error(e)
             raise e
 
+    def _filter_pgstats(self, game, pgstats_raw):
+        """
+        Filter the raw game stats so that all of them are Glicko-eligible.
+
+        :param pgstats_raw: the list of raw PlayerGameStat
+        :return: list of PlayerGameStat
+        """
         pgstats = []
         for pgstat in pgstats_raw:
             # ensure warmup isn't included in the pgstat records
@@ -254,6 +273,8 @@ class GlickoProcessor(object):
             k = KREDUCTION.eval(pgstat.alivetime.total_seconds(), game.duration.total_seconds())
             if k <= 0.0:
                 continue
+            elif pgstat.player_id <= 2:
+                continue
             else:
                 pgstats.append(pgstat)
 
@@ -261,7 +282,7 @@ class GlickoProcessor(object):
 
     def _load_glicko_wip(self, player_id, game_type_cd, category):
         """
-        Retrieve a PlayerGlicko record from the database.
+        Retrieve a PlayerGlicko record from the database or local cache.
 
         :param player_id: the player ID to fetch
         :param game_type_cd: the game type code
@@ -287,12 +308,18 @@ class GlickoProcessor(object):
 
         return wip
 
-    def load(self, game_id):
+    def load(self, game_id, game=None, pgstats=None):
         """
         Load all of the needed information from the database. Compute results for each player pair.
         """
-        game = self._load_game(game_id)
-        pgstats = self._load_pgstats(game)
+        if game is None:
+            game = self._load_game(game_id)
+
+        if pgstats is None:
+            pgstats = self._load_pgstats(game)
+
+        pgstats = self._filter_pgstats(game, pgstats)
+
         game_type_cd = game.game_type_cd
         category = game.category
 
@@ -340,16 +367,34 @@ class GlickoProcessor(object):
     def process(self):
         """
         Calculate the Glicko2 ratings, deviations, and volatility updates for the records loaded.
-        :return: bool
         """
-        pass
+        for wip in self.wips.values():
+            new_pg = rate(wip.pg, wip.opponents, wip.results)
+
+            log.debug("New rating for player {} before factors: mu={} phi={} sigma={}"
+                      .format(new_pg.player_id, new_pg.mu, new_pg.phi, new_pg.sigma))
+
+            avg_k_factor = sum(wip.k_factors)/len(wip.k_factors)
+            avg_ping_factor = LATENCY_TREND_FACTOR * sum(wip.ping_factors)/len(wip.ping_factors)
 
-    def save(self, session):
+            points_delta = (new_pg.mu - wip.pg.mu) * avg_k_factor * avg_ping_factor
+
+            wip.pg.mu += points_delta
+            wip.pg.phi = new_pg.phi
+            wip.pg.sigma = new_pg.sigma
+
+            log.debug("New rating for player {} after factors: mu={} phi={} sigma={}"
+                      .format(wip.pg.player_id, wip.pg.mu, wip.pg.phi, wip.pg.sigma))
+
+    def save(self):
         """
         Put all changed PlayerElo and PlayerGameStat instances into the
         session to be updated or inserted upon commit.
         """
-        pass
+        for wip in self.wips.values():
+            self.session.add(wip.pg)
+
+        self.session.commit()
 
 
 def main():