rework analysis of upper bounds: compute all bounds now, insert into DB properly
This commit is contained in:
parent
91f3c73eb3
commit
29cae8f139

@ 1,7 +1,8 @@


from hanabi.live import compress


from enum import Enum


from typing import List




from hanabi import database


from hanabi import logger


from hanabi import hanab_game


from hanabi.live import compress





@ 9,107 +10,85 @@ from hanabi.live import compress


class InfeasibilityType(Enum):


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


NotTrivial = 2


CritAtBottom = 3






class InfeasibilityReason():


def __init__(self, infeasibility_type, idx, value=None):


class InfeasibilityReason:


def __init__(self, infeasibility_type: InfeasibilityType, score_upper_bound, value=None):


self.type = infeasibility_type


self.index = idx


self.score_upper_bound = score_upper_bound


self.value = value




def __repr__(self):


match self.type:


case InfeasibilityType.OutOfPace:


return "Deck runs out of pace ({}) after drawing card {}".format(self.value, self.index)


return "Upper bound {}, since deck runs out of pace after drawing card {}".format(self.score_upper_bound, self.value)


case InfeasibilityType.OutOfHandSize:


return "Deck runs out of hand size after drawing card {}".format(self.index)


return "Upper bound {}, since deck runs out of hand size after drawing card {}".format(self.score_upper_bound, self.value)


case InfeasibilityType.CritAtBottom:


return "Deck has crit non5 at bottom (index {})".format(self.index)


return "Upper bound {}, sicne deck has critical non5 at bottom".format(self.score_upper_bound)






def analyze_suit(occurrences):


# denotes the indexes of copies we can use wlog


picks = {


1: 0,


**{r: None for r in range(2, 5)},


5: 0


}


def analyze(instance: hanab_game.HanabiInstance, only_find_first=False) > List[InfeasibilityReason]:


"""


Checks instance for the following (easy) certificates for unfeasibility


 There is a critical non5 at the bottom


 We necessarily run out of pace when playing this deck:


At some point, among all drawn cards, there are too few playable ones so that the next discard


reduces pace to a negative amount


 We run out of hand size when playing this deck:


At some point, there are too many critical cards (that also could not have been played) for the players


to hold collectively


:param instance: Instance to be analyzed


:param only_find_first: If true, we immediately return when finding the first infeasibility reason and don't


check for further ones. Might be slightly faster on some instances, especially dark ones.


:return: List with all reasons found. Empty if none is found.


In particular, if return value is not the empty list, the analyzed instance is unfeasible


"""


reasons = []




# denotes the intervals when cards will be played wlog


play_times = {


1: [occurrences[1][0]],


**{r: None for _ in range(instance.num_suits)


for r in range(2, 6)


}


}


# check for critical nonfives 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(


InfeasibilityType.CritAtBottom,


instance.max_score  5 + bottom_card.rank,


instance.deck_size  1


))


if only_find_first:


return reasons




print("occurrences are: {}".format(occurrences))




for rank in range(2, 6):




# general analysis


earliest_play = max(min(play_times[rank  1]), min(occurrences[rank]))


latest_play = max(*play_times[rank  1], *occurrences[rank])


play_times[rank] = [earliest_play, latest_play]




# check a few extra cases regarding the picks when the rank is not 5


if rank != 5:


# check if we can just play the first copy


if max(play_times[rank  1]) < min(occurrences[rank]):


picks[rank] = 0


play_times[rank] = [min(occurrences[rank])]


continue




# check if the second copy is not worse than the first when it comes,


# because we either have to wait for smaller cards anyway


# or the next card is not there anyway


if max(occurrences[rank]) < max(earliest_play, min(occurrences[rank + 1])):


picks[rank] = 1




return picks, play_times






def analyze_card_usage(instance: hanab_game.HanabiInstance):


storage_size = instance.num_players * instance.hand_size


for suit in range(instance.num_suits):


print("analysing suit {}: {}".format(


suit,


hanab_game.pp_deck((c for c in instance.deck if c.suitIndex == suit))


)


)




occurrences = {


rank: [max(0, i  storage_size + 1) for (i, card) in enumerate(instance.deck) if


card == hanab_game.DeckCard(suit, rank)]


for rank in range(1, 6)


}




picks, play_times = analyze_suit(occurrences)




print("did analysis:")


print("play times: ", play_times)


print("picks: ", picks)


print()






