forked from Hanabi/hanabi-league
Implement correct values for rating system
Added k-factors to config. Implemented the season 0 rating change logic.
This commit is contained in:
parent
a6c96c7b04
commit
a377dd74af
4 changed files with 104 additions and 16 deletions
|
@ -19,14 +19,33 @@ variant_base_ratings:
|
||||||
5p: 1700
|
5p: 1700
|
||||||
min_player_count: 3
|
min_player_count: 3
|
||||||
max_player_count: 5
|
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
|
max_suits: 6
|
||||||
|
|
||||||
# Corresponds to game IDs from hanab.live
|
# Corresponds to game IDs from hanab.live
|
||||||
starting_game_id: 1000000
|
starting_game_id: 1000000
|
||||||
ending_game_id: 9999999
|
ending_game_id: 9999999
|
||||||
|
|
||||||
# EST = Eastern Standard Time, so USA/Eastern
|
# EST = Eastern Standard Time, so USA/Eastern
|
||||||
starting_time: "2023-10-10 00:00:00 EST"
|
starting_time: "2023-10-10 00:00:00 EST"
|
||||||
ending_time: "2023-12-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.
|
# Any variant that contains one of these keywords will not be allowed for the league.
|
||||||
excluded_variants:
|
excluded_variants:
|
||||||
- Alternating
|
- Alternating
|
||||||
|
|
|
@ -141,6 +141,36 @@ class Config:
|
||||||
def excluded_variants(self) -> List[str]:
|
def excluded_variants(self) -> List[str]:
|
||||||
return [var.lower() for var in self._config["excluded_variants"]]
|
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
|
@check_config_attr
|
||||||
def variant_base_rating(self, variant_name: str, player_count: int) -> int:
|
def variant_base_rating(self, variant_name: str, player_count: int) -> int:
|
||||||
global_base_rating = self._config["variant_base_rating"]
|
global_base_rating = self._config["variant_base_rating"]
|
||||||
|
|
|
@ -44,3 +44,6 @@ FORBIDDEN_GAME_OPTIONS = [
|
||||||
# Cache time (in seconds) for history requests of players
|
# 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.
|
# In case of frequent reruns (especially during development), we do not want to stress the server too much.
|
||||||
USER_HISTORY_CACHE_TIME = 5 * 60
|
USER_HISTORY_CACHE_TIME = 5 * 60
|
||||||
|
|
||||||
|
# Fraction of seeds which is assumed to be unwinnable
|
||||||
|
UNWINNABLE_SEED_FRACTION = 0.02
|
||||||
|
|
|
@ -5,18 +5,43 @@ from database import conn_manager
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
|
|
||||||
from log_setup import logger
|
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 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 variant_rating: Rating of the variant that was played
|
||||||
@param win: Whether the team won the game
|
@param win: Whether the team won the game
|
||||||
@return: Mapping of user ids to their rating *changes* and *change* in variant rating
|
@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
|
expected_score = sum(expected_result(player_rating, variant_rating) for player_rating in user_ratings.values()) / len(user_ratings)
|
||||||
return {user_id: 1 for user_id in user_ratings.keys()}, -1
|
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():
|
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()
|
league_id, num_players, score, num_suits, clue_starved, variant_id = cur.fetchone()
|
||||||
|
|
||||||
# Fetch game participants
|
# Fetch game participants and how many games they played each so far
|
||||||
cur.execute("SELECT game_participants.user_id FROM games "
|
cur.execute("SELECT game_participants.user_id, COUNT(games.id) "
|
||||||
|
"FROM game_participants "
|
||||||
|
"INNER JOIN games "
|
||||||
|
" ON games.id = game_participants.game_id "
|
||||||
|
"WHERE user_id IN"
|
||||||
|
" ("
|
||||||
|
" SELECT game_participants.user_id FROM games "
|
||||||
" INNER JOIN game_participants "
|
" INNER JOIN game_participants "
|
||||||
" ON games.id = game_participants.game_id "
|
" ON games.id = game_participants.game_id "
|
||||||
"WHERE games.id = %s",
|
" WHERE games.id = %s"
|
||||||
(game_id,)
|
" )"
|
||||||
|
"AND league_id <= %s "
|
||||||
|
"GROUP BY user_id",
|
||||||
|
(game_id, league_id)
|
||||||
)
|
)
|
||||||
user_ids = cur.fetchall()
|
games_played = {}
|
||||||
if len(user_ids) != num_players:
|
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(
|
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)
|
logger.error(err_msg)
|
||||||
raise ValueError(err_msg)
|
raise ValueError(err_msg)
|
||||||
|
|
||||||
# Fetch current ratings of variant and players involved
|
# Fetch current ratings of variant and players involved
|
||||||
rating_type = utils.get_rating_type(clue_starved)
|
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)
|
variant_rating = get_current_variant_rating(variant_id, num_players)
|
||||||
|
|
||||||
# Calculate changes in rating
|
# 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 = compute_rating_changes(user_ratings, games_played, variant_rating, score == 5 * num_suits)
|
||||||
user_changes, variant_change = rating_change(user_ratings, variant_rating, score == 5 * num_suits)
|
|
||||||
|
|
||||||
# Update database for variants
|
# Update database for variants
|
||||||
cur.execute("INSERT INTO variant_ratings (league_id, variant_id, num_players, change, value_after) "
|
cur.execute("INSERT INTO variant_ratings (league_id, variant_id, num_players, change, value_after) "
|
||||||
|
|
Loading…
Reference in a new issue