from typing import Tuple, Union, List import math import numpy as np from trueskill import TrueSkill, Rating, rate_1vs1 class EloCalculator(object): """ Overview: A class that calculates Elo ratings for players based on game results. Attributes: - score (:obj:`dict`): A dictionary that maps game results to scores. Interfaces: ``__init__``, ``get_new_rating``, ``get_new_rating_array``. """ score = { 1: 1.0, # win 0: 0.5, # draw -1: 0.0, # lose } @classmethod def get_new_rating(cls, rating_a: int, rating_b: int, result: int, k_factor: int = 32, beta: int = 200) -> Tuple[int, int]: """ Overview: Calculates the new ratings for two players based on their current ratings and game result. Arguments: - rating_a (:obj:`int`): The current rating of player A. - rating_b (:obj:`int`): The current rating of player B. - result (:obj:`int`): The result of the game: 1 for player A win, 0 for draw, -1 for player B win. - k_factor (:obj:`int`): The K-factor used in the Elo rating system. Defaults to 32. - beta (:obj:`int`): The beta value used in the Elo rating system. Defaults to 200. Returns: -ret (:obj:`Tuple[int, int]`): The new ratings for player A and player B, respectively. """ assert result in [1, 0, -1] expect_a = 1. / (1. + math.pow(10, (rating_b - rating_a) / (2. * beta))) expect_b = 1. / (1. + math.pow(10, (rating_a - rating_b) / (2. * beta))) new_rating_a = rating_a + k_factor * (EloCalculator.score[result] - expect_a) new_rating_b = rating_b + k_factor * (1 - EloCalculator.score[result] - expect_b) return round(new_rating_a), round(new_rating_b) @classmethod def get_new_rating_array( cls, rating: np.ndarray, result: np.ndarray, game_count: np.ndarray, k_factor: int = 32, beta: int = 200 ) -> np.ndarray: """ Overview: Calculates the new ratings for multiple players based on their current ratings, game results, \ and game counts. Arguments: - rating (obj:`np.ndarray`): An array of current ratings for each player. - result (obj:`np.ndarray`): An array of game results, where 1 represents a win, 0 represents a draw, \ and -1 represents a loss. - game_count (obj:`np.ndarray`): An array of game counts for each player. - k_factor (obj:`int`): The K-factor used in the Elo rating system. Defaults to 32. - beta (obj:`int`): The beta value used in the Elo rating system. Defaults to 200. Returns: -ret(obj:`np.ndarray`): An array of new ratings for each player. Shapes: - rating (obj:`np.ndarray`): :math:`(N, )`, N is the number of player - result (obj:`np.ndarray`): :math:`(N, N)` - game_count (obj:`np.ndarray`): :math:`(N, N)` """ rating_diff = np.expand_dims(rating, 0) - np.expand_dims(rating, 1) expect = 1. / (1. + np.power(10, rating_diff / (2. * beta))) * game_count delta = ((result + 1.) / 2 - expect) * (game_count > 0) delta = delta.sum(axis=1) return np.round(rating + k_factor * delta).astype(np.int64) class PlayerRating(Rating): """ Overview: Represents the rating of a player. Interfaces: ``__init__``, ``__repr__``. """ def __init__(self, mu: float = None, sigma: float = None, elo_init: int = None) -> None: super(PlayerRating, self).__init__(mu, sigma) self.elo = elo_init def __repr__(self) -> str: c = type(self) args = ('.'.join([c.__module__, c.__name__]), self.mu, self.sigma, self.exposure, self.elo) return '%s(mu=%.3f, sigma=%.3f, exposure=%.3f, elo=%d)' % args class LeagueMetricEnv(TrueSkill): """ Overview: A class that represents a TrueSkill rating system for game players. Inherits from the TrueSkill class. \ For more details, please refer to https://trueskill.org/. Interfaces: ``__init__``, ``create_rating``, ``rate_1vs1``, ``rate_1vsC``. """ def __init__(self, *args, elo_init: int = 1200, **kwargs) -> None: super(LeagueMetricEnv, self).__init__(*args, **kwargs) self.elo_init = elo_init def create_rating(self, mu: float = None, sigma: float = None, elo_init: int = None) -> PlayerRating: """ Overview: Creates a new player rating object with the specified mean, standard deviation, and Elo rating. Arguments: - mu (:obj:`float`): The mean value of the player's skill rating. If not provided, the default \ TrueSkill mean is used. - sigma (:obj:`float`): The standard deviation of the player's skill rating. If not provided, \ the default TrueSkill sigma is used. - elo_init (:obj:int`): The initial Elo rating value for the player. If not provided, the default \ elo_init value of the LeagueMetricEnv class is used. Returns: - PlayerRating: A player rating object with the specified mean, standard deviation, and Elo rating. """ if mu is None: mu = self.mu if sigma is None: sigma = self.sigma if elo_init is None: elo_init = self.elo_init return PlayerRating(mu, sigma, elo_init) @staticmethod def _rate_1vs1(t1, t2, **kwargs): t1_elo, t2_elo = t1.elo, t2.elo t1, t2 = rate_1vs1(t1, t2, **kwargs) if 'drawn' in kwargs: result = 0 else: result = 1 t1_elo, t2_elo = EloCalculator.get_new_rating(t1_elo, t2_elo, result) t1 = PlayerRating(t1.mu, t1.sigma, t1_elo) t2 = PlayerRating(t2.mu, t2.sigma, t2_elo) return t1, t2 def rate_1vs1(self, team1: PlayerRating, team2: PlayerRating, result: List[str] = None, **kwargs) \ -> Tuple[PlayerRating, PlayerRating]: """ Overview: Rates two teams of players against each other in a 1 vs 1 match and returns the updated ratings \ for both teams. Arguments: - team1 (:obj:`PlayerRating`): The rating object representing the first team of players. - team2 (:obj:`PlayerRating`): The rating object representing the second team of players. - result (:obj:`List[str]`): The result of the match. Can be 'wins', 'draws', or 'losses'. If \ not provided, the default behavior is to rate the match as a win for team1. Returns: - ret (:obj:`Tuple[PlayerRating, PlayerRating]`): A tuple containing the updated ratings for team1 \ and team2. """ if result is None: return self._rate_1vs1(team1, team2, **kwargs) else: for r in result: if r == 'wins': team1, team2 = self._rate_1vs1(team1, team2) elif r == 'draws': team1, team2 = self._rate_1vs1(team1, team2, drawn=True) elif r == 'losses': team2, team1 = self._rate_1vs1(team2, team1) else: raise RuntimeError("invalid result: {}".format(r)) return team1, team2 def rate_1vsC(self, team1: PlayerRating, team2: PlayerRating, result: List[str]) -> PlayerRating: """ Overview: Rates a team of players against a single player in a 1 vs C match and returns the updated rating \ for the team. Arguments: - team1 (:obj:`PlayerRating`): The rating object representing the team of players. - team2 (:obj:`PlayerRating`): The rating object representing the single player. - result (:obj:`List[str]`): The result of the match. Can be 'wins', 'draws', or 'losses'. Returns: - PlayerRating: The updated rating for the team of players. """ for r in result: if r == 'wins': team1, _ = self._rate_1vs1(team1, team2) elif r == 'draws': team1, _ = self._rate_1vs1(team1, team2, drawn=True) elif r == 'losses': _, team1 = self._rate_1vs1(team2, team1) else: raise RuntimeError("invalid result: {}".format(r)) return team1 get_elo = EloCalculator.get_new_rating get_elo_array = EloCalculator.get_new_rating_array