diff --git a/database.py b/database.py index 8bc7d26..1b6a1a8 100644 --- a/database.py +++ b/database.py @@ -1,5 +1,5 @@ import json -from typing import Optional +from typing import Optional, List, Dict import psycopg2 import psycopg2.extensions @@ -9,6 +9,7 @@ import requests import unidecode import constants +import utils from config import config_manager from log_setup import logger @@ -123,17 +124,44 @@ def initialize_variant_base_ratings(): conn.commit() -def normalize_username(username: str) -> str: - decoded = unidecode.unidecode(username) - return decoded.lower() - - -def get_user_id(player_name: str) -> Optional[id]: +def get_user_id(player_name: str) -> Optional[int]: cur = conn_manager.get_new_cursor() cur.execute("SELECT id FROM users WHERE player_name = (%s)", (player_name,)) return cur.fetchone() +def get_user_ids_from_normalized_usernames(normalized_usernames: List[str]) -> List[int] | str: + """ + @rtype: If all users are registered, list of their ids in the same order. + Otherwise, name of a user that is not registered. + @warning If usernames are not present in the database, there is no corresponding key in the returned dictionary + """ + cur = conn_manager.get_new_cursor() + cur.execute("SELECT normalized_username, user_id " + "FROM user_accounts " + "WHERE normalized_username IN ({})".format(",".join("%s" for _ in normalized_usernames)), + normalized_usernames + ) + # Build up dict from the specified user ids + user_dict: Dict[str, int] = {} + for normalized_username, user_id in cur.fetchall(): + user_dict[normalized_username] = user_id + + user_ids = [] + for normalized_username in normalized_usernames: + if normalized_username not in user_dict.keys(): + return normalized_username + else: + user_ids.append(user_dict[normalized_username]) + return user_ids + + +def get_variant_id(variant_name: str) -> Optional[int]: + cur = conn_manager.get_new_cursor() + cur.execute("SELECT id FROM variants WHERE name = %s", (variant_name,)) + return cur.fetchone() + + def add_player_name(player_name: str): conn = conn_manager.get_connection() cur = conn.cursor() @@ -146,7 +174,7 @@ def add_player_name(player_name: str): def add_user_name_to_player(hanabi_username: str, player_name: str): - normalized_username = normalize_username(hanabi_username) + normalized_username = utils.normalize_username(hanabi_username) user_id = get_user_id(player_name) if user_id is None: logger.error("Player {} not found in database, cannot add username to it.".format(player_name)) diff --git a/deps/py-hanabi b/deps/py-hanabi index 40baa59..daea750 160000 --- a/deps/py-hanabi +++ b/deps/py-hanabi @@ -1 +1 @@ -Subproject commit 40baa59bd3b6ad622b39b975a363d65a5bfe39c0 +Subproject commit daea75053553fcc18cbc4dffaf00a7cf94fd32a1 diff --git a/fetch_games.py b/fetch_games.py index 3e9f860..2222831 100644 --- a/fetch_games.py +++ b/fetch_games.py @@ -5,8 +5,12 @@ import platformdirs import requests_cache import psycopg2.extras +import hanabi.live.hanab_live +import hanabi.hanab_game + import constants import database +import utils from database import conn_manager from log_setup import logger from config import config_manager @@ -48,7 +52,8 @@ def fetch_games_for_player(username: str, latest_game_id: int): def process_game_entry(game_json: Dict, username_dict: Dict, variant_ids: List[int]) -> Optional[GameInfo]: logger.debug("Processing entry {}".format(game_json)) config = config_manager.get_config() - # Check if the game is one that we accept + + # Parse game properties game_id = game_json["id"] players = game_json["playerNames"] num_players = len(players) @@ -59,20 +64,16 @@ def process_game_entry(game_json: Dict, username_dict: Dict, variant_ids: List[i game_options = game_json["options"] var_id = game_options["variantID"] - normalized_usernames = [database.normalize_username(username) for username in players] + normalized_usernames = [utils.normalize_username(username) for username in players] - # Check that the game has no special options enabled - for forbidden_option in constants.FORBIDDEN_GAME_OPTIONS: - if game_options.get(forbidden_option, False): - logger.debug("Rejected game {} due to option {} set".format(game_id, forbidden_option)) - return + # Now, check if the game is one that we accept for league - # Check if player count matches - if not (config.min_player_count <= num_players <= config.max_player_count): - logger.debug("Rejected game {} due to invalid number of players ({})".format(game_id, num_players)) + if not utils.are_game_options_allowed(game_id, game_options): + return + + if not utils.is_player_count_allowed(game_id, num_players): return - # Check if the variant was ok if var_id not in variant_ids: logger.debug("Rejected game {} due to invalid variant id {}".format(game_id, var_id)) return @@ -188,10 +189,69 @@ def detailed_fetch_game(game_id: int): logger.error(err_msg) raise ConnectionError(err_msg) game_json = json.loads(response.text) + + instance, actions = hanabi.live.hanab_live.parse_json_game(game_json, False) + print(instance, actions, instance.deck) + print(game_json) + + game_id = game_json["id"] + players = game_json["players"] + num_players = len(players) + seed = game_json["seed"] + game_options = game_json.get("options", {}) + var_name = game_options.get("variant", "No Variant") + + if not utils.are_game_options_allowed(game_id, game_options): + return + + if not utils.is_player_count_allowed(game_id, num_players): + return + + var_id = database.get_variant_id(var_name) + if var_id is None: + logger.debug("Rejected game {} due to invalid variant id {}".format(game_id, var_id)) + return + + # All game options are ok, now check if the number of players is okay. + normalized_usernames = [utils.normalize_username(username) for username in players] + user_ids = database.get_user_ids_from_normalized_usernames(normalized_usernames) + # The return value here is a str if there was an unregistered participant + if type(user_ids) is str: + logger.debug("Rejected game {} due to unregistered participant {}".format(game_id, user_ids)) + return + + # Now, we can start to actually process the game details + instance, actions = hanabi.live.hanab_live.parse_json_game(game_json, False) + game = hanabi.hanab_game.GameState(instance) + for action in actions: + game.make_action(action) + + conn = conn_manager.get_connection() cur = conn_manager.get_new_cursor() - cur.execute("SELECT user_accounts.normalized_username, user_accounts.user_id FROM user_accounts") - user_name_dict: Dict[str, int] = {} - for username, user_id in cur.fetchall(): - user_name_dict[username] = user_id - info = process_game_entry(game_json, user_name_dict, database.get_variant_ids()) - print(info) + # Insert the game into the database if not present + cur.execute("INSERT INTO games " + "(id, num_players, variant_id, seed, score, num_turns) " + "VALUES " + "(%s, %s, %s, %s, %s, %s) " + "ON CONFLICT (id) DO NOTHING", + (game_id, num_players, var_id, seed, game.score, len(actions))) + + game_participants_vals = [] + for seat, user_id in enumerate(user_ids): + tup = (game_id, user_id, seat) + game_participants_vals.append(tup) + + # This inserts the game participants now. + # Note that the participants might already be stored, but not their seat (since we do not know the seat when only + # getting a game list from the server on fetching the games played by some player) + psycopg2.extras.execute_values( + cur, + "INSERT INTO game_participants " + "(game_id, user_id, seat) " + "VALUES %s " + "ON CONFLICT (game_id, user_id) DO UPDATE " + "SET seat = EXCLUDED.seat", + game_participants_vals + ) + # DB is now in a consistent state again: We made sure the game and its participants are added. + conn.commit() diff --git a/requirements.txt b/requirements.txt index 59e53d1..3666148 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ python-dateutil unidecode requests requests_cache +termcolor diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..5fbcfd7 --- /dev/null +++ b/utils.py @@ -0,0 +1,36 @@ +from typing import Dict + +import unidecode + +import constants +from config import config_manager + +from log_setup import logger + + +def normalize_username(username: str) -> str: + decoded = unidecode.unidecode(username) + return decoded.lower() + + +def are_game_options_allowed(game_id: int, game_options: Dict) -> bool: + """ + Check if the game options are allowed for league. + """ + for forbidden_option in constants.FORBIDDEN_GAME_OPTIONS: + if game_options.get(forbidden_option, False): + logger.debug("Rejected game {} due to option {} set".format(game_id, forbidden_option)) + return False + # All options ok + return True + + +def is_player_count_allowed(game_id: int, num_players: int) -> bool: + """ + Check if the player count is allowed for league. + """ + config = config_manager.get_config() + if not (config.min_player_count <= num_players <= config.max_player_count): + logger.debug("Rejected game {} due to invalid number of players ({})".format(game_id, num_players)) + return False + return True