diff --git a/CMakeLists.txt b/CMakeLists.txt index 50bc886..572de7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25) project(dynamic_program CXX) set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wpedantic -Werror") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wpedantic -Werror -lreadline") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") find_package(cpr) @@ -11,7 +11,7 @@ FIND_PACKAGE( Boost 1.81 COMPONENTS program_options REQUIRED ) include_directories(.) INCLUDE_DIRECTORIES( ${Boost_INCLUDE_DIR} ) -add_executable(dynamic_program main.cpp game_state.h) +add_executable(dynamic_program main.cpp game_state.h cli_interface.cpp) target_link_libraries(dynamic_program cpr) TARGET_LINK_LIBRARIES(dynamic_program Boost::program_options ) diff --git a/cli_interface.cpp b/cli_interface.cpp new file mode 100644 index 0000000..0f4ec78 --- /dev/null +++ b/cli_interface.cpp @@ -0,0 +1,155 @@ +#include +#include +#include +#include +#include +#include "game_state.h" + +namespace Hanabi { + + std::ostream& operator<<(std::ostream& os, const std::optional& prob) { + if (prob.has_value()) { + os << prob.value() << " ~ " << std::setprecision(5) << boost::rational_cast(prob.value()) * 100 << "%"; + } else { + os << "unknown"; + } + return os; + } + + 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; + } + + Card parse_card(std::string card_str) { + if(card_str.size() != 2) { + return unknown_card; + } + 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; + } + 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; + } + } + + bool ask_for_card_and_rotate_draw(const std::shared_ptr& game, hand_index_t index, bool play) { + const auto next_states = game->possible_next_states(index, play); + if (next_states.size() <= 1) { + return true; + } + std::cout << "Choose drawn card: " << std::endl; + for(const auto &[card_multiplicity, probability]: next_states) { + if (game->is_trash(card_multiplicity.card)) { + std::cout << "kt " << std::endl; + } else { + std::cout << card_multiplicity.card << " "; + } + 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) { + 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; + }; + game->rotate_next_draw(drawn_card); + return true; + } + + [[noreturn]] void cli(const std::shared_ptr& game) { + while (true) { + 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; + continue; + } + + if (prompt.starts_with("state")) { + std::cout << *game << std::endl; + const std::optional prob = game->lookup(); + std::cout << "Winning chance: " << prob << std::endl; + continue; + } + + if (prompt.starts_with("revert")) { + std::cout << "Reverting one turn" << std::endl; + game->revert(); + continue; + } + + if (prompt.starts_with("play")) { + const Card card = parse_card(prompt.substr(5,2)); + if (card == unknown_card) { + std::cout << "Could not parse card " << prompt.substr(5,2) << 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, true)) { + continue; + } + game->play(index); + continue; + } + + 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; + 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)) { + continue; + } + game->discard(index); + continue; + } + + if (prompt.starts_with("clue")) { + game->clue(); + continue; + } + + if (prompt.starts_with("actions")) { + for (const auto &[action, probability] : game->get_reasonable_actions()) { + std::cout << action << ": " << probability << std::endl; + } + continue; + } + + std::cout << "Unrecognized command. Type 'help' for a list of available commands." << std::endl; + } + } +} \ No newline at end of file diff --git a/cli_interface.h b/cli_interface.h new file mode 100644 index 0000000..21e05d6 --- /dev/null +++ b/cli_interface.h @@ -0,0 +1,12 @@ +#ifndef DYNAMIC_PROGRAM_CLI_INTERFACE_H +#define DYNAMIC_PROGRAM_CLI_INTERFACE_H + +#include +#include "game_state.h" + +namespace Hanabi { + [[noreturn]] [[noreturn]] void cli(const std::shared_ptr& game); +} + + +#endif //DYNAMIC_PROGRAM_CLI_INTERFACE_H diff --git a/game_state.h b/game_state.h index c0cbe3c..438eb17 100644 --- a/game_state.h +++ b/game_state.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -48,9 +49,9 @@ namespace Hanabi { bool in_starting_hand; bool initial_trash; - Card &operator++(); + inline Card &operator++(); - const Card operator++(int); + inline const Card operator++(int); auto operator<=>(const Card &) const = default; }; @@ -67,7 +68,7 @@ namespace std { namespace Hanabi { -std::ostream &operator<<(std::ostream &os, const Card &card); +inline std::ostream &operator<<(std::ostream &os, const Card &card); constexpr Card r0 = {0, 5}; constexpr Card r1 = {0, 4}; @@ -99,7 +100,7 @@ std::ostream& operator<<(std::ostream &os, const Stacks &stacks); struct CardMultiplicity { Card card; - std::uint8_t multiplicity; + unsigned multiplicity; auto operator<=>(const CardMultiplicity &) const = default; }; @@ -150,7 +151,7 @@ struct Action { Card card {}; }; -std::ostream& operator<<(std::ostream& os, const Action& action); +inline std::ostream& operator<<(std::ostream& os, const Action& action); /** Would like to have 2 versions: * All: @@ -181,8 +182,10 @@ public: virtual void init_backtracking_information() = 0; virtual probability_t evaluate_state() = 0; + virtual std::optional lookup() const = 0; + virtual std::vector>> get_reasonable_actions() = 0; - virtual std::vector> possible_next_states(hand_index_t index, bool play) = 0; + virtual std::vector>> possible_next_states(hand_index_t index, bool play) = 0; virtual ~HanabiStateIF() = default; @@ -192,6 +195,8 @@ protected: friend std::ostream& operator<<(std::ostream&, HanabiStateIF const&); }; +inline std::ostream &operator<<(std::ostream &os, HanabiStateIF const &hanabi_state); + template class HanabiState : public HanabiStateIF { public: @@ -221,7 +226,7 @@ public: std::optional lookup() const; std::vector>> get_reasonable_actions() final; - std::vector> possible_next_states(hand_index_t index, bool play) final; + std::vector>> possible_next_states(hand_index_t index, bool play) final; auto operator<=>(const HanabiState &) const = default; @@ -274,7 +279,7 @@ private: unsigned long discard_and_potentially_update(hand_index_t index); unsigned long play_and_potentially_update(hand_index_t index); - unsigned long draw(hand_index_t index); + unsigned draw(hand_index_t index); void revert_draw(hand_index_t index, Card discarded_card); void revert_clue(); diff --git a/game_state.hpp b/game_state.hpp index 0cfa684..1815a09 100644 --- a/game_state.hpp +++ b/game_state.hpp @@ -244,7 +244,7 @@ namespace Hanabi { } template - unsigned long HanabiState::draw(uint8_t index) { + unsigned HanabiState::draw(uint8_t index) { ASSERT(index < _hands[_turn].size()); // update card position of the card we are about to discard @@ -470,18 +470,17 @@ namespace Hanabi { } template - std::vector> HanabiState::possible_next_states(hand_index_t index, bool play) { - std::vector> next_states; - do_for_each_potential_draw(index, play, [this, &next_states](unsigned long multiplicity){ + std::vector>> HanabiState::possible_next_states(hand_index_t index, bool play) { + std::vector>> next_states; + do_for_each_potential_draw(index, play, [this, &next_states, &index](unsigned multiplicity){ auto prob = lookup(); - ASSERT(lookup().has_value()); // bit hacky to get drawn card here decr_turn(); - const CardMultiplicity drawn_card = {_hands[_turn], multiplicity}; + const CardMultiplicity drawn_card = {_hands[_turn][index], multiplicity}; incr_turn(); - next_states.emplace_back(drawn_card, prob.value()); + next_states.emplace_back(drawn_card, prob); }); return next_states; } diff --git a/main.cpp b/main.cpp index 7c7d620..a93b399 100644 --- a/main.cpp +++ b/main.cpp @@ -11,6 +11,7 @@ #include "game_state.h" #include "download.h" #include "myassert.h" +#include "cli_interface.h" namespace Hanabi { @@ -23,20 +24,6 @@ namespace Hanabi { std::cout << "Probability with optimal play: " << res << std::endl; std::cout << "Enumerated " << game->enumerated_states() << " states" << std::endl; std::cout << "Visited " << game->position_tablebase().size() << " unique game states. " << std::endl; - - game->rotate_next_draw(r1); - game->discard(0); - - game->rotate_next_draw(r1); - game->play(game->find_card_in_hand(y3)); - - game->clue(); - - game->rotate_next_draw(r1); - game->play(game->find_card_in_hand(y4)); - - std::cout << *game << std::endl; - for (const auto &[action, probability] : game->get_reasonable_actions()) { std::cout << action; if(probability.has_value()) { @@ -52,6 +39,10 @@ namespace Hanabi { biggest_key = std::max(biggest_key, key); } std::cout << "Biggest key generated is " << biggest_key << std::endl; + + auto game_shared = std::shared_ptr(game.release()); + auto states = game_shared->possible_next_states(0, false); + cli(game_shared); } void print_sizes() {