From 6ad728de4d69d7144d1fd9cd2f38192e9257ba06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Tue, 12 Nov 2024 13:58:49 +0100 Subject: [PATCH] Store cert games in DB. Check for bottom/topdeck losses --- src/hanabi/database/games_db_interface.py | 8 +- src/hanabi/live/generate_seeds.py | 21 +++- src/hanabi/solvers/deck_analyzer.py | 114 +++++++++++++++++++++- 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/hanabi/database/games_db_interface.py b/src/hanabi/database/games_db_interface.py index 115ad6e..87caaf5 100644 --- a/src/hanabi/database/games_db_interface.py +++ b/src/hanabi/database/games_db_interface.py @@ -93,7 +93,7 @@ def load_instance(seed: str) -> Optional[hanabi.live.hanab_live.HanabLiveInstanc return hanabi.live.hanab_live.HanabLiveInstance(deck, num_players, var_id) -def load_game_parts(game_id: int) -> Tuple[hanabi.live.hanab_live.HanabLiveInstance, List[hanabi.hanab_game.Action]]: +def load_game_parts(game_id: int, cert_game: bool = False) -> Tuple[hanabi.live.hanab_live.HanabLiveInstance, List[hanabi.hanab_game.Action]]: """ Loads information on game from database @param game_id: ID of game @@ -119,7 +119,7 @@ def load_game_parts(game_id: int) -> Tuple[hanabi.live.hanab_live.HanabLiveInsta # Unpack results now (num_players, seed, one_extra_card, one_less_card, deck_plays, all_or_nothing, clue_starved, variant_name, variant_id, throw_it_in_a_hole) = res - actions = load_actions(game_id) + actions = load_actions(game_id, cert_game) deck = load_deck(seed) instance = hanabi.live.hanab_live.HanabLiveInstance( @@ -136,8 +136,8 @@ def load_game_parts(game_id: int) -> Tuple[hanabi.live.hanab_live.HanabLiveInsta return instance, actions -def load_game(game_id: int) -> hanabi.live.hanab_live.HanabLiveGameState: - instance, actions = load_game_parts(game_id) +def load_game(game_id: int, cert_game: bool = False) -> hanabi.live.hanab_live.HanabLiveGameState: + instance, actions = load_game_parts(game_id, cert_game) game = hanabi.live.hanab_live.HanabLiveGameState(instance) for action in actions: game.make_action(action) diff --git a/src/hanabi/live/generate_seeds.py b/src/hanabi/live/generate_seeds.py index 2e891b2..c042715 100644 --- a/src/hanabi/live/generate_seeds.py +++ b/src/hanabi/live/generate_seeds.py @@ -1,9 +1,13 @@ +import hanabi.live.compress from hanabi.hanab_game import DeckCard from hanabi import database from hanabi.live.variants import Variant from hanabi.database import games_db_interface import random +from src.hanabi.solvers.sat import solve_sat + + def get_deck(variant: Variant): deck = [] for suit_index, suit in enumerate(variant.suits): @@ -33,6 +37,20 @@ def generate_deck(variant: Variant, num_players: int, seed: int, seed_class: int random.shuffle(deck) return seed, deck +def link(): + seed = "p5v0sunblinkingly-kobe-prescriptively" + + deck = database.games_db_interface.load_deck(seed) + database.cur.execute("SELECT id FROM certificate_games WHERE seed = %s", (seed,)) + (game_id, ) = database.cur.fetchone() + actions = database.games_db_interface.load_actions(game_id, True) + inst = hanabi.hanab_game.HanabiInstance(deck, 5) + game = hanabi.hanab_game.GameState(inst) + for action in actions: + game.make_action(action) + + print(hanabi.live.compress.link(game)) + def generate_decks_for_variant(variant_id: int, num_players: int, num_seeds: int, seed_class: int = 1): variant = Variant.from_db(variant_id) for seed_num in range(num_seeds): @@ -48,7 +66,8 @@ def generate_decks_for_variant(variant_id: int, num_players: int, num_seeds: int def main(): database.global_db_connection_manager.read_config() database.global_db_connection_manager.connect() - generate_decks_for_variant(0, 2, 100) + link() +# generate_decks_for_variant(0, 2, 100) if __name__ == '__main__': main() diff --git a/src/hanabi/solvers/deck_analyzer.py b/src/hanabi/solvers/deck_analyzer.py index d91d741..a21acce 100644 --- a/src/hanabi/solvers/deck_analyzer.py +++ b/src/hanabi/solvers/deck_analyzer.py @@ -1,14 +1,19 @@ +import collections from enum import Enum -from typing import List +from typing import List, Any, Optional, Tuple from dataclasses import dataclass import alive_progress +import hanabi.hanab_game from hanabi import database from hanabi import logger from hanabi import hanab_game +from hanabi.hanab_game import DeckCard from hanabi.live import compress +from hanabi.database import games_db_interface + class InfeasibilityType(Enum): Pace = 0 # idx denotes index of last card drawn before being forced to reduce pace, value denotes how bad pace is @@ -21,11 +26,13 @@ class InfeasibilityType(Enum): HandSizeWithSqueeze = 12 HandSizeWithBdr = 13 HandSizeWithBdrSqueeze = 14 + BottomTopDeck = 20 # further reasons, currently not scanned for - BottomTopDeck = 20 DoubleBottomTopDeck = 30 CritAtBottom = 40 + + # Default reason when we have nothing else SAT = 50 @@ -46,6 +53,77 @@ class InfeasibilityReason: return "Critical non-5 at bottom" +def generate_all_choices(l: List[List[Any]]): + if len(l) == 0: + yield [] + return + head, *tail = l + for option in head: + for back in generate_all_choices(tail): + yield [option] + back + +def check_for_top_bottom_deck_loss(instance: hanab_game.HanabiInstance) -> bool: + hands = [instance.deck[p * instance.hand_size : (p+1) * instance.hand_size] for p in range(instance.num_players)] + + # scan the deck in reverse order if any card is forced to be late + found = {} + # Note that only the last 4 cards are relevant for single-suit distribution loss + for i, card in enumerate(reversed(instance.deck[-4:])): + if card in found.keys(): + found[card] += 1 + else: + found[card] = 1 + + if found[card] >= 3 or (card.rank != 1 and found[card] >= 2): + max_rank_starting_extra_round = card.rank + (instance.deck_size - card.deck_index - 2) + + # Next, need to figure out what positions of cards of the same suit are fixed + positions_by_rank = [[] for _ in range(6)] + for rank in range(max_rank_starting_extra_round, 6): + for player, hand in enumerate(hands): + card_test = DeckCard(card.suitIndex, rank) + for card_hand in hand: + if card_test == card_hand: + positions_by_rank[rank].append(player) + + + # clean up where we have free choice anyway + for rank, positions in enumerate(positions_by_rank): + if rank != 5 and len(positions) < 2: + positions.clear() + if len(positions) == 0: + positions.append(None) + + + + # Now, iterate through all choices in starting hands (None stands for free choice of a card) and check them + assignment_found = False + for assignment in generate_all_choices(positions_by_rank): + cur_player = None + num_turns = 0 + for rank in range(max_rank_starting_extra_round, 6): + if cur_player is None or assignment[rank] is None: + num_turns += 1 + else: + # Note the -1 and +1 to output things in range [1,5] instead of [0,4] + num_turns += (assignment[rank] - cur_player - 1) % instance.num_players + 1 + + if assignment[rank] is not None: + cur_player = assignment[rank] + elif cur_player is not None: + cur_player = (cur_player + 1) % instance.num_players + + if num_turns <= instance.num_players + 1: + assignment_found = True + + # If no assignment worked out, the deck is infeasible because of this suit + if not assignment_found: + return True + + # If we reach this point, we checked for every card near the bottom of the deck and found a possible endgame each + return False + + def analyze(instance: hanab_game.HanabiInstance, only_find_first=False) -> List[InfeasibilityReason]: """ @@ -65,6 +143,12 @@ def analyze(instance: hanab_game.HanabiInstance, only_find_first=False) -> List[ """ reasons = [] + top_bottom_deck_loss = check_for_top_bottom_deck_loss(instance) + if top_bottom_deck_loss: + reasons.append(InfeasibilityReason(InfeasibilityType.BottomTopDeck)) + if only_find_first: + return reasons + # check for critical non-fives at bottom of the deck bottom_card = instance.deck[-1] if bottom_card.rank != 5 and bottom_card.suitIndex in instance.dark_suits: @@ -133,7 +217,8 @@ def analyze(instance: hanab_game.HanabiInstance, only_find_first=False) -> List[ artificial_crits.add(filtered_deck[-2]) # Last card in the deck can never be played - artificial_crits.add(filtered_deck[-1]) + if instance.deck[-1].rank != 5: + artificial_crits.add(instance.deck[-1]) for (i, card) in enumerate(instance.deck): if card.rank == stacks[card.suitIndex] + 1: @@ -240,3 +325,26 @@ def run_on_database(variant_id): ) bar() database.conn.commit() + + +def main(): + seed = "p5v0sporcupines-underclass-phantasmagorical" + seed = 'p5c1s98804' + seed = 'p4c1s1116' + seed = 'p5c1s14459' + num_players = 5 + database.global_db_connection_manager.read_config() + database.global_db_connection_manager.connect() + + database.cur.execute("SELECT seed, num_players FROM seeds WHERE (feasible IS NULL OR feasible = false) AND class = 1 AND num_players = 5") +# for (seed, num_players) in database.cur.fetchall(): + for _ in range(1): + deck = database.games_db_interface.load_deck(seed) + inst = hanabi.hanab_game.HanabiInstance(deck, num_players) + lost = check_for_top_bottom_deck_loss(inst) + if lost: + print(seed) + + +if __name__ == "__main__": + main() \ No newline at end of file