summaryrefslogtreecommitdiff
path: root/rater.py
diff options
context:
space:
mode:
Diffstat (limited to 'rater.py')
-rw-r--r--rater.py186
1 files changed, 186 insertions, 0 deletions
diff --git a/rater.py b/rater.py
new file mode 100644
index 0000000..2d07e03
--- /dev/null
+++ b/rater.py
@@ -0,0 +1,186 @@
+#!/usr/bin/python3
+import os
+import sys
+import logging
+
+import trueskill
+import mpmath
+mpmath.mp.dps = 50 # increase if FloatingPointError
+
+UNKNOWN_PLAYER_SIGMA = int(os.environ.get("UNKNOWN_PLAYER_SIGMA") or 500)
+TAU = float(os.environ.get("TAU") or 1000/100.0)
+
+# mapping from Tier (-1 - 30) to average skill tier points
+vst_points = {
+ -1: 1,
+ 0: 1
+}
+for c in range(1, 12):
+ vst_points[c] = (109 + 1/11) * (c + 0.5)
+for c in range(1, 5):
+ vst_points[11 + c] = vst_points[11] + 50 * (c + 0.5)
+for c in range(1, 10):
+ vst_points[15 + c] = vst_points[15] + (66 + 2/3) * (c + 0.5)
+for c in range(1, 4):
+ vst_points[24 + c] = vst_points[24] + (133 + 1/3) * (c + 0.5)
+for c in range(1, 3):
+ vst_points[27 + c] = vst_points[27] + 200 * (c + 0.5)
+# ---
+
+env = trueskill.TrueSkill(
+ backend="mpmath",
+ mu=1500,
+ sigma=1000,
+ beta=10.0/30*3000,
+ tau=TAU,
+ draw_probability=0
+)
+
+"""
+Return a (mu, sigma) based on information known about a player.
+"""
+def get_trueskill_seed(player):
+ # fallback 1 - approximate by max(ranked, blitz) points
+ rank_points = None
+ if player.rank_points_ranked is not None and player.rank_points_ranked != 0:
+ rank_points = player.rank_points_ranked
+ if player.rank_points_blitz is not None and player.rank_points_blitz != 0:
+ if rank_points is None:
+ rank_points = player.rank_points_blitz
+ else:
+ if player.rank_points_blitz > rank_points:
+ rank_points = player.rank_points_blitz
+
+ if rank_points is not None:
+ sigma = UNKNOWN_PLAYER_SIGMA * (2.0/3.0) # more accurate than skill tier = more trust
+ mu = float(rank_points) + sigma
+ else:
+ # fallback 2 - approximate rank points by skill tier
+ sigma = UNKNOWN_PLAYER_SIGMA
+ mu = vst_points[player.skill_tier] + sigma
+
+ return (mu, sigma)
+
+
+"""
+Mutate a match structure by updating TrueSkill values.
+Returns changed match object.
+"""
+def rate_match(match):
+ trueskill_column = None # on player
+ if match.game_mode == "casual": # mode names mapped by processor!!!
+ trueskill_column = "trueskill_casual"
+ if match.game_mode == "ranked":
+ trueskill_column = "trueskill_ranked"
+ if match.game_mode == "blitz":
+ trueskill_column = "trueskill_blitz"
+ if match.game_mode == "br":
+ trueskill_column = "trueskill_br"
+ if trueskill_column is None:
+ logger.info("got unsupported game mode %s", match.game_mode)
+ return
+
+ matchup_shared = [] # trueskill shared across all modes
+ matchup = []
+
+ anyAfk = False
+ if len(match.rosters) != 2 \
+ or len(match.rosters[0].participants) != 3 \
+ or len(match.rosters[1].participants) != 3:
+ logger.error("got an invalid matchup %s", match.api_id)
+ anyAfk = True
+
+ for participant in match.participants:
+ participant.participant_items[0].any_afk = False
+ if participant.went_afk == 1:
+ logger.info("got an afk matchup %s", match.api_id)
+ anyAfk = True
+ break
+
+ if anyAfk:
+ match.trueskill_quality = 0
+ for participant in match.participants:
+ participant.participant_items[0].any_afk = True
+ return
+
+ for roster in match.rosters:
+ team_shared = []
+ team = []
+ for participant in roster.participants:
+ player = participant.player[0]
+
+ # calculate TrueSkill shared across all modes = starting point for all modes
+ if player.trueskill_mu is not None:
+ mu_shared = player.trueskill_mu
+ sigma_shared = player.trueskill_sigma
+ else:
+ mu_shared, sigma_shared = get_trueskill_seed(player)
+
+ team_shared.append(env.create_rating(float(mu_shared), float(sigma_shared)))
+
+ # calculate queue specific TrueSkill
+ if getattr(player, trueskill_column + "_mu") is not None:
+ sigma = getattr(player, trueskill_column + "_sigma")
+ mu = getattr(player, trueskill_column + "_mu")
+ else:
+ # fallback 1 - approximate by shared trueskill
+ sigma = sigma_shared
+ mu = mu_shared
+
+ team.append(env.create_rating(float(mu), float(sigma)))
+
+
+ matchup_shared.append(team_shared)
+ matchup.append(team)
+
+ logger.info("got a valid matchup %s", match.api_id)
+
+ # store the fairness of the match using the shared TrueSkill
+ match.trueskill_quality = env.quality(matchup)
+
+ # shared TrueSkill (participant)
+ for team, roster in zip(env.rate(matchup_shared, ranks=[int(not r.winner) for r in match.rosters]),
+ match.rosters):
+ # lower rank is better = winner!
+ for rating, participant in zip(team, roster.participants):
+ player = participant.player[0]
+ # delta = current - pre
+ if player.trueskill_mu is not None:
+ participant.trueskill_delta = (float(rating.mu) - float(rating.sigma)) - (float(player.trueskill_mu) - float(player.trueskill_sigma))
+ else:
+ participant.trueskill_delta = 0
+ player.trueskill_mu = rating.mu
+ participant.trueskill_mu = rating.mu
+ player.trueskill_sigma = rating.sigma
+ participant.trueskill_sigma = rating.sigma
+
+ # queue specific TrueSkill (participant_items)
+ # no delta
+ for team, roster in zip(env.rate(matchup, ranks=[int(not r.winner) for r in match.rosters]),
+ match.rosters):
+ for rating, participant in zip(team, roster.participants):
+ player = participant.player[0]
+ pi = participant.participant_items[0]
+ setattr(player, trueskill_column + "_mu", rating.mu)
+ setattr(pi, trueskill_column + "_mu", rating.mu)
+ setattr(player, trueskill_column + "_sigma", rating.sigma)
+ setattr(pi, trueskill_column + "_sigma", rating.sigma)
+
+
+# TODO share this between the two classes
+# log errs to stderr, debug to stdout
+class InfoFilter(logging.Filter):
+ def filter(self, record):
+ return record.levelno in (logging.DEBUG, logging.INFO)
+
+logger = logging.getLogger("__name__")
+logger.setLevel(logging.INFO)
+
+h1 = logging.StreamHandler(sys.stdout)
+h1.setLevel(logging.INFO)
+h1.addFilter(InfoFilter())
+logger.addHandler(h1)
+
+h2 = logging.StreamHandler(sys.stderr)
+h2.setLevel(logging.WARNING)
+logger.addHandler(h2)