From a377dd74af8fae6fb6a64f2c24b3eef851795b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Fri, 24 Nov 2023 17:15:38 +0100 Subject: [PATCH] Implement correct values for rating system Added k-factors to config. Implemented the season 0 rating change logic. --- install/default_config.yaml | 21 +++++++++++- src/config.py | 30 +++++++++++++++++ src/constants.py | 3 ++ src/ratings.py | 66 ++++++++++++++++++++++++++++--------- 4 files changed, 104 insertions(+), 16 deletions(-) diff --git a/install/default_config.yaml b/install/default_config.yaml index 83fb57a..5c4f85b 100644 --- a/install/default_config.yaml +++ b/install/default_config.yaml @@ -19,14 +19,33 @@ variant_base_ratings: 5p: 1700 min_player_count: 3 max_player_count: 5 -min_suits: 5 + +# This adjusts the speed in rating change for players +k-factor: + values: + # Early is applied for players with at most conditions.num_early_games games + early: 40 + # This is the regular coefficient for people with a good amount of games + normal: 30 + # For people with rating at least conditions.high_rating, the coefficient is adapted again, + high_rating: 15 + # Controls how fast the variant ratings change + variants: 5 + conditions: + num_early_games: 30 + high_rating: 1700 + +min_suits: 4 max_suits: 6 + # Corresponds to game IDs from hanab.live starting_game_id: 1000000 ending_game_id: 9999999 + # EST = Eastern Standard Time, so USA/Eastern starting_time: "2023-10-10 00:00:00 EST" ending_time: "2023-12-10 00:00:00 EST" + # Any variant that contains one of these keywords will not be allowed for the league. excluded_variants: - Alternating diff --git a/src/config.py b/src/config.py index ebaf39b..98facd6 100644 --- a/src/config.py +++ b/src/config.py @@ -141,6 +141,36 @@ class Config: def excluded_variants(self) -> List[str]: return [var.lower() for var in self._config["excluded_variants"]] + @property + @check_config_attr + def k_factor_num_early_games(self): + return self._config["k-factor"]["conditions"]["num_early_games"] + + @property + @check_config_attr + def k_factor_high_rating_cutoff(self): + return self._config["k-factor"]["conditions"]["high_rating"] + + @property + @check_config_attr + def k_factor_for_few_games(self): + return self._config["k-factor"]["values"]["early"] + + @property + @check_config_attr + def k_factor_normal(self): + return self._config["k-factor"]["values"]["normal"] + + @property + @check_config_attr + def k_factor_for_high_rating(self): + return self._config["k-factor"]["values"]["high_rating"] + + @property + @check_config_attr + def k_factor_for_variants(self): + return self._config["k-factor"]["values"]["variants"] + @check_config_attr def variant_base_rating(self, variant_name: str, player_count: int) -> int: global_base_rating = self._config["variant_base_rating"] diff --git a/src/constants.py b/src/constants.py index 2a55294..a21feb8 100644 --- a/src/constants.py +++ b/src/constants.py @@ -44,3 +44,6 @@ FORBIDDEN_GAME_OPTIONS = [ # Cache time (in seconds) for history requests of players # In case of frequent reruns (especially during development), we do not want to stress the server too much. USER_HISTORY_CACHE_TIME = 5 * 60 + +# Fraction of seeds which is assumed to be unwinnable +UNWINNABLE_SEED_FRACTION = 0.02 diff --git a/src/ratings.py b/src/ratings.py index 7d1cc03..c41c075 100644 --- a/src/ratings.py +++ b/src/ratings.py @@ -5,18 +5,43 @@ from database import conn_manager import psycopg2.extras from log_setup import logger +import constants +from config import config_manager -def rating_change(user_ratings: Dict[int, float], variant_rating: float, win: bool) -> Tuple[Dict[int, float], float]: +def get_development_coefficient(num_games, player_rating): + config = config_manager.get_config() + if num_games <= config.k_factor_num_early_games: + return config.k_factor_for_few_games + if player_rating >= config.k_factor_high_rating_cutoff: + return config.k_factor_for_high_rating + return config.k_factor_normal + + +def expected_result(player_rating, var_rating): + expected = (1 - constants.UNWINNABLE_SEED_FRACTION) / (1 + pow(10, (var_rating - player_rating) / 400)) + return expected + + +def compute_rating_changes(user_ratings: Dict[int, float], games_played: Dict[int, float], variant_rating: float, win: bool) -> Tuple[Dict[int, float], float]: """ @param user_ratings: Mapping of user ids to ratings that played this game. + @param games_played: Mapping of users ids to the number of games these users played so far. @param variant_rating: Rating of the variant that was played @param win: Whether the team won the game @return: Mapping of user ids to their rating *changes* and *change* in variant rating """ - # TODO: Implement this properly (We have not decided how this will work exactly) - # For now, return +1 elo for players and -1 elo for variants - return {user_id: 1 for user_id in user_ratings.keys()}, -1 + + expected_score = sum(expected_result(player_rating, variant_rating) for player_rating in user_ratings.values()) / len(user_ratings) + actual_score = 1 if win else 0 + user_changes = {} + + for user_id, num_games in games_played.items(): + coefficient = get_development_coefficient(num_games, user_ratings[user_id]) + user_changes[user_id] = coefficient * (actual_score - expected_score) + + variant_change = config_manager.get_config().k_factor_for_variants * (expected_score - actual_score) + return user_changes, variant_change def next_game_to_rate(): @@ -137,29 +162,40 @@ def process_rating_of_next_game() -> bool: ) league_id, num_players, score, num_suits, clue_starved, variant_id = cur.fetchone() - # Fetch game participants - cur.execute("SELECT game_participants.user_id FROM games " - "INNER JOIN game_participants " + # Fetch game participants and how many games they played each so far + cur.execute("SELECT game_participants.user_id, COUNT(games.id) " + "FROM game_participants " + "INNER JOIN games " " ON games.id = game_participants.game_id " - "WHERE games.id = %s", - (game_id,) + "WHERE user_id IN" + " (" + " SELECT game_participants.user_id FROM games " + " INNER JOIN game_participants " + " ON games.id = game_participants.game_id " + " WHERE games.id = %s" + " )" + "AND league_id <= %s " + "GROUP BY user_id", + (game_id, league_id) ) - user_ids = cur.fetchall() - if len(user_ids) != num_players: + games_played = {} + for (user_id, num_games) in cur.fetchall(): + games_played[user_id] = num_games + + if len(games_played) != num_players: err_msg = "Player number mismatch: Expected {} participants for game {}, but only found {} in DB: [{}]".format( - num_players, game_id, len(user_ids), ", ".join(user_ids) + num_players, game_id, len(games_played), ", ".join(games_played) ) logger.error(err_msg) raise ValueError(err_msg) # Fetch current ratings of variant and players involved rating_type = utils.get_rating_type(clue_starved) - user_ratings = get_current_user_ratings(user_ids, rating_type) + user_ratings = get_current_user_ratings(list(games_played.keys()), rating_type) variant_rating = get_current_variant_rating(variant_id, num_players) # Calculate changes in rating - # TODO: If we want to use, we still have to think about how to define the K-factor and add it here - user_changes, variant_change = rating_change(user_ratings, variant_rating, score == 5 * num_suits) + user_changes, variant_change = compute_rating_changes(user_ratings, games_played, variant_rating, score == 5 * num_suits) # Update database for variants cur.execute("INSERT INTO variant_ratings (league_id, variant_id, num_players, change, value_after) "