reformat code
This commit is contained in:
parent
43b4bec7c6
commit
ea881c5e6a
20 changed files with 2076 additions and 1644 deletions
|
@ -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
|
||||
|
|
|
@ -9,11 +9,12 @@
|
|||
|
||||
#include "game_interface.h"
|
||||
|
||||
namespace Download {
|
||||
namespace Download
|
||||
{
|
||||
|
||||
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
|
||||
|
@ -27,7 +28,7 @@ namespace Download {
|
|||
*/
|
||||
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
|
||||
|
||||
|
|
|
@ -21,43 +21,63 @@ 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 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_relative_state_initialized() 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;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
|
||||
#include "game_interface.h"
|
||||
|
||||
namespace Hanabi {
|
||||
namespace Hanabi
|
||||
{
|
||||
|
||||
template<size_t num_suits>
|
||||
using Stacks = std::array<rank_t, num_suits>;
|
||||
|
@ -27,21 +28,26 @@ template<size_t num_suits>
|
|||
std::ostream & operator<<(std::ostream & os, const Stacks<num_suits> & stacks);
|
||||
|
||||
template<typename T>
|
||||
struct InnerCardArray {
|
||||
struct InnerCardArray
|
||||
{
|
||||
template<size_t N>
|
||||
using array_t = std::array<T, N>;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct InnerCardArray<bool> {
|
||||
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);
|
||||
|
@ -61,47 +67,67 @@ private:
|
|||
// 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 {
|
||||
class HanabiState : public HanabiStateIF
|
||||
{
|
||||
public:
|
||||
HanabiState() = default;
|
||||
|
||||
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;
|
||||
|
||||
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]] 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;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -109,13 +135,14 @@ protected:
|
|||
void print(std::ostream & os) const final;
|
||||
|
||||
private:
|
||||
struct BacktrackAction {
|
||||
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
|
||||
, Card discarded_or_played = Cards::unknown
|
||||
, hand_index_t index = 0
|
||||
, bool was_on_8_clues = false
|
||||
, bool strike = false
|
||||
);
|
||||
|
||||
ActionType action_type{};
|
||||
|
@ -136,20 +163,21 @@ private:
|
|||
|
||||
// This keeps track of the representation of the gamestate relative to some starting state
|
||||
// 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 play_stack = num_players + 2;
|
||||
enum CardPosition : uint8_t {
|
||||
hand = 0,
|
||||
played = 1,
|
||||
discarded = 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;
|
||||
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
|
||||
|
@ -163,13 +191,17 @@ private:
|
|||
};
|
||||
|
||||
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;
|
||||
|
@ -212,7 +245,12 @@ private:
|
|||
};
|
||||
|
||||
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(
|
||||
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);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,8 @@
|
|||
|
||||
#include <boost/rational.hpp>
|
||||
|
||||
namespace Hanabi {
|
||||
namespace Hanabi
|
||||
{
|
||||
|
||||
using rank_t = std::uint8_t;
|
||||
using suit_t = std::uint8_t;
|
||||
|
@ -33,6 +34,7 @@ namespace Hanabi {
|
|||
#endif
|
||||
|
||||
std::ostream & print_probability(std::ostream & os, const rational_probability & prob);
|
||||
|
||||
std::ostream & print_probability(std::ostream & os, double prob);
|
||||
|
||||
template<typename T>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -74,29 +77,34 @@ namespace Hanabi {
|
|||
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 {
|
||||
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);
|
||||
|
||||
|
||||
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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -3,16 +3,22 @@
|
|||
|
||||
#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; }
|
||||
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) {}
|
||||
NullStream() : std::ostream(&_m_sb)
|
||||
{}
|
||||
|
||||
private:
|
||||
NullBuffer _m_sb;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -45,13 +49,11 @@ namespace Parsing {
|
|||
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
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
#include <memory>
|
||||
#include "game_interface.h"
|
||||
|
||||
namespace Hanabi {
|
||||
namespace Hanabi
|
||||
{
|
||||
void cli(Game const & game);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,23 +2,29 @@
|
|||
|
||||
#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);
|
||||
|
||||
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";
|
||||
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++) {
|
||||
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) {
|
||||
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;
|
||||
std::cout << "Finished game " << game_id << " with draw pile size " << draw_pile_size << ": " << chance
|
||||
<< std::endl;
|
||||
|
||||
total_chance += chance;
|
||||
}
|
||||
|
@ -30,19 +36,23 @@ void check_games(unsigned num_players, unsigned max_draw_pile_size, unsigned fir
|
|||
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++) {
|
||||
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++) {
|
||||
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++) {
|
||||
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++) {
|
||||
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;
|
||||
|
|
|
@ -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)
|
||||
|
@ -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,7 +150,8 @@ 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
|
||||
|
@ -150,13 +159,16 @@ namespace Hanabi {
|
|||
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) {
|
||||
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;
|
||||
|
@ -200,27 +212,29 @@ namespace Hanabi {
|
|||
("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-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 "
|
||||
("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.")
|
||||
;
|
||||
("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;
|
||||
|
|
|
@ -7,20 +7,25 @@
|
|||
#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));
|
||||
if (r.header["content-type"] != "application/json; charset=utf-8") {
|
||||
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::optional<boost::json::object> open_game_json(const char *filename)
|
||||
{
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
if (!file.is_open())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string game_json((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
|
@ -30,7 +35,8 @@ namespace Download {
|
|||
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, {}};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
#include "game_interface.h"
|
||||
|
||||
namespace Hanabi {
|
||||
namespace Hanabi
|
||||
{
|
||||
|
||||
std::ostream & operator<<(std::ostream & os, HanabiStateIF const & hanabi_state)
|
||||
{
|
||||
|
@ -15,14 +16,15 @@ namespace Hanabi {
|
|||
{
|
||||
// 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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
#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));
|
||||
case 4:
|
||||
|
@ -24,7 +26,8 @@ namespace Hanabi {
|
|||
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));
|
||||
case 4:
|
||||
|
@ -37,7 +40,8 @@ namespace Hanabi {
|
|||
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));
|
||||
case 4:
|
||||
|
@ -50,7 +54,8 @@ namespace Hanabi {
|
|||
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));
|
||||
case 4:
|
||||
|
@ -63,7 +68,8 @@ namespace Hanabi {
|
|||
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));
|
||||
case 4:
|
||||
|
|
|
@ -2,17 +2,22 @@
|
|||
|
||||
#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();
|
||||
Parsing::extract(obj, card.rank, "rank");
|
||||
|
@ -21,15 +26,18 @@ namespace Hanabi {
|
|||
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();
|
||||
|
@ -38,7 +46,8 @@ namespace Parsing {
|
|||
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,16 +67,19 @@ 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};
|
||||
|
@ -78,14 +90,15 @@ namespace Parsing {
|
|||
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){
|
||||
hanab_live_actions.begin()
|
||||
, hanab_live_actions.end()
|
||||
, std::back_inserter(actions)
|
||||
, [&deck](HanabLiveAction const & action) {
|
||||
return Hanabi::Action{action.type, deck[action.target]};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -11,15 +11,20 @@
|
|||
|
||||
#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;
|
||||
if (line == nullptr) {
|
||||
if (line == nullptr)
|
||||
{
|
||||
ret = "";
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = std::string(line);
|
||||
}
|
||||
free(line);
|
||||
|
@ -27,25 +32,17 @@ namespace Hanabi {
|
|||
}
|
||||
|
||||
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",
|
||||
"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) {
|
||||
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) {
|
||||
for (auto & command: cli_commands)
|
||||
{
|
||||
if (command.starts_with(text_str) && state-- <= 0)
|
||||
{
|
||||
return strdup(command.c_str());
|
||||
}
|
||||
}
|
||||
|
@ -60,39 +57,50 @@ namespace Hanabi {
|
|||
}
|
||||
|
||||
|
||||
Card parse_card(std::string card_str) {
|
||||
if (card_str == "trash" or card_str == "kt") {
|
||||
Card parse_card(std::string card_str)
|
||||
{
|
||||
if (card_str == "trash" or card_str == "kt")
|
||||
{
|
||||
return Cards::trash;
|
||||
}
|
||||
if(card_str.size() != 2) {
|
||||
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()) {
|
||||
if (it == suit_initials.end())
|
||||
{
|
||||
return Cards::unknown;
|
||||
}
|
||||
const suit_t suit = std::distance(suit_initials.begin(), it);
|
||||
try {
|
||||
try
|
||||
{
|
||||
const rank_t rank = 5 - std::stoi(card_str.substr(1, 1));
|
||||
return Card{suit, rank};
|
||||
} catch(std::invalid_argument&) {
|
||||
}
|
||||
catch (std::invalid_argument &)
|
||||
{
|
||||
return Cards::unknown;
|
||||
}
|
||||
}
|
||||
|
||||
int representation_length(const rational_probability& probability) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
if (next_states.size() <= 1)
|
||||
{
|
||||
// No need to ask for anything if draw contains only one possible type of card
|
||||
return true;
|
||||
}
|
||||
|
@ -102,21 +110,27 @@ namespace Hanabi {
|
|||
|
||||
std::cout << "Choose drawn card: " << std::endl;
|
||||
int max_rational_digit_len = 0;
|
||||
for(const auto &[card_multiplicity, probability]: next_states) {
|
||||
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)) {
|
||||
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 {
|
||||
}
|
||||
else
|
||||
{
|
||||
states_to_show.emplace_back(card_multiplicity, probability);
|
||||
}
|
||||
if (probability.has_value()) {
|
||||
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) {
|
||||
if (states_to_show.front().first.multiplicity == 0)
|
||||
{
|
||||
states_to_show.front() = std::move(states_to_show.back());
|
||||
states_to_show.pop_back();
|
||||
}
|
||||
|
@ -125,7 +139,8 @@ namespace Hanabi {
|
|||
return left.second > right.second;
|
||||
});
|
||||
|
||||
for (const auto &[card_multiplicity, probability]: states_to_show) {
|
||||
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;
|
||||
|
@ -136,21 +151,27 @@ namespace Hanabi {
|
|||
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()) {
|
||||
if (card_str.empty())
|
||||
{
|
||||
return states_to_show.front().first.card;
|
||||
}
|
||||
return parse_card(card_str);
|
||||
}();
|
||||
|
||||
if (drawn_card == Cards::unknown) {
|
||||
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) {
|
||||
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()){
|
||||
if (selected_draw_it == next_states.end())
|
||||
{
|
||||
std::cout << "That card is not in the draw pile, aborting." << std::endl;
|
||||
return false;
|
||||
};
|
||||
|
@ -158,11 +179,13 @@ namespace Hanabi {
|
|||
return true;
|
||||
}
|
||||
|
||||
void signal_handler(int) {
|
||||
void signal_handler(int)
|
||||
{
|
||||
std::cout << "Use 'quit' to exit the interactive shell." << std::endl << "> ";
|
||||
}
|
||||
|
||||
void cli(Game const & game) {
|
||||
void cli(Game const & game)
|
||||
{
|
||||
std::signal(SIGINT, signal_handler);
|
||||
// Set up GNU readline
|
||||
rl_attempted_completion_function = cli_command_completion;
|
||||
|
@ -171,64 +194,82 @@ namespace Hanabi {
|
|||
// Tracks the depth of the replay the user explores. We have to ensure that we don't revert too much.
|
||||
unsigned depth = 0;
|
||||
|
||||
while (true) {
|
||||
while (true)
|
||||
{
|
||||
const std::string prompt = read_line_memory_safe("> ");
|
||||
add_history(prompt.c_str());
|
||||
|
||||
if (prompt.starts_with("help")) {
|
||||
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
|
||||
<< "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 << "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 << "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("dump-id-parts")) {
|
||||
for (const auto val: game.state->dump_unique_id_parts().first) {
|
||||
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) {
|
||||
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") {
|
||||
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) {
|
||||
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++) {
|
||||
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) {
|
||||
for (const char c: suit_initials)
|
||||
{
|
||||
std::cout << c;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prompt.starts_with("state")) {
|
||||
if (prompt.starts_with("state"))
|
||||
{
|
||||
std::cout << *game.state << std::endl;
|
||||
const std::optional<probability_t> prob = game.state->lookup();
|
||||
std::cout << "Winning chance: ";
|
||||
|
@ -236,60 +277,75 @@ namespace Hanabi {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (prompt.starts_with("evaluate")) {
|
||||
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) {
|
||||
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 {
|
||||
if (prompt.length() > 7)
|
||||
{
|
||||
try
|
||||
{
|
||||
turns_to_revert = std::stoi(prompt.substr(7));
|
||||
} catch(const std::invalid_argument&) {
|
||||
}
|
||||
catch (const std::invalid_argument &)
|
||||
{
|
||||
std::cout << "Could not parse number of turns to revert." << std::endl;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (turns_to_revert > depth) {
|
||||
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--) {
|
||||
while (turns_to_revert--)
|
||||
{
|
||||
game.state->revert();
|
||||
depth--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prompt.starts_with("id")) {
|
||||
if (prompt.starts_with("id"))
|
||||
{
|
||||
std::cout << game.state->unique_id() << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prompt.starts_with("play")) {
|
||||
if (prompt.starts_with("play"))
|
||||
{
|
||||
const Card card = parse_card(prompt.substr(5, 2));
|
||||
if (prompt.length() < 7) {
|
||||
if (prompt.length() < 7)
|
||||
{
|
||||
std::cout << "No card specified." << std::endl;
|
||||
continue;
|
||||
}
|
||||
if (card == Cards::unknown) {
|
||||
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)) {
|
||||
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)) {
|
||||
if (!ask_for_card_and_rotate_draw(*game.state, index, true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
game.state->play(index);
|
||||
|
@ -297,24 +353,30 @@ namespace Hanabi {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (prompt.starts_with("discard")) {
|
||||
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])) {
|
||||
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) {
|
||||
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) {
|
||||
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)) {
|
||||
if (!ask_for_card_and_rotate_draw(*game.state, trash_index, false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
game.state->discard(trash_index);
|
||||
|
@ -322,8 +384,10 @@ namespace Hanabi {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (prompt.starts_with("clue")) {
|
||||
if (game.state->num_clues() == 0) {
|
||||
if (prompt.starts_with("clue"))
|
||||
{
|
||||
if (game.state->num_clues() == 0)
|
||||
{
|
||||
std::cout << "You cannot give a clue at 0 clues." << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
@ -332,50 +396,60 @@ namespace Hanabi {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (prompt.starts_with("actions")) {
|
||||
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){
|
||||
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) {
|
||||
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()) {
|
||||
if (reasonable_actions.empty())
|
||||
{
|
||||
std::cout << "Game is over, no actions to take." << std::endl;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prompt.starts_with("opt")) {
|
||||
if (prompt.starts_with("opt"))
|
||||
{
|
||||
const auto reasonable_actions = game.state->get_reasonable_actions();
|
||||
if(reasonable_actions.empty()) {
|
||||
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())) {
|
||||
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) {
|
||||
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)) {
|
||||
if (!ask_for_card_and_rotate_draw(*game.state, index, true))
|
||||
{
|
||||
continue;
|
||||
};
|
||||
game.state->play(game.state->find_card_in_hand(best_action.card));
|
||||
|
@ -383,7 +457,8 @@ namespace Hanabi {
|
|||
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)) {
|
||||
if (!ask_for_card_and_rotate_draw(*game.state, index, false))
|
||||
{
|
||||
continue;
|
||||
};
|
||||
game.state->discard(game.state->find_card_in_hand(best_action.card));
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
#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))) {
|
||||
if (!(res == Hanabi::probability_t(7, 8)))
|
||||
{
|
||||
std::cerr << "Test " << ("1005195") << " failed." << std::endl;
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
std::cout << "Test " << ("1005195") << " succeeded." << std::endl;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue