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"
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<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
* to be winning.
* 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.
*/
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<bool> interactive {};
boost::optional<bool> 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<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
*/
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

View file

@ -9,25 +9,26 @@
#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
* @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<uint8_t> 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<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

View file

@ -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<std::vector<Card>> hands() const = 0;
[[nodiscard]] virtual std::vector<Card> 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<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 probability_t evaluate_state() = 0;
[[nodiscard]] virtual std::optional<probability_t> lookup() 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;
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;
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<HanabiStateIF> 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;

View file

@ -18,158 +18,190 @@
#include "game_interface.h"
namespace Hanabi {
namespace Hanabi
{
template<size_t num_suits>
using Stacks = std::array<rank_t, num_suits>;
template<size_t num_suits>
using Stacks = std::array<rank_t, num_suits>;
template<size_t num_suits>
std::ostream &operator<<(std::ostream &os, const Stacks<num_suits> &stacks);
template<size_t num_suits>
std::ostream & operator<<(std::ostream & os, const Stacks<num_suits> & stacks);
template<typename T>
struct InnerCardArray {
template<typename T>
struct InnerCardArray
{
template<size_t N>
using array_t = std::array<T, N>;
};
};
template<>
struct InnerCardArray<bool> {
template<>
struct InnerCardArray<bool>
{
template<size_t 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;
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<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
// history by making and reverting the stored actions.
template <suit_t num_suits, player_t num_players, hand_index_t hand_size>
class HanabiState : public HanabiStateIF {
public:
template<suit_t num_suits, player_t num_players, hand_index_t hand_size>
class HanabiState : public HanabiStateIF
{
public:
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 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<std::vector<Card>> hands() const final;
[[nodiscard]] std::vector<Card> 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<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;
probability_t evaluate_state() final;
[[nodiscard]] std::optional<probability_t> lookup() 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;
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;
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<Card, 30> 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<Card, 30> good_cards_draw;
// 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;
// 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;
// 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<CardPosition, num_players * hand_size> 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<CardPosition, num_players * hand_size> 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<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>
bool same_up_to_discard_permutation(HanabiState<num_suits, num_players, hand_size> state1, HanabiState<num_suits, num_players, 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);
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
)
{
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;
}
}

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,8 @@
#include <boost/rational.hpp>
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<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
@ -57,7 +59,8 @@ namespace Hanabi {
// 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'};
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<typename T>
std::ostream& print_probability(std::ostream& os, const std::optional<T>& prob) {
if (prob.has_value()) {
std::ostream & print_probability(std::ostream & os, const std::optional<T> & prob)
{
if (prob.has_value())
{
return print_probability(os, prob.value());
} else {
}
else
{
os << "unknown";
}
return os;

View file

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

View file

@ -3,21 +3,27 @@
#include <iosfwd>
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;
}

View file

@ -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<Card>, 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<Card>, 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<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
* @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.
* @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<HanabLiveAction> const & hanab_live_actions,
std::vector<Hanabi::Card> const & deck
std::vector<HanabLiveAction> const & hanab_live_actions, std::vector<Hanabi::Card> const & deck
);
Hanabi::GameInfo parse_game(boost::json::object const & game_json);
}
#endif //DYNAMIC_PROGRAM_PARSE_GAME_H

View file

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

View file

@ -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<std::vector<Hanabi::probability_t>> 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<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++) {
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();
}

View file

@ -8,7 +8,8 @@
namespace bpo = boost::program_options;
namespace Hanabi {
namespace Hanabi
{
template<class T>
std::optional<T> convert_optional(boost::optional<T> 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<int>(parms.game))
{
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(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;
} else {
}
else
{
std::cout << "Failed to open file " << std::get<std::string>(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<int>(), "Game ID from hanab.live.")
("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 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.")
("score-goal,s", bpo::value<boost::optional<uint8_t>>(&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<int>(), "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<boost::optional<bool>>(&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<int>(), "Game ID from hanab.live.")
("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 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.")
("score-goal,s"
, bpo::value<boost::optional<uint8_t>>(&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<int>(), "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<boost::optional<bool>>(&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;

View file

@ -7,30 +7,36 @@
#include "download.h"
namespace Download {
namespace Download
{
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));
if (r.header["content-type"] != "application/json; charset=utf-8") {
return std::nullopt;
}
return boost::json::parse(r.text).as_object();
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));
if (r.header["content-type"] != "application/json; charset=utf-8")
{
return std::nullopt;
}
return boost::json::parse(r.text).as_object();
}
std::optional<boost::json::object> open_game_json(const char *filename) {
std::ifstream file(filename);
if (!file.is_open()) {
return std::nullopt;
}
std::string game_json((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
return boost::json::parse(game_json).as_object();
std::optional<boost::json::object> open_game_json(const char *filename)
{
std::ifstream file(filename);
if (!file.is_open())
{
return std::nullopt;
}
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)
{
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, {}};
}
@ -41,7 +47,8 @@ namespace Download {
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());
if (!game_json.has_value() or game_json.value().empty()) {
if (!game_json.has_value() or game_json.value().empty())
{
return {nullptr, {}};
}

View file

@ -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<HanabiStateIF> state, Hanabi::GameInfo game_info):
GameInfo(std::move(game_info)), state(std::move(state)), next_action(0)
Game::Game(std::unique_ptr<HanabiStateIF> 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;

View file

@ -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<double>(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;
}

View file

@ -3,7 +3,8 @@
#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);
if (parms.has_value())
{

View file

@ -1,77 +1,83 @@
#include "game_state.h"
#include "game_interface.h"
namespace Hanabi {
namespace Hanabi
{
std::unique_ptr<Hanabi::HanabiStateIF> make_game_state(
std::size_t num_suits,
Hanabi::player_t num_players,
std::vector<Hanabi::Card> const &deck,
std::optional<uint8_t> score_goal)
std::size_t num_suits, Hanabi::player_t num_players, std::vector<Hanabi::Card> 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<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:
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:
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:
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:
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<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:
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:
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:
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:
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<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:
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:
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:
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:
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<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:
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:
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:
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:
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<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:
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:
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:
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:
throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits));
}

View file

@ -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<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));
}
}
namespace Hanabi {
Card tag_invoke(boost::json::value_to_tag<Card>,
boost::json::value const &jv) {
namespace Hanabi
{
Card tag_invoke(
boost::json::value_to_tag<Card>, 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<HanabLiveAction>, boost::json::value const &jv) {
HanabLiveAction tag_invoke(boost::json::value_to_tag<HanabLiveAction>, 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<Hanabi::ActionType>(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<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);
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<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);
}
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::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;

View file

@ -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<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)
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<int>(std::ceil(std::log10(probability.denominator()))) + \
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;
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())));
}
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) {
return static_cast<int>(std::ceil(std::log10(probability)));
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()));
}
}
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<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;
// 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 <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;
}
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;
}
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<probability_t> 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 <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 (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;
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<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 "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;
};
}
}