From bbb2b19df03d5baa4e9de1145cfbb4dd86215ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Thu, 23 Nov 2023 15:24:33 +0100 Subject: [PATCH] Starting rating implementation: Fetch current ratings --- install/database_schema.sql | 6 ++- ratings.py | 87 +++++++++++++++++++++++++++++++++++++ utils.py | 10 +++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 ratings.py diff --git a/install/database_schema.sql b/install/database_schema.sql index ab8078e..724f2db 100644 --- a/install/database_schema.sql +++ b/install/database_schema.sql @@ -288,7 +288,11 @@ CREATE TABLE user_base_ratings ( DROP TABLE IF EXISTS user_ratings CASCADE; CREATE TABLE user_ratings ( - /** This should reference the game that triggered the elo update. I would use the league_id here for proper ordering. */ + /** + * This should reference the game that triggered the elo update. + * I would use the league_id here for proper ordering. + * Also note that this can then be used to identify whether a given league game has already been processed for rating change. + */ league_id INTEGER PRIMARY KEY REFERENCES games, user_id SMALLINT NOT NULL, diff --git a/ratings.py b/ratings.py new file mode 100644 index 0000000..3121b4d --- /dev/null +++ b/ratings.py @@ -0,0 +1,87 @@ +from typing import List, Dict + +import utils +from database import conn_manager + +from log_setup import logger + + +def next_game_to_rate(): + cur = conn_manager.get_new_cursor() + cur.execute("SELECT games.id FROM games " + "LEFT OUTER JOIN user_ratings" + " ON games.league_id = user_ratings.league_id " + "WHERE user_ratings.league_id IS NULL " + "ORDER BY games.league_id ASC " + "LIMIT 1" + ) + query_result = cur.fetchone() + if query_result is None: + return + + (game_id,) = query_result + return game_id + + +def get_current_ratings(user_ids: List[int], rating_type: int) -> Dict[int, float]: + """ + Fetches the current ratings for specified players and rating type from DB + @return: Mapping user_id -> current rating + """ + cur = conn_manager.get_new_cursor() + cur.execute("SELECT user_id, rating FROM user_base_ratings " + "WHERE user_id IN ({}) AND type = %s".format(", ".join("%s" for _ in user_ids)), + user_ids + [rating_type] + ) + base_ratings = cur.fetchall() + cur.execute("SELECT user_ratings.user_id, value_after FROM user_ratings " + "INNER JOIN (" + " SELECT user_id, type, MAX(league_id) AS max_league_id" + " FROM user_ratings " + " GROUP BY (user_id, type)" + " ) AS latest_user_ratings " + " ON user_ratings.league_id = latest_user_ratings.max_league_id " + ) + current_ratings = cur.fetchall() + + ratings: Dict[int, float] = {} + for user_id, base_rating in base_ratings: + ratings[user_id] = base_rating + for user_id, rating in current_ratings: + ratings[user_id] = rating + + return ratings + + +def process_rating_of_next_game(): + game_id = next_game_to_rate() + if game_id is None: + logger.info("All games already processed for rating changes.") + return + logger.verbose("Processing rating for game {}".format) + cur = conn_manager.get_new_cursor() + cur.execute("SELECT games.league_id, games.num_players, games.score, variants.num_suits, variants.clue_starved " + "FROM games " + "INNER JOIN variants " + " ON games.variant_id = variants.id " + "WHERE games.id = %s", + (game_id,) + ) + league_id, num_players, score, num_suits, clue_starved = cur.fetchone() + + cur.execute("SELECT game_participants.user_id FROM games " + "INNER JOIN game_participants " + " ON games.id = game_participants.game_id " + "WHERE games.id = %s", + (game_id,) + ) + user_ids = cur.fetchall() + if len(user_ids) != 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) + ) + logger.error(err_msg) + raise ValueError(err_msg) + + rating_type = utils.get_rating_type(clue_starved) + ratings = get_current_ratings(user_ids, rating_type) diff --git a/utils.py b/utils.py index 5fbcfd7..124f7fc 100644 --- a/utils.py +++ b/utils.py @@ -34,3 +34,13 @@ def is_player_count_allowed(game_id: int, num_players: int) -> bool: logger.debug("Rejected game {} due to invalid number of players ({})".format(game_id, num_players)) return False return True + + +def get_rating_type(clue_starved: bool): + """ + Convenience function to get the magic values that we use to identify the different rating types of a player + """ + if clue_starved: + return 1 + else: + return 0