2023-11-23 18:11:34 +01:00
|
|
|
import enum
|
|
|
|
from typing import List, Tuple
|
|
|
|
|
|
|
|
from hanabi import hanab_game
|
2023-11-23 21:33:32 +01:00
|
|
|
import utils
|
2023-11-23 18:31:59 +01:00
|
|
|
from database import conn_manager
|
|
|
|
|
2023-11-23 18:11:34 +01:00
|
|
|
|
|
|
|
class GameOutcome(enum.Enum):
|
|
|
|
win = 0
|
|
|
|
discard_crit = 1
|
|
|
|
bomb_crit = 2
|
|
|
|
strikeout = 3
|
|
|
|
bottom_deck = 4
|
|
|
|
vote_to_kill = 5
|
|
|
|
out_of_pace = 6
|
|
|
|
|
|
|
|
|
|
|
|
class GameAnalysisResult:
|
|
|
|
def __init__(self, outcomes: List[GameOutcome], bdrs: List[Tuple[hanab_game.DeckCard, int]]):
|
|
|
|
self.outcome = GameOutcome
|
|
|
|
self.bdrs = bdrs
|
|
|
|
|
|
|
|
|
|
|
|
def analyze_game(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 = []
|
|
|
|
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)
|
|
|
|
elif card.rank != 1:
|
|
|
|
if card in game.deck[game.progress:]:
|
|
|
|
bdrs.append((card, game.draw_pile_size))
|
|
|
|
else:
|
|
|
|
if game.deck[game.progress:].count(card) == 2:
|
|
|
|
bdrs.append((card, game.draw_pile_size))
|
|
|
|
|
|
|
|
for action in actions:
|
|
|
|
if action.type == hanab_game.ActionType.Discard:
|
|
|
|
discarded_card = instance.deck[action.target]
|
|
|
|
handle_lost_card(discarded_card, game, False)
|
|
|
|
if action.type == hanab_game.ActionType.Play:
|
|
|
|
played_card = instance.deck[action.target]
|
|
|
|
if not game.is_playable(played_card) and not game.is_trash(played_card):
|
|
|
|
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.strikes == 3:
|
|
|
|
outcomes.append(GameOutcome.strikeout)
|
|
|
|
elif actions[-1].type in [hanab_game.ActionType.EndGame, hanab_game.ActionType.VoteTerminate]:
|
|
|
|
outcomes.append(GameOutcome.vote_to_kill)
|
|
|
|
if game.score == 5 * instance.num_suits:
|
|
|
|
outcomes.append(GameOutcome.win)
|
|
|
|
|
|
|
|
return GameAnalysisResult(outcomes, bdrs)
|
2023-11-23 18:31:59 +01:00
|
|
|
|
|
|
|
|
2023-11-23 21:33:32 +01:00
|
|
|
def update_user_statistics():
|
2023-11-23 18:31:59 +01:00
|
|
|
"""
|
|
|
|
Update the cumulative user statistics for this user, assuming that the corresponding game statistics have
|
|
|
|
been computed already.
|
|
|
|
@param user_ids:
|
|
|
|
@return:
|
|
|
|
"""
|
2023-11-23 21:33:32 +01:00
|
|
|
# Note that some of these statistics could be computed by updating them on each new game insertion.
|
|
|
|
# However, it would be tedious to ensure that *every* new game triggers an update of these statistics.
|
|
|
|
# Also, this would be error-prone, since doing a mistake once means that values will be off forever
|
|
|
|
# (unless the DB is reset).
|
|
|
|
# Since it is cheap to accumulate some values over the whole DB, we therefore recreate the statistics as a whole,
|
|
|
|
# reusing only the individual results (that never change and therefore can only be missing, but never wrong)
|
|
|
|
cur = conn_manager.get_new_cursor()
|
|
|
|
|
|
|
|
# Update total number of moves
|
|
|
|
for clue_starved in [True, False]:
|
|
|
|
# We insert 0 here to ensure that we have an entry for each player
|
|
|
|
# Note that this will immediately be changed by the next query in case it is nonzero,
|
|
|
|
# so the zero value never shows up in the database if it was nonzero before.
|
|
|
|
cur.execute(
|
|
|
|
"INSERT INTO user_statistics (user_id, variant_type, total_game_moves)"
|
|
|
|
" ("
|
|
|
|
" SELECT id, %s, %s FROM users"
|
|
|
|
" )"
|
|
|
|
"ON CONFLICT (user_id, variant_type) DO UPDATE "
|
|
|
|
"SET total_game_moves = EXCLUDED.total_game_moves",
|
|
|
|
(utils.get_rating_type(clue_starved), 0)
|
|
|
|
)
|
|
|
|
cur.execute(
|
|
|
|
"INSERT INTO user_statistics (user_id, variant_type, total_game_moves)"
|
|
|
|
" ("
|
|
|
|
" SELECT users.id, %s, SUM(games.num_turns) FROM users "
|
|
|
|
" LEFT OUTER JOIN game_participants "
|
|
|
|
" ON game_participants.user_id = users.id "
|
|
|
|
" LEFT OUTER JOIN games "
|
|
|
|
" ON game_participants.game_id = games.id "
|
|
|
|
" LEFT OUTER JOIN variants"
|
|
|
|
" ON variants.id = games.variant_id "
|
|
|
|
" WHERE variants.clue_starved = %s OR variants.clue_starved IS NULL"
|
|
|
|
" GROUP BY users.id "
|
|
|
|
" ) "
|
|
|
|
"ON CONFLICT (user_id, variant_type) DO UPDATE "
|
|
|
|
"SET total_game_moves = EXCLUDED.total_game_moves",
|
|
|
|
(utils.get_rating_type(clue_starved), clue_starved)
|
|
|
|
)
|
|
|
|
conn_manager.get_connection().commit()
|