Add code to analyze all games for bdrs

Also update stats code to accumulate these into the user stats
This commit is contained in:
Maximilian Keßler 2023-11-24 12:19:37 +01:00
parent 852f2ea329
commit ddd751f0f3
Signed by: max
GPG key ID: BCC5A619923C0BA5
3 changed files with 88 additions and 16 deletions

View file

@ -320,7 +320,7 @@ DROP TABLE IF EXISTS game_statistics CASCADE;
CREATE TABLE game_statistics ( CREATE TABLE game_statistics (
game_id INTEGER PRIMARY KEY REFERENCES games (id), 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. */ /** 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 num_crits_lost SMALLINT
); );

View file

@ -1,9 +1,10 @@
from typing import List from typing import List, Tuple
import psycopg2.extras import psycopg2.extras
from database import conn_manager from database import conn_manager
import hanabi.hanab_game import hanabi.hanab_game
from log_setup import logger
def store_actions(game_id: int, actions: List[hanabi.hanab_game.Action]): 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( actions.append(
hanabi.hanab_game.Action(hanabi.hanab_game.ActionType(action_type), target, value) 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 return actions
@ -69,4 +74,34 @@ def load_deck(seed: str) -> List[hanabi.hanab_game.DeckCard]:
deck.append( deck.append(
hanabi.hanab_game.DeckCard(suit_index, rank, card_index) 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 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

View file

@ -1,9 +1,11 @@
import enum import enum
from typing import List, Tuple from typing import List, Tuple, Set
from hanabi import hanab_game from hanabi import hanab_game
import utils import utils
from database import conn_manager from database import conn_manager
import games_db_interface
from log_setup import logger
class GameOutcome(enum.Enum): class GameOutcome(enum.Enum):
@ -17,22 +19,29 @@ class GameOutcome(enum.Enum):
class GameAnalysisResult: 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.outcome = GameOutcome
self.bdrs = bdrs 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 # List of bdrs
bdrs = [] bdrs = []
# This is the default value if we find no other reason why the game was lost (or won) # 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) game = hanab_game.GameState(instance)
def handle_lost_card(card, game, play: bool): def handle_lost_card(card, game, play: bool):
if not game.is_trash(card): if not game.is_trash(card):
if game.is_critical(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: elif card.rank != 1:
if card in game.deck[game.progress:]: if card in game.deck[game.progress:]:
bdrs.append((card, game.draw_pile_size)) 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] bombed_card = instance.deck[action.target]
handle_lost_card(bombed_card, game, True) handle_lost_card(bombed_card, game, True)
game.make_action(action) game.make_action(action)
if game.pace < 0 and GameOutcome.out_of_pace not in outcomes: if game.pace < 0:
outcomes.append(GameOutcome.out_of_pace) outcomes.add(GameOutcome.out_of_pace)
if game.strikes == 3: 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]: 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: 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(): def update_user_statistics():
@ -106,7 +143,7 @@ def update_user_statistics():
" SUM(games.num_turns)," " SUM(games.num_turns),"
" COUNT(*)," " COUNT(*),"
" COUNT(*) FILTER ( WHERE variants.num_suits * 5 = games.score )," " COUNT(*) FILTER ( WHERE variants.num_suits * 5 = games.score ),"
" SUM (game_statistics.bottom_deck_risk)" " SUM (game_statistics.num_bottom_deck_risks)"
"FROM users" "FROM users"
" INNER JOIN game_participants " " INNER JOIN game_participants "
" ON game_participants.user_id = users.id " " ON game_participants.user_id = users.id "
@ -120,9 +157,9 @@ def update_user_statistics():
" ) " " ) "
"ON CONFLICT (user_id, variant_type) DO UPDATE " "ON CONFLICT (user_id, variant_type) DO UPDATE "
"SET" "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)) (utils.get_rating_type(True), utils.get_rating_type(False))
) )
cur.execute( cur.execute(