diff --git a/install/database_schema.sql b/install/database_schema.sql index 724f2db..d51724f 100644 --- a/install/database_schema.sql +++ b/install/database_schema.sql @@ -260,7 +260,7 @@ CREATE TABLE variant_base_ratings ( DROP TABLE IF EXISTS variant_ratings CASCADE; CREATE TABLE variant_ratings ( /** This should reference the game that triggered the elo update */ - league_id INTEGER PRIMARY KEY REFERENCES games, + league_id INTEGER PRIMARY KEY REFERENCES games (league_id), variant_id SMALLINT NOT NULL, player_count SMALLINT NOT NULL, @@ -293,7 +293,7 @@ CREATE TABLE user_ratings ( * 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, + league_id INTEGER REFERENCES games (league_id), user_id SMALLINT NOT NULL, type SMALLINT NOT NULL, @@ -301,12 +301,15 @@ CREATE TABLE user_ratings ( /** * Do we want to store this here as well? Would be nice to be displayed in some elo page imo. * Note: We don't need to store the result (i guess), since we can easily retrieve that info by looking up the game using the league_id + * TODO: Since I'm not even sure on the rating model yet (I could imagine something slightly different than a team rating), + * I'll leave this here as potentially null for now and don't implement it. */ - team_rating REAL NOT NULL, + team_rating REAL, change REAL NOT NULL, value_after REAL NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (user_id) REFERENCES users (id), + CONSTRAINT user_change_per_game_unique UNIQUE (league_id, user_id) ); diff --git a/ratings.py b/ratings.py index 3121b4d..300b1a8 100644 --- a/ratings.py +++ b/ratings.py @@ -1,11 +1,24 @@ -from typing import List, Dict +from typing import List, Dict, Tuple import utils from database import conn_manager +import psycopg2.extras from log_setup import logger +def rating_change(user_ratings: 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 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 + + def next_game_to_rate(): cur = conn_manager.get_new_cursor() cur.execute("SELECT games.id FROM games " @@ -23,7 +36,7 @@ def next_game_to_rate(): return game_id -def get_current_ratings(user_ids: List[int], rating_type: int) -> Dict[int, float]: +def get_current_user_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 @@ -41,6 +54,8 @@ def get_current_ratings(user_ids: List[int], rating_type: int) -> Dict[int, floa " GROUP BY (user_id, type)" " ) AS latest_user_ratings " " ON user_ratings.league_id = latest_user_ratings.max_league_id " + "WHERE user_ratings.user_id IN ({}) AND user_ratings.type = %s".format(", ".join("%s" for _ in user_ids)), + user_ids + [rating_type] ) current_ratings = cur.fetchall() @@ -53,6 +68,38 @@ def get_current_ratings(user_ids: List[int], rating_type: int) -> Dict[int, floa return ratings +def get_current_variant_rating(variant_id: int, player_count: int) -> float: + cur = conn_manager.get_new_cursor() + cur.execute("SELECT value_after FROM variant_ratings " + "INNER JOIN (" + " SELECT variant_id, player_count, MAX(league_id) AS max_league_id" + " FROM variant_ratings " + " GROUP BY (variant_id, player_count)" + " ) AS latest_variant_ratings " + " ON variant_ratings.league_id = latest_variant_ratings.max_league_id " + "WHERE variant_ratings.variant_id = %s AND variant_ratings.player_count = %s", + (variant_id, player_count) + ) + query_result = cur.fetchone() + if query_result is not None: + (current_rating, ) = query_result + return current_rating + + # Reaching this point of code execution just means this is the first game for this variant rating + cur.execute("SELECT rating FROM variant_base_ratings " + "WHERE variant_id = %s AND player_count = %s", + (variant_id, player_count) + ) + query_result = cur.fetchone() + if query_result is None: + err_msg = "Failed to get current variant rating for variant {}.".format(variant_id) + logger.error(err_msg) + raise ValueError(err_msg) + + (base_rating, ) = query_result + return base_rating + + def process_rating_of_next_game(): game_id = next_game_to_rate() if game_id is None: @@ -60,14 +107,16 @@ def process_rating_of_next_game(): 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() + # Fetch data on the game played + cur.execute( + "SELECT games.league_id, games.num_players, games.score, variants.num_suits, variants.clue_starved, variants.id " + "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, variant_id = cur.fetchone() cur.execute("SELECT game_participants.user_id FROM games " "INNER JOIN game_participants " @@ -84,4 +133,23 @@ def process_rating_of_next_game(): raise ValueError(err_msg) rating_type = utils.get_rating_type(clue_starved) - ratings = get_current_ratings(user_ids, rating_type) + user_ratings = get_current_user_ratings(user_ids, rating_type) + variant_rating = get_current_variant_rating(variant_id, num_players) + + user_changes, variant_change = rating_change(user_ratings, variant_rating, score == 5 * num_suits) + cur.execute("INSERT INTO variant_ratings (league_id, variant_id, player_count, change, value_after) " + "VALUES (%s, %s, %s, %s, %s)", + (league_id, variant_id, num_players, variant_change, variant_rating + variant_change) + ) + + user_ratings_vals = [] + for user_id, change in user_changes.items(): + user_ratings_vals.append((league_id, user_id, rating_type, change, user_ratings[user_id] + change)) + + psycopg2.extras.execute_values( + cur, + "INSERT INTO user_ratings (league_id, user_id, type, change, value_after) " + "VALUES %s", + user_ratings_vals + ) + conn_manager.get_connection().commit()