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()
# first, sanity check on running out of pace
result = deck_analyzer.analyze(instance)
if len(result) != 0:
if len(result.infeasibility_reasons) != 0:
logger.verbose("found infeasible deck by preliminary analysis")
retval.feasible = False
retval.infeasibility_reasons = result
retval.infeasibility_reasons = result.infeasibility_reasons
return retval
for num_remaining_cards in [0, 10, 20]:
# logger.info("trying with {} remaining cards".format(num_remaining_cards))

View file

@ -1,4 +1,5 @@
import collections
import dataclasses
from enum import Enum
from typing import List, Any, Optional, Tuple, Set
from dataclasses import dataclass
@ -139,8 +140,30 @@ def analyze_2p_bottom_loss(instance: hanab_game.HanabiInstance) -> List[Infeasib
return reasons
def analyze_pace_and_hand_size(instance: hanab_game.HanabiInstance, do_squeeze: bool = True) -> List[InfeasibilityReason]:
reasons = []
@dataclass
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 keep all non-trash cards in our hands
# - 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
artificial_crits = set()
# 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.
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
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
# 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 squeeze:
# 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:
reasons.append(InfeasibilityReason(InfeasibilityType.Pace, card_index))
reasons.infeasibility_reasons.append(InfeasibilityReason(InfeasibilityType.Pace, card_index))
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
def analyze(instance: hanab_game.HanabiInstance) -> List[InfeasibilityReason]:
reasons: List[InfeasibilityReason] = []
def analyze(instance: hanab_game.HanabiInstance) -> AnalysisResult:
# 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_loss = check_for_top_bottom_deck_loss(instance)
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
reasons += analyze_2p_bottom_loss(instance)
result.infeasibility_reasons += analyze_2p_bottom_loss(instance)
# 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:
reasons.append(InfeasibilityReason(
result.infeasibility_reasons.append(InfeasibilityReason(
InfeasibilityType.CritAtBottom,
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
return list(set(reasons))
result.infeasibility_reasons = list(set(result.infeasibility_reasons))
return result
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:
for (seed, num_players, deck_str) in res:
deck = compress.decompress_deck(deck_str)
reasons = analyze(hanab_game.HanabiInstance(deck, num_players))
for reason in reasons:
result = analyze(hanab_game.HanabiInstance(deck, num_players))
for reason in result.infeasibility_reasons:
database.cur.execute(
"INSERT INTO score_upper_bounds (seed, score_upper_bound, reason) "
"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]]
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
for states in hand_states:
counter = collections.Counter(map(lambda state: state.card, states))