From ddd751f0f3a09d94be32b10df73d9c4dce708ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Fri, 24 Nov 2023 12:19:37 +0100 Subject: [PATCH] Add code to analyze all games for bdrs Also update stats code to accumulate these into the user stats --- install/database_schema.sql | 2 +- src/games_db_interface.py | 37 ++++++++++++++++++++- src/stats.py | 65 +++++++++++++++++++++++++++++-------- 3 files changed, 88 insertions(+), 16 deletions(-) diff --git a/install/database_schema.sql b/install/database_schema.sql index 0498889..899940b 100644 --- a/install/database_schema.sql +++ b/install/database_schema.sql @@ -320,7 +320,7 @@ DROP TABLE IF EXISTS game_statistics CASCADE; CREATE TABLE game_statistics ( game_id INTEGER PRIMARY KEY REFERENCES games (id), /** I'd say all of the following can just be null in case we have not evaluated them yet. */ - bottom_deck_risk SMALLINT, + num_bottom_deck_risks SMALLINT, num_crits_lost SMALLINT ); diff --git a/src/games_db_interface.py b/src/games_db_interface.py index 13b3313..e42e6b8 100644 --- a/src/games_db_interface.py +++ b/src/games_db_interface.py @@ -1,9 +1,10 @@ -from typing import List +from typing import List, Tuple import psycopg2.extras from database import conn_manager import hanabi.hanab_game +from log_setup import logger def store_actions(game_id: int, actions: List[hanabi.hanab_game.Action]): @@ -53,6 +54,10 @@ def load_actions(game_id: int) -> List[hanabi.hanab_game.Action]: actions.append( hanabi.hanab_game.Action(hanabi.hanab_game.ActionType(action_type), target, value) ) + if len(actions) == 0: + err_msg = "Failed to load actions for game id {} from DB: No actions stored.".format(game_id) + logger.error(err_msg) + raise ValueError(err_msg) return actions @@ -69,4 +74,34 @@ def load_deck(seed: str) -> List[hanabi.hanab_game.DeckCard]: deck.append( hanabi.hanab_game.DeckCard(suit_index, rank, card_index) ) + if len(deck) == 0: + err_msg = "Failed to load deck for seed {} from DB: No cards stored.".format(seed) + logger.error(err_msg) + raise ValueError(err_msg) return deck + + +def load_game(game_id: int) -> Tuple[hanabi.hanab_game.HanabiInstance, List[hanabi.hanab_game.Action]]: + cur = conn_manager.get_new_cursor() + cur.execute( + "SELECT games.num_players, games.seed, variants.clue_starved " + "FROM games " + "INNER JOIN variants" + " ON games.variant_id = variants.id " + "WHERE games.id = %s", + (game_id,) + ) + res = cur.fetchone() + if res is None: + err_msg = "Failed to retrieve game details of game {}.".format(game_id) + logger.error(err_msg) + raise ValueError(err_msg) + + # Unpack results now + (num_players, seed, clue_starved) = res + + actions = load_actions(game_id) + deck = load_deck(seed) + + instance = hanabi.hanab_game.HanabiInstance(deck, num_players, clue_starved=clue_starved) + return instance, actions diff --git a/src/stats.py b/src/stats.py index 8ad0e64..4fda873 100644 --- a/src/stats.py +++ b/src/stats.py @@ -1,9 +1,11 @@ import enum -from typing import List, Tuple +from typing import List, Tuple, Set from hanabi import hanab_game import utils from database import conn_manager +import games_db_interface +from log_setup import logger class GameOutcome(enum.Enum): @@ -17,22 +19,29 @@ class GameOutcome(enum.Enum): class GameAnalysisResult: - def __init__(self, outcomes: List[GameOutcome], bdrs: List[Tuple[hanab_game.DeckCard, int]]): + def __init__(self, + outcomes: Set[GameOutcome], + bdrs: List[Tuple[hanab_game.DeckCard, int]], + lost_crits: List[hanab_game.DeckCard] + ): self.outcome = GameOutcome self.bdrs = bdrs + self.lost_crits = lost_crits -def analyze_game(instance: hanab_game.HanabiInstance, actions: List[hanab_game.Action]) -> GameAnalysisResult: +def analyze_replay(instance: hanab_game.HanabiInstance, actions: List[hanab_game.Action]) -> GameAnalysisResult: # List of bdrs bdrs = [] # This is the default value if we find no other reason why the game was lost (or won) - outcomes = [] + outcomes = set() + lost_crits = [] game = hanab_game.GameState(instance) def handle_lost_card(card, game, play: bool): if not game.is_trash(card): if game.is_critical(card): - outcomes.append(GameOutcome.bomb_crit if play else GameOutcome.discard_crit) + outcomes.add(GameOutcome.bomb_crit if play else GameOutcome.discard_crit) + lost_crits.append(card) elif card.rank != 1: if card in game.deck[game.progress:]: bdrs.append((card, game.draw_pile_size)) @@ -50,17 +59,45 @@ def analyze_game(instance: hanab_game.HanabiInstance, actions: List[hanab_game.A bombed_card = instance.deck[action.target] handle_lost_card(bombed_card, game, True) game.make_action(action) - if game.pace < 0 and GameOutcome.out_of_pace not in outcomes: - outcomes.append(GameOutcome.out_of_pace) + if game.pace < 0: + outcomes.add(GameOutcome.out_of_pace) if game.strikes == 3: - outcomes.append(GameOutcome.strikeout) + outcomes.add(GameOutcome.strikeout) elif actions[-1].type in [hanab_game.ActionType.EndGame, hanab_game.ActionType.VoteTerminate]: - outcomes.append(GameOutcome.vote_to_kill) + outcomes.add(GameOutcome.vote_to_kill) if game.score == 5 * instance.num_suits: - outcomes.append(GameOutcome.win) + outcomes.add(GameOutcome.win) - return GameAnalysisResult(outcomes, bdrs) + return GameAnalysisResult(outcomes, bdrs, lost_crits) + + +def analyze_game_and_store_stats(game_id: int): + instance, actions = games_db_interface.load_game(game_id) + analysis = analyze_replay(instance, actions) + + cur = conn_manager.get_new_cursor() + cur.execute( + "INSERT INTO game_statistics (game_id, num_bottom_deck_risks, num_crits_lost) " + "VALUES (%s, %s, %s) " + "ON CONFLICT (game_id) DO UPDATE " + "SET (num_crits_lost, num_bottom_deck_risks) = (EXCLUDED.num_crits_lost, EXCLUDED.num_bottom_deck_risks)", + (game_id, len(analysis.bdrs), len(analysis.lost_crits)) + ) + conn_manager.get_connection().commit() + + +def analyze_all_games(): + cur = conn_manager.get_new_cursor() + cur.execute( + "SELECT id FROM games " + "LEFT OUTER JOIN game_statistics " + " ON games.id = game_statistics.game_id " + "WHERE game_statistics.game_id IS NULL " + "ORDER BY games.id" + ) + for (game_id, ) in cur.fetchall(): + analyze_game_and_store_stats(game_id) def update_user_statistics(): @@ -106,7 +143,7 @@ def update_user_statistics(): " SUM(games.num_turns)," " COUNT(*)," " COUNT(*) FILTER ( WHERE variants.num_suits * 5 = games.score )," - " SUM (game_statistics.bottom_deck_risk)" + " SUM (game_statistics.num_bottom_deck_risks)" "FROM users" " INNER JOIN game_participants " " ON game_participants.user_id = users.id " @@ -120,9 +157,9 @@ def update_user_statistics(): " ) " "ON CONFLICT (user_id, variant_type) DO UPDATE " "SET" - " (total_game_moves, games_played, games_won)" + " (total_game_moves, games_played, games_won, total_bdr)" " =" - " (EXCLUDED.total_game_moves, EXCLUDED.games_played, EXCLUDED.games_won)", + " (EXCLUDED.total_game_moves, EXCLUDED.games_played, EXCLUDED.games_won, EXCLUDED.total_bdr)", (utils.get_rating_type(True), utils.get_rating_type(False)) ) cur.execute(