From ea881c5e6aee600e900e04d2b22a1ecb99e3156b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Thu, 16 Nov 2023 16:20:04 +0100 Subject: [PATCH] reformat code --- include/command_line_interface.h | 33 +- include/download.h | 31 +- include/game_interface.h | 45 +- include/game_state.h | 206 +-- include/game_state.hpp | 2026 ++++++++++++++++-------------- include/hanabi_types.hpp | 67 +- include/myassert.h | 2 + include/null_buffer.h | 30 +- include/parse_game.h | 24 +- include/state_explorer.h | 5 +- src/check_games.cpp | 90 +- src/command_line_interface.cpp | 92 +- src/download.cpp | 41 +- src/game_interface.cpp | 44 +- src/hanabi_types.cpp | 28 +- src/main.cpp | 3 +- src/make_state.cpp | 68 +- src/parse_game.cpp | 59 +- src/state_explorer.cpp | 805 ++++++------ src/test.cpp | 21 +- 20 files changed, 2076 insertions(+), 1644 deletions(-) diff --git a/include/command_line_interface.h b/include/command_line_interface.h index 89ce958..81faa80 100644 --- a/include/command_line_interface.h +++ b/include/command_line_interface.h @@ -6,13 +6,14 @@ #include "hanabi_types.hpp" -namespace Hanabi { - enum class GameStateSpecType { - turn = 0, - draw_pile_size = 1, - }; +namespace Hanabi +{ + enum class GameStateSpecType + { + turn = 0, draw_pile_size = 1, }; - struct CLIParms { + struct CLIParms + { /** * The data source for the game to be analysed. * If of type int, assumed to be a game ID from hanab.live @@ -20,19 +21,19 @@ namespace Hanabi { * see https://raw.githubusercontent.com/Hanabi-Live/hanabi-live/main/misc/example_game_with_comments.jsonc * for a format specification. */ - std::variant game {}; + std::variant game{}; /** * Definition of a 'winning' game, i.e. what score (number of cards played) is considered * to be winning. * If std::nullopt, then the maximum score of the game will be used. */ - boost::optional score_goal {}; + boost::optional score_goal{}; /** * Whether game_state_spec denotes a turn number or draw pile size. */ - GameStateSpecType game_state_spec_type { GameStateSpecType::draw_pile_size }; + GameStateSpecType game_state_spec_type{GameStateSpecType::draw_pile_size}; /** * Either a turn number or a draw pile size, depending on game_state_spec_type. @@ -42,36 +43,36 @@ namespace Hanabi { * (since this is also the case on hanab.live) * Thus, a turn number of 0 is undefined and a turn number of 1 corresponds to no actions taken in the game. */ - unsigned game_state_spec { 5 }; + unsigned game_state_spec{5}; /** * Whether to launch an interactive exploration shell for the game state after performing analysis. */ - boost::optional interactive {}; + boost::optional interactive{}; /** * If true, deactivates non-essential output (to cout). */ - bool quiet { false }; + bool quiet{false}; /** * If this holds std::monostate, then all clue numbers are evaluated. * Otherwise, the specified clue modifier is applied (relative to actual number of clues). * Thus, setting this to 0 has no effect. */ - std::variant clue_spec {static_cast(0)}; + std::variant clue_spec{static_cast(0)}; /** * If true, then all states corresponding to smaller draw pile sizes */ - bool recursive { false }; + bool recursive{false}; }; /** * @brief Get an output stream that is std::cout or a Null-Stream. * @param quiet If true, NullStream is returned, otherwise std::cout */ - std::ostream & quiet_ostream(bool quiet); + std::ostream &quiet_ostream(bool quiet); constexpr int download_failed = 1; constexpr int state_unreachable = 2; @@ -88,6 +89,6 @@ namespace Hanabi { /** * @brief Execute parsed parameters. */ - int run_cli(CLIParms const & parms); + int run_cli(CLIParms const &parms); } #endif //DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H diff --git a/include/download.h b/include/download.h index 8c9450f..a95c6cc 100644 --- a/include/download.h +++ b/include/download.h @@ -9,25 +9,26 @@ #include "game_interface.h" -namespace Download { +namespace Download +{ - std::optional download_game_json(int game_id); + std::optional download_game_json(int game_id); - std::optional open_game_json(const char *filename); + std::optional open_game_json(char const *filename); - /** - * @brief Create game object from given source - * @param game_spec Either an id to download from hanab.live or a filename with a json specification - * @param score_goal What score counts as a win for this game. If left empty, the maximum score is inserted. - * @return Game state - * - * If both turn and draw_pile_break are specified, the game skips until the specified turn or the first time the - * draw pile hits the given size, whichever comes first - * - */ - Hanabi::Game get_game(int game_id, std::optional score_goal); + /** + * @brief Create game object from given source + * @param game_spec Either an id to download from hanab.live or a filename with a json specification + * @param score_goal What score counts as a win for this game. If left empty, the maximum score is inserted. + * @return Game state + * + * If both turn and draw_pile_break are specified, the game skips until the specified turn or the first time the + * draw pile hits the given size, whichever comes first + * + */ + Hanabi::Game get_game(int game_id, std::optional score_goal); - Hanabi::Game get_game(const std::string& filename, std::optional score_goal); + Hanabi::Game get_game(std::string const & filename, std::optional score_goal); } // namespace Download diff --git a/include/game_interface.h b/include/game_interface.h index c69f761..01f486e 100644 --- a/include/game_interface.h +++ b/include/game_interface.h @@ -21,53 +21,73 @@ namespace Hanabi bool operator==(const CardMultiplicity &) const = default; }; - class HanabiStateIF { + class HanabiStateIF + { public: virtual void give_clue() = 0; + virtual void discard(hand_index_t index) = 0; + virtual void play(hand_index_t index) = 0; - virtual void rotate_next_draw(const Card& card) = 0; + virtual void rotate_next_draw(const Card & card) = 0; + virtual ActionType last_action_type() const = 0; + virtual void revert() = 0; virtual void modify_clues(clue_t change) = 0; + virtual void set_clues(clue_t clues) = 0; [[nodiscard]] virtual player_t turn() const = 0; + [[nodiscard]] virtual clue_t num_clues() const = 0; + [[nodiscard]] virtual unsigned score() const = 0; + [[nodiscard]] virtual std::vector> hands() const = 0; + [[nodiscard]] virtual std::vector cur_hand() const = 0; + [[nodiscard]] virtual size_t draw_pile_size() const = 0; - [[nodiscard]] virtual bool is_trash(const Card& card) const = 0; - [[nodiscard]] virtual bool is_playable(const Card& card) const = 0; + [[nodiscard]] virtual bool is_trash(const Card & card) const = 0; + + [[nodiscard]] virtual bool is_playable(const Card & card) const = 0; + [[nodiscard]] virtual bool is_relative_state_initialized() const = 0; - [[nodiscard]] virtual hand_index_t find_card_in_hand(const Card& card) const = 0; + + [[nodiscard]] virtual hand_index_t find_card_in_hand(const Card & card) const = 0; [[nodiscard]] virtual std::uint64_t enumerated_states() const = 0; - [[nodiscard]] virtual const std::unordered_map& position_tablebase() const = 0; + + [[nodiscard]] virtual const std::unordered_map & position_tablebase() const = 0; virtual void init_backtracking_information() = 0; + virtual probability_t evaluate_state() = 0; [[nodiscard]] virtual std::optional lookup() const = 0; + [[nodiscard]] virtual std::uint64_t unique_id() const = 0; + [[nodiscard]] virtual std::pair, std::vector> dump_unique_id_parts() const = 0; virtual std::vector>> get_reasonable_actions() = 0; - virtual std::vector>> possible_next_states(hand_index_t index, bool play) = 0; + + virtual std::vector>> + possible_next_states(hand_index_t index, bool play) = 0; virtual ~HanabiStateIF() = default; protected: - virtual void print(std::ostream& os) const = 0; + virtual void print(std::ostream & os) const = 0; - friend std::ostream& operator<<(std::ostream&, HanabiStateIF const&); + friend std::ostream & operator<<(std::ostream &, HanabiStateIF const &); }; - std::ostream &operator<<(std::ostream &os, HanabiStateIF const &hanabi_state); + std::ostream & operator<<(std::ostream & os, HanabiStateIF const & hanabi_state); struct GameInfo { @@ -77,16 +97,19 @@ namespace Hanabi Hanabi::player_t num_players; }; - struct Game : private GameInfo { + struct Game : private GameInfo + { public: Game(std::unique_ptr state, GameInfo game_info); [[nodiscard]] unsigned cur_turn() const; void make_turn(); + void revert_turn(); bool goto_draw_pile_size(size_t draw_pile_break); + bool goto_turn(size_t turn); [[nodiscard]] bool holds_state() const; diff --git a/include/game_state.h b/include/game_state.h index 575955f..622ca83 100644 --- a/include/game_state.h +++ b/include/game_state.h @@ -18,158 +18,190 @@ #include "game_interface.h" -namespace Hanabi { +namespace Hanabi +{ -template -using Stacks = std::array; + template + using Stacks = std::array; -template -std::ostream &operator<<(std::ostream &os, const Stacks &stacks); + template + std::ostream & operator<<(std::ostream & os, const Stacks & stacks); -template -struct InnerCardArray { + template + struct InnerCardArray + { template using array_t = std::array; -}; + }; -template<> -struct InnerCardArray { + template<> + struct InnerCardArray + { template using array_t = std::bitset; -}; + }; -template struct CardArray { + template + struct CardArray + { using value_type = T; CardArray() = default; + explicit CardArray(value_type default_val); void fill(value_type val); - const value_type &operator[](const Card &card) const; + const value_type & operator[](const Card & card) const; - value_type &operator[](const Card &card); + value_type & operator[](const Card & card); auto operator<=>(const CardArray &) const = default; -private: + private: using inner_array_t = typename InnerCardArray::template array_t; - std::array _array {}; -}; + std::array _array{}; + }; // A game mimics a game state together with a list of actions and allows to traverse the game // history by making and reverting the stored actions. -template -class HanabiState : public HanabiStateIF { -public: + template + class HanabiState : public HanabiStateIF + { + public: HanabiState() = default; - explicit HanabiState(const std::vector& deck, uint8_t score_goal = 5 * num_suits); + + explicit HanabiState(const std::vector & deck, uint8_t score_goal = 5 * num_suits); void give_clue() final; + void discard(hand_index_t index) final; + void play(hand_index_t index) final; - void rotate_next_draw(const Card& card) final; + void rotate_next_draw(const Card & card) final; + ActionType last_action_type() const final; void revert() final; void modify_clues(clue_t change) final; + void set_clues(clue_t clues) final; [[nodiscard]] player_t turn() const final; + [[nodiscard]] clue_t num_clues() const final; + [[nodiscard]] unsigned score() const final; + [[nodiscard]] std::vector> hands() const final; + [[nodiscard]] std::vector cur_hand() const final; + [[nodiscard]] size_t draw_pile_size() const final; - [[nodiscard]] hand_index_t find_card_in_hand(const Card& card) const final; - [[nodiscard]] bool is_trash(const Card& card) const final; - [[nodiscard]] bool is_playable(const Card& card) const final; + [[nodiscard]] hand_index_t find_card_in_hand(const Card & card) const final; + + [[nodiscard]] bool is_trash(const Card & card) const final; + + [[nodiscard]] bool is_playable(const Card & card) const final; + [[nodiscard]] bool is_relative_state_initialized() const final; [[nodiscard]] std::uint64_t enumerated_states() const final; - [[nodiscard]] const std::unordered_map& position_tablebase() const final; + + [[nodiscard]] const std::unordered_map & position_tablebase() const final; void init_backtracking_information() final; + probability_t evaluate_state() final; [[nodiscard]] std::optional lookup() const final; + [[nodiscard]] std::uint64_t unique_id() const final; + [[nodiscard]] std::pair, std::vector> dump_unique_id_parts() const final; std::vector>> get_reasonable_actions() final; - std::vector>> possible_next_states(hand_index_t index, bool play) final; + + std::vector>> + possible_next_states(hand_index_t index, bool play) final; auto operator<=>(const HanabiState &) const = default; -protected: - void print(std::ostream& os) const final; + protected: + void print(std::ostream & os) const final; -private: - struct BacktrackAction { - explicit BacktrackAction( - ActionType action_type, - Card discarded_or_played = Cards::unknown, - hand_index_t index = 0, - bool was_on_8_clues = false, - bool strike = false - ); + private: + struct BacktrackAction + { + explicit BacktrackAction( + ActionType action_type + , Card discarded_or_played = Cards::unknown + , hand_index_t index = 0 + , bool was_on_8_clues = false + , bool strike = false + ); - ActionType action_type{}; - // The card that was discarded or played - Card discarded{}; - // Index of card in hand that was discarded or played - hand_index_t index{}; + ActionType action_type{}; + // The card that was discarded or played + Card discarded{}; + // Index of card in hand that was discarded or played + hand_index_t index{}; - // Indicates whether before the action was taken, we had 8 clues. - // This is important so that we know if we go back to 7 or 8 clues when we revert playing a 5 - bool was_on_8_clues {false}; + // Indicates whether before the action was taken, we had 8 clues. + // This is important so that we know if we go back to 7 or 8 clues when we revert playing a 5 + bool was_on_8_clues{false}; - // Indicates whether playing this card triggered a bomb. - // This cannot be deduced just from the stacks since we cannot differentiate between a card - // having been played correctly or the top card of the draw pile being bombed. - bool strike {false}; + // Indicates whether playing this card triggered a bomb. + // This cannot be deduced just from the stacks since we cannot differentiate between a card + // having been played correctly or the top card of the draw pile being bombed. + bool strike{false}; }; - // This keeps track of the representation of the gamestate relative to some starting state - // and is used for id calculation - struct RelativeRepresentationData { - static constexpr player_t draw_pile = num_players; - static constexpr player_t discard_pile = num_players + 1; - static constexpr player_t play_stack = num_players + 2; - enum CardPosition : uint8_t { - hand = 0, - played = 1, - discarded = 2 - }; - // List of unique non-trash cards in draw pile - boost::container::static_vector good_cards_draw; + // This keeps track of the representation of the gamestate relative to some starting state + // and is used for id calculation + struct RelativeRepresentationData + { + static constexpr player_t draw_pile = num_players; + static constexpr player_t discard_pile = num_players + 1; + static constexpr player_t play_stack = num_players + 2; + enum CardPosition : uint8_t + { + hand = 0, played = 1, discarded = 2 + }; + // 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; + // Card positions of these cards. Indexes correspond to the cards stored in _good_cards_draw vector + boost::container::static_vector + , 30> card_positions_draw; - // 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 - boost::container::static_vector card_positions_hands {}; + // 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 + boost::container::static_vector card_positions_hands{}; - // Note this is not the same as _good_cards_draw.size(), since this accounts for multiplicities - std::uint8_t initial_draw_pile_size { 0 }; + // Note this is not the same as _good_cards_draw.size(), since this accounts for multiplicities + std::uint8_t initial_draw_pile_size{0}; - // Whether we initialized the values above and marked cards accordingly - bool initialized { false }; - }; + // Whether we initialized the values above and marked cards accordingly + bool initialized{false}; + }; unsigned long discard_and_potentially_update(hand_index_t index, bool cycle = false); + unsigned long play_and_potentially_update(hand_index_t index, bool cycle = false); unsigned draw(hand_index_t index, bool cycle = false, bool played = true); void revert_draw(hand_index_t index, Card discarded_card, bool cycle = false, bool played = true); + void revert_clue(); + void revert_discard(bool cycle = false); + void revert_play(bool cycle = false); @@ -179,6 +211,7 @@ private: void do_for_each_potential_draw(hand_index_t index, bool play, Function f); void incr_turn(); + void decr_turn(); void check_draw_pile_integrity() const; @@ -208,19 +241,24 @@ private: // Lookup table for states. Uses the ids calculated using the relative representation std::unordered_map _position_tablebase; - std::uint64_t _enumerated_states {}; -}; + std::uint64_t _enumerated_states{}; + }; -template -bool same_up_to_discard_permutation(HanabiState state1, HanabiState state2) { - auto comp = [](CardMultiplicity &m1, CardMultiplicity &m2) -> bool { - return m1.card.suit < m2.card.suit || (m1.card.suit == m2.card.suit and m1.card.rank < m2.card.rank) || - (m1.card.suit == m2.card.suit and m1.card.rank == m2.card.rank and m1.multiplicity < m2.multiplicity); + template + bool same_up_to_discard_permutation( + HanabiState state1, HanabiState state2 + ) + { + auto comp = [](CardMultiplicity & m1, CardMultiplicity & m2) -> bool { + return m1.card.suit < m2.card.suit || (m1.card.suit == m2.card.suit and m1.card.rank < m2.card.rank) || + (m1.card.suit == m2.card.suit and m1.card.rank == m2.card.rank and m1.multiplicity < m2.multiplicity); }; - state1._draw_pile.sort(comp); - state2._draw_pile.sort(comp); - return state1 == state2; -} + state1._draw_pile.sort(comp); + state2._draw_pile.sort(comp); + return state1 == state2; + } } diff --git a/include/game_state.hpp b/include/game_state.hpp index 61bc49b..52d8cc7 100644 --- a/include/game_state.hpp +++ b/include/game_state.hpp @@ -4,970 +4,1164 @@ #include "myassert.h" #include "game_state.h" -namespace Hanabi { +namespace Hanabi +{ - template - std::ostream &operator<<(std::ostream &os, const Stacks &stacks) { - for (size_t i = 0; i < stacks.size(); i++) { - os << suit_initials[i] << starting_card_rank - stacks[i]; - if(i < stacks.size() - 1) { - os << ", "; - } - } - return os; - } - - template - void CardArray::fill(T val) { - for (size_t suit = 0; suit < num_suits; suit++) { - for (rank_t rank = 0; rank < starting_card_rank; rank++) { - _array[suit][rank] = val; - } - } - } - - template - CardArray::CardArray(T default_val) { - fill(default_val); - } - - template - const T &CardArray::operator[](const Card &card) const { - return _array[card.suit][card.rank]; - }; - - template - T &CardArray::operator[](const Card &card) { - return _array[card.suit][card.rank]; - }; - - template - HanabiState::BacktrackAction::BacktrackAction( - Hanabi::ActionType action_type, Hanabi::Card discarded_or_played, Hanabi::hand_index_t index, - bool was_on_8_clues, bool strike - ): - action_type(action_type), - discarded(discarded_or_played), - index(index), - was_on_8_clues(was_on_8_clues), strike(strike) { - } - - template - HanabiState::HanabiState(const std::vector &deck, uint8_t score_goal): - _turn(0), - _num_clues(max_num_clues), - _weighted_draw_pile_size(deck.size()), - _stacks(), - _hands(), - _draw_pile(), - _endgame_turns_left(no_endgame), - _pace(deck.size() - score_goal - num_players * (hand_size - 1)), - _score(0), - _score_goal(score_goal), - _actions_log(), - _relative_representation(), - _position_tablebase(), - _enumerated_states(0) { - std::ranges::fill(_stacks, starting_card_rank); - for (const Card &card: deck) { - _draw_pile.push_back({card, 1}); - } - for (player_t player = 0; player < num_players; player++) { - for (std::uint8_t index = 0; index < hand_size; index++) { - draw(index); - } - incr_turn(); - } - ASSERT(_turn == 0); - } - - template - void HanabiState::give_clue() { - ASSERT(_num_clues > 0); - --_num_clues; - - _actions_log.emplace(ActionType::clue, Cards::unknown, 0); - incr_turn(); - } - - template - void HanabiState::incr_turn() { - _turn = (_turn + 1) % num_players; - if (_endgame_turns_left != no_endgame) { - _endgame_turns_left--; - } - } - - template - void HanabiState::decr_turn() { - _turn = (_turn + num_players - 1) % num_players; - if (_endgame_turns_left != no_endgame) { - _endgame_turns_left++; - } - } - - template - void HanabiState::check_draw_pile_integrity() const + template + std::ostream & operator<<(std::ostream & os, const Stacks & stacks) + { + for (size_t i = 0; i < stacks.size(); i++) { - if (not _relative_representation.initialized) + os << suit_initials[i] << starting_card_rank - stacks[i]; + if (i < stacks.size() - 1) { - return; + os << ", "; } - if (_draw_pile.size() >= 2) { - auto copy = _draw_pile; - copy.sort([](CardMultiplicity const & card1, CardMultiplicity const & card2){ - return card1.card.rank < card2.card.rank or (card1.card.rank == card2.card.rank and card1.card.suit < card2.card.suit); - }); - auto before = copy.begin(); - for(auto it = std::next(copy.begin()); it != copy.end(); ++it) { - ASSERT(before->card != it->card); - ++before; + } + return os; + } + + template + void CardArray::fill(T val) + { + for (size_t suit = 0; suit < num_suits; suit++) + { + for (rank_t rank = 0; rank < starting_card_rank; rank++) + { + _array[suit][rank] = val; + } + } + } + + template + CardArray::CardArray(T default_val) + { + fill(default_val); + } + + template + const T & CardArray::operator[](const Card & card) const + { + return _array[card.suit][card.rank]; + }; + + template + T & CardArray::operator[](const Card & card) + { + return _array[card.suit][card.rank]; + }; + + template + HanabiState::BacktrackAction::BacktrackAction( + Hanabi::ActionType action_type + , Hanabi::Card discarded_or_played + , Hanabi::hand_index_t index + , bool was_on_8_clues + , bool strike + ): + action_type(action_type), discarded(discarded_or_played), index(index), was_on_8_clues(was_on_8_clues), strike( + strike) + { + } + + template + HanabiState::HanabiState(const std::vector & deck, uint8_t score_goal): + _turn(0), _num_clues(max_num_clues), _weighted_draw_pile_size(deck.size()), _stacks(), _hands(), _draw_pile() + , _endgame_turns_left(no_endgame), _pace(deck.size() - score_goal - num_players * (hand_size - 1)), _score(0) + , _score_goal(score_goal), _actions_log(), _relative_representation(), _position_tablebase() + , _enumerated_states(0) + { + std::ranges::fill(_stacks, starting_card_rank); + for (const Card & card: deck) + { + _draw_pile.push_back({card, 1}); + } + for (player_t player = 0; player < num_players; player++) + { + for (std::uint8_t index = 0; index < hand_size; index++) + { + draw(index); + } + incr_turn(); + } + ASSERT(_turn == 0); + } + + template + void HanabiState::give_clue() + { + ASSERT(_num_clues > 0); + --_num_clues; + + _actions_log.emplace(ActionType::clue, Cards::unknown, 0); + incr_turn(); + } + + template + void HanabiState::incr_turn() + { + _turn = (_turn + 1) % num_players; + if (_endgame_turns_left != no_endgame) + { + _endgame_turns_left--; + } + } + + template + void HanabiState::decr_turn() + { + _turn = (_turn + num_players - 1) % num_players; + if (_endgame_turns_left != no_endgame) + { + _endgame_turns_left++; + } + } + + template + void HanabiState::check_draw_pile_integrity() const + { + if (not _relative_representation.initialized) + { + return; + } + if (_draw_pile.size() >= 2) + { + auto copy = _draw_pile; + copy.sort([](CardMultiplicity const & card1, CardMultiplicity const & card2) { + return card1.card.rank < card2.card.rank or + (card1.card.rank == card2.card.rank and card1.card.suit < card2.card.suit); + }); + auto before = copy.begin(); + for (auto it = std::next(copy.begin()); it != copy.end(); ++it) + { + ASSERT(before->card != it->card); + ++before; + } + } + } + + template + bool HanabiState::is_playable(const Hanabi::Card & card) const + { + return card.rank == _stacks[card.suit] - 1; + } + + template + std::uint64_t HanabiState::enumerated_states() const + { + return _enumerated_states; + } + + template + bool HanabiState::is_trash(const Hanabi::Card & card) const + { + return card.rank >= _stacks[card.suit]; + } + + template + void HanabiState::play(Hanabi::hand_index_t index) + { + play_and_potentially_update(index); + } + + template + unsigned long + HanabiState::play_and_potentially_update(hand_index_t index, bool cycle) + { + check_draw_pile_integrity(); + ASSERT(index < _hands[_turn].size()); + const Card played_card = _hands[_turn][index]; + + bool const strike = !is_playable(played_card); + + _actions_log.emplace(ActionType::play, played_card, index, _num_clues == 8, strike); + + if (is_playable(played_card)) + { + --_stacks[played_card.suit]; + _score++; + if (played_card.rank == 0 and _num_clues < max_num_clues) + { + // update clues if we played the last played_card of a stack + _num_clues++; + } + } + + const unsigned long multiplicity = draw(index, cycle, !strike); + + incr_turn(); + check_draw_pile_integrity(); + return multiplicity; + } + + template + void HanabiState::discard(hand_index_t index) + { + discard_and_potentially_update(index); + } + + template + unsigned long + HanabiState::discard_and_potentially_update(hand_index_t index, bool cycle) + { + check_draw_pile_integrity(); + ASSERT(index < _hands[_turn].size()); + ASSERT(_num_clues != max_num_clues); + + const Card discarded_card = _hands[_turn][index]; + _num_clues++; + _pace--; + + unsigned long multiplicity = draw(index, cycle, false); + _actions_log.emplace(ActionType::discard, discarded_card, index); + + incr_turn(); + check_draw_pile_integrity(); + return multiplicity; + } + + template + std::uint8_t HanabiState::find_card_in_hand( + const Hanabi::Card & card + ) const + { + auto it = std::find_if(_hands[_turn].begin(), _hands[_turn].end(), [&card, this](Card const & card_in_hand) { + return card_in_hand == card or (is_trash(card) and is_trash(card_in_hand)); + }); + if (it != _hands[_turn].end()) + { + return std::distance(_hands[_turn].begin(), it); + } + return -1; + } + + template + void HanabiState::print(std::ostream & os) const + { + os << "Stacks: " << _stacks << " (score " << +_score << ")"; + os << ", clues: " << +_num_clues << ", turn: " << +_turn; + if (_endgame_turns_left != no_endgame) + { + os << ", " << +_endgame_turns_left << " turns left"; + } + os << std::endl; + os << "Draw pile: "; + unsigned num_trash = 0; + for (const auto & [card, mul]: _draw_pile) + { + if (is_trash(card)) + { + num_trash += mul; + continue; + } + os << card; + if (mul > 1) + { + os << " (" << +mul << ")"; + } + os << ", "; + } + if (num_trash > 0) + { + os << Cards::trash << " (" << num_trash << ") "; + } + os << "[size " << +_weighted_draw_pile_size << "]" << std::endl; + os << "Hands: "; + for (const auto & hand: _hands) + { + os << "["; + for (hand_index_t index = 0; index < hand.size(); index++) + { + os << hand[index]; + if (index < hand.size() - 1) + { + os << " "; } } + os << "] "; } + } - template - bool HanabiState::is_playable(const Hanabi::Card &card) const { - return card.rank == _stacks[card.suit] - 1; - } + template + unsigned HanabiState::draw(uint8_t index, bool cycle, bool played) + { + ASSERT(index < _hands[_turn].size()); - template - std::uint64_t HanabiState::enumerated_states() const { - return _enumerated_states; - } - - template - bool HanabiState::is_trash(const Hanabi::Card &card) const { - return card.rank >= _stacks[card.suit]; - } - - template - void HanabiState::play(Hanabi::hand_index_t index) { - play_and_potentially_update(index); - } - - template - unsigned long HanabiState::play_and_potentially_update(hand_index_t index, bool cycle) { - check_draw_pile_integrity(); - ASSERT(index < _hands[_turn].size()); - const Card played_card = _hands[_turn][index]; - - bool const strike = !is_playable(played_card); - - _actions_log.emplace(ActionType::play, played_card, index, _num_clues == 8, strike); - - if(is_playable(played_card)) + // update card position of the card we are about to discard + if (_relative_representation.initialized) + { + const Card discarded = _hands[_turn][index]; + if (!discarded.initial_trash) + { + if (discarded.in_starting_hand) { - --_stacks[played_card.suit]; - _score++; - if (played_card.rank == 0 and _num_clues < max_num_clues) { - // update clues if we played the last played_card of a stack - _num_clues++; + ASSERT(_relative_representation.card_positions_hands[discarded.local_index] == + RelativeRepresentationData::hand); + if (played) + { + _relative_representation.card_positions_hands[discarded.local_index] = RelativeRepresentationData::played; + } + else + { + _relative_representation.card_positions_hands[discarded.local_index] = RelativeRepresentationData::discarded; } } - - const unsigned long multiplicity = draw(index, cycle, !strike); - - incr_turn(); - check_draw_pile_integrity(); - return multiplicity; - } - - template - void HanabiState::discard(hand_index_t index) { - discard_and_potentially_update(index); - } - - template - unsigned long HanabiState::discard_and_potentially_update(hand_index_t index, bool cycle) { - check_draw_pile_integrity(); - ASSERT(index < _hands[_turn].size()); - ASSERT(_num_clues != max_num_clues); - - const Card discarded_card = _hands[_turn][index]; - _num_clues++; - _pace--; - - unsigned long multiplicity = draw(index, cycle, false); - _actions_log.emplace(ActionType::discard, discarded_card, index); - - incr_turn(); - check_draw_pile_integrity(); - return multiplicity; - } - - template - std::uint8_t HanabiState::find_card_in_hand( - const Hanabi::Card &card) const { - auto it = std::find_if(_hands[_turn].begin(), _hands[_turn].end(),[&card, this](Card const & card_in_hand){ - return card_in_hand == card or (is_trash(card) and is_trash(card_in_hand)); - }); - if (it != _hands[_turn].end()) { - return std::distance(_hands[_turn].begin(), it); - } - return -1; - } - - template - void HanabiState::print(std::ostream &os) const { - os << "Stacks: " << _stacks << " (score " << +_score << ")"; - os << ", clues: " << +_num_clues << ", turn: " << +_turn; - if (_endgame_turns_left != no_endgame) { - os << ", " << +_endgame_turns_left << " turns left"; - } - os << std::endl; - os << "Draw pile: "; - unsigned num_trash = 0; - for (const auto &[card, mul]: _draw_pile) { - if (is_trash(card)) { - num_trash += mul; - continue; - } - os << card; - if (mul > 1) { - os << " (" << +mul << ")"; - } - os << ", "; - } - if (num_trash > 0) { - os << Cards::trash << " (" << num_trash << ") "; - } - os << "[size " << +_weighted_draw_pile_size << "]" << std::endl; - os << "Hands: "; - for (const auto &hand: _hands) { - os << "["; - for(hand_index_t index = 0; index < hand.size(); index++) { - os << hand[index]; - if (index < hand.size() - 1) { - os << " "; - } - } - os << "] "; - } - } - - template - unsigned HanabiState::draw(uint8_t index, bool cycle, bool played) { - ASSERT(index < _hands[_turn].size()); - - // update card position of the card we are about to discard - if (_relative_representation.initialized) { - const Card discarded = _hands[_turn][index]; - if (!discarded.initial_trash) { - if (discarded.in_starting_hand) { - ASSERT(_relative_representation.card_positions_hands[discarded.local_index] == RelativeRepresentationData::hand); - if (played) - { - _relative_representation.card_positions_hands[discarded.local_index] = RelativeRepresentationData::played; - } - else - { - _relative_representation.card_positions_hands[discarded.local_index] = RelativeRepresentationData::discarded; - } - } else { - 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()); - if (played) - { - *replaced_card_it = RelativeRepresentationData::play_stack; - } - else - { - *replaced_card_it = RelativeRepresentationData::discard_pile; - } - std::ranges::sort(_relative_representation.card_positions_draw[discarded.local_index]); - } - } - } - - // draw a new card if the draw pile is not empty - if (!_draw_pile.empty()) { - --_weighted_draw_pile_size; - - const CardMultiplicity draw = _draw_pile.front(); - _draw_pile.pop_front(); - ASSERT(draw.multiplicity > 0); - - if (draw.multiplicity > 1) { - if (cycle) { - _draw_pile.push_back(draw); - _draw_pile.back().multiplicity--; - } else { - _draw_pile.push_front(draw); - _draw_pile.front().multiplicity--; - } - } - - if (_relative_representation.initialized) { - // 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(_relative_representation.card_positions_draw[draw.card.local_index], RelativeRepresentationData::draw_pile); - ASSERT(new_card_it != _relative_representation.card_positions_draw[draw.card.local_index].end()); - *new_card_it = _turn; - std::ranges::sort(_relative_representation.card_positions_draw[draw.card.local_index]); - } - } - - _hands[_turn][index] = draw.card; - - if (_draw_pile.empty()) { - // Note the +1, since we will immediately decrement this when moving to the next player - _endgame_turns_left = num_players + 1; - } - return draw.multiplicity; - } - return 1; - } - - template - void HanabiState::revert_draw(std::uint8_t index, Card discarded_card, bool cycle, bool played) { - // Put the card that is currently in hand back into the draw pile (this does not happen in the last round!) - if (_endgame_turns_left == num_players + 1 || _endgame_turns_left == no_endgame) { - ASSERT(index < _hands[_turn].size()); - const Card &drawn = _hands[_turn][index]; - - if (cycle) - { - // put discarded_card back into draw pile (at the back) - if (!_draw_pile.empty() and _draw_pile.back().card.suit == drawn.suit and - _draw_pile.back().card.rank == drawn.rank) { - _draw_pile.back().multiplicity++; - } else { - _draw_pile.push_back({drawn, 1}); - } - } else - { - // We don't know where the card came from (between the card having been removed from the draw pile - // and re-adding it now, the user may have arbitrarily permuted the draw pile implicitly) - // so we have to check if it is already contained in the draw pile somewhere - auto it = std::find_if(_draw_pile.begin(), _draw_pile.end(), [&drawn](CardMultiplicity const & mult){ - return mult.card == drawn; - }); - if (it != _draw_pile.end()) - { - it->multiplicity++; - } - else - { - _draw_pile.push_front({drawn, 1}); - } - } - - if (_relative_representation.initialized && !drawn.initial_trash) { - ASSERT(drawn.in_starting_hand == false); - 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 = RelativeRepresentationData::draw_pile; - std::ranges::sort(_relative_representation.card_positions_draw[drawn.local_index]); - } - - _weighted_draw_pile_size++; - _endgame_turns_left = no_endgame; - } else { - ASSERT(_hands[_turn][index] == discarded_card); - } - - if (_relative_representation.initialized && !discarded_card.initial_trash) { - if (discarded_card.in_starting_hand) { - ASSERT(_relative_representation.card_positions_hands[discarded_card.local_index] != RelativeRepresentationData::hand); - _relative_representation.card_positions_hands[discarded_card.local_index] = RelativeRepresentationData::hand; - } else { - player_t const old_position = [&played]{ - if (played) - { - return RelativeRepresentationData::play_stack; - } - else - { - return RelativeRepresentationData::discard_pile; - } - }(); - auto hand_card_it = std::ranges::find(_relative_representation.card_positions_draw[discarded_card.local_index], - old_position); - ASSERT(hand_card_it != _relative_representation.card_positions_draw[discarded_card.local_index].end()); - *hand_card_it = _turn; - std::ranges::sort(_relative_representation.card_positions_draw[discarded_card.local_index]); - } - } - - _hands[_turn][index] = discarded_card; - } - - template - void HanabiState::init_backtracking_information() { - ASSERT(not _relative_representation.initialized); - // Note that this function does not have to be particularly performant, we only call it once to initialize. - const Card trash = [this]() -> Card { - for (suit_t suit = 0; suit < num_suits; suit++) { - if (_stacks[suit] < starting_card_rank) { - return {suit, starting_card_rank - 1, 0, false, true}; - } - } - return {0, 0}; - }(); - - CardArray nums_in_draw_pile; - for (const auto [card, multiplicity]: _draw_pile) { - if (_stacks[card.suit] > card.rank) { - nums_in_draw_pile[card] += multiplicity; - } else { - nums_in_draw_pile[trash] += multiplicity; - } - } - - // Prepare draw pile - _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(_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)) { - _relative_representation.card_positions_draw.push_back({}); - _relative_representation.card_positions_draw.back().resize(nums_in_draw_pile[card], RelativeRepresentationData::draw_pile); - _relative_representation.good_cards_draw.push_back(card); - } - } - } - } - _relative_representation.initial_draw_pile_size = _weighted_draw_pile_size; - - size_t num_useful_cards_in_starting_hands = 0; - - // Prepare cards in hands - for (player_t player = 0; player < num_players; player++) { - for (Card &card: _hands[player]) { - card.initial_trash = is_trash(card); - card.in_starting_hand = true; - // Needed to check for dupes in same hand - boost::container::static_vector good_cards_in_hand; - if (!is_trash(card)) { - if (std::count(good_cards_in_hand.begin(), good_cards_in_hand.end(), card) > 0) { - // 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++; - - good_cards_in_hand.push_back(card); - } - } - } - } - - _relative_representation.card_positions_hands.clear(); - _relative_representation.card_positions_hands.resize(num_useful_cards_in_starting_hands, RelativeRepresentationData::hand); - - _relative_representation.initialized = true; - } - - template - void - HanabiState::revert_play(bool cycle) { - check_draw_pile_integrity(); - const BacktrackAction last_action = _actions_log.top(); - _actions_log.pop(); - ASSERT(last_action.action_type == ActionType::play); - ASSERT(!last_action.was_on_8_clues or _num_clues == 8); - - decr_turn(); - if (last_action.discarded.rank == 0 and not last_action.was_on_8_clues and not last_action.strike) { - _num_clues--; - } - revert_draw(last_action.index, last_action.discarded, cycle, !last_action.strike); - if(not last_action.strike) { - _stacks[last_action.discarded.suit]++; - _score--; - } - check_draw_pile_integrity(); - } - - template - void HanabiState::revert_discard(bool cycle) { - check_draw_pile_integrity(); - const BacktrackAction last_action = _actions_log.top(); - _actions_log.pop(); - - ASSERT(last_action.action_type == ActionType::discard); - - decr_turn(); - ASSERT(_num_clues > 0); - - _num_clues--; - _pace++; - - revert_draw(last_action.index, last_action.discarded, cycle, false); - check_draw_pile_integrity(); - } - - template - void HanabiState::revert_clue() { - const BacktrackAction last_action = _actions_log.top(); - _actions_log.pop(); - - ASSERT(last_action.action_type == ActionType::clue); - - decr_turn(); - ASSERT(_num_clues < max_num_clues); - - _num_clues++; - } - - template - void HanabiState::revert() { - switch(_actions_log.top().action_type) { - case ActionType::clue: - revert_clue(); - break; - case ActionType::discard: - revert_discard(); - break; - case ActionType::play: - revert_play(); - break; - default: - return; - } - } - - - template - void HanabiState::modify_clues(Hanabi::clue_t change) - { - _num_clues += change; - if (_num_clues > 8) { - _num_clues = 8; - } - if (_num_clues < 0) { - _num_clues = 0; - } - } - - template - void HanabiState::set_clues(Hanabi::clue_t clues) { - ASSERT(0 <= clues); - ASSERT(clues <= 8); - _num_clues = clues; - } - - template - player_t HanabiState::turn() const { - return _turn; - } - - template - clue_t HanabiState::num_clues() const { - return _num_clues; - } - - template - unsigned HanabiState::score() const { - return _score; - } - - template - std::vector> HanabiState::hands() const { - std::vector> hands; - for(player_t player = 0; player < num_players; player++) { - hands.push_back({}); - for(const Card& card: _hands[player]) { - hands.back().push_back(card); - } - } - return hands; - } - - template - std::vector HanabiState::cur_hand() const { - std::vector hand; - for(const Card& card: _hands[_turn]) { - hand.push_back(card); - } - return hand; - } - - template - std::vector>> HanabiState::possible_next_states(hand_index_t index, bool play) { - std::vector>> next_states; - do_for_each_potential_draw(index, play, [this, &next_states, &index](unsigned multiplicity){ - auto prob = lookup(); - - // bit hacky to get drawn card here - decr_turn(); - const CardMultiplicity drawn_card = {_hands[_turn][index], multiplicity}; - incr_turn(); - - next_states.emplace_back(drawn_card, prob); - }); - return next_states; - } - - template - std::vector>> HanabiState::get_reasonable_actions() { - std::vector>> reasonable_actions {}; - - if(_score == _score_goal or _pace < 0 or _endgame_turns_left == 0) { - return reasonable_actions; - } - - const std::array& hand = _hands[_turn]; - // First, check for playable cards - for(std::uint8_t index = 0; index < hand_size; index++) { - if(is_playable(hand[index])) { - const Action action = {ActionType::play, hand[index]}; - bool known = true; - probability_t sum_of_probabilities = 0; - - do_for_each_potential_draw(index, true, [this, &sum_of_probabilities, &known](const unsigned long multiplicity){ - const std::optional prob = lookup(); - if (prob.has_value()) { - sum_of_probabilities += prob.value() * multiplicity; - } else { - known = false; - } - }); - - if (known) { - const unsigned long total_weight = std::max(static_cast(_weighted_draw_pile_size), 1ul); - const probability_t probability_play = sum_of_probabilities / total_weight; - reasonable_actions.emplace_back(action, probability_play); - } else { - reasonable_actions.emplace_back(action, std::nullopt); - } - } - } - - if(_pace > 0 and _num_clues < max_num_clues) { - for(std::uint8_t index = 0; index < hand_size; index++) { - if (is_trash(hand[index])) { - const Action action = {ActionType::discard, hand[index]}; - bool known = true; - probability_t sum_of_probabilities = 0; - - do_for_each_potential_draw(index, false, [this, &sum_of_probabilities, &known](const unsigned long multiplicity){ - const std::optional prob = lookup(); - if (prob.has_value()) { - sum_of_probabilities += prob.value() * multiplicity; - } else { - known = false; - } - }); - - if (known) { - const unsigned long total_weight = std::max(static_cast(_weighted_draw_pile_size), 1ul); - const probability_t probability_discard = sum_of_probabilities / total_weight; - reasonable_actions.emplace_back(action, probability_discard); - } else { - reasonable_actions.emplace_back(action, std::nullopt); - } - - // All discards are equivalent, do not continue searching for different trash - break; - } - } - } - - if(_num_clues > 0) { - give_clue(); - const std::optional prob = lookup(); - const Action action = {ActionType::clue, Cards::unknown}; - reasonable_actions.emplace_back(action, prob); - revert_clue(); - } - return reasonable_actions; - } - - template - std::optional HanabiState::lookup() const { - if (_score == 5 * num_suits) { - return 1; - } - if (_pace < 0 or _endgame_turns_left == 0) { - return 0; - } - const auto id = unique_id(); - if(_position_tablebase.contains(id)) { - return _position_tablebase.at(id); - } else { - return std::nullopt; - } - } - - template - void HanabiState::rotate_next_draw(const Card& card) { - auto card_it = std::find_if(_draw_pile.begin(), _draw_pile.end(), [&card, this](const CardMultiplicity& card_multiplicity){ - return (is_trash(card) and is_trash(card_multiplicity.card)) or (card_multiplicity.card.rank == card.rank and card_multiplicity.card.suit == card.suit); - }); - ASSERT(card_it != _draw_pile.end()); - std::swap(*card_it, _draw_pile.front()); - } - - template - ActionType HanabiState::last_action_type() const - { - ASSERT(not _actions_log.empty()); - return _actions_log.top().action_type; - } - - template - probability_t HanabiState::evaluate_state() { - ASSERT(_relative_representation.initialized); - _enumerated_states++; - const unsigned long id_of_state = unique_id(); - - const unsigned id = 55032; - if (id_of_state == id) + else { - std::cout << "Found state with id of " << id << "\n" << *this << std::endl; + 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()); + if (played) + { + *replaced_card_it = RelativeRepresentationData::play_stack; + } + else + { + *replaced_card_it = RelativeRepresentationData::discard_pile; + } + std::ranges::sort(_relative_representation.card_positions_draw[discarded.local_index]); } + } + } - if (_score == _score_goal) { - return 1; + // draw a new card if the draw pile is not empty + if (!_draw_pile.empty()) + { + --_weighted_draw_pile_size; + + const CardMultiplicity draw = _draw_pile.front(); + _draw_pile.pop_front(); + ASSERT(draw.multiplicity > 0); + + if (draw.multiplicity > 1) + { + if (cycle) + { + _draw_pile.push_back(draw); + _draw_pile.back().multiplicity--; } - if(_pace < 0 || _endgame_turns_left == 0) { - return 0; + else + { + _draw_pile.push_front(draw); + _draw_pile.front().multiplicity--; } - #ifndef GAME_STATE_NO_TABLEBASE_LOOKUP - if (_position_tablebase.contains(id_of_state)) { - return _position_tablebase[id_of_state]; + } + + if (_relative_representation.initialized) + { + // 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(_relative_representation.card_positions_draw[draw.card.local_index] + , RelativeRepresentationData::draw_pile); + ASSERT(new_card_it != _relative_representation.card_positions_draw[draw.card.local_index].end()); + *new_card_it = _turn; + std::ranges::sort(_relative_representation.card_positions_draw[draw.card.local_index]); } - #endif + } - // TODO: Have some endgame analysis here? + _hands[_turn][index] = draw.card; - probability_t best_probability = 0; - const std::array& hand = _hands[_turn]; + if (_draw_pile.empty()) + { + // Note the +1, since we will immediately decrement this when moving to the next player + _endgame_turns_left = num_players + 1; + } + return draw.multiplicity; + } + return 1; + } - // First, check for playables - for(std::uint8_t index = 0; index < hand_size; index++) { - if(is_playable(hand[index])) { - probability_t sum_of_probabilities = 0; + template + void HanabiState::revert_draw( + std::uint8_t index + , Card discarded_card + , bool cycle + , bool played + ) + { + // Put the card that is currently in hand back into the draw pile (this does not happen in the last round!) + if (_endgame_turns_left == num_players + 1 || _endgame_turns_left == no_endgame) + { + ASSERT(index < _hands[_turn].size()); + const Card & drawn = _hands[_turn][index]; - do_for_each_potential_draw(index, true, [this, &sum_of_probabilities](const unsigned long multiplicity){ - sum_of_probabilities += evaluate_state() * multiplicity; - }); + if (cycle) + { + // put discarded_card back into draw pile (at the back) + if (!_draw_pile.empty() and _draw_pile.back().card.suit == drawn.suit and + _draw_pile.back().card.rank == drawn.rank) + { + _draw_pile.back().multiplicity++; + } + else + { + _draw_pile.push_back({drawn, 1}); + } + } + else + { + // We don't know where the card came from (between the card having been removed from the draw pile + // and re-adding it now, the user may have arbitrarily permuted the draw pile implicitly) + // so we have to check if it is already contained in the draw pile somewhere + auto it = std::find_if(_draw_pile.begin(), _draw_pile.end(), [&drawn](CardMultiplicity const & mult) { + return mult.card == drawn; + }); + if (it != _draw_pile.end()) + { + it->multiplicity++; + } + else + { + _draw_pile.push_front({drawn, 1}); + } + } - const unsigned long total_weight = std::max(static_cast(_weighted_draw_pile_size), 1ul); - const probability_t probability_play = sum_of_probabilities / total_weight; + if (_relative_representation.initialized && !drawn.initial_trash) + { + ASSERT(drawn.in_starting_hand == false); + 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 = RelativeRepresentationData::draw_pile; + std::ranges::sort(_relative_representation.card_positions_draw[drawn.local_index]); + } - best_probability = std::max(best_probability, probability_play); - if (best_probability == 1) { - update_tablebase(id_of_state, best_probability); - return best_probability; - }; + _weighted_draw_pile_size++; + _endgame_turns_left = no_endgame; + } + else + { + ASSERT(_hands[_turn][index] == discarded_card); + } + + if (_relative_representation.initialized && !discarded_card.initial_trash) + { + if (discarded_card.in_starting_hand) + { + ASSERT(_relative_representation.card_positions_hands[discarded_card.local_index] != + RelativeRepresentationData::hand); + _relative_representation.card_positions_hands[discarded_card.local_index] = RelativeRepresentationData::hand; + } + else + { + player_t const old_position = [&played] { + if (played) + { + return RelativeRepresentationData::play_stack; + } + else + { + return RelativeRepresentationData::discard_pile; + } + }(); + auto hand_card_it = std::ranges::find(_relative_representation.card_positions_draw[discarded_card.local_index] + , old_position); + ASSERT(hand_card_it != _relative_representation.card_positions_draw[discarded_card.local_index].end()); + *hand_card_it = _turn; + std::ranges::sort(_relative_representation.card_positions_draw[discarded_card.local_index]); + } + } + + _hands[_turn][index] = discarded_card; + } + + template + void HanabiState::init_backtracking_information() + { + ASSERT(not _relative_representation.initialized); + // Note that this function does not have to be particularly performant, we only call it once to initialize. + const Card trash = [this]() -> Card { + for (suit_t suit = 0; suit < num_suits; suit++) + { + if (_stacks[suit] < starting_card_rank) + { + return {suit, starting_card_rank - 1, 0, false, true}; + } + } + return {0, 0}; + }(); + + CardArray nums_in_draw_pile; + for (const auto [card, multiplicity]: _draw_pile) + { + if (_stacks[card.suit] > card.rank) + { + nums_in_draw_pile[card] += multiplicity; + } + else + { + nums_in_draw_pile[trash] += multiplicity; + } + } + + // Prepare draw pile + _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(_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)) + { + _relative_representation.card_positions_draw.push_back({}); + _relative_representation.card_positions_draw + .back() + .resize(nums_in_draw_pile[card], RelativeRepresentationData::draw_pile); + _relative_representation.good_cards_draw.push_back(card); + } + } + } + } + _relative_representation.initial_draw_pile_size = _weighted_draw_pile_size; + + size_t num_useful_cards_in_starting_hands = 0; + + // Prepare cards in hands + for (player_t player = 0; player < num_players; player++) + { + for (Card & card: _hands[player]) + { + card.initial_trash = is_trash(card); + card.in_starting_hand = true; + // Needed to check for dupes in same hand + boost::container::static_vector good_cards_in_hand; + if (!is_trash(card)) + { + if (std::count(good_cards_in_hand.begin(), good_cards_in_hand.end(), card) > 0) + { + // 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++; + + good_cards_in_hand.push_back(card); + } + } + } + } + + _relative_representation.card_positions_hands.clear(); + _relative_representation.card_positions_hands + .resize(num_useful_cards_in_starting_hands, RelativeRepresentationData::hand); + + _relative_representation.initialized = true; + } + + template + void + HanabiState::revert_play(bool cycle) + { + check_draw_pile_integrity(); + const BacktrackAction last_action = _actions_log.top(); + _actions_log.pop(); + ASSERT(last_action.action_type == ActionType::play); + ASSERT(!last_action.was_on_8_clues or _num_clues == 8); + + decr_turn(); + if (last_action.discarded.rank == 0 and not last_action.was_on_8_clues and not last_action.strike) + { + _num_clues--; + } + revert_draw(last_action.index, last_action.discarded, cycle, !last_action.strike); + if (not last_action.strike) + { + _stacks[last_action.discarded.suit]++; + _score--; + } + check_draw_pile_integrity(); + } + + template + void HanabiState::revert_discard(bool cycle) + { + check_draw_pile_integrity(); + const BacktrackAction last_action = _actions_log.top(); + _actions_log.pop(); + + ASSERT(last_action.action_type == ActionType::discard); + + decr_turn(); + ASSERT(_num_clues > 0); + + _num_clues--; + _pace++; + + revert_draw(last_action.index, last_action.discarded, cycle, false); + check_draw_pile_integrity(); + } + + template + void HanabiState::revert_clue() + { + const BacktrackAction last_action = _actions_log.top(); + _actions_log.pop(); + + ASSERT(last_action.action_type == ActionType::clue); + + decr_turn(); + ASSERT(_num_clues < max_num_clues); + + _num_clues++; + } + + template + void HanabiState::revert() + { + switch (_actions_log.top().action_type) + { + case ActionType::clue: + revert_clue(); + break; + case ActionType::discard: + revert_discard(); + break; + case ActionType::play: + revert_play(); + break; + default: + return; + } + } + + + template + void HanabiState::modify_clues(Hanabi::clue_t change) + { + _num_clues += change; + if (_num_clues > 8) + { + _num_clues = 8; + } + if (_num_clues < 0) + { + _num_clues = 0; + } + } + + template + void HanabiState::set_clues(Hanabi::clue_t clues) + { + ASSERT(0 <= clues); + ASSERT(clues <= 8); + _num_clues = clues; + } + + template + player_t HanabiState::turn() const + { + return _turn; + } + + template + clue_t HanabiState::num_clues() const + { + return _num_clues; + } + + template + unsigned HanabiState::score() const + { + return _score; + } + + template + std::vector> HanabiState::hands() const + { + std::vector> hands; + for (player_t player = 0; player < num_players; player++) + { + hands.push_back({}); + for (const Card & card: _hands[player]) + { + hands.back().push_back(card); + } + } + return hands; + } + + template + std::vector HanabiState::cur_hand() const + { + std::vector hand; + for (const Card & card: _hands[_turn]) + { + hand.push_back(card); + } + return hand; + } + + template + std::vector>> + HanabiState::possible_next_states(hand_index_t index, bool play) + { + std::vector>> next_states; + do_for_each_potential_draw(index, play, [this, &next_states, &index](unsigned multiplicity) { + auto prob = lookup(); + + // bit hacky to get drawn card here + decr_turn(); + const CardMultiplicity drawn_card = {_hands[_turn][index], multiplicity}; + incr_turn(); + + next_states.emplace_back(drawn_card, prob); + }); + return next_states; + } + + template + std::vector>> + HanabiState::get_reasonable_actions() + { + std::vector>> reasonable_actions{}; + + if (_score == _score_goal or _pace < 0 or _endgame_turns_left == 0) + { + return reasonable_actions; + } + + const std::array & hand = _hands[_turn]; + // First, check for playable cards + for (std::uint8_t index = 0; index < hand_size; index++) + { + if (is_playable(hand[index])) + { + const Action action = {ActionType::play, hand[index]}; + bool known = true; + probability_t sum_of_probabilities = 0; + + do_for_each_potential_draw(index, true, [this, &sum_of_probabilities + , &known](const unsigned long multiplicity) { + const std::optional prob = lookup(); + if (prob.has_value()) + { + sum_of_probabilities += prob.value() * multiplicity; + } + else + { + known = false; + } + }); + + if (known) + { + const unsigned long total_weight = std::max(static_cast(_weighted_draw_pile_size), 1ul); + const probability_t probability_play = sum_of_probabilities / total_weight; + reasonable_actions.emplace_back(action, probability_play); + } + else + { + reasonable_actions.emplace_back(action, std::nullopt); + } + } + } + + if (_pace > 0 and _num_clues < max_num_clues) + { + for (std::uint8_t index = 0; index < hand_size; index++) + { + if (is_trash(hand[index])) + { + const Action action = {ActionType::discard, hand[index]}; + bool known = true; + probability_t sum_of_probabilities = 0; + + do_for_each_potential_draw(index, false, [this, &sum_of_probabilities + , &known](const unsigned long multiplicity) { + const std::optional prob = lookup(); + if (prob.has_value()) + { + sum_of_probabilities += prob.value() * multiplicity; } - } - - // Check for discards now - if(_pace > 0 and _num_clues < max_num_clues) { - for(std::uint8_t index = 0; index < hand_size; index++) { - if (is_trash(hand[index])) { - probability_t sum_of_probabilities = 0; - - do_for_each_potential_draw(index, false, [this, &sum_of_probabilities](const unsigned long multiplicity){ - sum_of_probabilities += evaluate_state() * multiplicity; - }); - - const unsigned long total_weight = std::max(static_cast(_weighted_draw_pile_size), 1ul); - const probability_t probability_discard = sum_of_probabilities / total_weight; - best_probability = std::max(best_probability, probability_discard); - - best_probability = std::max(best_probability, probability_discard); - if (best_probability == 1) { - update_tablebase(id_of_state, best_probability); - return best_probability; - }; - - // All discards are equivalent, do not continue searching for different trash - break; - } + else + { + known = false; } - } + }); - // Last option is to stall - if(_num_clues > 0) { - give_clue(); - const probability_t probability_stall = evaluate_state(); - revert_clue(); - best_probability = std::max(best_probability, probability_stall); - if (best_probability == 1) { - update_tablebase(id_of_state, best_probability); - return best_probability; - }; - } + if (known) + { + const unsigned long total_weight = std::max(static_cast(_weighted_draw_pile_size), 1ul); + const probability_t probability_discard = sum_of_probabilities / total_weight; + reasonable_actions.emplace_back(action, probability_discard); + } + else + { + reasonable_actions.emplace_back(action, std::nullopt); + } + // All discards are equivalent, do not continue searching for different trash + break; + } + } + } + + if (_num_clues > 0) + { + give_clue(); + const std::optional prob = lookup(); + const Action action = {ActionType::clue, Cards::unknown}; + reasonable_actions.emplace_back(action, prob); + revert_clue(); + } + return reasonable_actions; + } + + template + std::optional HanabiState::lookup() const + { + if (_score == 5 * num_suits) + { + return 1; + } + if (_pace < 0 or _endgame_turns_left == 0) + { + return 0; + } + const auto id = unique_id(); + if (_position_tablebase.contains(id)) + { + return _position_tablebase.at(id); + } + else + { + return std::nullopt; + } + } + + template + void HanabiState::rotate_next_draw(const Card & card) + { + auto card_it = std::find_if(_draw_pile.begin() + , _draw_pile.end() + , [&card, this](const CardMultiplicity & card_multiplicity) { + return (is_trash(card) and is_trash(card_multiplicity.card)) or + (card_multiplicity.card.rank == card.rank and card_multiplicity.card.suit == card.suit); + }); + ASSERT(card_it != _draw_pile.end()); + std::swap(*card_it, _draw_pile.front()); + } + + template + ActionType HanabiState::last_action_type() const + { + ASSERT(not _actions_log.empty()); + return _actions_log.top().action_type; + } + + template + probability_t HanabiState::evaluate_state() + { + ASSERT(_relative_representation.initialized); + _enumerated_states++; + const unsigned long id_of_state = unique_id(); + + const unsigned id = 55032; + if (id_of_state == id) + { + std::cout << "Found state with id of " << id << "\n" << *this << std::endl; + } + + if (_score == _score_goal) + { + return 1; + } + if (_pace < 0 || _endgame_turns_left == 0) + { + return 0; + } +#ifndef GAME_STATE_NO_TABLEBASE_LOOKUP + if (_position_tablebase.contains(id_of_state)) { + return _position_tablebase[id_of_state]; + } +#endif + + // TODO: Have some endgame analysis here? + + probability_t best_probability = 0; + const std::array & hand = _hands[_turn]; + + // First, check for playables + for (std::uint8_t index = 0; index < hand_size; index++) + { + if (is_playable(hand[index])) + { + probability_t sum_of_probabilities = 0; + + do_for_each_potential_draw(index, true, [this, &sum_of_probabilities](const unsigned long multiplicity) { + sum_of_probabilities += evaluate_state() * multiplicity; + }); + + const unsigned long total_weight = std::max(static_cast(_weighted_draw_pile_size), 1ul); + const probability_t probability_play = sum_of_probabilities / total_weight; + + best_probability = std::max(best_probability, probability_play); + if (best_probability == 1) + { + update_tablebase(id_of_state, best_probability); + return best_probability; + }; + } + } + + // Check for discards now + if (_pace > 0 and _num_clues < max_num_clues) + { + for (std::uint8_t index = 0; index < hand_size; index++) + { + if (is_trash(hand[index])) + { + probability_t sum_of_probabilities = 0; + + do_for_each_potential_draw(index, false, [this, &sum_of_probabilities](const unsigned long multiplicity) { + sum_of_probabilities += evaluate_state() * multiplicity; + }); + + const unsigned long total_weight = std::max(static_cast(_weighted_draw_pile_size), 1ul); + const probability_t probability_discard = sum_of_probabilities / total_weight; + best_probability = std::max(best_probability, probability_discard); + + best_probability = std::max(best_probability, probability_discard); + if (best_probability == 1) + { + update_tablebase(id_of_state, best_probability); + return best_probability; + }; + + // All discards are equivalent, do not continue searching for different trash + break; + } + } + } + + // Last option is to stall + if (_num_clues > 0) + { + give_clue(); + const probability_t probability_stall = evaluate_state(); + revert_clue(); + best_probability = std::max(best_probability, probability_stall); + if (best_probability == 1) + { update_tablebase(id_of_state, best_probability); return best_probability; + }; } - template - template - void HanabiState::do_for_each_potential_draw(hand_index_t index, bool play, Function f) { - auto copy = _draw_pile; - auto do_action = [this, index, play](){ - if (play) { - return play_and_potentially_update(index, true); - } else { - return discard_and_potentially_update(index, true); - } - }; + update_tablebase(id_of_state, best_probability); + return best_probability; + } - auto revert_action = [this, play](){ - if (play) { - revert_play(true); - } else { - revert_discard(true); - } - }; + template + template + void + HanabiState::do_for_each_potential_draw(hand_index_t index, bool play, Function f) + { + auto copy = _draw_pile; + auto do_action = [this, index, play]() { + if (play) + { + return play_and_potentially_update(index, true); + } + else + { + return discard_and_potentially_update(index, true); + } + }; - if(_draw_pile.empty()) { - do_action(); - f(1); - revert_action(); - } else { - unsigned sum_of_multiplicities = 0; - for(size_t i = 0; i < _draw_pile.size(); i++) { - const unsigned long multiplicity = do_action(); - sum_of_multiplicities += multiplicity; - f(multiplicity); - revert_action(); - } - ASSERT(sum_of_multiplicities == _weighted_draw_pile_size); - } - ASSERT(_draw_pile == copy); + auto revert_action = [this, play]() { + if (play) + { + revert_play(true); + } + else + { + revert_discard(true); + } + }; + + if (_draw_pile.empty()) + { + do_action(); + f(1); + revert_action(); } + else + { + unsigned sum_of_multiplicities = 0; + for (size_t i = 0; i < _draw_pile.size(); i++) + { + const unsigned long multiplicity = do_action(); + sum_of_multiplicities += multiplicity; + f(multiplicity); + revert_action(); + } + ASSERT(sum_of_multiplicities == _weighted_draw_pile_size); + } + ASSERT(_draw_pile == copy); + } - template - std::uint64_t HanabiState::unique_id() const { - unsigned long id = 0; + template + std::uint64_t HanabiState::unique_id() const + { + unsigned long id = 0; - // encode all positions of cards that started in draw pile - 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 + 3; - // 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(_relative_representation.good_cards_draw[i])) { - id += player; - } else { - id += RelativeRepresentationData::discard_pile; - } - } - } - - // encode number of clues - id *= max_num_clues + 1; - id += _num_clues; - - // we can encode draw pile size and extra turn in one metric, since we only have extra turns if draw pile is empty - const std::uint8_t draw_pile_size_and_extra_turns = [this]() -> uint8_t { - if(_endgame_turns_left == no_endgame) { - return _weighted_draw_pile_size + num_players; - } - else { - return _endgame_turns_left; - } - }(); - - id *= _relative_representation.initial_draw_pile_size + num_players; - id += draw_pile_size_and_extra_turns; - - // encode positions of cards that started in hands - for (typename RelativeRepresentationData::CardPosition const & position : _relative_representation.card_positions_hands) + // encode all positions of cards that started in draw pile + 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 + 3; + // 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(_relative_representation.good_cards_draw[i])) { - id *= 3; - id += static_cast>(position); + id += player; } - - id *= num_players; - id += _turn; - - // The id is unique now, since for all relevant cards, we know their position (including if they are played), - // the number of clues, the draw pile size and whose turn it is. - // This already uniquely determines the current players position, assuming that we never discard good cards - // (and only play them) - - return id; - } - - template - std::pair, std::vector> HanabiState::dump_unique_id_parts() const { - std::vector ret; - std::vector cards; - - // encode all positions of cards that started in draw pile - 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]) { - // 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(_relative_representation.good_cards_draw[i])) { - ret.push_back(player); - } else { - ret.push_back(RelativeRepresentationData::discard_pile); - } - cards.push_back(_relative_representation.good_cards_draw[i]); - } - } - - // encode number of clues - ret.push_back(_num_clues); - - // we can encode draw pile size and extra turn in one metric, since we only have extra turns if draw pile is empty - const std::uint8_t draw_pile_size_and_extra_turns = [this]() -> uint8_t { - if(_endgame_turns_left == no_endgame) { - return _weighted_draw_pile_size + num_players; - } - else { - return _endgame_turns_left; - } - }(); - - ret.push_back(draw_pile_size_and_extra_turns); - - // encode positions of cards that started in hands - for (typename RelativeRepresentationData::CardPosition const & position : _relative_representation.card_positions_hands) + else { - ret.push_back(static_cast>(position)); + id += RelativeRepresentationData::discard_pile; } - - ret.push_back(_turn); - - // The id is unique now, since for all relevant cards, we know their position (including if they are played), - // the number of clues, the draw pile size and whose turn it is. - // This already uniquely determines the current players position, assuming that we never discard good cards - // (and only play them) - - return {ret, cards}; + } } - template - const std::unordered_map& HanabiState::position_tablebase() const { - return _position_tablebase; + // encode number of clues + id *= max_num_clues + 1; + id += _num_clues; + + // we can encode draw pile size and extra turn in one metric, since we only have extra turns if draw pile is empty + const std::uint8_t draw_pile_size_and_extra_turns = [this]() -> uint8_t { + if (_endgame_turns_left == no_endgame) + { + return _weighted_draw_pile_size + num_players; + } + else + { + return _endgame_turns_left; + } + }(); + + id *= _relative_representation.initial_draw_pile_size + num_players; + id += draw_pile_size_and_extra_turns; + + // encode positions of cards that started in hands + for (typename RelativeRepresentationData::CardPosition const & position: _relative_representation.card_positions_hands) + { + id *= 3; + id += static_cast>(position); } - template - size_t HanabiState::draw_pile_size() const { - return _weighted_draw_pile_size; - } + id *= num_players; + id += _turn; - template - bool HanabiState::is_relative_state_initialized() const { - return _relative_representation.initialized; - } + // The id is unique now, since for all relevant cards, we know their position (including if they are played), + // the number of clues, the draw pile size and whose turn it is. + // This already uniquely determines the current players position, assuming that we never discard good cards + // (and only play them) - template - void HanabiState::update_tablebase( - unsigned long id, - Hanabi::probability_t probability) { - if (_position_tablebase.contains(id)) { - ASSERT(_position_tablebase[id] == probability); + return id; + } + + template + std::pair, std::vector> + HanabiState::dump_unique_id_parts() const + { + std::vector ret; + std::vector cards; + + // encode all positions of cards that started in draw pile + 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]) + { + // 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(_relative_representation.good_cards_draw[i])) + { + ret.push_back(player); } - _position_tablebase[id] = probability; + else + { + ret.push_back(RelativeRepresentationData::discard_pile); + } + cards.push_back(_relative_representation.good_cards_draw[i]); + } } + // encode number of clues + ret.push_back(_num_clues); + + // we can encode draw pile size and extra turn in one metric, since we only have extra turns if draw pile is empty + const std::uint8_t draw_pile_size_and_extra_turns = [this]() -> uint8_t { + if (_endgame_turns_left == no_endgame) + { + return _weighted_draw_pile_size + num_players; + } + else + { + return _endgame_turns_left; + } + }(); + + ret.push_back(draw_pile_size_and_extra_turns); + + // encode positions of cards that started in hands + for (typename RelativeRepresentationData::CardPosition const & position: _relative_representation.card_positions_hands) + { + ret.push_back(static_cast>(position)); + } + + ret.push_back(_turn); + + // The id is unique now, since for all relevant cards, we know their position (including if they are played), + // the number of clues, the draw pile size and whose turn it is. + // This already uniquely determines the current players position, assuming that we never discard good cards + // (and only play them) + + return {ret, cards}; + } + + template + const std::unordered_map & + HanabiState::position_tablebase() const + { + return _position_tablebase; + } + + template + size_t HanabiState::draw_pile_size() const + { + return _weighted_draw_pile_size; + } + + template + bool HanabiState::is_relative_state_initialized() const + { + return _relative_representation.initialized; + } + + template + void HanabiState::update_tablebase( + unsigned long id, Hanabi::probability_t probability + ) + { + if (_position_tablebase.contains(id)) + { + ASSERT(_position_tablebase[id] == probability); + } + _position_tablebase[id] = probability; + } + } // namespace Hanabi \ No newline at end of file diff --git a/include/hanabi_types.hpp b/include/hanabi_types.hpp index 2fa170e..d48594e 100644 --- a/include/hanabi_types.hpp +++ b/include/hanabi_types.hpp @@ -8,7 +8,8 @@ #include -namespace Hanabi { +namespace Hanabi +{ using rank_t = std::uint8_t; using suit_t = std::uint8_t; @@ -32,11 +33,12 @@ namespace Hanabi { using probability_t = double; #endif - std::ostream& print_probability(std::ostream& os, const rational_probability& prob); - std::ostream& print_probability(std::ostream& os, double prob); + std::ostream & print_probability(std::ostream & os, const rational_probability & prob); + + std::ostream & print_probability(std::ostream & os, double prob); template - std::ostream& print_probability(std::ostream& os, const std::optional& prob); + std::ostream & print_probability(std::ostream & os, const std::optional & prob); /** * We will generally assume that stacks are played from n to 0 @@ -57,7 +59,8 @@ namespace Hanabi { // Note that this is therefore not static so that we have external linking inline std::array suit_initials = {'r', 'y', 'g', 'b', 'p', 't'}; - struct Card { + struct Card + { suit_t suit; rank_t rank; @@ -71,32 +74,37 @@ namespace Hanabi { * @brief Compares cards *only* regarding suit and rank. * This is inlined as this is a runtime critical function when backtracking. */ - inline bool operator==(const Card &other) const; + inline bool operator==(const Card & other) const; }; - enum class ActionType : std::uint8_t { - play = 0, - discard = 1, - clue = 2, - color_clue = 2, - rank_clue = 3, - end_game = 4, - vote_terminate_players = 5, - vote_terminate = 10, - }; + enum class ActionType : std::uint8_t + { + play = 0 + , discard = 1 + , clue = 2 + , color_clue = 2 + , rank_clue = 3 + , end_game = 4 + , vote_terminate_players = 5 + , vote_terminate = 10 + , }; - struct Action { - ActionType type {}; - Card card {}; + struct Action + { + ActionType type{}; + Card card{}; }; // Output utilities for Cards and Actions - std::string to_string(const Card &card); - std::ostream &operator<<(std::ostream &os, const Card & card); - std::ostream& operator<<(std::ostream& os, const Action& action); + std::string to_string(const Card & card); + + std::ostream & operator<<(std::ostream & os, const Card & card); + + std::ostream & operator<<(std::ostream & os, const Action & action); - namespace Cards { + namespace Cards + { static constexpr Card r0 = {0, 5}; static constexpr Card r1 = {0, 4}; static constexpr Card r2 = {0, 3}; @@ -139,15 +147,20 @@ namespace Hanabi { //// INLINE SECTION - bool Card::operator==(const Card &other) const { + bool Card::operator==(const Card & other) const + { return suit == other.suit and rank == other.rank; } template - std::ostream& print_probability(std::ostream& os, const std::optional& prob) { - if (prob.has_value()) { + std::ostream & print_probability(std::ostream & os, const std::optional & prob) + { + if (prob.has_value()) + { return print_probability(os, prob.value()); - } else { + } + else + { os << "unknown"; } return os; diff --git a/include/myassert.h b/include/myassert.h index 174f12f..5eecfb5 100644 --- a/include/myassert.h +++ b/include/myassert.h @@ -4,7 +4,9 @@ #ifdef NDEBUG #define ASSERT(x) do { (void)sizeof(x);} while (0) #else + #include + #define ASSERT(x) assert(x) #endif diff --git a/include/null_buffer.h b/include/null_buffer.h index 75bba42..f329cee 100644 --- a/include/null_buffer.h +++ b/include/null_buffer.h @@ -3,21 +3,27 @@ #include -namespace NullBuffer { +namespace NullBuffer +{ -class NullBuffer final : public std::streambuf { -public: - int overflow(int c) override { return c; } -}; + class NullBuffer final : public std::streambuf + { + public: + int overflow(int c) override + { return c; } + }; -class NullStream final : public std::ostream { -public: - NullStream() : std::ostream(&_m_sb) {} -private: - NullBuffer _m_sb; -}; + class NullStream final : public std::ostream + { + public: + NullStream() : std::ostream(&_m_sb) + {} -NullStream null_stream; + private: + NullBuffer _m_sb; + }; + + NullStream null_stream; } diff --git a/include/parse_game.h b/include/parse_game.h index 2047631..d1e4e34 100644 --- a/include/parse_game.h +++ b/include/parse_game.h @@ -5,22 +5,26 @@ #include "game_interface.h" -namespace Hanabi { +namespace Hanabi +{ // These are overloads that the boost/json library uses for parsing. // They convert a Card from/to json. // This has to be in the same namespace as Hanabi::Card. - Card tag_invoke(boost::json::value_to_tag, boost::json::value const &jv); - void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, Hanabi::Card const &card); + Card tag_invoke(boost::json::value_to_tag, boost::json::value const & jv); + + void tag_invoke(boost::json::value_from_tag, boost::json::value & jv, Hanabi::Card const & card); } -namespace Parsing { +namespace Parsing +{ /** * Represents a single action (turn) in a Hanab game. * Note that this is slightly differen than Hanabi::Action, * since this uses indices for specifying the discarded/played cards. * We only want to work with this type while parsing, converting to Hanabi::Action after. */ - struct HanabLiveAction { + struct HanabLiveAction + { Hanabi::ActionType type{}; /** * In case the action is of type discard or play, @@ -30,28 +34,26 @@ namespace Parsing { }; // Overload for parsing from json to HanabLiveAction - HanabLiveAction tag_invoke(boost::json::value_to_tag, boost::json::value const &jv); + HanabLiveAction tag_invoke(boost::json::value_to_tag, boost::json::value const & jv); /* * @brief Parse deck from hanab.live format * @return List of cards (in order) and number of suits */ - std::pair, Hanabi::suit_t> parse_deck(const boost::json::value &deck_json); + std::pair, Hanabi::suit_t> parse_deck(const boost::json::value & deck_json); /** * @brief Parse actions from hanab.live format. * @return List of actions */ - std::vector parse_actions(const boost::json::value &action_json); + std::vector parse_actions(const boost::json::value & action_json); std::vector convert_actions( - std::vector const & hanab_live_actions, - std::vector const & deck + std::vector const & hanab_live_actions, std::vector const & deck ); Hanabi::GameInfo parse_game(boost::json::object const & game_json); } - #endif //DYNAMIC_PROGRAM_PARSE_GAME_H diff --git a/include/state_explorer.h b/include/state_explorer.h index f6ced08..7cd1858 100644 --- a/include/state_explorer.h +++ b/include/state_explorer.h @@ -4,8 +4,9 @@ #include #include "game_interface.h" -namespace Hanabi { - void cli(Game const & game); +namespace Hanabi +{ + void cli(Game const & game); } diff --git a/src/check_games.cpp b/src/check_games.cpp index 71b4985..99a7059 100644 --- a/src/check_games.cpp +++ b/src/check_games.cpp @@ -2,49 +2,59 @@ #include "download.h" -void check_games(unsigned num_players, unsigned max_draw_pile_size, unsigned first_game = 0, unsigned last_game = 9999) { - std::vector> winning_percentages(last_game + 2); +void check_games(unsigned num_players, unsigned max_draw_pile_size, unsigned first_game = 0, unsigned last_game = 9999) +{ + std::vector> winning_percentages(last_game + 2); - for(size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) { - Hanabi::probability_t total_chance = 0; - const std::string output_fname = "games_" + std::to_string(num_players) + "p_draw_size_" + std::to_string(draw_pile_size) + ".txt"; - std::ofstream file (output_fname); - for(size_t game_id = first_game; game_id <= last_game; game_id++) { - const std::string input_fname = "json/" + std::to_string(num_players) + "p/" + std::to_string(game_id) + ".json"; - auto game = Download::get_game(input_fname.c_str(), 50, draw_pile_size); - const Hanabi::probability_t chance = game->evaluate_state(); - winning_percentages[game_id].push_back(chance); - if(chance != 1) { - file << "Game " << game_id << ": " << chance << std::endl; - file << *game << std::endl << std::endl; - } - std::cout << "Finished game " << game_id << " with draw pile size " << draw_pile_size << ": " << chance << std::endl; + for (size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) + { + Hanabi::probability_t total_chance = 0; + const std::string output_fname = + "games_" + std::to_string(num_players) + "p_draw_size_" + std::to_string(draw_pile_size) + ".txt"; + std::ofstream file(output_fname); + for (size_t game_id = first_game; game_id <= last_game; game_id++) + { + const std::string input_fname = "json/" + std::to_string(num_players) + "p/" + std::to_string(game_id) + ".json"; + auto game = Download::get_game(input_fname.c_str(), 50, draw_pile_size); + const Hanabi::probability_t chance = game->evaluate_state(); + winning_percentages[game_id].push_back(chance); + if (chance != 1) + { + file << "Game " << game_id << ": " << chance << std::endl; + file << *game << std::endl << std::endl; + } + std::cout << "Finished game " << game_id << " with draw pile size " << draw_pile_size << ": " << chance + << std::endl; - total_chance += chance; - } - const Hanabi::probability_t total_average = total_chance / (last_game - first_game + 1); - winning_percentages.back().push_back(total_average); - file << "Total chance found over " << last_game - first_game + 1 << " many games: " << total_average << std::endl; - file.close(); + total_chance += chance; } - const std::string results_file_name {"results_" + std::to_string(num_players) + "p.txt"}; - std::ofstream results_file (results_file_name); - results_file << "game_id, "; - for(size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) { - results_file << std::to_string(draw_pile_size) << ", "; - } - results_file << "\n"; - for(size_t game_id = first_game; game_id <= last_game; game_id++) { - results_file << game_id << ", "; - for(size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) { - results_file << winning_percentages[game_id][draw_pile_size] << ", "; - } - results_file << std::endl; - } - results_file << "total, "; - for(size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) { - results_file << winning_percentages.back()[draw_pile_size] << ", "; + const Hanabi::probability_t total_average = total_chance / (last_game - first_game + 1); + winning_percentages.back().push_back(total_average); + file << "Total chance found over " << last_game - first_game + 1 << " many games: " << total_average << std::endl; + file.close(); + } + const std::string results_file_name{"results_" + std::to_string(num_players) + "p.txt"}; + std::ofstream results_file(results_file_name); + results_file << "game_id, "; + for (size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) + { + results_file << std::to_string(draw_pile_size) << ", "; + } + results_file << "\n"; + for (size_t game_id = first_game; game_id <= last_game; game_id++) + { + results_file << game_id << ", "; + for (size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) + { + results_file << winning_percentages[game_id][draw_pile_size] << ", "; } results_file << std::endl; - results_file.close(); + } + results_file << "total, "; + for (size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) + { + results_file << winning_percentages.back()[draw_pile_size] << ", "; + } + results_file << std::endl; + results_file.close(); } diff --git a/src/command_line_interface.cpp b/src/command_line_interface.cpp index b696af1..6a16026 100644 --- a/src/command_line_interface.cpp +++ b/src/command_line_interface.cpp @@ -8,7 +8,8 @@ namespace bpo = boost::program_options; -namespace Hanabi { +namespace Hanabi +{ template std::optional convert_optional(boost::optional val) @@ -17,10 +18,10 @@ namespace Hanabi { { return std::nullopt; } - return { std::move(val.value()) }; + return {std::move(val.value())}; } - std::ostream& quiet_ostream(bool const quiet) + std::ostream & quiet_ostream(bool const quiet) { if (quiet) { @@ -50,7 +51,7 @@ namespace Hanabi { quiet_os.precision(10); // Load game, either from file or from hanab.live - Game game = [&parms]{ + Game game = [&parms] { if (std::holds_alternative(parms.game)) { return Download::get_game(std::get(parms.game), convert_optional(parms.score_goal)); @@ -63,9 +64,12 @@ namespace Hanabi { if (not game.holds_state()) { - if(std::holds_alternative(parms.game)) { + if (std::holds_alternative(parms.game)) + { std::cout << "Failed to download game " << std::get(parms.game) << " from hanab.live." << std::endl; - } else { + } + else + { std::cout << "Failed to open file " << std::get(parms.game) << "." << std::endl; } return download_failed; @@ -102,7 +106,8 @@ namespace Hanabi { throw std::logic_error("Invalid game state specification type encountered"); } std::cout << parms.game_state_spec << " cannot be reached with specified replay." << std::endl; - std::cout << "Replay ends at turn " << game.cur_turn() << " with score of " << game.state->score() << "." << std::endl; + std::cout << "Replay ends at turn " << game.cur_turn() << " with score of " << game.state->score() << "." + << std::endl; return state_unreachable; } @@ -125,13 +130,16 @@ namespace Hanabi { // (except for rare cases, where there is a forced win that does not need stalling). size_t const max_draw_pile_size = game.state->draw_pile_size(); bool printed_replay_end_msg = false; - for(size_t remaining_cards = 1; remaining_cards <= max_draw_pile_size; remaining_cards++) { + for (size_t remaining_cards = 1; remaining_cards <= max_draw_pile_size; remaining_cards++) + { if (!game.goto_draw_pile_size(remaining_cards)) { if (not printed_replay_end_msg) { - std::cout << "Draw pile size of " << game.state->draw_pile_size() -1 << " or lower cannot be obtained with the specified replay:" << std::endl; - std::cout << "Replay ends at turn " << game.cur_turn() << " with score of " << game.state->score() << "." << std::endl; + std::cout << "Draw pile size of " << game.state->draw_pile_size() - 1 + << " or lower cannot be obtained with the specified replay:" << std::endl; + std::cout << "Replay ends at turn " << game.cur_turn() << " with score of " << game.state->score() << "." + << std::endl; printed_replay_end_msg = true; } continue; @@ -142,21 +150,25 @@ namespace Hanabi { // When modifying the game state, we want to reset to the actual number of clues // to ensure that actions taken are legal. clue_t const original_num_clues = game.state->num_clues(); - for(clue_t num_clues = 0; num_clues <= 8; num_clues++) { + for (clue_t num_clues = 0; num_clues <= 8; num_clues++) + { game.state->set_clues(num_clues); probability_t const result = game.state->evaluate_state(); std::cout << "Probability with " << remaining_cards << " cards left in deck and " << +num_clues << " clues (" << std::showpos << +(num_clues - original_num_clues) << "): " << std::noshowpos; print_probability(std::cout, result) << std::endl; std::cout << *game.state << std::endl; - auto const [a,b] = game.state->dump_unique_id_parts(); - for (auto elem : a) { + auto const [a, b] = game.state->dump_unique_id_parts(); + for (auto elem: a) + { std::cout << elem << ", "; } std::cout << "-> " << game.state->unique_id() << std::endl; } game.state->set_clues(original_num_clues); - } else { + } + else + { probability_t const result = game.state->evaluate_state(); std::cout << "Probability with " << remaining_cards << " cards left in deck: "; print_probability(std::cout, result) << std::endl; @@ -194,33 +206,35 @@ namespace Hanabi { CLIParms parms; bpo::options_description desc("Allowed program options"); desc.add_options() - ("help,h", "Print this help message.") - ("game,g", bpo::value(), "Game ID from hanab.live.") - ("file,f", bpo::value(), "Input file containing game in hanab.live json format.") - ("turn,t", bpo::value(&parms.game_state_spec), "Turn number of state to analyze. " - "Turn 1 means no actions have been taken. ") - ("draw-pile-size,d", bpo::value(&parms.game_state_spec), "Draw pile size of state to analyze.") - ("score-goal,s", bpo::value>(&parms.score_goal), - "Score that counts as a win, i.e. is optimized for achieving. If unspecified, the maximum possible " - "score will be used.") - ("clue-modifier,c", bpo::value(), "Optional relative modification to the number of clues applied to " - "selected game state. If unspecified, has value 0 and thus no effect.") - ("interactive,i", bpo::value>(&parms.interactive), - "After computation, drop into interactive shell to explore game. If unspecified, a reasonable default " - "is chosen depending on other options.") - ("recursive,r", "If specified, also analyzes all game states with fewer cards in the draw pile than the " - "specified one, considering smaller draw pile sizes first. Also useful for infinite analysis " - "(specifying this with a complex base state") - ("all-clues", "Whenever evaluating a game state, evaluate it with all clue counts and output their " - "probabilities.") - ("quiet,q", "Deactivate all non-essential prints. Useful if output is parsed by another program.") - ; + ("help,h", "Print this help message.") + ("game,g", bpo::value(), "Game ID from hanab.live.") + ("file,f", bpo::value(), "Input file containing game in hanab.live json format.") + ("turn,t", bpo::value(&parms.game_state_spec), "Turn number of state to analyze. " + "Turn 1 means no actions have been taken. ") + ("draw-pile-size,d", bpo::value(&parms.game_state_spec), "Draw pile size of state to analyze.") + ("score-goal,s" + , bpo::value>(&parms.score_goal) + , "Score that counts as a win, i.e. is optimized for achieving. If unspecified, the maximum possible " + "score will be used.") + ("clue-modifier,c", bpo::value(), "Optional relative modification to the number of clues applied to " + "selected game state. If unspecified, has value 0 and thus no effect.") + ("interactive,i" + , bpo::value>(&parms.interactive) + , "After computation, drop into interactive shell to explore game. If unspecified, a reasonable default " + "is chosen depending on other options.") + ("recursive,r", "If specified, also analyzes all game states with fewer cards in the draw pile than the " + "specified one, considering smaller draw pile sizes first. Also useful for infinite analysis " + "(specifying this with a complex base state") + ("all-clues", "Whenever evaluating a game state, evaluate it with all clue counts and output their " + "probabilities.") + ("quiet,q", "Deactivate all non-essential prints. Useful if output is parsed by another program."); bpo::variables_map vm; bpo::store(bpo::parse_command_line(argc, argv, desc), vm); bpo::notify(vm); - if (vm.count("help")) { + if (vm.count("help")) + { std::cout << "This program performs endgame analysis of Hanabi. It calculates optimum strategies\n" "(and their winning percentages assuming a random draw pile distribution) under the\n" "assumption that all players know their hands at all times during the game.\n" @@ -238,7 +252,8 @@ namespace Hanabi { } // Parse file or game id specification, ensuring at most one is given - if (vm.count("file") + vm.count("game") != 1) { + if (vm.count("file") + vm.count("game") != 1) + { std::cout << "Exactly one option of 'file' and 'id' has to be given." << std::endl; std::cout << "Use '--help' to print a help message." << std::endl; return std::nullopt; @@ -253,7 +268,8 @@ namespace Hanabi { } // Parse game state options (turn or draw), ensuring at most one is given. - if (vm.count("draw-pile-size") + vm.count("turn") != 1) { + if (vm.count("draw-pile-size") + vm.count("turn") != 1) + { std::cout << "Conflicting options --draw and --turn." << std::endl; std::cout << "Use '--help' to print a help message." << std::endl; return std::nullopt; diff --git a/src/download.cpp b/src/download.cpp index 9f1ac36..1f2a725 100644 --- a/src/download.cpp +++ b/src/download.cpp @@ -7,30 +7,36 @@ #include "download.h" -namespace Download { +namespace Download +{ - std::optional download_game_json(int game_id) { - std::string request_str = "https://hanab.live/export/" + std::to_string(game_id); - cpr::Response r = cpr::Get(cpr::Url(request_str)); - if (r.header["content-type"] != "application/json; charset=utf-8") { - return std::nullopt; - } - return boost::json::parse(r.text).as_object(); + std::optional download_game_json(int game_id) + { + std::string request_str = "https://hanab.live/export/" + std::to_string(game_id); + cpr::Response r = cpr::Get(cpr::Url(request_str)); + if (r.header["content-type"] != "application/json; charset=utf-8") + { + return std::nullopt; } + return boost::json::parse(r.text).as_object(); + } - std::optional open_game_json(const char *filename) { - std::ifstream file(filename); - if (!file.is_open()) { - return std::nullopt; - } - std::string game_json((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - return boost::json::parse(game_json).as_object(); + std::optional open_game_json(const char *filename) + { + std::ifstream file(filename); + if (!file.is_open()) + { + return std::nullopt; } + std::string game_json((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return boost::json::parse(game_json).as_object(); + } Hanabi::Game get_game(int game_id, std::optional score_goal) { std::optional const game_json = download_game_json(game_id); - if (!game_json.has_value() or game_json.value().empty()) { + if (!game_json.has_value() or game_json.value().empty()) + { return {nullptr, {}}; } @@ -41,7 +47,8 @@ namespace Download { Hanabi::Game get_game(std::string const & filename, std::optional score_goal) { std::optional const game_json = open_game_json(filename.c_str()); - if (!game_json.has_value() or game_json.value().empty()) { + if (!game_json.has_value() or game_json.value().empty()) + { return {nullptr, {}}; } diff --git a/src/game_interface.cpp b/src/game_interface.cpp index 3781a45..d2b89e6 100644 --- a/src/game_interface.cpp +++ b/src/game_interface.cpp @@ -2,27 +2,29 @@ #include "game_interface.h" -namespace Hanabi { +namespace Hanabi +{ - std::ostream &operator<<(std::ostream &os, HanabiStateIF const &hanabi_state) + std::ostream & operator<<(std::ostream & os, HanabiStateIF const & hanabi_state) { hanabi_state.print(os); return os; } - Game::Game(std::unique_ptr state, Hanabi::GameInfo game_info): - GameInfo(std::move(game_info)), state(std::move(state)), next_action(0) + Game::Game(std::unique_ptr state, Hanabi::GameInfo game_info) : + GameInfo(std::move(game_info)), state(std::move(state)), next_action(0) { // If there is a 'Null' action that only signals the game's end, we want to get rid of it now, // as this will mess with our moves. - if(not this->actions.empty()) { - switch(this->actions.back().type) { + if (not this->actions.empty()) + { + switch (this->actions.back().type) + { case ActionType::vote_terminate: case ActionType::vote_terminate_players: case ActionType::end_game: this->actions.pop_back(); - default: - ; + default:; } } } @@ -39,7 +41,8 @@ namespace Hanabi { state->rotate_next_draw(next_draw); Action const & action = actions[next_action]; std::uint8_t index; - switch(action.type) { + switch (action.type) + { case Hanabi::ActionType::color_clue: case Hanabi::ActionType::rank_clue: state->give_clue(); @@ -56,8 +59,7 @@ namespace Hanabi { break; case Hanabi::ActionType::vote_terminate_players: case Hanabi::ActionType::vote_terminate: - case Hanabi::ActionType::end_game: - ; + case Hanabi::ActionType::end_game:; } ++next_action; } @@ -71,12 +73,17 @@ namespace Hanabi { bool Game::goto_turn(size_t turn) { size_t const cur_turn = next_action + 1; - if (cur_turn >= turn) { - for(size_t i = 0; i < cur_turn - turn; i++) { + if (cur_turn >= turn) + { + for (size_t i = 0; i < cur_turn - turn; i++) + { revert_turn(); } - } else { - while(next_action < actions.size() and next_action + 1 < turn) { + } + else + { + while (next_action < actions.size() and next_action + 1 < turn) + { make_turn(); } } @@ -90,10 +97,13 @@ namespace Hanabi { bool Game::goto_draw_pile_size(size_t draw_pile_break) { - while (state->draw_pile_size() > draw_pile_break and next_action < actions.size()) { + while (state->draw_pile_size() > draw_pile_break and next_action < actions.size()) + { make_turn(); } - while(state->draw_pile_size() < draw_pile_break or (state->draw_pile_size() == draw_pile_break and state->last_action_type() == ActionType::clue)) { + while (state->draw_pile_size() < draw_pile_break or + (state->draw_pile_size() == draw_pile_break and state->last_action_type() == ActionType::clue)) + { revert_turn(); } return state->draw_pile_size() == draw_pile_break; diff --git a/src/hanabi_types.cpp b/src/hanabi_types.cpp index b82ef5d..5eda863 100644 --- a/src/hanabi_types.cpp +++ b/src/hanabi_types.cpp @@ -1,8 +1,11 @@ #include "hanabi_types.hpp" -namespace Hanabi { - std::ostream &operator<<(std::ostream &os, Action const& action) { - switch(action.type) { +namespace Hanabi +{ + std::ostream & operator<<(std::ostream & os, Action const & action) + { + switch (action.type) + { case ActionType::play: os << "play " + to_string(action.card); break; @@ -19,25 +22,32 @@ namespace Hanabi { } - std::string to_string(const Hanabi::Card &card) { - if (card == Hanabi::Cards::trash) { + std::string to_string(const Hanabi::Card & card) + { + if (card == Hanabi::Cards::trash) + { return "kt"; - } else { + } + else + { return Hanabi::suit_initials[card.suit] + std::to_string(5 - card.rank); } } - std::ostream &operator<<(std::ostream &os, const Card &card) { + std::ostream & operator<<(std::ostream & os, const Card & card) + { os << to_string(card); return os; } - std::ostream& print_probability(std::ostream& os, const rational_probability & prob) { + std::ostream & print_probability(std::ostream & os, const rational_probability & prob) + { os << prob << " ~ " << std::setprecision(5) << boost::rational_cast(prob) * 100 << "%"; return os; } - std::ostream& print_probability(std::ostream& os, double prob) { + std::ostream & print_probability(std::ostream & os, double prob) + { os << std::setprecision(5) << prob; return os; } diff --git a/src/main.cpp b/src/main.cpp index 5745737..3cc4d26 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,7 +3,8 @@ #include "command_line_interface.h" -int main(int argc, char *argv[]) { +int main(int argc, char *argv[]) +{ std::optional parms = Hanabi::parse_parms(argc, argv); if (parms.has_value()) { diff --git a/src/make_state.cpp b/src/make_state.cpp index 8ef5824..136c2a8 100644 --- a/src/make_state.cpp +++ b/src/make_state.cpp @@ -1,77 +1,83 @@ #include "game_state.h" #include "game_interface.h" -namespace Hanabi { +namespace Hanabi +{ std::unique_ptr make_game_state( - std::size_t num_suits, - Hanabi::player_t num_players, - std::vector const &deck, - std::optional score_goal) + std::size_t num_suits, Hanabi::player_t num_players, std::vector const & deck, std::optional< + uint8_t> score_goal + ) { uint8_t actual_score_goal = score_goal.value_or(5 * num_suits); - switch(num_players) { + switch (num_players) + { case 2: - switch(num_suits) { + switch (num_suits) + { case 3: - return std::unique_ptr(new Hanabi::HanabiState<3,2,5>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<3, 2, 5>(deck, actual_score_goal)); case 4: - return std::unique_ptr(new Hanabi::HanabiState<4,2,5>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<4, 2, 5>(deck, actual_score_goal)); case 5: - return std::unique_ptr(new Hanabi::HanabiState<5,2,5>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<5, 2, 5>(deck, actual_score_goal)); case 6: - return std::unique_ptr(new Hanabi::HanabiState<6,2,5>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<6, 2, 5>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } case 3: - switch(num_suits) { + switch (num_suits) + { case 3: - return std::unique_ptr(new Hanabi::HanabiState<3,3,5>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<3, 3, 5>(deck, actual_score_goal)); case 4: - return std::unique_ptr(new Hanabi::HanabiState<4,3,5>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<4, 3, 5>(deck, actual_score_goal)); case 5: - return std::unique_ptr(new Hanabi::HanabiState<5,3,5>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<5, 3, 5>(deck, actual_score_goal)); case 6: - return std::unique_ptr(new Hanabi::HanabiState<6,3,5>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<6, 3, 5>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } case 4: - switch(num_suits) { + switch (num_suits) + { case 3: - return std::unique_ptr(new Hanabi::HanabiState<3,4,4>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<3, 4, 4>(deck, actual_score_goal)); case 4: - return std::unique_ptr(new Hanabi::HanabiState<4,4,4>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<4, 4, 4>(deck, actual_score_goal)); case 5: - return std::unique_ptr(new Hanabi::HanabiState<5,4,4>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<5, 4, 4>(deck, actual_score_goal)); case 6: - return std::unique_ptr(new Hanabi::HanabiState<6,4,4>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<6, 4, 4>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } case 5: - switch(num_suits) { + switch (num_suits) + { case 3: - return std::unique_ptr(new Hanabi::HanabiState<3,5,4>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<3, 5, 4>(deck, actual_score_goal)); case 4: - return std::unique_ptr(new Hanabi::HanabiState<4,5,4>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<4, 5, 4>(deck, actual_score_goal)); case 5: - return std::unique_ptr(new Hanabi::HanabiState<5,5,4>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<5, 5, 4>(deck, actual_score_goal)); case 6: - return std::unique_ptr(new Hanabi::HanabiState<6,5,4>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<6, 5, 4>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } case 6: - switch(num_suits) { + switch (num_suits) + { case 3: - return std::unique_ptr(new Hanabi::HanabiState<3,6,3>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<3, 6, 3>(deck, actual_score_goal)); case 4: - return std::unique_ptr(new Hanabi::HanabiState<4,6,3>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<4, 6, 3>(deck, actual_score_goal)); case 5: - return std::unique_ptr(new Hanabi::HanabiState<5,6,3>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<5, 6, 3>(deck, actual_score_goal)); case 6: - return std::unique_ptr(new Hanabi::HanabiState<6,6,3>(deck, actual_score_goal)); + return std::unique_ptr(new Hanabi::HanabiState<6, 6, 3>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } diff --git a/src/parse_game.cpp b/src/parse_game.cpp index 21127fa..cd49b85 100644 --- a/src/parse_game.cpp +++ b/src/parse_game.cpp @@ -2,43 +2,52 @@ #include "myassert.h" -namespace Parsing { +namespace Parsing +{ // This helper function deduces the type and assigns the value with the matching key template - void extract(boost::json::object const &obj, T &t, std::string_view key) { + void extract(boost::json::object const & obj, T & t, std::string_view key) + { t = value_to(obj.at(key)); } } -namespace Hanabi { - Card tag_invoke(boost::json::value_to_tag, - boost::json::value const &jv) { +namespace Hanabi +{ + Card tag_invoke( + boost::json::value_to_tag, boost::json::value const & jv + ) + { Hanabi::Card card{}; - boost::json::object const &obj = jv.as_object(); + boost::json::object const & obj = jv.as_object(); Parsing::extract(obj, card.rank, "rank"); Parsing::extract(obj, card.suit, "suitIndex"); card.rank = 5 - card.rank; return card; } - void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, Hanabi::Card const &card) { - jv = {{"suitIndex", card.suit}, - {"rank", card.rank}}; + void tag_invoke(boost::json::value_from_tag, boost::json::value & jv, Hanabi::Card const & card) + { + jv = {{ "suitIndex", card.suit} + , {"rank" , card.rank}}; } } -namespace Parsing { +namespace Parsing +{ - HanabLiveAction tag_invoke(boost::json::value_to_tag, boost::json::value const &jv) { + HanabLiveAction tag_invoke(boost::json::value_to_tag, boost::json::value const & jv) + { HanabLiveAction action{}; uint8_t type; - boost::json::object const &obj = jv.as_object(); + boost::json::object const & obj = jv.as_object(); extract(obj, action.target, "target"); extract(obj, type, "type"); action.type = static_cast(type); - switch (action.type) { + switch (action.type) + { case Hanabi::ActionType::color_clue: case Hanabi::ActionType::rank_clue: action.type = Hanabi::ActionType::clue; @@ -58,35 +67,39 @@ namespace Parsing { return action; } - std::pair, Hanabi::suit_t> parse_deck(const boost::json::value &deck_json) { + std::pair, Hanabi::suit_t> parse_deck(const boost::json::value & deck_json) + { auto deck = boost::json::value_to>(deck_json); - for (auto &card: deck) { + for (auto & card: deck) + { ASSERT(card.rank < 5); ASSERT(card.rank >= 0); ASSERT(card.suit < 6); ASSERT(card.suit >= 0); } Hanabi::suit_t num_suits = 0; - for(const auto& card: deck) { + for (const auto & card: deck) + { num_suits = std::max(num_suits, card.suit); } return {deck, num_suits + 1}; } - std::vector parse_actions(const boost::json::value &action_json) + std::vector parse_actions(const boost::json::value & action_json) { return boost::json::value_to>(action_json); } - std::vector convert_actions(std::vector const & hanab_live_actions, std::vector const & deck) + std::vector + convert_actions(std::vector const & hanab_live_actions, std::vector const & deck) { std::vector actions; std::transform( - hanab_live_actions.begin(), - hanab_live_actions.end(), - std::back_inserter(actions), - [&deck](HanabLiveAction const & action){ - return Hanabi::Action {action.type, deck[action.target]}; + hanab_live_actions.begin() + , hanab_live_actions.end() + , std::back_inserter(actions) + , [&deck](HanabLiveAction const & action) { + return Hanabi::Action{action.type, deck[action.target]}; } ); return actions; diff --git a/src/state_explorer.cpp b/src/state_explorer.cpp index c65da65..a07186c 100644 --- a/src/state_explorer.cpp +++ b/src/state_explorer.cpp @@ -11,395 +11,470 @@ #include "game_state.h" -namespace Hanabi { +namespace Hanabi +{ - std::string read_line_memory_safe(const char *prompt) { - char *line = readline(prompt); - std::string ret; - if (line == nullptr) { - ret = ""; - } else { - ret = std::string(line); - } - free(line); - return ret; - } - - constexpr static std::array cli_commands = { - "play", - "clue", - "discard", - "opt", - "state", - "id", - "revert", - "actions", - "evaluate", - "help", - "quit", - "set-initials", - "dump-id-parts", - }; - - char * cli_commands_generator(const char *text, int state) { - std::string text_str (text); - for(auto& command : cli_commands) { - if (command.starts_with(text_str) && state-- <= 0) { - return strdup(command.c_str()); - } - } - return nullptr; - } - - char ** - cli_command_completion(const char *text, int start, int end) + std::string read_line_memory_safe(const char *prompt) + { + char *line = readline(prompt); + std::string ret; + if (line == nullptr) { - rl_attempted_completion_over = 1; - return rl_completion_matches(text, cli_commands_generator); + ret = ""; } - - - Card parse_card(std::string card_str) { - if (card_str == "trash" or card_str == "kt") { - return Cards::trash; - } - if(card_str.size() != 2) { - return Cards::unknown; - } - auto it = std::find(suit_initials.begin(), suit_initials.end(), card_str[0]); - if (it == suit_initials.end()) { - return Cards::unknown; - } - const suit_t suit = std::distance(suit_initials.begin(), it); - try { - const rank_t rank = 5 - std::stoi(card_str.substr(1, 1)); - return Card {suit, rank}; - } catch(std::invalid_argument&) { - return Cards::unknown; - } + else + { + ret = std::string(line); } + free(line); + return ret; + } - int representation_length(const rational_probability& probability) { - return 1 + static_cast(std::ceil(std::log10(probability.denominator()))) + \ + constexpr static std::array cli_commands = { + "play", "clue", "discard", "opt", "state", "id", "revert", "actions", "evaluate", "help", "quit", "set-initials" + , "dump-id-parts", + }; + + char *cli_commands_generator(const char *text, int state) + { + std::string text_str(text); + for (auto & command: cli_commands) + { + if (command.starts_with(text_str) && state-- <= 0) + { + return strdup(command.c_str()); + } + } + return nullptr; + } + + char ** + cli_command_completion(const char *text, int start, int end) + { + rl_attempted_completion_over = 1; + return rl_completion_matches(text, cli_commands_generator); + } + + + Card parse_card(std::string card_str) + { + if (card_str == "trash" or card_str == "kt") + { + return Cards::trash; + } + if (card_str.size() != 2) + { + return Cards::unknown; + } + auto it = std::find(suit_initials.begin(), suit_initials.end(), card_str[0]); + if (it == suit_initials.end()) + { + return Cards::unknown; + } + const suit_t suit = std::distance(suit_initials.begin(), it); + try + { + const rank_t rank = 5 - std::stoi(card_str.substr(1, 1)); + return Card{suit, rank}; + } + catch (std::invalid_argument &) + { + return Cards::unknown; + } + } + + int representation_length(const rational_probability & probability) + { + return 1 + static_cast(std::ceil(std::log10(probability.denominator()))) + \ static_cast(std::ceil(std::log10(probability.numerator()))); + } + + int representation_length(const double probability) + { + return static_cast(std::ceil(std::log10(probability))); + } + + bool ask_for_card_and_rotate_draw(HanabiStateIF & state, hand_index_t index, bool play) + { + const auto next_states = state.possible_next_states(index, play); + + if (next_states.size() <= 1) + { + // No need to ask for anything if draw contains only one possible type of card + return true; } - int representation_length(const double probability) { - return static_cast(std::ceil(std::log10(probability))); + std::vector>> states_to_show; + states_to_show.push_back({{Hanabi::Cards::trash, 0}, 0}); + + std::cout << "Choose drawn card: " << std::endl; + int max_rational_digit_len = 0; + for (const auto & [card_multiplicity, probability]: next_states) + { + // If the card is played, we can treat it as a trash draw as well + if (state.is_trash(card_multiplicity.card) or (play and state.cur_hand()[index] == card_multiplicity.card)) + { + states_to_show.front().first.multiplicity += card_multiplicity.multiplicity; + states_to_show.front().second = probability; + } + else + { + states_to_show.emplace_back(card_multiplicity, probability); + } + if (probability.has_value()) + { + max_rational_digit_len = std::max(max_rational_digit_len, representation_length(probability.value())); + } } - bool ask_for_card_and_rotate_draw(HanabiStateIF & state, hand_index_t index, bool play) { - const auto next_states = state.possible_next_states(index, play); - - if (next_states.size() <= 1) { - // No need to ask for anything if draw contains only one possible type of card - return true; - } - - std::vector>> states_to_show; - states_to_show.push_back({{Hanabi::Cards::trash, 0}, 0}); - - std::cout << "Choose drawn card: " << std::endl; - int max_rational_digit_len = 0; - for(const auto &[card_multiplicity, probability]: next_states) { - // If the card is played, we can treat it as a trash draw as well - if (state.is_trash(card_multiplicity.card) or (play and state.cur_hand()[index] == card_multiplicity.card)) { - states_to_show.front().first.multiplicity += card_multiplicity.multiplicity; - states_to_show.front().second = probability; - } else { - states_to_show.emplace_back(card_multiplicity, probability); - } - if (probability.has_value()) { - max_rational_digit_len = std::max(max_rational_digit_len, representation_length(probability.value())); - } - } - - // Get rid of the trash collecting entry at the front - if (states_to_show.front().first.multiplicity == 0) { - states_to_show.front() = std::move(states_to_show.back()); - states_to_show.pop_back(); - } - - std::ranges::sort(states_to_show, [](const auto &left, const auto &right) { - return left.second > right.second; - }); - - for (const auto &[card_multiplicity, probability]: states_to_show) { - std::cout << card_multiplicity.card << " (" << card_multiplicity.multiplicity; - std::cout << " copie(s) in draw) " << std::setw(max_rational_digit_len); - print_probability(std::cout, probability) << std::endl; - } - - std::stringstream prompt; - prompt << "draw? [" << states_to_show.front().first.card << "] "; - const std::string card_str = read_line_memory_safe(prompt.str().c_str()); - - const Card drawn_card = [&card_str, &states_to_show](){ - if (card_str.empty()) { - return states_to_show.front().first.card; - } - return parse_card(card_str); - }(); - - if (drawn_card == Cards::unknown) { - std::cout << "Could not parse card " << card_str << std::endl; - return false; - } - - auto selected_draw_it = std::find_if(next_states.begin(), next_states.end(), [&drawn_card, &state](const std::pair>& pair) { - return (state.is_trash(pair.first.card) and drawn_card == Cards::trash) or pair.first.card == drawn_card; - }); - if (selected_draw_it == next_states.end()){ - std::cout << "That card is not in the draw pile, aborting." << std::endl; - return false; - }; - state.rotate_next_draw(selected_draw_it->first.card); - return true; + // Get rid of the trash collecting entry at the front + if (states_to_show.front().first.multiplicity == 0) + { + states_to_show.front() = std::move(states_to_show.back()); + states_to_show.pop_back(); } - void signal_handler(int) { - std::cout << "Use 'quit' to exit the interactive shell." << std::endl << "> "; + std::ranges::sort(states_to_show, [](const auto & left, const auto & right) { + return left.second > right.second; + }); + + for (const auto & [card_multiplicity, probability]: states_to_show) + { + std::cout << card_multiplicity.card << " (" << card_multiplicity.multiplicity; + std::cout << " copie(s) in draw) " << std::setw(max_rational_digit_len); + print_probability(std::cout, probability) << std::endl; } - void cli(Game const & game) { - std::signal(SIGINT, signal_handler); - // Set up GNU readline - rl_attempted_completion_function = cli_command_completion; - using_history(); + std::stringstream prompt; + prompt << "draw? [" << states_to_show.front().first.card << "] "; + const std::string card_str = read_line_memory_safe(prompt.str().c_str()); - // Tracks the depth of the replay the user explores. We have to ensure that we don't revert too much. - unsigned depth = 0; + const Card drawn_card = [&card_str, &states_to_show]() { + if (card_str.empty()) + { + return states_to_show.front().first.card; + } + return parse_card(card_str); + }(); - while (true) { - const std::string prompt = read_line_memory_safe("> "); - add_history(prompt.c_str()); + if (drawn_card == Cards::unknown) + { + std::cout << "Could not parse card " << card_str << std::endl; + return false; + } - if (prompt.starts_with("help")) { - std::cout << "state: print information on current game state." << std::endl; - std::cout << "clue: give a clue." << std::endl; - std::cout << "play : play specified card." << std::endl; - std::cout << "discard: discard trash from hand." << std::endl; - std::cout << "opt: take optimal action. In case of ties, prefers plays and discards in that order." << std::endl; - std::cout << "revert : revert specified number of turns (default 1)." << std::endl; - std::cout << "actions: display list of reasonable actions to take and their winning chances." << std::endl; - std::cout << "evaluate: evaluate current game state recursively. Potentially runtime-expensive." << std::endl; - std::cout << "set-initials : Set initials for the suits." << std::endl; - std::cout << "(q)uit: Quit this interactive shell." << std::endl; - std::cout << "id: display id of state. Has no inherent meaning, useful for debugging." << std::endl; - std::cout << "dump-id-parts: Dump parts used to calculate the id of the state as well as the cards associated to them." << std::endl; - std::cout << "help: Display this help message." << std::endl; - continue; - } + auto selected_draw_it = std::find_if(next_states.begin(), next_states.end(), [&drawn_card, &state]( + const std::pair> & pair + ) { + return (state.is_trash(pair.first.card) and drawn_card == Cards::trash) or pair.first.card == drawn_card; + }); + if (selected_draw_it == next_states.end()) + { + std::cout << "That card is not in the draw pile, aborting." << std::endl; + return false; + }; + state.rotate_next_draw(selected_draw_it->first.card); + return true; + } - if (prompt.starts_with("dump-id-parts")) { - for (const auto val: game.state->dump_unique_id_parts().first) { - std::cout << val << ", "; - } - std::cout << std::endl; - for (const auto card: game.state->dump_unique_id_parts().second) { - std::cout << card << " "; - } - std::cout << std::endl; - continue; - } + void signal_handler(int) + { + std::cout << "Use 'quit' to exit the interactive shell." << std::endl << "> "; + } - if (prompt.starts_with("quit") or prompt == "q") { - std::cout << "Quitting." << std::endl; - clear_history(); - std::signal(SIGINT, SIG_DFL); - break; - } + void cli(Game const & game) + { + std::signal(SIGINT, signal_handler); + // Set up GNU readline + rl_attempted_completion_function = cli_command_completion; + using_history(); - if (prompt.starts_with("set-initials")) { - if (prompt.length() < 16) { - std::cout << "At least 3 initials need to be specified" << std::endl; - continue; - } - const std::string new_initials = prompt.substr(13); - for(size_t i = 0; i < std::min(size_t(6), new_initials.length()); i++) { - suit_initials[i] = new_initials[i]; - } - std::cout << "Updated initials to "; - for(const char c: suit_initials) { - std::cout << c; - } - std::cout << std::endl; - continue; - } + // Tracks the depth of the replay the user explores. We have to ensure that we don't revert too much. + unsigned depth = 0; - if (prompt.starts_with("state")) { - std::cout << *game.state << std::endl; - const std::optional prob = game.state->lookup(); - std::cout << "Winning chance: "; - print_probability(std::cout, prob) << std::endl; - continue; - } + while (true) + { + const std::string prompt = read_line_memory_safe("> "); + add_history(prompt.c_str()); - if (prompt.starts_with("evaluate")) { - std::cout << "Evaluating current game state, this might take a while." << std::endl; - game.state->evaluate_state(); - std::cout << "Evaluated state." << std::endl; - continue; - } + if (prompt.starts_with("help")) + { + std::cout << "state: print information on current game state." << std::endl; + std::cout << "clue: give a clue." << std::endl; + std::cout << "play : play specified card." << std::endl; + std::cout << "discard: discard trash from hand." << std::endl; + std::cout + << "opt: take optimal action. In case of ties, prefers plays and discards in that order." + << std::endl; + std::cout << "revert : revert specified number of turns (default 1)." << std::endl; + std::cout << "actions: display list of reasonable actions to take and their winning chances." + << std::endl; + std::cout << "evaluate: evaluate current game state recursively. Potentially runtime-expensive." + << std::endl; + std::cout << "set-initials : Set initials for the suits." << std::endl; + std::cout << "(q)uit: Quit this interactive shell." << std::endl; + std::cout << "id: display id of state. Has no inherent meaning, useful for debugging." + << std::endl; + std::cout + << "dump-id-parts: Dump parts used to calculate the id of the state as well as the cards associated to them." + << std::endl; + std::cout << "help: Display this help message." << std::endl; + continue; + } - if (prompt.starts_with("revert")) { - if (depth == 0) { - std::cout << "Cannot revert more than base state." << std::endl; - continue; - } - unsigned turns_to_revert = 1; - if(prompt.length() > 7) { - try { - turns_to_revert = std::stoi(prompt.substr(7)); - } catch(const std::invalid_argument&) { - std::cout << "Could not parse number of turns to revert." << std::endl; - continue; - } - } - if (turns_to_revert > depth) { - turns_to_revert = depth; - std::cout << "Only revererting " << depth << " turns, since this is already the base state." << std::endl; - } - std::cout << "Reverting " << turns_to_revert << " turn(s)." << std::endl; - while(turns_to_revert--) { - game.state->revert(); - depth--; - } - continue; - } - - if (prompt.starts_with("id")) { - std::cout << game.state->unique_id() << std::endl; - continue; - } - - if (prompt.starts_with("play")) { - const Card card = parse_card(prompt.substr(5,2)); - if (prompt.length() < 7) { - std::cout << "No card specified." << std::endl; - continue; - } - if (card == Cards::unknown) { - std::cout << "Could not parse card " << prompt.substr(5,2) << std::endl; - continue; - } - const hand_index_t index = game.state->find_card_in_hand(card); - if (index == hand_index_t(-1)) { - std::cout << "This card is not in the current players hand, aborting." << std::endl; - continue; - } - if (!ask_for_card_and_rotate_draw(*game.state, index, true)) { - continue; - } - game.state->play(index); - depth++; - continue; - } - - if (prompt.starts_with("discard")) { - const auto hand = game.state->cur_hand(); - hand_index_t trash_index = invalid_hand_idx; - for(hand_index_t index = 0; index < hand.size(); index++) { - if (game.state->is_trash(hand[index])) { - trash_index = index; - break; - } - } - if (trash_index == invalid_hand_idx) { - std::cout << "No trash in hand found, discarding not supported." << std::endl; - continue; - } - if (game.state->num_clues() == max_num_clues) { - std::cout << "You cannot discard at " << max_num_clues << " clues." << std::endl; - continue; - } - if (!ask_for_card_and_rotate_draw(*game.state, trash_index, false)) { - continue; - } - game.state->discard(trash_index); - depth++; - continue; - } - - if (prompt.starts_with("clue")) { - if (game.state->num_clues() == 0) { - std::cout << "You cannot give a clue at 0 clues." << std::endl; - continue; - } - game.state->give_clue(); - depth++; - continue; - } - - if (prompt.starts_with("actions")) { - auto reasonable_actions = game.state->get_reasonable_actions(); - int max_rational_digit_len = std::accumulate( - reasonable_actions.begin(), - reasonable_actions.end(), - 0, - [](int old, const std::pair>& pair){ - return std::max(old, representation_length(pair.second.value_or(0))); - } - ); - for (const auto &[action, probability] : reasonable_actions) { - std::cout.setf(std::ios_base::left, std::ios_base::adjustfield); - std::cout << std::setw(7) << action << ": "; - std::cout.setf(std::ios_base::right, std::ios_base::adjustfield); - std::cout << std::setw(max_rational_digit_len); - print_probability(std::cout, probability) << std::endl; - } - if(reasonable_actions.empty()) { - std::cout << "Game is over, no actions to take." << std::endl; - } - continue; - } - - if (prompt.starts_with("opt")) { - const auto reasonable_actions = game.state->get_reasonable_actions(); - if(reasonable_actions.empty()) { - std::cout << "Game is over, no actions to take." << std::endl; - continue; - } - Action best_action; - std::optional best_probability; - for (const auto &[action, probability] : game.state->get_reasonable_actions()) { - if (!best_probability.has_value() or (probability.has_value() and probability.value() > best_probability.value())) { - best_action = action; - best_probability = probability; - } - } - - hand_index_t index = 0; - switch(best_action.type) { - case ActionType::play: - std::cout << "Playing " << best_action.card << std::endl; - index = game.state->find_card_in_hand(best_action.card); - if(!ask_for_card_and_rotate_draw(*game.state,index,true)) { - continue; - }; - game.state->play(game.state->find_card_in_hand(best_action.card)); - break; - case ActionType::discard: - std::cout << "Discarding" << std::endl; - index = game.state->find_card_in_hand(best_action.card); - if(!ask_for_card_and_rotate_draw(*game.state, index, false)) { - continue; - }; - game.state->discard(game.state->find_card_in_hand(best_action.card)); - break; - case ActionType::clue: - std::cout << "Giving a clue" << std::endl; - game.state->give_clue(); - break; - default: - break; - } - depth++; - continue; - } - - std::cout << "Unrecognized command. Type 'help' for a list of available commands." << std::endl; + if (prompt.starts_with("dump-id-parts")) + { + for (const auto val: game.state->dump_unique_id_parts().first) + { + std::cout << val << ", "; } + std::cout << std::endl; + for (const auto card: game.state->dump_unique_id_parts().second) + { + std::cout << card << " "; + } + std::cout << std::endl; + continue; + } + + if (prompt.starts_with("quit") or prompt == "q") + { + std::cout << "Quitting." << std::endl; + clear_history(); + std::signal(SIGINT, SIG_DFL); + break; + } + + if (prompt.starts_with("set-initials")) + { + if (prompt.length() < 16) + { + std::cout << "At least 3 initials need to be specified" << std::endl; + continue; + } + const std::string new_initials = prompt.substr(13); + for (size_t i = 0; i < std::min(size_t(6), new_initials.length()); i++) + { + suit_initials[i] = new_initials[i]; + } + std::cout << "Updated initials to "; + for (const char c: suit_initials) + { + std::cout << c; + } + std::cout << std::endl; + continue; + } + + if (prompt.starts_with("state")) + { + std::cout << *game.state << std::endl; + const std::optional prob = game.state->lookup(); + std::cout << "Winning chance: "; + print_probability(std::cout, prob) << std::endl; + continue; + } + + if (prompt.starts_with("evaluate")) + { + std::cout << "Evaluating current game state, this might take a while." << std::endl; + game.state->evaluate_state(); + std::cout << "Evaluated state." << std::endl; + continue; + } + + if (prompt.starts_with("revert")) + { + if (depth == 0) + { + std::cout << "Cannot revert more than base state." << std::endl; + continue; + } + unsigned turns_to_revert = 1; + if (prompt.length() > 7) + { + try + { + turns_to_revert = std::stoi(prompt.substr(7)); + } + catch (const std::invalid_argument &) + { + std::cout << "Could not parse number of turns to revert." << std::endl; + continue; + } + } + if (turns_to_revert > depth) + { + turns_to_revert = depth; + std::cout << "Only revererting " << depth << " turns, since this is already the base state." << std::endl; + } + std::cout << "Reverting " << turns_to_revert << " turn(s)." << std::endl; + while (turns_to_revert--) + { + game.state->revert(); + depth--; + } + continue; + } + + if (prompt.starts_with("id")) + { + std::cout << game.state->unique_id() << std::endl; + continue; + } + + if (prompt.starts_with("play")) + { + const Card card = parse_card(prompt.substr(5, 2)); + if (prompt.length() < 7) + { + std::cout << "No card specified." << std::endl; + continue; + } + if (card == Cards::unknown) + { + std::cout << "Could not parse card " << prompt.substr(5, 2) << std::endl; + continue; + } + const hand_index_t index = game.state->find_card_in_hand(card); + if (index == hand_index_t(-1)) + { + std::cout << "This card is not in the current players hand, aborting." << std::endl; + continue; + } + if (!ask_for_card_and_rotate_draw(*game.state, index, true)) + { + continue; + } + game.state->play(index); + depth++; + continue; + } + + if (prompt.starts_with("discard")) + { + const auto hand = game.state->cur_hand(); + hand_index_t trash_index = invalid_hand_idx; + for (hand_index_t index = 0; index < hand.size(); index++) + { + if (game.state->is_trash(hand[index])) + { + trash_index = index; + break; + } + } + if (trash_index == invalid_hand_idx) + { + std::cout << "No trash in hand found, discarding not supported." << std::endl; + continue; + } + if (game.state->num_clues() == max_num_clues) + { + std::cout << "You cannot discard at " << max_num_clues << " clues." << std::endl; + continue; + } + if (!ask_for_card_and_rotate_draw(*game.state, trash_index, false)) + { + continue; + } + game.state->discard(trash_index); + depth++; + continue; + } + + if (prompt.starts_with("clue")) + { + if (game.state->num_clues() == 0) + { + std::cout << "You cannot give a clue at 0 clues." << std::endl; + continue; + } + game.state->give_clue(); + depth++; + continue; + } + + if (prompt.starts_with("actions")) + { + auto reasonable_actions = game.state->get_reasonable_actions(); + int max_rational_digit_len = std::accumulate( + reasonable_actions.begin(), reasonable_actions.end(), 0, []( + int old, const std::pair> & pair + ) { + return std::max(old, representation_length(pair.second.value_or(0))); + } + ); + for (const auto & [action, probability]: reasonable_actions) + { + std::cout.setf(std::ios_base::left, std::ios_base::adjustfield); + std::cout << std::setw(7) << action << ": "; + std::cout.setf(std::ios_base::right, std::ios_base::adjustfield); + std::cout << std::setw(max_rational_digit_len); + print_probability(std::cout, probability) << std::endl; + } + if (reasonable_actions.empty()) + { + std::cout << "Game is over, no actions to take." << std::endl; + } + continue; + } + + if (prompt.starts_with("opt")) + { + const auto reasonable_actions = game.state->get_reasonable_actions(); + if (reasonable_actions.empty()) + { + std::cout << "Game is over, no actions to take." << std::endl; + continue; + } + Action best_action; + std::optional best_probability; + for (const auto & [action, probability]: game.state->get_reasonable_actions()) + { + if (!best_probability.has_value() or + (probability.has_value() and probability.value() > best_probability.value())) + { + best_action = action; + best_probability = probability; + } + } + + hand_index_t index = 0; + switch (best_action.type) + { + case ActionType::play: + std::cout << "Playing " << best_action.card << std::endl; + index = game.state->find_card_in_hand(best_action.card); + if (!ask_for_card_and_rotate_draw(*game.state, index, true)) + { + continue; + }; + game.state->play(game.state->find_card_in_hand(best_action.card)); + break; + case ActionType::discard: + std::cout << "Discarding" << std::endl; + index = game.state->find_card_in_hand(best_action.card); + if (!ask_for_card_and_rotate_draw(*game.state, index, false)) + { + continue; + }; + game.state->discard(game.state->find_card_in_hand(best_action.card)); + break; + case ActionType::clue: + std::cout << "Giving a clue" << std::endl; + game.state->give_clue(); + break; + default: + break; + } + depth++; + continue; + } + + std::cout << "Unrecognized command. Type 'help' for a list of available commands." << std::endl; } + } } \ No newline at end of file diff --git a/src/test.cpp b/src/test.cpp index 175e2df..2893c46 100644 --- a/src/test.cpp +++ b/src/test.cpp @@ -1,15 +1,18 @@ #include "download.h" #include "game_state.h" -void test() { +void test() +{ + { + auto game = Download::get_game("in/1005195", 43); + auto res = game->evaluate_state(); + if (!(res == Hanabi::probability_t(7, 8))) { - auto game = Download::get_game("in/1005195", 43); - auto res = game->evaluate_state(); - if (!(res == Hanabi::probability_t(7, 8))) { - std::cerr << "Test " << ("1005195") << " failed." << std::endl; - } - else { - std::cout << "Test " << ("1005195") << " succeeded." << std::endl; - }; + std::cerr << "Test " << ("1005195") << " failed." << std::endl; } + else + { + std::cout << "Test " << ("1005195") << " succeeded." << std::endl; + }; + } }