Merge branch 'greedy-solver'
This commit is contained in:
commit
558a341aeb
13 changed files with 352 additions and 120 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -12,5 +12,5 @@ remaining_cards.txt
|
||||||
lost_seeds.txt
|
lost_seeds.txt
|
||||||
utils.py
|
utils.py
|
||||||
infeasible_instances.txt
|
infeasible_instances.txt
|
||||||
debug_log.txt
|
|
||||||
verbose_log.txt
|
verbose_log.txt
|
||||||
|
debug_log.txt
|
||||||
|
|
54
cheating_strategy
Normal file
54
cheating_strategy
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
card types:
|
||||||
|
trash, playable, useful (dispensable), critical
|
||||||
|
|
||||||
|
|
||||||
|
pace := #(cards left in deck) + #players - #(cards left to play)
|
||||||
|
modified_pace := pace - #(players without useful cards)
|
||||||
|
endgame := #(cards left to play) - #(cards left in deck) = #players - pace
|
||||||
|
-> endgame >= 0 iff pace <= #players
|
||||||
|
in_endgame := endgame >= 0
|
||||||
|
|
||||||
|
discard_badness(card) :=
|
||||||
|
1 if trash
|
||||||
|
8 - #players if card useful but duplicate visible # TODO: should probably account for rank of card as well, currently, lowest one is chosen
|
||||||
|
80 - 10*rank if card is not critical but currently unique # this ensures we prefer to discard higher ranked cards
|
||||||
|
600 - 100*rank if only criticals in hand # essentially not relevant, since we are currently only optimizing for full score
|
||||||
|
|
||||||
|
|
||||||
|
Algorithm:
|
||||||
|
|
||||||
|
if (have playable card):
|
||||||
|
if (in endgame) and not (in extraround):
|
||||||
|
stall in the following situations:
|
||||||
|
- we have exactly one useful card, it is a 5, and a copy of each useful card is visible
|
||||||
|
- we have exactly one useful card, it is a 4, the player with the matching 5 has another critical card to play
|
||||||
|
- we have exactly one useful card (todo: maybe use critical here?), the deck has size 1, someone else has 2 crits
|
||||||
|
- we have exactly one playable card, it is a 4, and a further useful card, but the playable is redistributable in the following sense:
|
||||||
|
the other playing only has this one useful card, and the player holding the matching 5 sits after the to-be-redistributed player
|
||||||
|
- sth else that seems messy and is currently not understood, ignored for now
|
||||||
|
TODO: maybe introduce some midgame stalls here, since we know the deck?
|
||||||
|
play a card, matching the first of the following criteria. if several cards match, recurse with this set of cards
|
||||||
|
- if in extraround, play crit
|
||||||
|
- if in second last round and we have 2 crits, play crit
|
||||||
|
- play card with lowest rank
|
||||||
|
- play a critical card
|
||||||
|
- play unique card, i.e. not visible
|
||||||
|
- lowest suit index (for determinancy)
|
||||||
|
|
||||||
|
if 8 hints:
|
||||||
|
give a hint
|
||||||
|
|
||||||
|
if 0 hints:
|
||||||
|
discard card with lowest badness
|
||||||
|
|
||||||
|
stall in the following situations:
|
||||||
|
- #(cards in deck) == 2 and (card of rank 3 or lower is missing) and we have the connecting card
|
||||||
|
- #clues >= 8 - #(useful cards in hand), there are useful cards in the deck and either:
|
||||||
|
- the next player has no useful cards at all
|
||||||
|
- we have two more crits than the next player and they have trash
|
||||||
|
- we are in endgame and the deck only contains one card
|
||||||
|
- it is possible that no-one discards in the following round and we are not waiting for a card whose rank is smaller than pace // TODO: this feels like a weird condition
|
||||||
|
|
||||||
|
discard if (discard badness) + #hints < 10
|
||||||
|
|
||||||
|
stall if someone has a better discard
|
|
@ -0,0 +1 @@
|
||||||
|
from .database import cur, conn
|
|
@ -12,17 +12,18 @@ CREATE INDEX seeds_variant_idx ON seeds (variant_id);
|
||||||
|
|
||||||
DROP TABLE IF EXISTS games CASCADE;
|
DROP TABLE IF EXISTS games CASCADE;
|
||||||
CREATE TABLE games (
|
CREATE TABLE games (
|
||||||
id INT PRIMARY KEY,
|
id INT PRIMARY KEY,
|
||||||
seed TEXT NOT NULL REFERENCES seeds,
|
seed TEXT NOT NULL REFERENCES seeds,
|
||||||
num_players SMALLINT NOT NULL,
|
num_players SMALLINT NOT NULL,
|
||||||
score SMALLINT NOT NULL,
|
starting_player SMALLINT NOT NULL DEFAULT 0,
|
||||||
variant_id SMALLINT NOT NULL,
|
score SMALLINT NOT NULL,
|
||||||
deck_plays BOOLEAN,
|
variant_id SMALLINT NOT NULL,
|
||||||
one_extra_card BOOLEAN,
|
deck_plays BOOLEAN,
|
||||||
one_less_card BOOLEAN,
|
one_extra_card BOOLEAN,
|
||||||
all_or_nothing BOOLEAN,
|
one_less_card BOOLEAN,
|
||||||
num_turns SMALLINT,
|
all_or_nothing BOOLEAN,
|
||||||
actions TEXT
|
num_turns SMALLINT,
|
||||||
|
actions TEXT
|
||||||
);
|
);
|
||||||
CREATE INDEX games_seed_score_idx ON games (seed, score);
|
CREATE INDEX games_seed_score_idx ON games (seed, score);
|
||||||
CREATE INDEX games_var_seed_idx ON games (variant_id, seed);
|
CREATE INDEX games_var_seed_idx ON games (variant_id, seed);
|
||||||
|
|
|
@ -12,6 +12,7 @@ class InfeasibilityType(Enum):
|
||||||
OutOfPace = 0 # idx denotes index of last card drawn before being forced to reduce pace, value denotes how bad pace is
|
OutOfPace = 0 # idx denotes index of last card drawn before being forced to reduce pace, value denotes how bad pace is
|
||||||
OutOfHandSize = 1 # idx denotes index of last card drawn before being forced to discard a crit
|
OutOfHandSize = 1 # idx denotes index of last card drawn before being forced to discard a crit
|
||||||
NotTrivial = 2
|
NotTrivial = 2
|
||||||
|
CritAtBottom = 3
|
||||||
|
|
||||||
|
|
||||||
class InfeasibilityReason():
|
class InfeasibilityReason():
|
||||||
|
@ -26,6 +27,9 @@ class InfeasibilityReason():
|
||||||
return "Deck runs out of pace ({}) after drawing card {}".format(self.value, self.index)
|
return "Deck runs out of pace ({}) after drawing card {}".format(self.value, self.index)
|
||||||
case InfeasibilityType.OutOfHandSize:
|
case InfeasibilityType.OutOfHandSize:
|
||||||
return "Deck runs out of hand size after drawing card {}".format(self.index)
|
return "Deck runs out of hand size after drawing card {}".format(self.index)
|
||||||
|
case InfeasibilityType.CritAtBottom:
|
||||||
|
return "Deck has crit non-5 at bottom (index {})".format(self.index)
|
||||||
|
|
||||||
|
|
||||||
def analyze_suit(occurrences):
|
def analyze_suit(occurrences):
|
||||||
# denotes the indexes of copies we can use wlog
|
# denotes the indexes of copies we can use wlog
|
||||||
|
@ -97,6 +101,9 @@ def analyze_card_usage(instance: HanabiInstance):
|
||||||
|
|
||||||
def analyze(instance: HanabiInstance, find_non_trivial=False) -> InfeasibilityReason | None:
|
def analyze(instance: HanabiInstance, find_non_trivial=False) -> InfeasibilityReason | None:
|
||||||
|
|
||||||
|
if instance.deck[-1].rank != 5 and instance.deck[-1].suitIndex + instance.num_dark_suits >= instance.num_suits:
|
||||||
|
return InfeasibilityReason(InfeasibilityType.CritAtBottom, instance.deck_size - 1)
|
||||||
|
|
||||||
# we will sweep through the deck and pretend that we instantly play all cards
|
# we will sweep through the deck and pretend that we instantly play all cards
|
||||||
# as soon as we have them (and recurse this)
|
# as soon as we have them (and recurse this)
|
||||||
# this allows us to detect standard pace issue arguments
|
# this allows us to detect standard pace issue arguments
|
||||||
|
|
|
@ -42,6 +42,7 @@ def detailed_export_game(game_id: int, score: Optional[int] = None, var_id: Opti
|
||||||
one_extra_card = options.get('oneExtraCard', False)
|
one_extra_card = options.get('oneExtraCard', False)
|
||||||
one_less_card = options.get('oneLessCard', False)
|
one_less_card = options.get('oneLessCard', False)
|
||||||
all_or_nothing = options.get('allOrNothing', False)
|
all_or_nothing = options.get('allOrNothing', False)
|
||||||
|
starting_player = options.get('startingPlayer', 0)
|
||||||
actions = [Action.from_json(action) for action in game_json.get('actions', [])]
|
actions = [Action.from_json(action) for action in game_json.get('actions', [])]
|
||||||
deck = [DeckCard.from_json(card) for card in game_json.get('deck', None)]
|
deck = [DeckCard.from_json(card) for card in game_json.get('deck', None)]
|
||||||
|
|
||||||
|
@ -49,11 +50,18 @@ def detailed_export_game(game_id: int, score: Optional[int] = None, var_id: Opti
|
||||||
assert seed is not None, assert_msg
|
assert seed is not None, assert_msg
|
||||||
|
|
||||||
if score is None:
|
if score is None:
|
||||||
if deck_plays or one_less_card or one_extra_card or all_or_nothing:
|
|
||||||
# TODO: need to incorporate extra options here regarding hand size etc
|
|
||||||
raise RuntimeError('Not implemented.')
|
|
||||||
# need to play through the game once to find out its score
|
# need to play through the game once to find out its score
|
||||||
game = HanabLiveGameState(HanabLiveInstance(deck, num_players, var_id))
|
game = HanabLiveGameState(
|
||||||
|
HanabLiveInstance(
|
||||||
|
deck, num_players, var_id,
|
||||||
|
deck_plays=deck_plays,
|
||||||
|
one_less_card=one_less_card,
|
||||||
|
one_extra_card=one_extra_card,
|
||||||
|
all_or_nothing=all_or_nothing
|
||||||
|
),
|
||||||
|
starting_player
|
||||||
|
)
|
||||||
|
print(game.instance.hand_size, game.instance.num_players)
|
||||||
for action in actions:
|
for action in actions:
|
||||||
game.make_action(action)
|
game.make_action(action)
|
||||||
score = game.score
|
score = game.score
|
||||||
|
|
106
greedy_solver.py
106
greedy_solver.py
|
@ -3,6 +3,7 @@ import collections
|
||||||
import sys
|
import sys
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from log_setup import logger
|
from log_setup import logger
|
||||||
|
from typing import Tuple, List, Optional
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from hanabi import DeckCard, Action, ActionType, GameState, HanabiInstance
|
from hanabi import DeckCard, Action, ActionType, GameState, HanabiInstance
|
||||||
|
@ -11,10 +12,12 @@ from database.database import conn
|
||||||
|
|
||||||
|
|
||||||
class CardType(Enum):
|
class CardType(Enum):
|
||||||
|
Dispensable = -1
|
||||||
Trash = 0
|
Trash = 0
|
||||||
Playable = 1
|
Playable = 1
|
||||||
Critical = 2
|
Critical = 2
|
||||||
Dispensable = 3
|
DuplicateVisible = 3
|
||||||
|
UniqueVisible = 4
|
||||||
|
|
||||||
|
|
||||||
class CardState():
|
class CardState():
|
||||||
|
@ -31,8 +34,10 @@ class CardState():
|
||||||
return "Playable ({}) with weight {}".format(self.card, self.weight)
|
return "Playable ({}) with weight {}".format(self.card, self.weight)
|
||||||
case CardType.Critical:
|
case CardType.Critical:
|
||||||
return "Critical ({})".format(self.card)
|
return "Critical ({})".format(self.card)
|
||||||
case CardType.Dispensable:
|
case CardType.DuplicateVisible:
|
||||||
return "Dispensable ({}) with weight {}".format(self.card, self.weight)
|
return "Useful (duplicate visible) ({}) with weight {}".format(self.card, self.weight)
|
||||||
|
case CardType.UniqueVisible:
|
||||||
|
return "Useful (unique visible) ({}) with weight {}".format(self.card, self.weight)
|
||||||
|
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
@ -45,7 +50,92 @@ def card_type(game_state, card):
|
||||||
elif card.rank == 5 or card in game_state.trash:
|
elif card.rank == 5 or card in game_state.trash:
|
||||||
return CardType.Critical
|
return CardType.Critical
|
||||||
else:
|
else:
|
||||||
return CardType.Dispensable
|
visible_cards = sum((game_state.hands[player] for player in range(game_state.num_players)), [])
|
||||||
|
if visible_cards.count(card) >= 2:
|
||||||
|
return CardType.DuplicateVisible
|
||||||
|
else:
|
||||||
|
return CardType.UniqueVisible
|
||||||
|
|
||||||
|
|
||||||
|
class WeightedCard:
|
||||||
|
def __init__(self, card, weight: Optional[int] = None):
|
||||||
|
self.card = card
|
||||||
|
self.weight = weight
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "{} with weight {}".format(self.card, self.weight)
|
||||||
|
|
||||||
|
|
||||||
|
class HandState:
|
||||||
|
def __init__(self, player: int, game_state: GameState):
|
||||||
|
self.trash = []
|
||||||
|
self.playable = []
|
||||||
|
self.critical = []
|
||||||
|
self.dupes = []
|
||||||
|
self.uniques = []
|
||||||
|
for card in game_state.hands[player]:
|
||||||
|
match card_type(game_state, card):
|
||||||
|
case CardType.Trash:
|
||||||
|
self.trash.append(WeightedCard(card))
|
||||||
|
case CardType.Playable:
|
||||||
|
if card not in map(lambda c: c.card, self.playable):
|
||||||
|
self.playable.append(WeightedCard(card))
|
||||||
|
else:
|
||||||
|
self.trash.append(card)
|
||||||
|
case CardType.Critical:
|
||||||
|
self.critical.append(WeightedCard(card))
|
||||||
|
case CardType.UniqueVisible:
|
||||||
|
self.uniques.append(WeightedCard(card))
|
||||||
|
case CardType.DuplicateVisible:
|
||||||
|
copy = next((w for w in self.dupes if w.card == card), None)
|
||||||
|
if copy is not None:
|
||||||
|
self.dupes.remove(copy)
|
||||||
|
self.critical.append(copy)
|
||||||
|
self.trash.append(card)
|
||||||
|
else:
|
||||||
|
self.dupes.append(WeightedCard(card))
|
||||||
|
self.playable.sort(key=lambda c: c.card.rank)
|
||||||
|
self.dupes.sort(key=lambda c: c.card.rank)
|
||||||
|
self.uniques.sort(key=lambda c: c.card.rank)
|
||||||
|
if len(self.trash) > 0:
|
||||||
|
self.best_discard = self.trash[0]
|
||||||
|
self.discard_badness = 0
|
||||||
|
elif len(self.dupes) > 0:
|
||||||
|
self.best_discard = self.dupes[0]
|
||||||
|
self.discard_badness = 8 - game_state.num_players
|
||||||
|
elif len(self.uniques) > 0:
|
||||||
|
self.best_discard = self.uniques[-1]
|
||||||
|
self.discard_badness = 80 - 10 * self.best_discard.card.rank
|
||||||
|
elif len(self.playable) > 0:
|
||||||
|
self.best_discard = self.playable[-1]
|
||||||
|
self.discard_badness = 80 - 10 * self.best_discard.card.rank
|
||||||
|
else:
|
||||||
|
assert len(self.critical) > 0, "Programming error."
|
||||||
|
self.best_discard = self.critical[-1]
|
||||||
|
self.discard_badness = 600 - 100*self.best_discard.card.rank
|
||||||
|
|
||||||
|
def num_useful_cards(self):
|
||||||
|
return len(self.dupes) + len(self.uniques) + len(self.playable) + len(self.critical)
|
||||||
|
|
||||||
|
|
||||||
|
class CheatingStrategy:
|
||||||
|
def __init__(self, game_state: GameState):
|
||||||
|
self.game_state = game_state
|
||||||
|
|
||||||
|
def make_move(self):
|
||||||
|
hand_states = [HandState(player, self.game_state) for player in range(self.game_state.num_players)]
|
||||||
|
|
||||||
|
modified_pace = self.game_state.pace - sum(
|
||||||
|
1 for state in hand_states if len(state.trash) == self.game_state.hand_size
|
||||||
|
)
|
||||||
|
|
||||||
|
cur_hand = hand_states[self.game_state.turn]
|
||||||
|
|
||||||
|
print([state.__dict__ for state in hand_states])
|
||||||
|
print(self.game_state.pace)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GreedyStrategy():
|
class GreedyStrategy():
|
||||||
|
@ -157,7 +247,7 @@ class GreedyStrategy():
|
||||||
|
|
||||||
def run_deck(instance: HanabiInstance) -> GameState:
|
def run_deck(instance: HanabiInstance) -> GameState:
|
||||||
gs = GameState(instance)
|
gs = GameState(instance)
|
||||||
strat = GreedyStrategy(gs)
|
strat = CheatingStrategy(gs)
|
||||||
while not gs.is_over():
|
while not gs.is_over():
|
||||||
strat.make_move()
|
strat.make_move()
|
||||||
return gs
|
return gs
|
||||||
|
@ -190,3 +280,9 @@ def run_samples(num_players, sample_size):
|
||||||
logger.info("Won {} ({}%) and lost {} ({}%) from sample of {} test games using greedy strategy.".format(
|
logger.info("Won {} ({}%) and lost {} ({}%) from sample of {} test games using greedy strategy.".format(
|
||||||
won, round(100 * won / sample_size, 2), lost, round(100 * lost / sample_size, 2), sample_size
|
won, round(100 * won / sample_size, 2), lost, round(100 * lost / sample_size, 2), sample_size
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for p in range(2, 6):
|
||||||
|
run_samples(p, int(sys.argv[1]))
|
||||||
|
print()
|
||||||
|
|
|
@ -41,8 +41,8 @@ class HanabLiveInstance(hanabi.HanabiInstance):
|
||||||
|
|
||||||
|
|
||||||
class HanabLiveGameState(hanabi.GameState):
|
class HanabLiveGameState(hanabi.GameState):
|
||||||
def __init__(self, instance: HanabLiveInstance):
|
def __init__(self, instance: HanabLiveInstance, starting_player: int = 0):
|
||||||
super().__init__(instance)
|
super().__init__(instance, starting_player)
|
||||||
self.instance: HanabLiveInstance = instance
|
self.instance: HanabLiveInstance = instance
|
||||||
|
|
||||||
def make_action(self, action):
|
def make_action(self, action):
|
||||||
|
|
36
hanabi.py
36
hanabi.py
|
@ -103,6 +103,7 @@ class HanabiInstance:
|
||||||
self.fives_give_clue = fives_give_clue
|
self.fives_give_clue = fives_give_clue
|
||||||
self.deck_plays = deck_plays,
|
self.deck_plays = deck_plays,
|
||||||
self.all_or_nothing = all_or_nothing
|
self.all_or_nothing = all_or_nothing
|
||||||
|
assert not self.all_or_nothing, "All or nothing not implemented"
|
||||||
|
|
||||||
# normalize deck indices
|
# normalize deck indices
|
||||||
for (idx, card) in enumerate(self.deck):
|
for (idx, card) in enumerate(self.deck):
|
||||||
|
@ -114,6 +115,8 @@ class HanabiInstance:
|
||||||
self.player_names = constants.PLAYER_NAMES[:self.num_players]
|
self.player_names = constants.PLAYER_NAMES[:self.num_players]
|
||||||
self.deck_size = len(self.deck)
|
self.deck_size = len(self.deck)
|
||||||
|
|
||||||
|
self.initial_pace = self.deck_size - 5 * self.num_suits - self.num_players * (self.hand_size - 1)
|
||||||
|
|
||||||
# # maximum number of moves in any game that can achieve max score each suit gives 15 moves, as we can play
|
# # maximum number of moves in any game that can achieve max score each suit gives 15 moves, as we can play
|
||||||
# and discard 5 cards each and give 5 clues. dark suits only give 5 moves, since no discards are added number
|
# and discard 5 cards each and give 5 clues. dark suits only give 5 moves, since no discards are added number
|
||||||
# of cards that remain in players hands after end of game. they cost 2 turns each, since we cannot discard
|
# of cards that remain in players hands after end of game. they cost 2 turns each, since we cannot discard
|
||||||
|
@ -143,7 +146,7 @@ class HanabiInstance:
|
||||||
|
|
||||||
|
|
||||||
class GameState:
|
class GameState:
|
||||||
def __init__(self, instance: HanabiInstance):
|
def __init__(self, instance: HanabiInstance, starting_player: int = 0):
|
||||||
# will not be modified
|
# will not be modified
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
|
|
||||||
|
@ -154,9 +157,8 @@ class GameState:
|
||||||
self.stacks = [0 for i in range(0, self.instance.num_suits)]
|
self.stacks = [0 for i in range(0, self.instance.num_suits)]
|
||||||
self.strikes = 0
|
self.strikes = 0
|
||||||
self.clues = 8
|
self.clues = 8
|
||||||
self.turn = 0
|
self.turn = starting_player
|
||||||
self.pace = self.instance.deck_size - 5 * self.instance.num_suits - self.instance.num_players * (
|
self.pace = self.instance.initial_pace
|
||||||
self.instance.hand_size - 1)
|
|
||||||
self.remaining_extra_turns = self.instance.num_players + 1
|
self.remaining_extra_turns = self.instance.num_players + 1
|
||||||
self.trash = []
|
self.trash = []
|
||||||
|
|
||||||
|
@ -181,6 +183,7 @@ class GameState:
|
||||||
else:
|
else:
|
||||||
self.strikes += 1
|
self.strikes += 1
|
||||||
self.trash.append(self.instance.deck[card_idx])
|
self.trash.append(self.instance.deck[card_idx])
|
||||||
|
self.pace -= 1
|
||||||
self.actions.append(Action(ActionType.Play, target=card_idx))
|
self.actions.append(Action(ActionType.Play, target=card_idx))
|
||||||
self._replace(card_idx, allow_not_present=self.instance.deck_plays and (card_idx == self.deck_size - 1))
|
self._replace(card_idx, allow_not_present=self.instance.deck_plays and (card_idx == self.deck_size - 1))
|
||||||
self._make_turn()
|
self._make_turn()
|
||||||
|
@ -233,7 +236,7 @@ class GameState:
|
||||||
return self.over or self.is_known_lost()
|
return self.over or self.is_known_lost()
|
||||||
|
|
||||||
def is_won(self):
|
def is_won(self):
|
||||||
return self.score == 5 * instance.num_suits
|
return self.score == self.instance.max_score
|
||||||
|
|
||||||
def is_known_lost(self):
|
def is_known_lost(self):
|
||||||
return self.in_lost_state
|
return self.in_lost_state
|
||||||
|
@ -273,6 +276,27 @@ class GameState:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Query helpers for implementing bots
|
||||||
|
def copy_holders(self, card: DeckCard, exclude_player: Optional[int]):
|
||||||
|
return [
|
||||||
|
player for player in range(self.num_players)
|
||||||
|
if player != exclude_player and card in self.hands[player]
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def in_strict_order(player_a, player_b, player_c):
|
||||||
|
"""
|
||||||
|
Check whether the three given players sit in order, where equality is not allowed
|
||||||
|
:param player_a:
|
||||||
|
:param player_b:
|
||||||
|
:param player_c:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return player_a < player_b < player_c or player_b < player_c < player_a or player_c < player_a < player_b
|
||||||
|
|
||||||
|
def is_in_extra_round(self):
|
||||||
|
return self.remaining_extra_turns <= self.instance.num_players
|
||||||
|
|
||||||
# Private helpers
|
# Private helpers
|
||||||
|
|
||||||
# increments turn counter and tracks extra round
|
# increments turn counter and tracks extra round
|
||||||
|
@ -287,7 +311,7 @@ class GameState:
|
||||||
# replaces the specified card (has to be in current player's hand) with the next card of the deck (if nonempty)
|
# replaces the specified card (has to be in current player's hand) with the next card of the deck (if nonempty)
|
||||||
def _replace(self, card_idx, allow_not_present: bool = False):
|
def _replace(self, card_idx, allow_not_present: bool = False):
|
||||||
try:
|
try:
|
||||||
idx_in_hand = next((i for (i, card) in enumerate(self.cur_hand) if card.deck_index == card_idx), None)
|
idx_in_hand = next((i for (i, card) in enumerate(self.cur_hand) if card.deck_index == card_idx))
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
if not allow_not_present:
|
if not allow_not_present:
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
|
from typing import Optional
|
||||||
|
import pebble.concurrent
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
from sat import solve_sat
|
from sat import solve_sat
|
||||||
from database import conn
|
from database.database import conn, cur
|
||||||
from download_data import export_game
|
from download_data import detailed_export_game
|
||||||
from variants import VARIANTS, variant_name
|
|
||||||
from alive_progress import alive_bar
|
from alive_progress import alive_bar
|
||||||
from compress import decompress_deck, link
|
from compress import decompress_deck, link
|
||||||
import concurrent.futures
|
from hanabi import HanabiInstance
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
from greedy_solver import GameState, GreedyStrategy
|
from greedy_solver import GameState, GreedyStrategy
|
||||||
from log_setup.logger_setup import logger
|
from log_setup import logger
|
||||||
from deck_analyzer import analyze, InfeasibilityReason
|
from deck_analyzer import analyze, InfeasibilityReason
|
||||||
|
from variants import Variant
|
||||||
|
|
||||||
|
MAX_PROCESSES = 6
|
||||||
|
|
||||||
MAX_PROCESSES=4
|
|
||||||
|
|
||||||
def update_seeds_db():
|
def update_seeds_db():
|
||||||
cur2 = conn.cursor()
|
cur2 = conn.cursor()
|
||||||
|
@ -33,51 +40,45 @@ def update_seeds_db():
|
||||||
|
|
||||||
|
|
||||||
def get_decks_of_seeds():
|
def get_decks_of_seeds():
|
||||||
cur = conn.cursor()
|
|
||||||
cur2 = conn.cursor()
|
cur2 = conn.cursor()
|
||||||
cur.execute("SELECT seed FROM seeds WHERE deck is NULL")
|
cur.execute("SELECT seed, variant_id FROM seeds WHERE deck is NULL")
|
||||||
for (seed,) in cur:
|
for (seed, variant_id) in cur:
|
||||||
cur2.execute("SELECT id FROM games WHERE seed = (%s)", (seed,))
|
cur2.execute("SELECT id FROM games WHERE seed = (%s) LIMIT 1", (seed,))
|
||||||
(game_id,) = cur2.fetchone()
|
(game_id,) = cur2.fetchone()
|
||||||
print("Exporting game {} for seed {}.".format(game_id, seed))
|
logger.verbose("Exporting game {} for seed {}.".format(game_id, seed))
|
||||||
export_game(game_id)
|
detailed_export_game(game_id, var_id=variant_id, seed_exists=True)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def update_trivially_feasible_games():
|
def update_trivially_feasible_games(variant_id):
|
||||||
cur = conn.cursor()
|
variant: Variant = Variant.from_db(variant_id)
|
||||||
for var in VARIANTS:
|
cur.execute("SELECT seed FROM seeds WHERE variant_id = (%s) AND feasible is null", (variant_id,))
|
||||||
cur.execute("SELECT seed FROM seeds WHERE variant_id = (%s) AND feasible is null", (var['id'],))
|
seeds = cur.fetchall()
|
||||||
seeds = cur.fetchall()
|
print('Checking variant {} (id {}), found {} seeds to check...'.format(variant.name, variant_id, len(seeds)))
|
||||||
print('Checking variant {} (id {}), found {} seeds to check...'.format(var['name'], var['id'], len(seeds)))
|
|
||||||
|
|
||||||
with alive_bar(total=len(seeds), title='{} ({})'.format(var['name'], var['id'])) as bar:
|
|
||||||
for (seed,) in seeds:
|
|
||||||
cur.execute("SELECT id, deck_plays, one_extra_card, one_less_card, all_or_nothing "
|
|
||||||
"FROM games WHERE score = (%s) AND seed = (%s) ORDER BY id;",
|
|
||||||
(5 * len(var['suits']), seed)
|
|
||||||
)
|
|
||||||
res = cur.fetchall()
|
|
||||||
print("Checking seed {}: {:3} results".format(seed, len(res)))
|
|
||||||
for (game_id, a, b, c, d) in res:
|
|
||||||
if None in [a,b,c,d]:
|
|
||||||
print(' Game {} not found in database, exporting...'.format(game_id))
|
|
||||||
succ, valid = export_game(game_id)
|
|
||||||
if not succ:
|
|
||||||
print('Error exporting game {}.'.format(game_id))
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
valid = not any([a,b,c,d])
|
|
||||||
print(' Game {} already in database, valid: {}'.format(game_id, valid))
|
|
||||||
if valid:
|
|
||||||
print('Seed {:10} (variant {} / {}) found to be feasible via game {:6}'.format(seed, var['id'], var['name'], game_id))
|
|
||||||
cur.execute("UPDATE seeds SET feasible = (%s) WHERE seed = (%s)", (True, seed))
|
|
||||||
conn.commit()
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print(' Cheaty game found')
|
|
||||||
bar()
|
|
||||||
|
|
||||||
|
with alive_bar(total=len(seeds), title='{} ({})'.format(variant.name, variant_id)) as bar:
|
||||||
|
for (seed,) in seeds:
|
||||||
|
cur.execute("SELECT id, deck_plays, one_extra_card, one_less_card, all_or_nothing "
|
||||||
|
"FROM games WHERE score = (%s) AND seed = (%s) ORDER BY id;",
|
||||||
|
(variant.max_score, seed)
|
||||||
|
)
|
||||||
|
res = cur.fetchall()
|
||||||
|
logger.debug("Checking seed {}: {:3} results".format(seed, len(res)))
|
||||||
|
for (game_id, a, b, c, d) in res:
|
||||||
|
if None in [a, b, c, d]:
|
||||||
|
logger.debug(' Game {} not found in database, exporting...'.format(game_id))
|
||||||
|
detailed_export_game(game_id, var_id=variant_id)
|
||||||
|
else:
|
||||||
|
logger.debug(' Game {} already in database'.format(game_id, valid))
|
||||||
|
valid = not any([a, b, c, d])
|
||||||
|
if valid:
|
||||||
|
logger.verbose('Seed {:10} (variant {}) found to be feasible via game {:6}'.format(seed, variant_id, game_id))
|
||||||
|
cur.execute("UPDATE seeds SET feasible = (%s) WHERE seed = (%s)", (True, seed))
|
||||||
|
conn.commit()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.verbose(' Cheaty game found')
|
||||||
|
bar()
|
||||||
|
|
||||||
|
|
||||||
def get_decks_for_all_seeds():
|
def get_decks_for_all_seeds():
|
||||||
|
@ -103,90 +104,101 @@ def get_decks_for_all_seeds():
|
||||||
|
|
||||||
mutex = Lock()
|
mutex = Lock()
|
||||||
|
|
||||||
def solve_instance(num_players, deck):
|
|
||||||
|
def solve_instance(instance: HanabiInstance):
|
||||||
# first, sanity check on running out of pace
|
# first, sanity check on running out of pace
|
||||||
result = analyze(deck, num_players)
|
result = analyze(instance)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
assert type(result) == InfeasibilityReason
|
assert type(result) == InfeasibilityReason
|
||||||
logger.info("found infeasible deck")
|
logger.debug("found infeasible deck")
|
||||||
return False, None, None
|
return False, None, None
|
||||||
for num_remaining_cards in [0, 5, 10, 20, 30]:
|
for num_remaining_cards in [0, 20]:
|
||||||
# logger.info("trying with {} remaining cards".format(num_remaining_cards))
|
# logger.info("trying with {} remaining cards".format(num_remaining_cards))
|
||||||
game = GameState(num_players, deck)
|
game = GameState(instance)
|
||||||
strat = GreedyStrategy(game)
|
strat = GreedyStrategy(game)
|
||||||
|
|
||||||
# make a number of greedy moves
|
# make a number of greedy moves
|
||||||
while not game.is_over() and not game.is_known_lost():
|
while not game.is_over() and not game.is_known_lost():
|
||||||
if num_remaining_cards != 0 and game.progress == game.deck_size - num_remaining_cards:
|
if num_remaining_cards != 0 and game.progress == game.deck_size - num_remaining_cards:
|
||||||
break # stop solution here
|
break # stop solution here
|
||||||
strat.make_move()
|
strat.make_move()
|
||||||
|
|
||||||
# check if we won already
|
# check if we won already
|
||||||
if game.is_won():
|
if game.is_won():
|
||||||
# print("won with greedy strat")
|
# print("won with greedy strat")
|
||||||
return True, game, num_remaining_cards
|
return True, game, num_remaining_cards
|
||||||
|
|
||||||
# now, apply sat solver
|
# now, apply sat solver
|
||||||
if not game.is_over():
|
if not game.is_over():
|
||||||
logger.info("continuing greedy sol with SAT")
|
logger.debug("continuing greedy sol with SAT")
|
||||||
solvable, sol = solve_sat(game)
|
solvable, sol = solve_sat(game)
|
||||||
if solvable:
|
if solvable is None:
|
||||||
return True, sol, num_remaining_cards
|
return True, sol, num_remaining_cards
|
||||||
logger.info("No success with {} remaining cards, reducing number of greedy moves, failed attempt was: {}".format(num_remaining_cards, link(game.to_json())))
|
logger.debug(
|
||||||
# print("Aborting trying with greedy strat")
|
"No success with {} remaining cards, reducing number of greedy moves, failed attempt was: {}".format(
|
||||||
logger.info("Starting full SAT solver")
|
num_remaining_cards, link(game)))
|
||||||
game = GameState(num_players, deck)
|
# print("Aborting trying with greedy strat")
|
||||||
|
logger.debug("Starting full SAT solver")
|
||||||
|
game = GameState(instance)
|
||||||
a, b = solve_sat(game)
|
a, b = solve_sat(game)
|
||||||
return a, b, 99
|
return a, b, instance.draw_pile_size
|
||||||
|
|
||||||
|
|
||||||
def solve_seed(seed, num_players, deck_compressed, var_id):
|
@pebble.concurrent.process(timeout=150)
|
||||||
|
def solve_seed_with_timeout(seed, num_players, deck_compressed, var_name: Optional[str] = None):
|
||||||
try:
|
try:
|
||||||
|
logger.verbose("Starting to solve seed {}".format(seed))
|
||||||
deck = decompress_deck(deck_compressed)
|
deck = decompress_deck(deck_compressed)
|
||||||
t0 = perf_counter()
|
t0 = perf_counter()
|
||||||
solvable, solution, num_remaining_cards = solve_instance(num_players, deck)
|
solvable, solution, num_remaining_cards = solve_instance(HanabiInstance(deck, num_players))
|
||||||
t1 = perf_counter()
|
t1 = perf_counter()
|
||||||
logger.info("Solved instance {} in {} seconds".format(seed, round(t1-t0, 2)))
|
logger.verbose("Solved instance {} in {} seconds: {}".format(seed, round(t1 - t0, 2), solvable))
|
||||||
|
|
||||||
mutex.acquire()
|
mutex.acquire()
|
||||||
if solvable is not None:
|
if solvable is not None:
|
||||||
lcur = conn.cursor()
|
cur.execute("UPDATE seeds SET feasible = (%s) WHERE seed = (%s)", (solvable, seed))
|
||||||
lcur.execute("UPDATE seeds SET feasible = (%s) WHERE seed = (%s)", (solvable, seed))
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
mutex.release()
|
||||||
|
|
||||||
if solvable == True:
|
if solvable == True:
|
||||||
with open("remaining_cards.txt", "a") as f:
|
logger.verbose("Success with {} cards left in draw by greedy solver on seed {}: {}\n".format(
|
||||||
f.write("Success with {} cards left in draw by greedy solver on seed {}: {}\n".format(num_remaining_cards, seed ,link(solution.to_json())))
|
num_remaining_cards, seed, link(solution))
|
||||||
|
)
|
||||||
elif solvable == False:
|
elif solvable == False:
|
||||||
logger.info("seed {} was not solvable".format(seed))
|
logger.debug("seed {} was not solvable".format(seed))
|
||||||
with open('infeasible_instances.txt', 'a') as f:
|
logger.debug('{}-player, seed {:10}, {}\n'.format(num_players, seed, var_name))
|
||||||
f.write('{}-player, seed {:10}, {}\n'.format(num_players, seed, variant_name(var_id)))
|
|
||||||
elif solvable is None:
|
elif solvable is None:
|
||||||
logger.info("seed {} skipped".format(seed))
|
logger.verbose("seed {} skipped".format(seed))
|
||||||
else:
|
else:
|
||||||
raise Exception("Programming Error")
|
raise Exception("Programming Error")
|
||||||
|
|
||||||
mutex.release()
|
except Exception as e:
|
||||||
except Exception:
|
|
||||||
traceback.format_exc()
|
|
||||||
print("exception in subprocess:")
|
print("exception in subprocess:")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
def solve_unknown_seeds():
|
def solve_seed(seed, num_players, deck_compressed, var_name: Optional[str] = None):
|
||||||
cur = conn.cursor()
|
f = solve_seed_with_timeout(seed, num_players, deck_compressed, var_name)
|
||||||
for var in VARIANTS:
|
try:
|
||||||
cur.execute("SELECT seed, num_players, deck FROM seeds WHERE variant_id = (%s) AND feasible IS NULL AND deck IS NOT NULL", (var['id'],))
|
return f.result()
|
||||||
res = cur.fetchall()
|
except TimeoutError:
|
||||||
|
logger.verbose("Solving on seed {} timed out".format(seed))
|
||||||
# for r in res:
|
return
|
||||||
# solve_seed(r[0], r[1], r[2], var['id'])
|
|
||||||
|
|
||||||
with concurrent.futures.ProcessPoolExecutor(max_workers=MAX_PROCESSES) as executor:
|
|
||||||
fs = [executor.submit(solve_seed, r[0], r[1], r[2], var['id']) for r in res]
|
|
||||||
with alive_bar(len(res), title='Seed solving on {}'.format(var['name'])) as bar:
|
|
||||||
for f in concurrent.futures.as_completed(fs):
|
|
||||||
bar()
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
solve_unknown_seeds()
|
def solve_unknown_seeds(variant_id, variant_name: Optional[str] = None):
|
||||||
|
cur.execute("SELECT seed, num_players, deck FROM seeds WHERE variant_id = (%s) AND feasible IS NULL", (variant_id,))
|
||||||
|
res = cur.fetchall()
|
||||||
|
|
||||||
|
# for r in res:
|
||||||
|
# solve_seed(r[0], r[1], r[2], variant_name)
|
||||||
|
|
||||||
|
with concurrent.futures.ProcessPoolExecutor(max_workers=MAX_PROCESSES) as executor:
|
||||||
|
fs = [executor.submit(solve_seed, r[0], r[1], r[2], variant_name) for r in res]
|
||||||
|
with alive_bar(len(res), title='Seed solving on {}'.format(variant_name)) as bar:
|
||||||
|
for f in concurrent.futures.as_completed(fs):
|
||||||
|
bar()
|
||||||
|
|
||||||
|
|
||||||
|
update_trivially_feasible_games(0)
|
||||||
|
solve_unknown_seeds(0, "No Variant")
|
|
@ -7,3 +7,4 @@ psycopg2
|
||||||
alive_progress
|
alive_progress
|
||||||
argparse
|
argparse
|
||||||
verboselogs
|
verboselogs
|
||||||
|
pebble
|
||||||
|
|
24
test.py
24
test.py
|
@ -14,6 +14,28 @@ from database.database import conn, cur
|
||||||
from database.init_database import init_database_tables, populate_static_tables
|
from database.init_database import init_database_tables, populate_static_tables
|
||||||
|
|
||||||
|
|
||||||
|
def find_double_dark_games():
|
||||||
|
cur.execute("SELECT variants.id, variants.name, count(suits.id) from variants "
|
||||||
|
"inner join variant_suits on variants.id = variant_suits.variant_id "
|
||||||
|
"left join suits on suits.id = variant_suits.suit_id "
|
||||||
|
"where suits.dark = (%s) "
|
||||||
|
"group by variants.id "
|
||||||
|
"order by count(suits.id), variants.id",
|
||||||
|
(True,)
|
||||||
|
)
|
||||||
|
cur2 = conn.cursor()
|
||||||
|
r = []
|
||||||
|
for (var_id, var_name, num_dark_suits) in cur.fetchall():
|
||||||
|
if num_dark_suits == 2:
|
||||||
|
cur2.execute("select count(*) from games where variant_id = (%s)", (var_id,))
|
||||||
|
games = cur2.fetchone()[0]
|
||||||
|
cur2.execute("select count(*) from seeds where variant_id = (%s)", (var_id, ))
|
||||||
|
r.append((var_name, games, cur2.fetchone()[0]))
|
||||||
|
l = sorted(r, key=lambda e: -e[1])
|
||||||
|
for (name, games, seeds) in l:
|
||||||
|
print("{}: {} games on {} seeds".format(name, games, seeds))
|
||||||
|
|
||||||
|
|
||||||
def test_suits():
|
def test_suits():
|
||||||
suit = Suit.from_db(55)
|
suit = Suit.from_db(55)
|
||||||
print(suit.__dict__)
|
print(suit.__dict__)
|
||||||
|
@ -52,6 +74,8 @@ def export_all_seeds():
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
find_double_dark_games()
|
||||||
|
exit(0)
|
||||||
var_id = 964532
|
var_id = 964532
|
||||||
export_all_seeds()
|
export_all_seeds()
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
|
@ -218,6 +218,10 @@ class Variant:
|
||||||
return True
|
return True
|
||||||
return suit.color_touches(self.colors[value])
|
return suit.color_touches(self.colors[value])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_score(self):
|
||||||
|
return self.num_suits * 5
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_db(var_id):
|
def from_db(var_id):
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
Loading…
Reference in a new issue