hanabif interface: refactor hanab.live specific features into own class

This commit is contained in:
Maximilian Keßler 2023-05-13 17:27:34 +02:00
parent 35e78f4753
commit b7f6df7e0d
Signed by: max
GPG Key ID: BCC5A619923C0BA5

132
hanabi.py
View File

@ -5,7 +5,7 @@ from termcolor import colored
import constants import constants
class DeckCard(): class DeckCard:
def __init__(self, suitIndex: int, rank: int, deck_index=None): def __init__(self, suitIndex: int, rank: int, deck_index=None):
self.suitIndex: int = suitIndex self.suitIndex: int = suitIndex
self.rank: int = rank self.rank: int = rank
@ -29,21 +29,21 @@ class DeckCard():
# should be injective enough, we never use cards with ranks differing by 1000 # should be injective enough, we never use cards with ranks differing by 1000
return 1000 * self.suitIndex + self.rank return 1000 * self.suitIndex + self.rank
def pp_deck(deck: List[DeckCard]) -> str: def pp_deck(deck: List[DeckCard]) -> str:
return "[" + ", ".join(card.colorize() for card in deck) + "]" return "[" + ", ".join(card.colorize() for card in deck) + "]"
class ActionType(Enum): class ActionType(Enum):
Play = 0 Play = 0
Discard = 1 Discard = 1
ColorClue = 2 ColorClue = 2
RankClue = 3 RankClue = 3
EndGame = 4 EndGame = 4
VoteTerminate = 5 ## hack: online, this is encoded as a 10 VoteTerminate = 5 ## hack: online, this is encoded as a 10
class Action(): class Action:
def __init__(self, type_: ActionType, target: int, value: Optional[int] = None): def __init__(self, type_: ActionType, target: int, value: Optional[int] = None):
self.type = type_ self.type = type_
self.target = target self.target = target
@ -55,10 +55,10 @@ class Action():
@staticmethod @staticmethod
def from_json(action): def from_json(action):
return Action( return Action(
ActionType(action['type']), ActionType(action['type']),
int(action['target']), int(action['target']),
action.get('value', None) action.get('value', None)
) )
def __repr__(self): def __repr__(self):
match self.type: match self.type:
@ -83,15 +83,15 @@ class Action():
class HanabiInstance(): class HanabiInstance():
def __init__( def __init__(
self, self,
deck: List[DeckCard], # assumes a default deck, every suit has to be distributed either [1,1,1,2,2,3,3,4,4,5] or [1,2,3,4,5] deck: List[DeckCard],
num_players: int, # number of players that play this deck, in range [2,6] # assumes a default deck, every suit has to be distributed either [1,1,1,2,2,3,3,4,4,5] or [1,2,3,4,5]
num_players: int, # number of players that play this deck, in range [2,6]
hand_size: Optional[int] = None, # number of cards that each player holds hand_size: Optional[int] = None, # number of cards that each player holds
num_strikes: Optional[int] = None, # number of strikes that leads to game loss num_strikes: Optional[int] = None, # number of strikes that leads to game loss
clue_starved: bool = False, # if true, discarding and playing fives only gives back half a clue clue_starved: bool = False, # if true, discarding and playing fives only gives back half a clue
fives_give_clue: bool = True, # if false, then playing a five will not change the clue count fives_give_clue: bool = True, # if false, then playing a five will not change the clue count
): ):
# defining properties # defining properties
self.deck = deck self.deck = deck
self.num_players = num_players self.num_players = num_players
@ -110,15 +110,16 @@ 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)
## maximum number of moves in any game that can achieve max score # # maximum number of moves in any game that can achieve max score each suit gives 15 moves, as we can play
# 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 # and discard 5 cards each and give 5 clues. dark suits only give 5 moves, since no discards are added number
# number of cards that remain in players hands after end of game. they cost 2 turns each, since we cannot discard them and also have one clue less # of cards that remain in players hands after end of game. they cost 2 turns each, since we cannot discard
# 8 clues at beginning, one further clue for each suit but one (the clue of the last 5 is never useful since it is gained in the extra-round) # them and also have one clue less 8 clues at beginning, one further clue for each suit but one (the clue of
# subtract a further move for a second 5-clue that can't be used in 5 or 6-player games, since the extraround starts too soon # the last 5 is never useful since it is gained in the extra-round) subtract a further move for a second
self.max_winning_moves = 15 * self.num_suits - 10 * self.num_dark_suits \ # 5-clue that can't be used in 5 or 6-player games, since the extraround starts too soon
- 2 * self.num_players * (self.hand_size - 1) \ self.max_winning_moves = 15 * self.num_suits - 10 * self.num_dark_suits \
+ 8 + (self.num_suits - 1) \ - 2 * self.num_players * (self.hand_size - 1) \
+ (-1 if self.num_players >= 5 else 0) + 8 + (self.num_suits - 1) \
+ (-1 if self.num_players >= 5 else 0)
@property @property
def num_dealt_cards(self): def num_dealt_cards(self):
@ -143,80 +144,59 @@ class GameState():
self.instance = instance self.instance = instance
# dynamic game state # dynamic game state
self.progress = self.instance.num_players * self.instance.hand_size # index of next card to be drawn self.progress = self.instance.num_players * self.instance.hand_size # index of next card to be drawn
self.hands = [self.instance.deck[self.instance.hand_size * p : self.instance.hand_size * (p+1)] for p in range(0, self.instance.num_players)] self.hands = [self.instance.deck[self.instance.hand_size * p: self.instance.hand_size * (p + 1)] for p in
range(0, self.instance.num_players)]
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 = 0
self.pace = self.instance.deck_size - 5 * self.instance.num_suits - self.instance.num_players * (self.instance.hand_size - 1) self.pace = self.instance.deck_size - 5 * self.instance.num_suits - self.instance.num_players * (
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 = []
# can be set to true if game is known to be in a lost state # can be set to true if game is known to be in a lost state
self.in_lost_state = False self.in_lost_state = False
# automatically set upon third strike, when extar round is over or when explicitly taking EndGame or VoteTerminate actions # automatically set upon third strike, when extar round is over or when explicitly taking EndGame or
# VoteTerminate actions
self.over = False self.over = False
# will track replay as game progresses # will track replay as game progresses
self.actions = [] self.actions = []
# Methods to control game state change
## Methods to control game state change
def make_action(self, action):
match action.type:
case ActionType.ColorClue | ActionType.RankClue:
assert(self.clues > 0)
self.actions.append(action)
self.clues -= self.instance.clue_increment
self.__make_turn()
# TODO: could check that the clue specified is in fact legal
case ActionType.Play:
self.play(action.target)
case ActionType.Discard:
self.discard(action.target)
case ActionType.EndGame | ActionType.VoteTerminate:
self.over = True
def play(self, card_idx): def play(self, card_idx):
card = self.instance.deck[card_idx] card = self.instance.deck[card_idx]
if card.rank == self.stacks[card.suitIndex] + 1: if card.rank == self.stacks[card.suitIndex] + 1:
self.stacks[card.suitIndex] += 1 self.stacks[card.suitIndex] += 1
if card.rank == 5 and self.clues != 8 and self.fives_give_clue: if card.rank == 5 and self.clues != 8 and self.instance.fives_give_clue:
self.clues += self.instance.clue_increment self.clues += self.instance.clue_increment
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.actions.append(Action(ActionType.Play, target=card_idx)) self.actions.append(Action(ActionType.Play, target=card_idx))
self.__replace(card_idx) self._replace(card_idx)
self.__make_turn() self._make_turn()
if all(s == 5 for s in self.stacks) or self.strikes >= self.instance.num_strikes: if all(s == 5 for s in self.stacks) or self.strikes >= self.instance.num_strikes:
self.over = True self.over = True
def discard(self, card_idx): def discard(self, card_idx):
assert(self.clues < 8) assert (self.clues < 8)
self.actions.append(Action(ActionType.Discard, target=card_idx)) self.actions.append(Action(ActionType.Discard, target=card_idx))
self.clues += self.instance.clue_increment self.clues += self.instance.clue_increment
self.pace -= 1 self.pace -= 1
self.trash.append(self.instance.deck[card_idx]) self.trash.append(self.instance.deck[card_idx])
self.__replace(card_idx) self._replace(card_idx)
self.__make_turn() self._make_turn()
def clue(self): def clue(self):
assert(self.clues > 0) assert (self.clues > 0)
self.actions.append( self.actions.append(self._waste_clue())
Action(
ActionType.RankClue,
target=(self.turn +1) % self.instance.num_players, # clue next plyaer
value=self.hands[(self.turn +1) % self.instance.num_players][0].rank # clue index 0
)
)
self.clues -= 1 self.clues -= 1
self.__make_turn() self._make_turn()
# Forward some properties of the underlying instance # Forward some properties of the underlying instance
@property @property
@ -243,9 +223,8 @@ class GameState():
def deck_size(self): def deck_size(self):
return self.instance.deck_size return self.instance.deck_size
# Properties of GameState # Properties of GameState
def is_over(self): def is_over(self):
return self.over or self.is_known_lost() return self.over or self.is_known_lost()
@ -264,7 +243,6 @@ class GameState():
@property @property
def cur_hand(self): def cur_hand(self):
return self.hands[self.turn] return self.hands[self.turn]
# Utilities # Utilities
@ -273,14 +251,13 @@ class GameState():
if card in hand: if card in hand:
yield player yield player
def to_json(self): def to_json(self):
# ensure we have at least one action # ensure we have at least one action
if len(self.actions) == 0: if len(self.actions) == 0:
self.actions.append(Action( self.actions.append(Action(
ActionType.EndGame, ActionType.EndGame,
target=0 target=0
) )
) )
return { return {
"deck": self.instance.deck, "deck": self.instance.deck,
@ -289,14 +266,14 @@ class GameState():
"first_player": 0, "first_player": 0,
"options": { "options": {
"variant": "No Variant", "variant": "No Variant",
}
} }
}
# Private helpers # Private helpers
# increments turn counter and tracks extra round # increments turn counter and tracks extra round
def __make_turn(self): def _make_turn(self):
assert(not self.over) assert (not self.over)
self.turn = (self.turn + 1) % self.instance.num_players self.turn = (self.turn + 1) % self.instance.num_players
if self.progress == self.instance.deck_size: if self.progress == self.instance.deck_size:
self.remaining_extra_turns -= 1 self.remaining_extra_turns -= 1
@ -304,10 +281,10 @@ class GameState():
self.over = True self.over = True
# 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): def _replace(self, card_idx):
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), None)
assert(idx_in_hand is not None) assert (idx_in_hand is not None)
for i in range(idx_in_hand, self.instance.hand_size - 1): for i in range(idx_in_hand, self.instance.hand_size - 1):
self.cur_hand[i] = self.cur_hand[i + 1] self.cur_hand[i] = self.cur_hand[i + 1]
@ -315,3 +292,10 @@ class GameState():
self.cur_hand[self.instance.hand_size - 1] = self.instance.deck[self.progress] self.cur_hand[self.instance.hand_size - 1] = self.instance.deck[self.progress]
self.progress += 1 self.progress += 1
# in HanabLiveInstances, this will be overridden with something that checks defaults
def _waste_clue(self) -> Action:
return Action(
ActionType.RankClue,
target=(self.turn + 1) % self.instance.num_players, # clue next plyaer
value=self.hands[(self.turn + 1) % self.instance.num_players][0].rank # clue index 0
)