def analyze(instance: hanab_game.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


# as soon as we have them (and recurse this)


# we will sweep through the deck and pretend that


#  we keep all nontrash cards in our hands


#  we instantly play all playable cards as soon as we have them


#  we recurse on this instantplay


#


# For example, we assume that once we draw r2, we check if we can play r2.


# If yes, then we also check if we drew r3 earlier and so on.


# If not, then we keep r2 in our hands


#


# In total, this is equivalent to assuming that we infinitely many clues


# and infinite storage space in our hands (which is of course not true),


# but even in this setting, some games are infeasible due to pace issues


# that we can detect


#


# A small refinement is to pretend that we only have infinite storage for noncrit cards,


# for critcards, the usual hand card limit applies.


# This allows us to detect some seeds where there are simply too many unplayable cards to hold at some point


# that also can't be discarded


# this allows us to detect standard pace issue arguments




stacks = [0] * instance.num_suits




# we will ensure that stored_crits is a subset of stored_cards


stored_cards = set()


stored_crits = set()




min_forced_pace = 100


worst_index = 0


min_forced_pace = instance.initial_pace


worst_pace_index = 0




ret = None


max_forced_crit_discard = 0


worst_crit_index = 0




for (i, card) in enumerate(instance.deck):


if card.rank == stacks[card.suitIndex] + 1:



@ 133,68 +112,84 @@ def analyze(instance: hanab_game.HanabiInstance, find_non_trivial=False) > Infe


stored_crits.add(card)


stored_cards.add(card)




# check for out of handsize:


if len(stored_crits) == instance.num_players * instance.hand_size:


return InfeasibilityReason(InfeasibilityType.OutOfHandSize, i)




if find_non_trivial and len(stored_cards) == instance.num_players * instance.hand_size:


ret = InfeasibilityReason(InfeasibilityType.NotTrivial, i)


# check for out of handsize (this number can be negative, in which case nothing applies)


# Note the +1 at the end, which is there because we have to discard next,


# so even if we currently have as many crits as we can hold, we have to discard one


num_forced_crit_discards = len(stored_crits)  instance.num_players * instance.hand_size + 1


if len(stored_crits)  instance.num_players * instance.hand_size > max_forced_crit_discard:


worst_crit_index = i


max_forced_crit_discard = num_forced_crit_discards


if only_find_first:


reasons.append(InfeasibilityReason(


InfeasibilityType.OutOfPace,


instance.max_score + min_forced_pace,


worst_pace_index


))


return reasons




# the last  1 is there because we have to discard 'next', causing a further draw


max_remaining_plays = (instance.deck_size  i  1) + instance.num_players  1




needed_plays = 5 * instance.num_suits  sum(stacks)


missing = max_remaining_plays  needed_plays


if missing < min_forced_pace:


# print("update to {}: {}".format(i, missing))


min_forced_pace = missing


worst_index = i


needed_plays = instance.max_score  sum(stacks)


cur_pace = max_remaining_plays  needed_plays


if cur_pace < min(0, min_forced_pace):


min_forced_pace = cur_pace


worst_pace_index = i


if only_find_first:


reasons.append(InfeasibilityReason(


InfeasibilityType.OutOfPace,


instance.max_score + min_forced_pace,


worst_pace_index


))


return reasons




# check that we correctly walked through the deck


assert (len(stored_cards) == 0)


assert (len(stored_crits) == 0)


assert (sum(stacks) == 5 * instance.num_suits)


assert (sum(stacks) == instance.max_score)




if max_forced_crit_discard > 0:


reasons.append(


InfeasibilityReason(


InfeasibilityType.OutOfHandSize,


instance.max_score  max_forced_crit_discard,


worst_crit_index


)


)




if min_forced_pace < 0:


return InfeasibilityReason(InfeasibilityType.OutOfPace, worst_index, min_forced_pace)


elif ret is not None:


return ret


else:


return None


reasons.append(InfeasibilityReason(


InfeasibilityType.OutOfPace,


instance.max_score + min_forced_pace,


worst_pace_index


))




return reasons






def run_on_database():


cur = database.conn.cursor()


cur2 = database.conn.cursor()


for num_p in range(2, 6):


cur.execute(


"SELECT seed, num_players, deck from seeds where variant_id = 0 and num_players = (%s) order by seed asc",


(num_p,))


res = cur.fetchall()


hand = 0


pace = 0


non_trivial = 0


d = None


print("Checking {} {}player seeds from database".format(len(res), num_p))


for (seed, num_players, deck) in res:


deck = compress.decompress_deck(deck)


a = analyze(hanab_game.HanabiInstance(deck, num_players), True)


if type(a) == InfeasibilityReason:


if a.type == InfeasibilityType.OutOfHandSize:


# print("Seed {} infeasible: {}\n{}".format(seed, a, deck))


hand += 1


elif a.type == InfeasibilityType.OutOfPace:


pace += 1


elif a.type == InfeasibilityType.NotTrivial:


non_trivial += 1


d = seed, deck




print("Found {} seeds running out of hand size, {} running out of pace and {} that are not trivial".format(hand,


pace,


non_trivial))


if d is not None:


print("example nontrivial deck (seed {}): [{}]".format(


d[0],


", ".join(c.colorize() for c in d[1])


))


print()


def run_on_database(variant_id):


database.cur.execute(


"SELECT seed, num_players, deck FROM seeds WHERE variant_id = (%s) ORDER BY (num_players, seed)",


(variant_id,)


)


res = database.cur.fetchall()


logger.info("Checking {} seeds of variant {} for infeasibility".format(len(res), variant_id))


for (seed, num_players, deck_str) in res:


deck = compress.decompress_deck(deck_str)


reasons = analyze(hanab_game.HanabiInstance(deck, num_players))


if reasons:


print("found infeasible seed {}: {}".format(seed, reasons))


else:


print("found nothing for seed {}".format(seed))


for reason in reasons:


database.cur.execute(


"INSERT INTO score_upper_bounds (seed, score_upper_bound, reason) "


"VALUES (%s,%s,%s) "


"ON CONFLICT (seed, reason) DO UPDATE "


"SET score_upper_bound = EXCLUDED.score_upper_bound",


(seed, reason.score_upper_bound, reason.type.value)


)


database.cur.execute(


"UPDATE seeds SET feasible = (%s) WHERE seed = (%s)",


(False, seed)


)


database.conn.commit()




Loading…
Reference in New Issue