diff --git a/game_state.h b/game_state.h index 7a483de..30d04c2 100644 --- a/game_state.h +++ b/game_state.h @@ -25,8 +25,6 @@ namespace Hanabi { using hand_index_t = std::uint8_t; using probability_t = boost::rational; - using state_t = std::uint32_t; - /** * We will generally assume that stacks are played from n to 0 * Playing a 0 will yield a clue @@ -230,6 +228,29 @@ private: bool was_on_8_clues {false}; }; + // This keeps track of the representation of the gamestate relative to some starting state + // and is used for id calculation + struct RelativeRepresentationData { + // List of unique non-trash cards in draw pile + boost::container::static_vector good_cards_draw; + + // Card positions of these cards. Indexes correspond to the cards stored in _good_cards_draw vector + boost::container::static_vector, 30> card_positions_draw; + + // Note this is not the same as _good_cards_draw.size(), since this accounts for multiplicities + size_t initial_draw_pile_size {}; + + // This will indicate whether cards that were in hands initially still are in hand + // The first n bits are used and cards are assumed to have been marked with their indices in this bitset + std::bitset card_positions_hands {}; + + // Number of bits from above bitset that is meaningful + size_t num_useful_cards_in_starting_hands { 0 }; + + // Whether we initialized the values above and marked cards accordingly + bool initialized { false }; + }; + template unsigned long play_and_potentially_update(hand_index_t index); template unsigned long discard_and_potentially_update(hand_index_t index); @@ -250,6 +271,7 @@ private: std::uint64_t unique_id() const; + // Usual game state player_t _turn{}; clue_t _num_clues{}; std::uint8_t _weighted_draw_pile_size{}; @@ -258,26 +280,20 @@ private: std::list _draw_pile{}; std::uint8_t _endgame_turns_left{}; - // This will save the card positions of all cards that are in the draw pile when we start backtracking. - boost::container::static_vector, 10> _card_positions_draw; - // Note that 30 cards here is enough for all standard decks, since there are ot most 6 suits with 5 unique cards each. - boost::container::static_vector _good_cards_draw; - - // This will indicate whether cards that were in hands initially still are in hands - std::bitset _card_positions_hands; - - size_t _num_useful_cards_in_starting_hands{}; - size_t _initial_draw_pile_size{}; - - // further statistics that we might want to keep track of + // further values of game state that are technically determined, but we update them anyway int8_t _pace{}; uint8_t _score{}; - std::uint64_t _enumerated_states {}; + // For reverting the current game + std::stack _actions_log; + // For calculating ids of states during backtracking + RelativeRepresentationData _relative_representation; + + // Lookup table for states. Uses the ids calculated using the relative representation std::unordered_map _position_tablebase; - std::stack _actions_log; + std::uint64_t _enumerated_states {}; }; template diff --git a/game_state.hpp b/game_state.hpp index 40c7c4f..841772b 100644 --- a/game_state.hpp +++ b/game_state.hpp @@ -81,12 +81,11 @@ namespace Hanabi { _hands(), _draw_pile(), _endgame_turns_left(no_endgame), - _card_positions_draw(), - _card_positions_hands(), - _num_useful_cards_in_starting_hands(0), - _initial_draw_pile_size(0), _pace(deck.size() - 5 * num_suits - num_players * (hand_size - 1)), _score(0), + _actions_log(), + _relative_representation(), + _position_tablebase(), _enumerated_states(0) { std::ranges::fill(_stacks, starting_card_rank); for (const Card &card: deck) { @@ -239,11 +238,11 @@ namespace Hanabi { const Card discarded = _hands[_turn][index]; if (!discarded.initial_trash) { if (discarded.in_starting_hand) { - ASSERT(_card_positions_hands[discarded.local_index] == true); - _card_positions_hands[discarded.local_index] = false; + ASSERT(_relative_representation.card_positions_hands[discarded.local_index] == true); + _relative_representation.card_positions_hands[discarded.local_index] = false; } else { - auto replaced_card_it = std::ranges::find(_card_positions_draw[discarded.local_index], _turn); - ASSERT(replaced_card_it != _card_positions_draw[discarded.local_index].end()); + auto replaced_card_it = std::ranges::find(_relative_representation.card_positions_draw[discarded.local_index], _turn); + ASSERT(replaced_card_it != _relative_representation.card_positions_draw[discarded.local_index].end()); *replaced_card_it = trash_or_play_stack; } } @@ -266,8 +265,8 @@ namespace Hanabi { // update card position of the drawn card if (!draw.card.initial_trash) { ASSERT(draw.card.in_starting_hand == false); - auto new_card_it = std::ranges::find(_card_positions_draw[draw.card.local_index], draw_pile); - ASSERT(new_card_it != _card_positions_draw[draw.card.local_index].end()); + auto new_card_it = std::ranges::find(_relative_representation.card_positions_draw[draw.card.local_index], draw_pile); + ASSERT(new_card_it != _relative_representation.card_positions_draw[draw.card.local_index].end()); *new_card_it = _turn; } } @@ -300,8 +299,8 @@ namespace Hanabi { if (!drawn.initial_trash) { ASSERT(drawn.in_starting_hand == false); - auto drawn_card_it = std::ranges::find(_card_positions_draw[drawn.local_index], _turn); - ASSERT(drawn_card_it != _card_positions_draw[drawn.local_index].end()); + auto drawn_card_it = std::ranges::find(_relative_representation.card_positions_draw[drawn.local_index], _turn); + ASSERT(drawn_card_it != _relative_representation.card_positions_draw[drawn.local_index].end()); *drawn_card_it = draw_pile; } @@ -313,12 +312,12 @@ namespace Hanabi { if (!discarded_card.initial_trash) { if (discarded_card.in_starting_hand) { - ASSERT(_card_positions_hands[discarded_card.local_index] == false); - _card_positions_hands[discarded_card.local_index] = true; + ASSERT(_relative_representation.card_positions_hands[discarded_card.local_index] == false); + _relative_representation.card_positions_hands[discarded_card.local_index] = true; } else { - auto hand_card_it = std::ranges::find(_card_positions_draw[discarded_card.local_index], + auto hand_card_it = std::ranges::find(_relative_representation.card_positions_draw[discarded_card.local_index], trash_or_play_stack); - ASSERT(hand_card_it != _card_positions_draw[discarded_card.local_index].end()); + ASSERT(hand_card_it != _relative_representation.card_positions_draw[discarded_card.local_index].end()); *hand_card_it = _turn; } } @@ -351,17 +350,17 @@ namespace Hanabi { _draw_pile.clear(); for (suit_t suit = 0; suit < num_suits; suit++) { for (rank_t rank = 0; rank < starting_card_rank; rank++) { - Card card{suit, rank, static_cast(_card_positions_draw.size()), false, is_trash(card)}; + Card card{suit, rank, static_cast(_relative_representation.card_positions_draw.size()), false, is_trash(card)}; if (nums_in_draw_pile[card] > 0) { _draw_pile.push_back({card, nums_in_draw_pile[card]}); if (!is_trash(card)) { - _card_positions_draw.push_back({nums_in_draw_pile[card], draw_pile}); - _good_cards_draw.push_back(card); + _relative_representation.card_positions_draw.push_back({nums_in_draw_pile[card], draw_pile}); + _relative_representation.good_cards_draw.push_back(card); } } } } - _initial_draw_pile_size = _weighted_draw_pile_size; + _relative_representation.initial_draw_pile_size = _weighted_draw_pile_size; // Prepare cards in hands for (player_t player = 0; player < num_players; player++) { @@ -375,17 +374,17 @@ namespace Hanabi { // This card is already in hand, so just replace the second copy by some trash card = trash; } else { - card.local_index = _num_useful_cards_in_starting_hands; - _num_useful_cards_in_starting_hands++; + card.local_index = _relative_representation.num_useful_cards_in_starting_hands; + _relative_representation.num_useful_cards_in_starting_hands++; good_cards_in_hand.push_back(card); } } } } - _card_positions_hands.reset(); - for (size_t i = 0; i < _num_useful_cards_in_starting_hands; i++) { - _card_positions_hands[i] = true; + _relative_representation.card_positions_hands.reset(); + for (size_t i = 0; i < _relative_representation.num_useful_cards_in_starting_hands; i++) { + _relative_representation.card_positions_hands[i] = true; } } @@ -560,14 +559,14 @@ namespace Hanabi { unsigned long id = 0; // encode all positions of cards that started in draw pile - ASSERT(_card_positions_draw.size() == _good_cards_draw.size()); - for(size_t i = 0; i < _card_positions_draw.size(); i++) { - for(player_t player : _card_positions_draw[i]) { + ASSERT(_relative_representation.card_positions_draw.size() == _relative_representation.good_cards_draw.size()); + for(size_t i = 0; i < _relative_representation.card_positions_draw.size(); i++) { + for(player_t player : _relative_representation.card_positions_draw[i]) { id *= num_players + 2; // We normalize here: If a card is already played, then the positions of its other copies // do not matter, so we can just pretend that they are all in the trash already. // The resulting states will be equivalent. - if (!is_trash(_good_cards_draw[i])) { + if (!is_trash(_relative_representation.good_cards_draw[i])) { id += player; } else { id += trash_or_play_stack; @@ -589,12 +588,12 @@ namespace Hanabi { } }(); - id *= _initial_draw_pile_size + num_players; + id *= _relative_representation.initial_draw_pile_size + num_players; id += draw_pile_size_and_extra_turns; // encode positions of cards that started in hands - id = id << _num_useful_cards_in_starting_hands; - id += _card_positions_hands.to_ulong(); + id = id << _relative_representation.num_useful_cards_in_starting_hands; + id += _relative_representation.card_positions_hands.to_ulong(); id *= num_players; id += _turn;