reformat code

This commit is contained in:
Maximilian Keßler 2023-11-16 16:20:04 +01:00
parent 43b4bec7c6
commit ea881c5e6a
Signed by: max
GPG key ID: BCC5A619923C0BA5
20 changed files with 2076 additions and 1644 deletions

View file

@ -6,13 +6,14 @@
#include "hanabi_types.hpp" #include "hanabi_types.hpp"
namespace Hanabi { namespace Hanabi
enum class GameStateSpecType { {
turn = 0, enum class GameStateSpecType
draw_pile_size = 1, {
}; turn = 0, draw_pile_size = 1, };
struct CLIParms { struct CLIParms
{
/** /**
* The data source for the game to be analysed. * The data source for the game to be analysed.
* If of type int, assumed to be a game ID from hanab.live * 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 * see https://raw.githubusercontent.com/Hanabi-Live/hanabi-live/main/misc/example_game_with_comments.jsonc
* for a format specification. * for a format specification.
*/ */
std::variant<int, std::string> game {}; std::variant<int, std::string> game{};
/** /**
* Definition of a 'winning' game, i.e. what score (number of cards played) is considered * Definition of a 'winning' game, i.e. what score (number of cards played) is considered
* to be winning. * to be winning.
* If std::nullopt, then the maximum score of the game will be used. * If std::nullopt, then the maximum score of the game will be used.
*/ */
boost::optional<uint8_t> score_goal {}; boost::optional<uint8_t> score_goal{};
/** /**
* Whether game_state_spec denotes a turn number or draw pile size. * 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. * 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) * (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. * 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. * Whether to launch an interactive exploration shell for the game state after performing analysis.
*/ */
boost::optional<bool> interactive {}; boost::optional<bool> interactive{};
/** /**
* If true, deactivates non-essential output (to cout). * 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. * If this holds std::monostate, then all clue numbers are evaluated.
* Otherwise, the specified clue modifier is applied (relative to actual number of clues). * Otherwise, the specified clue modifier is applied (relative to actual number of clues).
* Thus, setting this to 0 has no effect. * Thus, setting this to 0 has no effect.
*/ */
std::variant<std::monostate, clue_t> clue_spec {static_cast<clue_t>(0)}; std::variant<std::monostate, clue_t> clue_spec{static_cast<clue_t>(0)};
/** /**
* If true, then all states corresponding to smaller draw pile sizes * 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. * @brief Get an output stream that is std::cout or a Null-Stream.
* @param quiet If true, NullStream is returned, otherwise std::cout * @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 download_failed = 1;
constexpr int state_unreachable = 2; constexpr int state_unreachable = 2;
@ -88,6 +89,6 @@ namespace Hanabi {
/** /**
* @brief Execute parsed parameters. * @brief Execute parsed parameters.
*/ */
int run_cli(CLIParms const & parms); int run_cli(CLIParms const &parms);
} }
#endif //DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H #endif //DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H

View file

@ -9,25 +9,26 @@
#include "game_interface.h" #include "game_interface.h"
namespace Download { namespace Download
{
std::optional<boost::json::object> download_game_json(int game_id); std::optional<boost::json::object> download_game_json(int game_id);
std::optional<boost::json::object> open_game_json(const char *filename); std::optional<boost::json::object> open_game_json(char const *filename);
/** /**
* @brief Create game object from given source * @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 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. * @param score_goal What score counts as a win for this game. If left empty, the maximum score is inserted.
* @return Game state * @return Game state
* *
* If both turn and draw_pile_break are specified, the game skips until the specified turn or the first time the * 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 * draw pile hits the given size, whichever comes first
* *
*/ */
Hanabi::Game get_game(int game_id, std::optional<uint8_t> score_goal); Hanabi::Game get_game(int game_id, std::optional<uint8_t> score_goal);
Hanabi::Game get_game(const std::string& filename, std::optional<uint8_t> score_goal); Hanabi::Game get_game(std::string const & filename, std::optional<uint8_t> score_goal);
} // namespace Download } // namespace Download

View file

@ -21,53 +21,73 @@ namespace Hanabi
bool operator==(const CardMultiplicity &) const = default; bool operator==(const CardMultiplicity &) const = default;
}; };
class HanabiStateIF { class HanabiStateIF
{
public: public:
virtual void give_clue() = 0; virtual void give_clue() = 0;
virtual void discard(hand_index_t index) = 0; virtual void discard(hand_index_t index) = 0;
virtual void play(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 ActionType last_action_type() const = 0;
virtual void revert() = 0; virtual void revert() = 0;
virtual void modify_clues(clue_t change) = 0; virtual void modify_clues(clue_t change) = 0;
virtual void set_clues(clue_t clues) = 0; virtual void set_clues(clue_t clues) = 0;
[[nodiscard]] virtual player_t turn() const = 0; [[nodiscard]] virtual player_t turn() const = 0;
[[nodiscard]] virtual clue_t num_clues() const = 0; [[nodiscard]] virtual clue_t num_clues() const = 0;
[[nodiscard]] virtual unsigned score() const = 0; [[nodiscard]] virtual unsigned score() const = 0;
[[nodiscard]] virtual std::vector<std::vector<Card>> hands() const = 0; [[nodiscard]] virtual std::vector<std::vector<Card>> hands() const = 0;
[[nodiscard]] virtual std::vector<Card> cur_hand() const = 0; [[nodiscard]] virtual std::vector<Card> cur_hand() const = 0;
[[nodiscard]] virtual size_t draw_pile_size() 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_trash(const Card & card) const = 0;
[[nodiscard]] virtual bool is_playable(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 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 std::uint64_t enumerated_states() const = 0;
[[nodiscard]] virtual const std::unordered_map<unsigned long, probability_t>& position_tablebase() const = 0;
[[nodiscard]] virtual const std::unordered_map<unsigned long, probability_t> & position_tablebase() const = 0;
virtual void init_backtracking_information() = 0; virtual void init_backtracking_information() = 0;
virtual probability_t evaluate_state() = 0; virtual probability_t evaluate_state() = 0;
[[nodiscard]] virtual std::optional<probability_t> lookup() const = 0; [[nodiscard]] virtual std::optional<probability_t> lookup() const = 0;
[[nodiscard]] virtual std::uint64_t unique_id() const = 0; [[nodiscard]] virtual std::uint64_t unique_id() const = 0;
[[nodiscard]] virtual std::pair<std::vector<std::uint64_t>, std::vector<Card>> dump_unique_id_parts() const = 0; [[nodiscard]] virtual std::pair<std::vector<std::uint64_t>, std::vector<Card>> dump_unique_id_parts() const = 0;
virtual std::vector<std::pair<Action, std::optional<probability_t>>> get_reasonable_actions() = 0; virtual std::vector<std::pair<Action, std::optional<probability_t>>> get_reasonable_actions() = 0;
virtual std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>> possible_next_states(hand_index_t index, bool play) = 0;
virtual std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>>
possible_next_states(hand_index_t index, bool play) = 0;
virtual ~HanabiStateIF() = default; virtual ~HanabiStateIF() = default;
protected: 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 struct GameInfo
{ {
@ -77,16 +97,19 @@ namespace Hanabi
Hanabi::player_t num_players; Hanabi::player_t num_players;
}; };
struct Game : private GameInfo { struct Game : private GameInfo
{
public: public:
Game(std::unique_ptr<HanabiStateIF> state, GameInfo game_info); Game(std::unique_ptr<HanabiStateIF> state, GameInfo game_info);
[[nodiscard]] unsigned cur_turn() const; [[nodiscard]] unsigned cur_turn() const;
void make_turn(); void make_turn();
void revert_turn(); void revert_turn();
bool goto_draw_pile_size(size_t draw_pile_break); bool goto_draw_pile_size(size_t draw_pile_break);
bool goto_turn(size_t turn); bool goto_turn(size_t turn);
[[nodiscard]] bool holds_state() const; [[nodiscard]] bool holds_state() const;

View file

@ -18,158 +18,190 @@
#include "game_interface.h" #include "game_interface.h"
namespace Hanabi { namespace Hanabi
{
template<size_t num_suits> template<size_t num_suits>
using Stacks = std::array<rank_t, num_suits>; using Stacks = std::array<rank_t, num_suits>;
template<size_t num_suits> template<size_t num_suits>
std::ostream &operator<<(std::ostream &os, const Stacks<num_suits> &stacks); std::ostream & operator<<(std::ostream & os, const Stacks<num_suits> & stacks);
template<typename T> template<typename T>
struct InnerCardArray { struct InnerCardArray
{
template<size_t N> template<size_t N>
using array_t = std::array<T, N>; using array_t = std::array<T, N>;
}; };
template<> template<>
struct InnerCardArray<bool> { struct InnerCardArray<bool>
{
template<size_t N> template<size_t N>
using array_t = std::bitset<N>; using array_t = std::bitset<N>;
}; };
template <suit_t num_suits, typename T> struct CardArray { template<suit_t num_suits, typename T>
struct CardArray
{
using value_type = T; using value_type = T;
CardArray() = default; CardArray() = default;
explicit CardArray(value_type default_val); explicit CardArray(value_type default_val);
void fill(value_type 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; auto operator<=>(const CardArray &) const = default;
private: private:
using inner_array_t = typename InnerCardArray<T>::template array_t<starting_card_rank>; using inner_array_t = typename InnerCardArray<T>::template array_t<starting_card_rank>;
std::array<inner_array_t , num_suits> _array {}; std::array<inner_array_t, num_suits> _array{};
}; };
// A game mimics a game state together with a list of actions and allows to traverse the game // 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. // history by making and reverting the stored actions.
template <suit_t num_suits, player_t num_players, hand_index_t hand_size> template<suit_t num_suits, player_t num_players, hand_index_t hand_size>
class HanabiState : public HanabiStateIF { class HanabiState : public HanabiStateIF
public: {
public:
HanabiState() = default; HanabiState() = default;
explicit HanabiState(const std::vector<Card>& deck, uint8_t score_goal = 5 * num_suits);
explicit HanabiState(const std::vector<Card> & deck, uint8_t score_goal = 5 * num_suits);
void give_clue() final; void give_clue() final;
void discard(hand_index_t index) final; void discard(hand_index_t index) final;
void play(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; ActionType last_action_type() const final;
void revert() final; void revert() final;
void modify_clues(clue_t change) final; void modify_clues(clue_t change) final;
void set_clues(clue_t clues) final; void set_clues(clue_t clues) final;
[[nodiscard]] player_t turn() const final; [[nodiscard]] player_t turn() const final;
[[nodiscard]] clue_t num_clues() const final; [[nodiscard]] clue_t num_clues() const final;
[[nodiscard]] unsigned score() const final; [[nodiscard]] unsigned score() const final;
[[nodiscard]] std::vector<std::vector<Card>> hands() const final; [[nodiscard]] std::vector<std::vector<Card>> hands() const final;
[[nodiscard]] std::vector<Card> cur_hand() const final; [[nodiscard]] std::vector<Card> cur_hand() const final;
[[nodiscard]] size_t draw_pile_size() const final; [[nodiscard]] size_t draw_pile_size() const final;
[[nodiscard]] hand_index_t find_card_in_hand(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_trash(const Card & card) const final;
[[nodiscard]] bool is_playable(const Card & card) const final;
[[nodiscard]] bool is_relative_state_initialized() const final; [[nodiscard]] bool is_relative_state_initialized() const final;
[[nodiscard]] std::uint64_t enumerated_states() const final; [[nodiscard]] std::uint64_t enumerated_states() const final;
[[nodiscard]] const std::unordered_map<unsigned long, probability_t>& position_tablebase() const final;
[[nodiscard]] const std::unordered_map<unsigned long, probability_t> & position_tablebase() const final;
void init_backtracking_information() final; void init_backtracking_information() final;
probability_t evaluate_state() final; probability_t evaluate_state() final;
[[nodiscard]] std::optional<probability_t> lookup() const final; [[nodiscard]] std::optional<probability_t> lookup() const final;
[[nodiscard]] std::uint64_t unique_id() const final; [[nodiscard]] std::uint64_t unique_id() const final;
[[nodiscard]] std::pair<std::vector<std::uint64_t>, std::vector<Card>> dump_unique_id_parts() const final; [[nodiscard]] std::pair<std::vector<std::uint64_t>, std::vector<Card>> dump_unique_id_parts() const final;
std::vector<std::pair<Action, std::optional<probability_t>>> get_reasonable_actions() final; std::vector<std::pair<Action, std::optional<probability_t>>> get_reasonable_actions() final;
std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>> possible_next_states(hand_index_t index, bool play) final;
std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>>
possible_next_states(hand_index_t index, bool play) final;
auto operator<=>(const HanabiState &) const = default; auto operator<=>(const HanabiState &) const = default;
protected: protected:
void print(std::ostream& os) const final; void print(std::ostream & os) const final;
private: private:
struct BacktrackAction { struct BacktrackAction
explicit BacktrackAction( {
ActionType action_type, explicit BacktrackAction(
Card discarded_or_played = Cards::unknown, ActionType action_type
hand_index_t index = 0, , Card discarded_or_played = Cards::unknown
bool was_on_8_clues = false, , hand_index_t index = 0
bool strike = false , bool was_on_8_clues = false
); , bool strike = false
);
ActionType action_type{}; ActionType action_type{};
// The card that was discarded or played // The card that was discarded or played
Card discarded{}; Card discarded{};
// Index of card in hand that was discarded or played // Index of card in hand that was discarded or played
hand_index_t index{}; hand_index_t index{};
// Indicates whether before the action was taken, we had 8 clues. // 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 // 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}; bool was_on_8_clues{false};
// Indicates whether playing this card triggered a bomb. // Indicates whether playing this card triggered a bomb.
// This cannot be deduced just from the stacks since we cannot differentiate between a card // 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. // having been played correctly or the top card of the draw pile being bombed.
bool strike {false}; bool strike{false};
}; };
// This keeps track of the representation of the gamestate relative to some starting state // This keeps track of the representation of the gamestate relative to some starting state
// and is used for id calculation // and is used for id calculation
struct RelativeRepresentationData { struct RelativeRepresentationData
static constexpr player_t draw_pile = num_players; {
static constexpr player_t discard_pile = num_players + 1; static constexpr player_t draw_pile = num_players;
static constexpr player_t play_stack = num_players + 2; static constexpr player_t discard_pile = num_players + 1;
enum CardPosition : uint8_t { static constexpr player_t play_stack = num_players + 2;
hand = 0, enum CardPosition : uint8_t
played = 1, {
discarded = 2 hand = 0, played = 1, discarded = 2
}; };
// List of unique non-trash cards in draw pile // List of unique non-trash cards in draw pile
boost::container::static_vector<Card, 30> good_cards_draw; boost::container::static_vector<Card, 30> good_cards_draw;
// Card positions of these cards. Indexes correspond to the cards stored in _good_cards_draw vector // Card positions of these cards. Indexes correspond to the cards stored in _good_cards_draw vector
boost::container::static_vector<boost::container::static_vector<player_t, max_card_duplicity>, 30> card_positions_draw; boost::container::static_vector<boost::container::static_vector<player_t, max_card_duplicity>
, 30> card_positions_draw;
// This will indicate whether cards that were in hands initially still are in hand // 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 // The first n bits are used and cards are assumed to have been marked with their indices in this bitset
boost::container::static_vector<CardPosition, num_players * hand_size> card_positions_hands {}; boost::container::static_vector<CardPosition, num_players * hand_size> card_positions_hands{};
// Note this is not the same as _good_cards_draw.size(), since this accounts for multiplicities // Note this is not the same as _good_cards_draw.size(), since this accounts for multiplicities
std::uint8_t initial_draw_pile_size { 0 }; std::uint8_t initial_draw_pile_size{0};
// Whether we initialized the values above and marked cards accordingly // Whether we initialized the values above and marked cards accordingly
bool initialized { false }; bool initialized{false};
}; };
unsigned long discard_and_potentially_update(hand_index_t index, bool cycle = 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 long play_and_potentially_update(hand_index_t index, bool cycle = false);
unsigned draw(hand_index_t index, bool cycle = false, bool played = true); 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_draw(hand_index_t index, Card discarded_card, bool cycle = false, bool played = true);
void revert_clue(); void revert_clue();
void revert_discard(bool cycle = false); void revert_discard(bool cycle = false);
void revert_play(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 do_for_each_potential_draw(hand_index_t index, bool play, Function f);
void incr_turn(); void incr_turn();
void decr_turn(); void decr_turn();
void check_draw_pile_integrity() const; void check_draw_pile_integrity() const;
@ -208,19 +241,24 @@ private:
// Lookup table for states. Uses the ids calculated using the relative representation // Lookup table for states. Uses the ids calculated using the relative representation
std::unordered_map<unsigned long, probability_t> _position_tablebase; std::unordered_map<unsigned long, probability_t> _position_tablebase;
std::uint64_t _enumerated_states {}; std::uint64_t _enumerated_states{};
}; };
template <std::size_t num_suits, player_t num_players, std::size_t hand_size> template<std::size_t num_suits, player_t num_players, std::size_t hand_size>
bool same_up_to_discard_permutation(HanabiState<num_suits, num_players, hand_size> state1, HanabiState<num_suits, num_players, hand_size> state2) { bool same_up_to_discard_permutation(
auto comp = [](CardMultiplicity &m1, CardMultiplicity &m2) -> bool { HanabiState<num_suits, num_players, hand_size> state1, HanabiState<num_suits
return m1.card.suit < m2.card.suit || (m1.card.suit == m2.card.suit and m1.card.rank < m2.card.rank) || , num_players
(m1.card.suit == m2.card.suit and m1.card.rank == m2.card.rank and m1.multiplicity < m2.multiplicity); , hand_size> 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); state1._draw_pile.sort(comp);
state2._draw_pile.sort(comp); state2._draw_pile.sort(comp);
return state1 == state2; return state1 == state2;
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,8 @@
#include <boost/rational.hpp> #include <boost/rational.hpp>
namespace Hanabi { namespace Hanabi
{
using rank_t = std::uint8_t; using rank_t = std::uint8_t;
using suit_t = std::uint8_t; using suit_t = std::uint8_t;
@ -32,11 +33,12 @@ namespace Hanabi {
using probability_t = double; using probability_t = double;
#endif #endif
std::ostream& print_probability(std::ostream& os, const rational_probability& prob); 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, double prob);
template<typename T> template<typename T>
std::ostream& print_probability(std::ostream& os, const std::optional<T>& prob); std::ostream & print_probability(std::ostream & os, const std::optional<T> & prob);
/** /**
* We will generally assume that stacks are played from n to 0 * 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 // Note that this is therefore not static so that we have external linking
inline std::array<char, 6> suit_initials = {'r', 'y', 'g', 'b', 'p', 't'}; inline std::array<char, 6> suit_initials = {'r', 'y', 'g', 'b', 'p', 't'};
struct Card { struct Card
{
suit_t suit; suit_t suit;
rank_t rank; rank_t rank;
@ -71,32 +74,37 @@ namespace Hanabi {
* @brief Compares cards *only* regarding suit and rank. * @brief Compares cards *only* regarding suit and rank.
* This is inlined as this is a runtime critical function when backtracking. * 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 { enum class ActionType : std::uint8_t
play = 0, {
discard = 1, play = 0
clue = 2, , discard = 1
color_clue = 2, , clue = 2
rank_clue = 3, , color_clue = 2
end_game = 4, , rank_clue = 3
vote_terminate_players = 5, , end_game = 4
vote_terminate = 10, , vote_terminate_players = 5
}; , vote_terminate = 10
, };
struct Action { struct Action
ActionType type {}; {
Card card {}; ActionType type{};
Card card{};
}; };
// Output utilities for Cards and Actions // Output utilities for Cards and Actions
std::string to_string(const Card &card); 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::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 r0 = {0, 5};
static constexpr Card r1 = {0, 4}; static constexpr Card r1 = {0, 4};
static constexpr Card r2 = {0, 3}; static constexpr Card r2 = {0, 3};
@ -139,15 +147,20 @@ namespace Hanabi {
//// INLINE SECTION //// INLINE SECTION
bool Card::operator==(const Card &other) const { bool Card::operator==(const Card & other) const
{
return suit == other.suit and rank == other.rank; return suit == other.suit and rank == other.rank;
} }
template<typename T> template<typename T>
std::ostream& print_probability(std::ostream& os, const std::optional<T>& prob) { std::ostream & print_probability(std::ostream & os, const std::optional<T> & prob)
if (prob.has_value()) { {
if (prob.has_value())
{
return print_probability(os, prob.value()); return print_probability(os, prob.value());
} else { }
else
{
os << "unknown"; os << "unknown";
} }
return os; return os;

View file

@ -4,7 +4,9 @@
#ifdef NDEBUG #ifdef NDEBUG
#define ASSERT(x) do { (void)sizeof(x);} while (0) #define ASSERT(x) do { (void)sizeof(x);} while (0)
#else #else
#include <assert.h> #include <assert.h>
#define ASSERT(x) assert(x) #define ASSERT(x) assert(x)
#endif #endif

View file

@ -3,21 +3,27 @@
#include <iosfwd> #include <iosfwd>
namespace NullBuffer { namespace NullBuffer
{
class NullBuffer final : public std::streambuf { class NullBuffer final : public std::streambuf
public: {
int overflow(int c) override { return c; } public:
}; int overflow(int c) override
{ return c; }
};
class NullStream final : public std::ostream { class NullStream final : public std::ostream
public: {
NullStream() : std::ostream(&_m_sb) {} public:
private: NullStream() : std::ostream(&_m_sb)
NullBuffer _m_sb; {}
};
NullStream null_stream; private:
NullBuffer _m_sb;
};
NullStream null_stream;
} }

View file

@ -5,22 +5,26 @@
#include "game_interface.h" #include "game_interface.h"
namespace Hanabi { namespace Hanabi
{
// These are overloads that the boost/json library uses for parsing. // These are overloads that the boost/json library uses for parsing.
// They convert a Card from/to json. // They convert a Card from/to json.
// This has to be in the same namespace as Hanabi::Card. // This has to be in the same namespace as Hanabi::Card.
Card tag_invoke(boost::json::value_to_tag<Card>, boost::json::value const &jv); Card tag_invoke(boost::json::value_to_tag<Card>, boost::json::value const & jv);
void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, Hanabi::Card const &card);
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. * Represents a single action (turn) in a Hanab game.
* Note that this is slightly differen than Hanabi::Action, * Note that this is slightly differen than Hanabi::Action,
* since this uses indices for specifying the discarded/played cards. * 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. * We only want to work with this type while parsing, converting to Hanabi::Action after.
*/ */
struct HanabLiveAction { struct HanabLiveAction
{
Hanabi::ActionType type{}; Hanabi::ActionType type{};
/** /**
* In case the action is of type discard or play, * In case the action is of type discard or play,
@ -30,28 +34,26 @@ namespace Parsing {
}; };
// Overload for parsing from json to HanabLiveAction // Overload for parsing from json to HanabLiveAction
HanabLiveAction tag_invoke(boost::json::value_to_tag<HanabLiveAction>, boost::json::value const &jv); HanabLiveAction tag_invoke(boost::json::value_to_tag<HanabLiveAction>, boost::json::value const & jv);
/* /*
* @brief Parse deck from hanab.live format * @brief Parse deck from hanab.live format
* @return List of cards (in order) and number of suits * @return List of cards (in order) and number of suits
*/ */
std::pair<std::vector<Hanabi::Card>, Hanabi::suit_t> parse_deck(const boost::json::value &deck_json); std::pair<std::vector<Hanabi::Card>, Hanabi::suit_t> parse_deck(const boost::json::value & deck_json);
/** /**
* @brief Parse actions from hanab.live format. * @brief Parse actions from hanab.live format.
* @return List of actions * @return List of actions
*/ */
std::vector<HanabLiveAction> parse_actions(const boost::json::value &action_json); std::vector<HanabLiveAction> parse_actions(const boost::json::value & action_json);
std::vector<Hanabi::Action> convert_actions( std::vector<Hanabi::Action> convert_actions(
std::vector<HanabLiveAction> const & hanab_live_actions, std::vector<HanabLiveAction> const & hanab_live_actions, std::vector<Hanabi::Card> const & deck
std::vector<Hanabi::Card> const & deck
); );
Hanabi::GameInfo parse_game(boost::json::object const & game_json); Hanabi::GameInfo parse_game(boost::json::object const & game_json);
} }
#endif //DYNAMIC_PROGRAM_PARSE_GAME_H #endif //DYNAMIC_PROGRAM_PARSE_GAME_H

View file

@ -4,8 +4,9 @@
#include <memory> #include <memory>
#include "game_interface.h" #include "game_interface.h"
namespace Hanabi { namespace Hanabi
void cli(Game const & game); {
void cli(Game const & game);
} }

View file

@ -2,49 +2,59 @@
#include "download.h" #include "download.h"
void check_games(unsigned num_players, unsigned max_draw_pile_size, unsigned first_game = 0, unsigned last_game = 9999) { void check_games(unsigned num_players, unsigned max_draw_pile_size, unsigned first_game = 0, unsigned last_game = 9999)
std::vector<std::vector<Hanabi::probability_t>> winning_percentages(last_game + 2); {
std::vector<std::vector<Hanabi::probability_t>> winning_percentages(last_game + 2);
for(size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) { 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"; Hanabi::probability_t total_chance = 0;
std::ofstream file (output_fname); const std::string output_fname =
for(size_t game_id = first_game; game_id <= last_game; game_id++) { "games_" + std::to_string(num_players) + "p_draw_size_" + std::to_string(draw_pile_size) + ".txt";
const std::string input_fname = "json/" + std::to_string(num_players) + "p/" + std::to_string(game_id) + ".json"; std::ofstream file(output_fname);
auto game = Download::get_game(input_fname.c_str(), 50, draw_pile_size); for (size_t game_id = first_game; game_id <= last_game; game_id++)
const Hanabi::probability_t chance = game->evaluate_state(); {
winning_percentages[game_id].push_back(chance); const std::string input_fname = "json/" + std::to_string(num_players) + "p/" + std::to_string(game_id) + ".json";
if(chance != 1) { auto game = Download::get_game(input_fname.c_str(), 50, draw_pile_size);
file << "Game " << game_id << ": " << chance << std::endl; const Hanabi::probability_t chance = game->evaluate_state();
file << *game << std::endl << std::endl; winning_percentages[game_id].push_back(chance);
} if (chance != 1)
std::cout << "Finished game " << game_id << " with draw pile size " << draw_pile_size << ": " << chance << std::endl; {
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; 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();
} }
const std::string results_file_name {"results_" + std::to_string(num_players) + "p.txt"}; const Hanabi::probability_t total_average = total_chance / (last_game - first_game + 1);
std::ofstream results_file (results_file_name); winning_percentages.back().push_back(total_average);
results_file << "game_id, "; file << "Total chance found over " << last_game - first_game + 1 << " many games: " << total_average << std::endl;
for(size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) { file.close();
results_file << std::to_string(draw_pile_size) << ", "; }
} const std::string results_file_name{"results_" + std::to_string(num_players) + "p.txt"};
results_file << "\n"; std::ofstream results_file(results_file_name);
for(size_t game_id = first_game; game_id <= last_game; game_id++) { results_file << "game_id, ";
results_file << game_id << ", "; for (size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++)
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::to_string(draw_pile_size) << ", ";
} }
results_file << std::endl; results_file << "\n";
} for (size_t game_id = first_game; game_id <= last_game; game_id++)
results_file << "total, "; {
for(size_t draw_pile_size = 0; draw_pile_size <= max_draw_pile_size; draw_pile_size++) { results_file << game_id << ", ";
results_file << winning_percentages.back()[draw_pile_size] << ", "; 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 << 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();
} }

View file

@ -8,7 +8,8 @@
namespace bpo = boost::program_options; namespace bpo = boost::program_options;
namespace Hanabi { namespace Hanabi
{
template<class T> template<class T>
std::optional<T> convert_optional(boost::optional<T> val) std::optional<T> convert_optional(boost::optional<T> val)
@ -17,10 +18,10 @@ namespace Hanabi {
{ {
return std::nullopt; 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) if (quiet)
{ {
@ -50,7 +51,7 @@ namespace Hanabi {
quiet_os.precision(10); quiet_os.precision(10);
// Load game, either from file or from hanab.live // Load game, either from file or from hanab.live
Game game = [&parms]{ Game game = [&parms] {
if (std::holds_alternative<int>(parms.game)) if (std::holds_alternative<int>(parms.game))
{ {
return Download::get_game(std::get<int>(parms.game), convert_optional(parms.score_goal)); return Download::get_game(std::get<int>(parms.game), convert_optional(parms.score_goal));
@ -63,9 +64,12 @@ namespace Hanabi {
if (not game.holds_state()) if (not game.holds_state())
{ {
if(std::holds_alternative<int>(parms.game)) { if (std::holds_alternative<int>(parms.game))
{
std::cout << "Failed to download game " << std::get<int>(parms.game) << " from hanab.live." << std::endl; std::cout << "Failed to download game " << std::get<int>(parms.game) << " from hanab.live." << std::endl;
} else { }
else
{
std::cout << "Failed to open file " << std::get<std::string>(parms.game) << "." << std::endl; std::cout << "Failed to open file " << std::get<std::string>(parms.game) << "." << std::endl;
} }
return download_failed; return download_failed;
@ -102,7 +106,8 @@ namespace Hanabi {
throw std::logic_error("Invalid game state specification type encountered"); 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 << 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; return state_unreachable;
} }
@ -125,13 +130,16 @@ namespace Hanabi {
// (except for rare cases, where there is a forced win that does not need stalling). // (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(); size_t const max_draw_pile_size = game.state->draw_pile_size();
bool printed_replay_end_msg = false; 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 (!game.goto_draw_pile_size(remaining_cards))
{ {
if (not printed_replay_end_msg) 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 << "Draw pile size of " << game.state->draw_pile_size() - 1
std::cout << "Replay ends at turn " << game.cur_turn() << " with score of " << game.state->score() << "." << std::endl; << " 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; printed_replay_end_msg = true;
} }
continue; continue;
@ -142,21 +150,25 @@ namespace Hanabi {
// When modifying the game state, we want to reset to the actual number of clues // When modifying the game state, we want to reset to the actual number of clues
// to ensure that actions taken are legal. // to ensure that actions taken are legal.
clue_t const original_num_clues = game.state->num_clues(); 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); game.state->set_clues(num_clues);
probability_t const result = game.state->evaluate_state(); probability_t const result = game.state->evaluate_state();
std::cout << "Probability with " << remaining_cards << " cards left in deck and " << +num_clues std::cout << "Probability with " << remaining_cards << " cards left in deck and " << +num_clues
<< " clues (" << std::showpos << +(num_clues - original_num_clues) << "): " << std::noshowpos; << " clues (" << std::showpos << +(num_clues - original_num_clues) << "): " << std::noshowpos;
print_probability(std::cout, result) << std::endl; print_probability(std::cout, result) << std::endl;
std::cout << *game.state << std::endl; std::cout << *game.state << std::endl;
auto const [a,b] = game.state->dump_unique_id_parts(); auto const [a, b] = game.state->dump_unique_id_parts();
for (auto elem : a) { for (auto elem: a)
{
std::cout << elem << ", "; std::cout << elem << ", ";
} }
std::cout << "-> " << game.state->unique_id() << std::endl; std::cout << "-> " << game.state->unique_id() << std::endl;
} }
game.state->set_clues(original_num_clues); game.state->set_clues(original_num_clues);
} else { }
else
{
probability_t const result = game.state->evaluate_state(); probability_t const result = game.state->evaluate_state();
std::cout << "Probability with " << remaining_cards << " cards left in deck: "; std::cout << "Probability with " << remaining_cards << " cards left in deck: ";
print_probability(std::cout, result) << std::endl; print_probability(std::cout, result) << std::endl;
@ -194,33 +206,35 @@ namespace Hanabi {
CLIParms parms; CLIParms parms;
bpo::options_description desc("Allowed program options"); bpo::options_description desc("Allowed program options");
desc.add_options() desc.add_options()
("help,h", "Print this help message.") ("help,h", "Print this help message.")
("game,g", bpo::value<int>(), "Game ID from hanab.live.") ("game,g", bpo::value<int>(), "Game ID from hanab.live.")
("file,f", bpo::value<std::string>(), "Input file containing game in hanab.live json format.") ("file,f", bpo::value<std::string>(), "Input file containing game in hanab.live json format.")
("turn,t", bpo::value<unsigned>(&parms.game_state_spec), "Turn number of state to analyze. " ("turn,t", bpo::value<unsigned>(&parms.game_state_spec), "Turn number of state to analyze. "
"Turn 1 means no actions have been taken. ") "Turn 1 means no actions have been taken. ")
("draw-pile-size,d", bpo::value<unsigned>(&parms.game_state_spec), "Draw pile size of state to analyze.") ("draw-pile-size,d", bpo::value<unsigned>(&parms.game_state_spec), "Draw pile size of state to analyze.")
("score-goal,s", bpo::value<boost::optional<uint8_t>>(&parms.score_goal), ("score-goal,s"
"Score that counts as a win, i.e. is optimized for achieving. If unspecified, the maximum possible " , bpo::value<boost::optional<uint8_t>>(&parms.score_goal)
"score will be used.") , "Score that counts as a win, i.e. is optimized for achieving. If unspecified, the maximum possible "
("clue-modifier,c", bpo::value<int>(), "Optional relative modification to the number of clues applied to " "score will be used.")
"selected game state. If unspecified, has value 0 and thus no effect.") ("clue-modifier,c", bpo::value<int>(), "Optional relative modification to the number of clues applied to "
("interactive,i", bpo::value<boost::optional<bool>>(&parms.interactive), "selected game state. If unspecified, has value 0 and thus no effect.")
"After computation, drop into interactive shell to explore game. If unspecified, a reasonable default " ("interactive,i"
"is chosen depending on other options.") , bpo::value<boost::optional<bool>>(&parms.interactive)
("recursive,r", "If specified, also analyzes all game states with fewer cards in the draw pile than the " , "After computation, drop into interactive shell to explore game. If unspecified, a reasonable default "
"specified one, considering smaller draw pile sizes first. Also useful for infinite analysis " "is chosen depending on other options.")
"(specifying this with a complex base state") ("recursive,r", "If specified, also analyzes all game states with fewer cards in the draw pile than the "
("all-clues", "Whenever evaluating a game state, evaluate it with all clue counts and output their " "specified one, considering smaller draw pile sizes first. Also useful for infinite analysis "
"probabilities.") "(specifying this with a complex base state")
("quiet,q", "Deactivate all non-essential prints. Useful if output is parsed by another program.") ("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::variables_map vm;
bpo::store(bpo::parse_command_line(argc, argv, desc), vm); bpo::store(bpo::parse_command_line(argc, argv, desc), vm);
bpo::notify(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" 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" "(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" "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 // 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 << "Exactly one option of 'file' and 'id' has to be given." << std::endl;
std::cout << "Use '--help' to print a help message." << std::endl; std::cout << "Use '--help' to print a help message." << std::endl;
return std::nullopt; return std::nullopt;
@ -253,7 +268,8 @@ namespace Hanabi {
} }
// Parse game state options (turn or draw), ensuring at most one is given. // 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 << "Conflicting options --draw and --turn." << std::endl;
std::cout << "Use '--help' to print a help message." << std::endl; std::cout << "Use '--help' to print a help message." << std::endl;
return std::nullopt; return std::nullopt;

View file

@ -7,30 +7,36 @@
#include "download.h" #include "download.h"
namespace Download { namespace Download
{
std::optional<boost::json::object> download_game_json(int game_id) { std::optional<boost::json::object> 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)); std::string request_str = "https://hanab.live/export/" + std::to_string(game_id);
if (r.header["content-type"] != "application/json; charset=utf-8") { cpr::Response r = cpr::Get(cpr::Url(request_str));
return std::nullopt; if (r.header["content-type"] != "application/json; charset=utf-8")
} {
return boost::json::parse(r.text).as_object(); return std::nullopt;
} }
return boost::json::parse(r.text).as_object();
}
std::optional<boost::json::object> open_game_json(const char *filename) { std::optional<boost::json::object> open_game_json(const char *filename)
std::ifstream file(filename); {
if (!file.is_open()) { std::ifstream file(filename);
return std::nullopt; if (!file.is_open())
} {
std::string game_json((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); return std::nullopt;
return boost::json::parse(game_json).as_object();
} }
std::string game_json((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
return boost::json::parse(game_json).as_object();
}
Hanabi::Game get_game(int game_id, std::optional<uint8_t> score_goal) Hanabi::Game get_game(int game_id, std::optional<uint8_t> score_goal)
{ {
std::optional<boost::json::object> const game_json = download_game_json(game_id); std::optional<boost::json::object> 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, {}}; return {nullptr, {}};
} }
@ -41,7 +47,8 @@ namespace Download {
Hanabi::Game get_game(std::string const & filename, std::optional<uint8_t> score_goal) Hanabi::Game get_game(std::string const & filename, std::optional<uint8_t> score_goal)
{ {
std::optional<boost::json::object> const game_json = open_game_json(filename.c_str()); std::optional<boost::json::object> 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, {}}; return {nullptr, {}};
} }

View file

@ -2,27 +2,29 @@
#include "game_interface.h" #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); hanabi_state.print(os);
return os; return os;
} }
Game::Game(std::unique_ptr<HanabiStateIF> state, Hanabi::GameInfo game_info): Game::Game(std::unique_ptr<HanabiStateIF> state, Hanabi::GameInfo game_info) :
GameInfo(std::move(game_info)), state(std::move(state)), next_action(0) 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, // 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. // as this will mess with our moves.
if(not this->actions.empty()) { if (not this->actions.empty())
switch(this->actions.back().type) { {
switch (this->actions.back().type)
{
case ActionType::vote_terminate: case ActionType::vote_terminate:
case ActionType::vote_terminate_players: case ActionType::vote_terminate_players:
case ActionType::end_game: case ActionType::end_game:
this->actions.pop_back(); this->actions.pop_back();
default: default:;
;
} }
} }
} }
@ -39,7 +41,8 @@ namespace Hanabi {
state->rotate_next_draw(next_draw); state->rotate_next_draw(next_draw);
Action const & action = actions[next_action]; Action const & action = actions[next_action];
std::uint8_t index; std::uint8_t index;
switch(action.type) { switch (action.type)
{
case Hanabi::ActionType::color_clue: case Hanabi::ActionType::color_clue:
case Hanabi::ActionType::rank_clue: case Hanabi::ActionType::rank_clue:
state->give_clue(); state->give_clue();
@ -56,8 +59,7 @@ namespace Hanabi {
break; break;
case Hanabi::ActionType::vote_terminate_players: case Hanabi::ActionType::vote_terminate_players:
case Hanabi::ActionType::vote_terminate: case Hanabi::ActionType::vote_terminate:
case Hanabi::ActionType::end_game: case Hanabi::ActionType::end_game:;
;
} }
++next_action; ++next_action;
} }
@ -71,12 +73,17 @@ namespace Hanabi {
bool Game::goto_turn(size_t turn) bool Game::goto_turn(size_t turn)
{ {
size_t const cur_turn = next_action + 1; size_t const cur_turn = next_action + 1;
if (cur_turn >= turn) { if (cur_turn >= turn)
for(size_t i = 0; i < cur_turn - turn; i++) { {
for (size_t i = 0; i < cur_turn - turn; i++)
{
revert_turn(); 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(); make_turn();
} }
} }
@ -90,10 +97,13 @@ namespace Hanabi {
bool Game::goto_draw_pile_size(size_t draw_pile_break) 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(); 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(); revert_turn();
} }
return state->draw_pile_size() == draw_pile_break; return state->draw_pile_size() == draw_pile_break;

View file

@ -1,8 +1,11 @@
#include "hanabi_types.hpp" #include "hanabi_types.hpp"
namespace Hanabi { namespace Hanabi
std::ostream &operator<<(std::ostream &os, Action const& action) { {
switch(action.type) { std::ostream & operator<<(std::ostream & os, Action const & action)
{
switch (action.type)
{
case ActionType::play: case ActionType::play:
os << "play " + to_string(action.card); os << "play " + to_string(action.card);
break; break;
@ -19,25 +22,32 @@ namespace Hanabi {
} }
std::string to_string(const Hanabi::Card &card) { std::string to_string(const Hanabi::Card & card)
if (card == Hanabi::Cards::trash) { {
if (card == Hanabi::Cards::trash)
{
return "kt"; return "kt";
} else { }
else
{
return Hanabi::suit_initials[card.suit] + std::to_string(5 - card.rank); 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); os << to_string(card);
return os; 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<double>(prob) * 100 << "%"; os << prob << " ~ " << std::setprecision(5) << boost::rational_cast<double>(prob) * 100 << "%";
return os; 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; os << std::setprecision(5) << prob;
return os; return os;
} }

View file

@ -3,7 +3,8 @@
#include "command_line_interface.h" #include "command_line_interface.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[])
{
std::optional<Hanabi::CLIParms> parms = Hanabi::parse_parms(argc, argv); std::optional<Hanabi::CLIParms> parms = Hanabi::parse_parms(argc, argv);
if (parms.has_value()) if (parms.has_value())
{ {

View file

@ -1,77 +1,83 @@
#include "game_state.h" #include "game_state.h"
#include "game_interface.h" #include "game_interface.h"
namespace Hanabi { namespace Hanabi
{
std::unique_ptr<Hanabi::HanabiStateIF> make_game_state( std::unique_ptr<Hanabi::HanabiStateIF> make_game_state(
std::size_t num_suits, std::size_t num_suits, Hanabi::player_t num_players, std::vector<Hanabi::Card> const & deck, std::optional<
Hanabi::player_t num_players, uint8_t> score_goal
std::vector<Hanabi::Card> const &deck, )
std::optional<uint8_t> score_goal)
{ {
uint8_t actual_score_goal = score_goal.value_or(5 * num_suits); uint8_t actual_score_goal = score_goal.value_or(5 * num_suits);
switch(num_players) { switch (num_players)
{
case 2: case 2:
switch(num_suits) { switch (num_suits)
{
case 3: case 3:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,2,5>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3, 2, 5>(deck, actual_score_goal));
case 4: case 4:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,2,5>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4, 2, 5>(deck, actual_score_goal));
case 5: case 5:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,2,5>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5, 2, 5>(deck, actual_score_goal));
case 6: case 6:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6,2,5>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6, 2, 5>(deck, actual_score_goal));
default: default:
throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits));
} }
case 3: case 3:
switch(num_suits) { switch (num_suits)
{
case 3: case 3:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,3,5>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3, 3, 5>(deck, actual_score_goal));
case 4: case 4:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,3,5>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4, 3, 5>(deck, actual_score_goal));
case 5: case 5:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,3,5>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5, 3, 5>(deck, actual_score_goal));
case 6: case 6:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6,3,5>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6, 3, 5>(deck, actual_score_goal));
default: default:
throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits));
} }
case 4: case 4:
switch(num_suits) { switch (num_suits)
{
case 3: case 3:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,4,4>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3, 4, 4>(deck, actual_score_goal));
case 4: case 4:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,4,4>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4, 4, 4>(deck, actual_score_goal));
case 5: case 5:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,4,4>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5, 4, 4>(deck, actual_score_goal));
case 6: case 6:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6,4,4>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6, 4, 4>(deck, actual_score_goal));
default: default:
throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits));
} }
case 5: case 5:
switch(num_suits) { switch (num_suits)
{
case 3: case 3:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,5,4>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3, 5, 4>(deck, actual_score_goal));
case 4: case 4:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,5,4>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4, 5, 4>(deck, actual_score_goal));
case 5: case 5:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,5,4>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5, 5, 4>(deck, actual_score_goal));
case 6: case 6:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6,5,4>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6, 5, 4>(deck, actual_score_goal));
default: default:
throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits));
} }
case 6: case 6:
switch(num_suits) { switch (num_suits)
{
case 3: case 3:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,6,3>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3, 6, 3>(deck, actual_score_goal));
case 4: case 4:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,6,3>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4, 6, 3>(deck, actual_score_goal));
case 5: case 5:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,6,3>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5, 6, 3>(deck, actual_score_goal));
case 6: case 6:
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6,6,3>(deck, actual_score_goal)); return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6, 6, 3>(deck, actual_score_goal));
default: default:
throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits));
} }

View file

@ -2,43 +2,52 @@
#include "myassert.h" #include "myassert.h"
namespace Parsing { namespace Parsing
{
// This helper function deduces the type and assigns the value with the matching key // This helper function deduces the type and assigns the value with the matching key
template<class T> template<class T>
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<T>(obj.at(key)); t = value_to<T>(obj.at(key));
} }
} }
namespace Hanabi { namespace Hanabi
Card tag_invoke(boost::json::value_to_tag<Card>, {
boost::json::value const &jv) { Card tag_invoke(
boost::json::value_to_tag<Card>, boost::json::value const & jv
)
{
Hanabi::Card card{}; 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.rank, "rank");
Parsing::extract(obj, card.suit, "suitIndex"); Parsing::extract(obj, card.suit, "suitIndex");
card.rank = 5 - card.rank; card.rank = 5 - card.rank;
return card; return card;
} }
void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, Hanabi::Card const &card) { void tag_invoke(boost::json::value_from_tag, boost::json::value & jv, Hanabi::Card const & card)
jv = {{"suitIndex", card.suit}, {
{"rank", card.rank}}; jv = {{ "suitIndex", card.suit}
, {"rank" , card.rank}};
} }
} }
namespace Parsing { namespace Parsing
{
HanabLiveAction tag_invoke(boost::json::value_to_tag<HanabLiveAction>, boost::json::value const &jv) { HanabLiveAction tag_invoke(boost::json::value_to_tag<HanabLiveAction>, boost::json::value const & jv)
{
HanabLiveAction action{}; HanabLiveAction action{};
uint8_t type; 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, action.target, "target");
extract(obj, type, "type"); extract(obj, type, "type");
action.type = static_cast<Hanabi::ActionType>(type); action.type = static_cast<Hanabi::ActionType>(type);
switch (action.type) { switch (action.type)
{
case Hanabi::ActionType::color_clue: case Hanabi::ActionType::color_clue:
case Hanabi::ActionType::rank_clue: case Hanabi::ActionType::rank_clue:
action.type = Hanabi::ActionType::clue; action.type = Hanabi::ActionType::clue;
@ -58,35 +67,39 @@ namespace Parsing {
return action; return action;
} }
std::pair<std::vector<Hanabi::Card>, Hanabi::suit_t> parse_deck(const boost::json::value &deck_json) { std::pair<std::vector<Hanabi::Card>, Hanabi::suit_t> parse_deck(const boost::json::value & deck_json)
{
auto deck = boost::json::value_to<std::vector<Hanabi::Card>>(deck_json); auto deck = boost::json::value_to<std::vector<Hanabi::Card>>(deck_json);
for (auto &card: deck) { for (auto & card: deck)
{
ASSERT(card.rank < 5); ASSERT(card.rank < 5);
ASSERT(card.rank >= 0); ASSERT(card.rank >= 0);
ASSERT(card.suit < 6); ASSERT(card.suit < 6);
ASSERT(card.suit >= 0); ASSERT(card.suit >= 0);
} }
Hanabi::suit_t num_suits = 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); num_suits = std::max(num_suits, card.suit);
} }
return {deck, num_suits + 1}; return {deck, num_suits + 1};
} }
std::vector<HanabLiveAction> parse_actions(const boost::json::value &action_json) std::vector<HanabLiveAction> parse_actions(const boost::json::value & action_json)
{ {
return boost::json::value_to<std::vector<HanabLiveAction>>(action_json); return boost::json::value_to<std::vector<HanabLiveAction>>(action_json);
} }
std::vector<Hanabi::Action> convert_actions(std::vector<HanabLiveAction> const & hanab_live_actions, std::vector<Hanabi::Card> const & deck) std::vector<Hanabi::Action>
convert_actions(std::vector<HanabLiveAction> const & hanab_live_actions, std::vector<Hanabi::Card> const & deck)
{ {
std::vector<Hanabi::Action> actions; std::vector<Hanabi::Action> actions;
std::transform( std::transform(
hanab_live_actions.begin(), hanab_live_actions.begin()
hanab_live_actions.end(), , hanab_live_actions.end()
std::back_inserter(actions), , std::back_inserter(actions)
[&deck](HanabLiveAction const & action){ , [&deck](HanabLiveAction const & action) {
return Hanabi::Action {action.type, deck[action.target]}; return Hanabi::Action{action.type, deck[action.target]};
} }
); );
return actions; return actions;

View file

@ -11,395 +11,470 @@
#include "game_state.h" #include "game_state.h"
namespace Hanabi { namespace Hanabi
{
std::string read_line_memory_safe(const char *prompt) { std::string read_line_memory_safe(const char *prompt)
char *line = readline(prompt); {
std::string ret; char *line = readline(prompt);
if (line == nullptr) { std::string ret;
ret = ""; if (line == nullptr)
} else {
ret = std::string(line);
}
free(line);
return ret;
}
constexpr static std::array<std::string, 13> 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; ret = "";
return rl_completion_matches(text, cli_commands_generator);
} }
else
{
Card parse_card(std::string card_str) { ret = std::string(line);
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;
}
} }
free(line);
return ret;
}
int representation_length(const rational_probability& probability) { constexpr static std::array<std::string, 13> cli_commands = {
return 1 + static_cast<int>(std::ceil(std::log10(probability.denominator()))) + \ "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<int>(std::ceil(std::log10(probability.denominator()))) + \
static_cast<int>(std::ceil(std::log10(probability.numerator()))); static_cast<int>(std::ceil(std::log10(probability.numerator())));
}
int representation_length(const double probability)
{
return static_cast<int>(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) { std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>> states_to_show;
return static_cast<int>(std::ceil(std::log10(probability))); 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) { // Get rid of the trash collecting entry at the front
const auto next_states = state.possible_next_states(index, play); if (states_to_show.front().first.multiplicity == 0)
{
if (next_states.size() <= 1) { states_to_show.front() = std::move(states_to_show.back());
// No need to ask for anything if draw contains only one possible type of card states_to_show.pop_back();
return true;
}
std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>> 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<CardMultiplicity, std::optional<probability_t>>& 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;
} }
void signal_handler(int) { std::ranges::sort(states_to_show, [](const auto & left, const auto & right) {
std::cout << "Use 'quit' to exit the interactive shell." << std::endl << "> "; 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::stringstream prompt;
std::signal(SIGINT, signal_handler); prompt << "draw? [" << states_to_show.front().first.card << "] ";
// Set up GNU readline const std::string card_str = read_line_memory_safe(prompt.str().c_str());
rl_attempted_completion_function = cli_command_completion;
using_history();
// Tracks the depth of the replay the user explores. We have to ensure that we don't revert too much. const Card drawn_card = [&card_str, &states_to_show]() {
unsigned depth = 0; if (card_str.empty())
{
return states_to_show.front().first.card;
}
return parse_card(card_str);
}();
while (true) { if (drawn_card == Cards::unknown)
const std::string prompt = read_line_memory_safe("> "); {
add_history(prompt.c_str()); std::cout << "Could not parse card " << card_str << std::endl;
return false;
}
if (prompt.starts_with("help")) { auto selected_draw_it = std::find_if(next_states.begin(), next_states.end(), [&drawn_card, &state](
std::cout << "state: print information on current game state." << std::endl; const std::pair<CardMultiplicity
std::cout << "clue: give a clue." << std::endl; , std::optional<probability_t>> & pair
std::cout << "play <card>: play specified card." << std::endl; ) {
std::cout << "discard: discard trash from hand." << std::endl; return (state.is_trash(pair.first.card) and drawn_card == Cards::trash) or pair.first.card == drawn_card;
std::cout << "opt: take optimal action. In case of ties, prefers plays and discards in that order." << std::endl; });
std::cout << "revert <turns>: revert specified number of turns (default 1)." << std::endl; if (selected_draw_it == next_states.end())
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 << "That card is not in the draw pile, aborting." << std::endl;
std::cout << "set-initials <chars>: Set initials for the suits." << std::endl; return false;
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; state.rotate_next_draw(selected_draw_it->first.card);
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; return true;
std::cout << "help: Display this help message." << std::endl; }
continue;
}
if (prompt.starts_with("dump-id-parts")) { void signal_handler(int)
for (const auto val: game.state->dump_unique_id_parts().first) { {
std::cout << val << ", "; std::cout << "Use 'quit' to exit the interactive shell." << std::endl << "> ";
} }
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") { void cli(Game const & game)
std::cout << "Quitting." << std::endl; {
clear_history(); std::signal(SIGINT, signal_handler);
std::signal(SIGINT, SIG_DFL); // Set up GNU readline
break; rl_attempted_completion_function = cli_command_completion;
} using_history();
if (prompt.starts_with("set-initials")) { // Tracks the depth of the replay the user explores. We have to ensure that we don't revert too much.
if (prompt.length() < 16) { unsigned depth = 0;
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")) { while (true)
std::cout << *game.state << std::endl; {
const std::optional<probability_t> prob = game.state->lookup(); const std::string prompt = read_line_memory_safe("> ");
std::cout << "Winning chance: "; add_history(prompt.c_str());
print_probability(std::cout, prob) << std::endl;
continue;
}
if (prompt.starts_with("evaluate")) { if (prompt.starts_with("help"))
std::cout << "Evaluating current game state, this might take a while." << std::endl; {
game.state->evaluate_state(); std::cout << "state: print information on current game state." << std::endl;
std::cout << "Evaluated state." << std::endl; std::cout << "clue: give a clue." << std::endl;
continue; std::cout << "play <card>: 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 <turns>: 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 <chars>: 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 (prompt.starts_with("dump-id-parts"))
if (depth == 0) { {
std::cout << "Cannot revert more than base state." << std::endl; for (const auto val: game.state->dump_unique_id_parts().first)
continue; {
} std::cout << val << ", ";
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<Action, std::optional<probability_t>>& 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<probability_t> 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;
} }
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<probability_t> 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<Action
, std::optional<probability_t>> & 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<probability_t> 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;
} }
}
} }

View file

@ -1,15 +1,18 @@
#include "download.h" #include "download.h"
#include "game_state.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); std::cerr << "Test " << ("1005195") << " failed." << std::endl;
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;
};
} }
else
{
std::cout << "Test " << ("1005195") << " succeeded." << std::endl;
};
}
} }