From bdad37dd3f350a1ef059811ec4c0a02e3933d2d6 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 30 Dec 2017 12:29:44 +0100 Subject: refactor and add tests --- rater.py | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 rater.py (limited to 'rater.py') 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) -- cgit v1.3.1