From 74fe5513da5c62bc07955b3629896c0421f94fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sat, 12 Aug 2023 08:50:28 +0200 Subject: [PATCH] improve cli interface: easier handling, more input validation --- cli_interface.cpp | 112 +++++++++++++++++++++++++++++++++------------- download.h | 2 +- game_state.h | 91 ++++++++++++++++++++++++++----------- game_state.hpp | 51 ++++++++++++++++++--- 4 files changed, 192 insertions(+), 64 deletions(-) diff --git a/cli_interface.cpp b/cli_interface.cpp index 0f4ec78..7afbea9 100644 --- a/cli_interface.cpp +++ b/cli_interface.cpp @@ -29,20 +29,23 @@ namespace Hanabi { } Card parse_card(std::string card_str) { + if (card_str == "trash") { + return Cards::trash; + } if(card_str.size() != 2) { - return unknown_card; + return Cards::unknown; } constexpr std::array color_initials = {'r', 'y', 'g', 'b', 'p', 't'}; auto it = std::find(color_initials.begin(), color_initials.end(), card_str[0]); if (it == color_initials.end()) { - return unknown_card; + return Cards::unknown; } const suit_t suit = std::distance(color_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 unknown_card; + return Cards::unknown; } } @@ -52,27 +55,51 @@ namespace Hanabi { return true; } std::cout << "Choose drawn card: " << std::endl; + unsigned num_trash = 0; for(const auto &[card_multiplicity, probability]: next_states) { if (game->is_trash(card_multiplicity.card)) { - std::cout << "kt " << std::endl; + num_trash += card_multiplicity.multiplicity; } else { - std::cout << card_multiplicity.card << " "; + std::cout << card_multiplicity.card << " (" << card_multiplicity.multiplicity; + std::cout << " copie(s) in draw) " << probability << std::endl; } - std::cout << "(" << card_multiplicity.multiplicity << " copie(s) in draw) " << probability << std::endl; } - const std::string card_str = read_line_memory_safe("draw? "); - const Card drawn_card = parse_card(card_str); - if (drawn_card == unknown_card) { + if (num_trash > 0) { + std::cout << Cards::trash << " (" << num_trash << " copie(s) in draw" << std::endl; + } + + std::stringstream prompt; + prompt << "draw? ["; + if (num_trash > 0) { + prompt << Cards::trash; + } else { + prompt << next_states.front().first.card; + } + prompt << "] "; + const std::string card_str = read_line_memory_safe(prompt.str().c_str()); + const Card drawn_card = [&card_str, &num_trash, &next_states](){ + if (card_str.empty()) { + if (num_trash > 0) { + return Cards::trash; + } + return next_states.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; } - if (std::find_if(next_states.begin(), next_states.end(), [&drawn_card](const std::pair>& pair) { - return pair.first.card.suit == drawn_card.suit and pair.first.card.rank == drawn_card.rank; - }) == next_states.end()) { - std::cout << "That card is not in the draw pile" << std::endl; + auto selected_draw_it = std::find_if(next_states.begin(), next_states.end(), [&drawn_card, &game](const std::pair>& pair) { + return (game->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; }; - game->rotate_next_draw(drawn_card); + game->rotate_next_draw(selected_draw_it->first.card); return true; } @@ -81,12 +108,14 @@ namespace Hanabi { const std::string prompt = read_line_memory_safe("> "); if (prompt.starts_with("help")) { - std::cout << "state: print information on current game state" << std::endl; - std::cout << "clue: give a clue" << std::endl; - std::cout << "play : play specified card" << std::endl; - std::cout << "discard : discard specified card" << std::endl; - std::cout << "revert: revert last turn of game" << std::endl; - std::cout << "actions: display list of reasonable actions to take and their winning chances" << std::endl; + std::cout << "state: print information on current game state." << std::endl; + std::cout << "clue: give a clue." << std::endl; + std::cout << "play : play specified card." << std::endl; + std::cout << "discard: discard trash from hand." << std::endl; + std::cout << "revert: revert last turn of game." << std::endl; + std::cout << "actions: display list of reasonable actions to take and their winning chances." << std::endl; + std::cout << "id: display id of state. Has no inherent meaning, useful for debugging." << std::endl; + std::cout << "evaluate: evaluate current game state recursively. Potentially runtime-expensive." << std::endl; continue; } @@ -97,15 +126,29 @@ namespace Hanabi { continue; } + if (prompt.starts_with("evaluate")) { + std::cout << "Evaluating current game state, this might take a while." << std::endl; + game->evaluate_state(); + std::cout << "Evaluated state." << std::endl; + } + if (prompt.starts_with("revert")) { std::cout << "Reverting one turn" << std::endl; game->revert(); continue; } + if (prompt.starts_with("id")) { + std::cout << game->unique_id() << std::endl; + continue; + } + if (prompt.starts_with("play")) { const Card card = parse_card(prompt.substr(5,2)); - if (card == unknown_card) { + if (prompt.length() < 7) { + std::cout << "No card specified." << std::endl; + } + if (card == Cards::unknown) { std::cout << "Could not parse card " << prompt.substr(5,2) << std::endl; continue; } @@ -121,24 +164,31 @@ namespace Hanabi { } if (prompt.starts_with("discard")) { - const Card card = parse_card(prompt.substr(8,2)); - if (card == unknown_card) { - std::cout << "Could not parse card " << prompt.substr(5,2) << std::endl; + const auto hand = game->cur_hand(); + hand_index_t trash_index = invalid_hand_idx; + for(hand_index_t index = 0; index < hand.size(); index++) { + if (game->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; } - const hand_index_t index = game->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; - } - if (!ask_for_card_and_rotate_draw(game, index, false)) { + if (game->num_clues() == max_num_clues) { + std::cout << "You cannot discard at " << max_num_clues << " clues." << std::endl; continue; } - game->discard(index); + if (!ask_for_card_and_rotate_draw(game, trash_index, false)) { + continue; + } + game->discard(trash_index); continue; } if (prompt.starts_with("clue")) { - game->clue(); + game->give_clue(); continue; } diff --git a/download.h b/download.h index 1689a38..9e3b88a 100644 --- a/download.h +++ b/download.h @@ -123,7 +123,7 @@ namespace Download { switch(actions[i].type) { case Hanabi::ActionType::color_clue: case Hanabi::ActionType::rank_clue: - game->clue(); + game->give_clue(); break; case Hanabi::ActionType::discard: index = game->find_card_in_hand(deck[actions[i].target]); diff --git a/game_state.h b/game_state.h index 438eb17..522471b 100644 --- a/game_state.h +++ b/game_state.h @@ -39,8 +39,11 @@ namespace Hanabi { constexpr size_t max_card_duplicity = 3; constexpr clue_t max_num_clues = 8; constexpr uint8_t not_in_starting_hand = std::numeric_limits::max(); + constexpr hand_index_t invalid_hand_idx = std::numeric_limits::max(); - constexpr std::array suit_initials{"r", "y", "g", "b", "p", "t"}; + // We might want to change these at runtime to adapt to other variants. + // However, a global variable is used so that we can have an output operator for cards reading from here + static std::array suit_initials = {'r', 'y', 'g', 'b', 'p', 't'}; struct Card { suit_t suit; @@ -53,7 +56,48 @@ namespace Hanabi { inline const Card operator++(int); - auto operator<=>(const Card &) const = default; + inline bool operator==(const Card &other) const; + }; + + namespace Cards { + static constexpr Card r0 = {0, 5}; + static constexpr Card r1 = {0, 4}; + static constexpr Card r2 = {0, 3}; + static constexpr Card r3 = {0, 2}; + static constexpr Card r4 = {0, 1}; + static constexpr Card r5 = {0, 0}; + static constexpr Card y0 = {1, 5}; + static constexpr Card y1 = {1, 4}; + static constexpr Card y2 = {1, 3}; + static constexpr Card y3 = {1, 2}; + static constexpr Card y4 = {1, 1}; + static constexpr Card y5 = {1, 0}; + static constexpr Card g0 = {2, 5}; + static constexpr Card g1 = {2, 4}; + static constexpr Card g2 = {2, 3}; + static constexpr Card g3 = {2, 2}; + static constexpr Card g4 = {2, 1}; + static constexpr Card g5 = {2, 0}; + static constexpr Card b0 = {3, 5}; + static constexpr Card b1 = {3, 4}; + static constexpr Card b2 = {3, 3}; + static constexpr Card b3 = {3, 2}; + static constexpr Card b4 = {3, 1}; + static constexpr Card b5 = {3, 0}; + static constexpr Card p0 = {4, 5}; + static constexpr Card p1 = {4, 4}; + static constexpr Card p2 = {4, 3}; + static constexpr Card p3 = {4, 2}; + static constexpr Card p4 = {4, 1}; + static constexpr Card p5 = {4, 0}; + static constexpr Card t0 = {5, 5}; + static constexpr Card t1 = {5, 4}; + static constexpr Card t2 = {5, 3}; + static constexpr Card t3 = {5, 2}; + static constexpr Card t4 = {5, 1}; + static constexpr Card t5 = {5, 0}; + static constexpr Card unknown = {std::numeric_limits::max(), 0}; + static constexpr Card trash = {std::numeric_limits::max(), 1}; }; } @@ -70,19 +114,6 @@ namespace Hanabi { inline std::ostream &operator<<(std::ostream &os, const Card &card); -constexpr Card r0 = {0, 5}; -constexpr Card r1 = {0, 4}; -constexpr Card r2 = {0, 3}; -constexpr Card r3 = {0, 2}; -constexpr Card r4 = {0, 1}; -constexpr Card r5 = {0, 0}; -constexpr Card y0 = {1, 5}; -constexpr Card y1 = {1, 4}; -constexpr Card y2 = {1, 3}; -constexpr Card y3 = {1, 2}; -constexpr Card y4 = {1, 1}; -constexpr Card y5 = {1, 0}; -constexpr Card unknown_card = {0, 6}; /** * To store: @@ -162,19 +193,23 @@ inline std::ostream& operator<<(std::ostream& os, const Action& action); class HanabiStateIF { public: - virtual void clue() = 0; + 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 revert() = 0; - [[nodiscard]] virtual hand_index_t find_card_in_hand(const Card& card) const = 0; + [[nodiscard]] virtual player_t turn() const = 0; + [[nodiscard]] virtual clue_t num_clues() const = 0; + [[nodiscard]] virtual std::vector> hands() const = 0; + [[nodiscard]] virtual std::vector cur_hand() const = 0; + [[nodiscard]] virtual size_t draw_pile_size() const = 0; + [[nodiscard]] virtual bool is_trash(const Card& card) const = 0; [[nodiscard]] virtual bool is_playable(const Card& card) const = 0; - [[nodiscard]] virtual size_t draw_pile_size() 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& position_tablebase() const = 0; @@ -182,8 +217,8 @@ public: virtual void init_backtracking_information() = 0; virtual probability_t evaluate_state() = 0; - virtual std::optional lookup() const = 0; - + [[nodiscard]] virtual std::optional lookup() const = 0; + [[nodiscard]] virtual std::uint64_t unique_id() const = 0; virtual std::vector>> get_reasonable_actions() = 0; virtual std::vector>> possible_next_states(hand_index_t index, bool play) = 0; @@ -203,7 +238,7 @@ public: HanabiState() = default; explicit HanabiState(const std::vector& deck); - void clue() final; + void give_clue() final; void discard(hand_index_t index) final; void play(hand_index_t index) final; @@ -211,10 +246,15 @@ public: void revert() final; + [[nodiscard]] player_t turn() const final; + [[nodiscard]] clue_t num_clues() const final; + [[nodiscard]] std::vector> hands() const final; + [[nodiscard]] std::vector cur_hand() const final; + [[nodiscard]] size_t draw_pile_size() const final; + [[nodiscard]] hand_index_t find_card_in_hand(const Card& card) const final; [[nodiscard]] bool is_trash(const Card& card) const final; [[nodiscard]] bool is_playable(const Card& card) const final; - [[nodiscard]] size_t draw_pile_size() const final; [[nodiscard]] bool is_relative_state_initialized() const final; [[nodiscard]] std::uint64_t enumerated_states() const final; @@ -224,6 +264,7 @@ public: probability_t evaluate_state() final; std::optional lookup() const; + std::uint64_t unique_id() const final; std::vector>> get_reasonable_actions() final; std::vector>> possible_next_states(hand_index_t index, bool play) final; @@ -237,7 +278,7 @@ private: struct BacktrackAction { explicit BacktrackAction( ActionType action_type, - Card discarded_or_played = unknown_card, + Card discarded_or_played = Cards::unknown, hand_index_t index = 0, bool was_on_8_clues = false ); @@ -299,8 +340,6 @@ private: static constexpr player_t draw_pile = num_players; static constexpr player_t trash_or_play_stack = num_players + 1; - std::uint64_t unique_id() const; - // Usual game state player_t _turn{}; clue_t _num_clues{}; diff --git a/game_state.hpp b/game_state.hpp index 1815a09..f780304 100644 --- a/game_state.hpp +++ b/game_state.hpp @@ -40,8 +40,16 @@ namespace Hanabi { return ret; } + bool Card::operator==(const Card &other) const { + return suit == other.suit and rank == other.rank; + } + std::ostream &operator<<(std::ostream &os, const Card &card) { - os << suit_initials[card.suit] << 5 - card.rank; + if (card == Cards::trash) { + os << "kt"; + } else { + os << suit_initials[card.suit] << 5 - card.rank; + } return os; } @@ -118,11 +126,11 @@ namespace Hanabi { } template - void HanabiState::clue() { + void HanabiState::give_clue() { ASSERT(_num_clues > 0); --_num_clues; - _actions_log.emplace(ActionType::clue, unknown_card, 0); + _actions_log.emplace(ActionType::clue, Cards::unknown, 0); incr_turn(); } @@ -469,6 +477,37 @@ namespace Hanabi { } } + template + player_t HanabiState::turn() const { + return _turn; + } + + template + clue_t HanabiState::num_clues() const { + return _num_clues; + }; + + template + std::vector> HanabiState::hands() const { + std::vector> hands; + for(player_t player = 0; player < num_players; player++) { + hands.push_back({}); + for(const Card& card: _hands[player]) { + hands.back().push_back(card); + } + } + return hands; + } + + template + std::vector HanabiState::cur_hand() const { + std::vector hand; + for(const Card& card: _hands[_turn]) { + hand.push_back(card); + } + return hand; + } + template std::vector>> HanabiState::possible_next_states(hand_index_t index, bool play) { std::vector>> next_states; @@ -551,9 +590,9 @@ namespace Hanabi { } if(_num_clues > 0) { - clue(); + give_clue(); const std::optional prob = lookup(); - const Action action = {ActionType::clue, unknown_card}; + const Action action = {ActionType::clue, Cards::unknown}; reasonable_actions.emplace_back(action, prob); revert_clue(); } @@ -648,7 +687,7 @@ namespace Hanabi { // Last option is to stall if(_num_clues > 0) { - clue(); + give_clue(); const probability_t probability_stall = evaluate_state(); revert_clue(); best_probability = std::max(best_probability, probability_stall);