deck analyzer: more detailed results

This commit is contained in:
Maximilian Keßler 2024-11-25 23:04:15 +01:00
parent a4b9112ce7
commit e29a26ed09
3 changed files with 50 additions and 22 deletions

View file

@ -112,10 +112,10 @@ def solve_instance(instance: hanab_game.HanabiInstance)-> SolutionData:
retval = SolutionData() retval = SolutionData()
# first, sanity check on running out of pace # first, sanity check on running out of pace
result = deck_analyzer.analyze(instance) result = deck_analyzer.analyze(instance)
if len(result) != 0: if len(result.infeasibility_reasons) != 0:
logger.verbose("found infeasible deck by preliminary analysis") logger.verbose("found infeasible deck by preliminary analysis")
retval.feasible = False retval.feasible = False
retval.infeasibility_reasons = result retval.infeasibility_reasons = result.infeasibility_reasons
return retval return retval
for num_remaining_cards in [0, 10, 20]: for num_remaining_cards in [0, 10, 20]:
# logger.info("trying with {} remaining cards".format(num_remaining_cards)) # logger.info("trying with {} remaining cards".format(num_remaining_cards))

View file

@ -1,4 +1,5 @@
import collections import collections
import dataclasses
from enum import Enum from enum import Enum
from typing import List, Any, Optional, Tuple, Set from typing import List, Any, Optional, Tuple, Set
from dataclasses import dataclass from dataclasses import dataclass
@ -139,8 +140,30 @@ def analyze_2p_bottom_loss(instance: hanab_game.HanabiInstance) -> List[Infeasib
return reasons return reasons
def analyze_pace_and_hand_size(instance: hanab_game.HanabiInstance, do_squeeze: bool = True) -> List[InfeasibilityReason]: @dataclass
reasons = [] class ValueWithIndex:
value: int
index: int
stores_minimum: bool = True
def update(self, value, index):
if (self.stores_minimum and value < self.value) or (not self.stores_minimum and value > self.value):
self.value = value
self.index = index
def __repr__(self):
return "{} (at {})".format(self.value, self.index)
@dataclass
class AnalysisResult:
infeasibility_reasons: List[InfeasibilityReason] = dataclasses.field(default_factory=lambda: [])
min_pace: ValueWithIndex = dataclasses.field(default_factory=lambda: ValueWithIndex(100, 0, True))
max_stored_crits: ValueWithIndex = dataclasses.field(default_factory=lambda: ValueWithIndex(0, 0, False))
max_stored_cards: ValueWithIndex = dataclasses.field(default_factory=lambda: ValueWithIndex(0, 0, False))
def analyze_pace_and_hand_size(instance: hanab_game.HanabiInstance, do_squeeze: bool = True) -> AnalysisResult:
reasons = AnalysisResult()
# we will sweep through the deck and pretend that # we will sweep through the deck and pretend that
# - we keep all non-trash cards in our hands # - we keep all non-trash cards in our hands
# - we instantly play all playable cards as soon as we have them # - we instantly play all playable cards as soon as we have them
@ -171,6 +194,7 @@ def analyze_pace_and_hand_size(instance: hanab_game.HanabiInstance, do_squeeze:
squeeze = False squeeze = False
artificial_crits = set() artificial_crits = set()
# Investigate BDRs. This catches special cases of Pace losses in 2p, as well as mark some cards critical because # Investigate BDRs. This catches special cases of Pace losses in 2p, as well as mark some cards critical because
# their second copies cannot be used. # their second copies cannot be used.
filtered_deck = [card for card in instance.deck if card.rank != 5] filtered_deck = [card for card in instance.deck if card.rank != 5]
@ -223,7 +247,7 @@ def analyze_pace_and_hand_size(instance: hanab_game.HanabiInstance, do_squeeze:
# Use a bool flag to only mark this reason once # Use a bool flag to only mark this reason once
if hand_size_left_for_crits < 0 and not hand_size_found: if hand_size_left_for_crits < 0 and not hand_size_found:
reasons.append(InfeasibilityReason(InfeasibilityType.HandSize, card_index)) reasons.infeasibility_reasons.append(InfeasibilityReason(InfeasibilityType.HandSize, card_index))
hand_size_found = True hand_size_found = True
# the last - 1 is there because we have to discard 'next', causing a further draw # the last - 1 is there because we have to discard 'next', causing a further draw
@ -233,42 +257,46 @@ def analyze_pace_and_hand_size(instance: hanab_game.HanabiInstance, do_squeeze:
if cur_pace < 0 and not pace_found: if cur_pace < 0 and not pace_found:
if squeeze: if squeeze:
# We checked single-suit pace losses beforehand (which can only occur in 2p) # We checked single-suit pace losses beforehand (which can only occur in 2p)
reasons.append(InfeasibilityReason(InfeasibilityType.PaceAfterSqueeze, card_index)) reasons.infeasibility_reasons.append(InfeasibilityReason(InfeasibilityType.PaceAfterSqueeze, card_index))
else: else:
reasons.append(InfeasibilityReason(InfeasibilityType.Pace, card_index)) reasons.infeasibility_reasons.append(InfeasibilityReason(InfeasibilityType.Pace, card_index))
pace_found = True pace_found = True
# if card_index != instance.deck_size - 1:
reasons.min_pace.update(cur_pace, card_index)
reasons.max_stored_cards.update(len(stored_cards), card_index)
reasons.max_stored_crits.update(len(stored_crits), card_index)
return reasons return reasons
def analyze(instance: hanab_game.HanabiInstance) -> List[InfeasibilityReason]: def analyze(instance: hanab_game.HanabiInstance) -> AnalysisResult:
reasons: List[InfeasibilityReason] = [] # Check for pace and hand size problems:
result = analyze_pace_and_hand_size(instance)
# In case pace ran out after a squeeze from hand size, we want to run a clean pace analysis again
if any(map(lambda r: r.type == InfeasibilityType.PaceAfterSqueeze, result.infeasibility_reasons)):
result.infeasibility_reasons += analyze_pace_and_hand_size(instance, False).infeasibility_reasons
# Top/bottom deck losses in a single suit. # Top/bottom deck losses in a single suit.
top_bottom_deck_loss = check_for_top_bottom_deck_loss(instance) top_bottom_deck_loss = check_for_top_bottom_deck_loss(instance)
if top_bottom_deck_loss is not None: if top_bottom_deck_loss is not None:
reasons.append(InfeasibilityReason(InfeasibilityType.BottomTopDeck, top_bottom_deck_loss)) result.infeasibility_reasons.append(InfeasibilityReason(InfeasibilityType.BottomTopDeck, top_bottom_deck_loss))
# Special cases of pace loss, categorization for 2p only # Special cases of pace loss, categorization for 2p only
reasons += analyze_2p_bottom_loss(instance) result.infeasibility_reasons += analyze_2p_bottom_loss(instance)
# check for critical non-fives at bottom of the deck # check for critical non-fives at bottom of the deck
bottom_card = instance.deck[-1] bottom_card = instance.deck[-1]
if bottom_card.rank != 5 and bottom_card.suitIndex in instance.dark_suits: if bottom_card.rank != 5 and bottom_card.suitIndex in instance.dark_suits:
reasons.append(InfeasibilityReason( result.infeasibility_reasons.append(InfeasibilityReason(
InfeasibilityType.CritAtBottom, InfeasibilityType.CritAtBottom,
instance.deck_size - 1 instance.deck_size - 1
)) ))
# Check for pace and hand size problems:
reasons += analyze_pace_and_hand_size(instance)
# In case pace ran out after a squeeze from hand size, we want to run a clean pace analysis again
if any(map(lambda r: r.type == InfeasibilityType.PaceAfterSqueeze, reasons)):
reasons += analyze_pace_and_hand_size(instance, False)
# clean up reasons to unique # clean up reasons to unique
return list(set(reasons)) result.infeasibility_reasons = list(set(result.infeasibility_reasons))
return result
def run_on_database(variant_id): def run_on_database(variant_id):
@ -281,8 +309,8 @@ def run_on_database(variant_id):
with alive_progress.alive_bar(total=len(res), title='Check for infeasibility reasons in var {}'.format(variant_id)) as bar: with alive_progress.alive_bar(total=len(res), title='Check for infeasibility reasons in var {}'.format(variant_id)) as bar:
for (seed, num_players, deck_str) in res: for (seed, num_players, deck_str) in res:
deck = compress.decompress_deck(deck_str) deck = compress.decompress_deck(deck_str)
reasons = analyze(hanab_game.HanabiInstance(deck, num_players)) result = analyze(hanab_game.HanabiInstance(deck, num_players))
for reason in reasons: for reason in result.infeasibility_reasons:
database.cur.execute( database.cur.execute(
"INSERT INTO score_upper_bounds (seed, score_upper_bound, reason) " "INSERT INTO score_upper_bounds (seed, score_upper_bound, reason) "
"VALUES (%s,%s,%s) " "VALUES (%s,%s,%s) "

View file

@ -157,7 +157,7 @@ class GreedyStrategy():
hand_states = [[CardState(card_type(self.game_state, card), card, None) for card in self.game_state.hands[p]] hand_states = [[CardState(card_type(self.game_state, card), card, None) for card in self.game_state.hands[p]]
for p in range(self.game_state.num_players)] for p in range(self.game_state.num_players)]
# find dupes in players hands, marke one card crit and the other one trash # find dupes in players hands, mark one card crit and the other one trash
p = False p = False
for states in hand_states: for states in hand_states:
counter = collections.Counter(map(lambda state: state.card, states)) counter = collections.Counter(map(lambda state: state.card, states))