forked from Hanabi/hanabi-league
Add code to analyze all games for bdrs
Also update stats code to accumulate these into the user stats
This commit is contained in:
parent
852f2ea329
commit
ddd751f0f3
3 changed files with 88 additions and 16 deletions
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
65
src/stats.py
65
src/stats.py
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue