From 32af52ae9e186c2afe322e82419343c654f8798b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sun, 12 Nov 2023 17:30:44 +0100 Subject: [PATCH] Rework CLI This should cover all use cases / exceptions now and is in a reasonably good code state. --- CMakeLists.txt | 5 +- include/command_line_interface.h | 35 ++++ include/download.h | 2 +- include/game_state.h | 2 + include/null_buffer.h | 25 +++ src/command_line_interface.cpp | 264 +++++++++++++++++++++++++++++++ src/download.cpp | 4 +- src/game_state.cpp | 5 + src/main.cpp | 154 +----------------- 9 files changed, 344 insertions(+), 152 deletions(-) create mode 100644 include/command_line_interface.h create mode 100644 include/null_buffer.h create mode 100644 src/command_line_interface.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d5a6633..9a004cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,10 @@ include_directories(.) include_directories(${Boost_INCLUDE_DIR}) add_executable(endgame-analyzer src/main.cpp src/state_explorer.cpp src/download.cpp - src/game_state.cpp) + src/game_state.cpp + include/null_buffer.h + include/command_line_interface.h + src/command_line_interface.cpp) target_link_libraries(endgame-analyzer cpr) target_link_libraries(endgame-analyzer Boost::program_options) diff --git a/include/command_line_interface.h b/include/command_line_interface.h new file mode 100644 index 0000000..68ae8b4 --- /dev/null +++ b/include/command_line_interface.h @@ -0,0 +1,35 @@ +#ifndef DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H +#define DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H + +#include +#include +#include "game_state.h" + + +namespace Hanabi { + enum class GameStateSpecType { + turn = 0, + draw_pile_size = 1, + }; + + struct CLIParms { + std::variant game {}; + boost::optional score_goal {}; + GameStateSpecType game_state_spec_type { GameStateSpecType::draw_pile_size }; + unsigned game_state_spec { 5 }; + boost::optional interactive {}; + bool quiet { false }; + // If this holds std::monostate, then all clue numbers are evaluated + std::variant clue_spec; + bool recursive { false }; + }; + + std::ostream & quiet_ostream(bool quiet); + + constexpr int download_failed = 1; + constexpr int state_unreachable = 2; + + std::optional parse_parms(int argc, char *argv[]); + int run_cli(CLIParms parms); +} +#endif //DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H diff --git a/include/download.h b/include/download.h index f9764e0..f4847b9 100644 --- a/include/download.h +++ b/include/download.h @@ -43,7 +43,7 @@ namespace Download { * draw pile hits the given size, whichever comes first * */ - Hanabi::Game get_game(std::variant game_spec, std::optional score_goal); + Hanabi::Game get_game(std::variant game_spec, std::optional score_goal); } // namespace Download diff --git a/include/game_state.h b/include/game_state.h index 344d574..62c91fd 100644 --- a/include/game_state.h +++ b/include/game_state.h @@ -266,6 +266,8 @@ struct Game { bool goto_draw_pile_size(size_t draw_pile_break); bool goto_turn(size_t turn); + bool holds_state(); + std::unique_ptr state; std::vector actions; std::vector deck; diff --git a/include/null_buffer.h b/include/null_buffer.h new file mode 100644 index 0000000..2fb791f --- /dev/null +++ b/include/null_buffer.h @@ -0,0 +1,25 @@ +#ifndef DYNAMIC_PROGRAM_NULL_BUFFER_H +#define DYNAMIC_PROGRAM_NULL_BUFFER_H + +#include + +namespace NullBuffer { + +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; +}; + +NullStream null_stream; + +} + + +#endif //DYNAMIC_PROGRAM_NULL_BUFFER_H diff --git a/src/command_line_interface.cpp b/src/command_line_interface.cpp new file mode 100644 index 0000000..d1f7074 --- /dev/null +++ b/src/command_line_interface.cpp @@ -0,0 +1,264 @@ +#include "command_line_interface.h" +#include "download.h" +#include "null_buffer.h" +#include "state_explorer.h" +#include "boost/program_options.hpp" + +namespace bpo = boost::program_options; + +namespace Hanabi { + + template + std::optional convert_optional(boost::optional val) + { + if (not val.has_value()) + { + return std::nullopt; + } + return { std::move(val.value()) }; + } + + std::ostream& quiet_ostream(bool const quiet) + { + if (quiet) + { + return NullBuffer::null_stream; + } + else + { + return std::cout; + } + } + + int run_cli(CLIParms parms) + { + // We want to do this sanity check here again, + // so that the run_cli method itself can ensure that arguments are fully valid + // and we cannot run into crashes due to bad specified parameters + if (parms.recursive and std::holds_alternative(parms.clue_spec) and std::get(parms.clue_spec) != 0) + { + throw std::logic_error("Cannot use nonzero clue modifier together with recursive evaluation mode."); + } + + // For convenience, we use a custom output stream, + // which is either std:cout or an ostream that does nothing. + // This enables us to easily respect the 'quiet option' without + // too much logic overhead. + std::ostream & quiet_os = quiet_ostream(parms.quiet); + quiet_os.precision(10); + + // Convert unset option to useful default, depending on some other options + if (not parms.interactive.has_value()) + { + parms.interactive = !(parms.quiet or parms.recursive); + } + + // Load game, either from file or from hanab.live + Game game = Download::get_game(parms.game, convert_optional(parms.score_goal)); + if (not game.holds_state()) + { + if(std::holds_alternative(parms.game)) { + std::cout << "Failed to download game " << std::get(parms.game) << " from hanab.live." << std::endl; + } else { + std::cout << "Failed to open file " << std::get(parms.game) << "." << std::endl; + } + return download_failed; + } + + // Go to specified game state + bool reached_state; + switch (parms.game_state_spec_type) + { + case GameStateSpecType::turn: + reached_state = game.goto_turn(parms.game_state_spec); + break; + case GameStateSpecType::draw_pile_size: + reached_state = game.goto_draw_pile_size(parms.game_state_spec); + break; + default: + throw std::logic_error("Invalid game state specification type encountered"); + } + + // In some cases, it is not possible to reach the specified game state, + // because the game simply does not contain enough actions for a specific turn + // or draw pile size to be reached. In this case, we can't analyze anything. + if (not reached_state) + { + switch (parms.game_state_spec_type) + { + case GameStateSpecType::turn: + std::cout << "Specified turn number of "; + break; + case GameStateSpecType::draw_pile_size: + std::cout << "Specified draw pile size of "; + break; + default: + throw std::logic_error("Invalid game state specification type encountered"); + } + std::cout << parms.game_state_spec << " cannot be reached with specified replay." << std::endl; + return state_unreachable; + } + + // Adjust clues now. Note that we already checked that this does nothing + // in case 'recursive' is specified, so further game actions will still + // be legal in that case. + if (std::holds_alternative(parms.clue_spec)) + { + game.state->modify_clues(std::get(parms.clue_spec)); + } + + // We are ready to start backtracking now + game.state->init_backtracking_information(); + + if (parms.recursive) + { + // Regardless of whether the game state was specified by turn or by the number of cards in the + // draw pile, we will always want to iterate with respect to draw pile size here: + // This already evaluates all intermediate game states as well, because stalling is an option + // (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(); + for(size_t remaining_cards = 1; remaining_cards <= max_draw_pile_size; remaining_cards++) { + if (!game.goto_draw_pile_size(remaining_cards)) + { + std::cout << "The given draw pile size (" << remaining_cards << ") cannot be obtained with the specified replay." << std::endl; + continue; + }; + if (std::holds_alternative(parms.clue_spec)) + { + // Here, it is important that we keep track of the correct number of clues: + // When modifying the game state, we want to reset to the actual number of clues + // to ensure that actions taken are legal. + clue_t const original_num_clues = game.state->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; + } + game.state->set_clues(original_num_clues); + } 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; + } + } + } + else + { + quiet_os << "Analysing state: \n\n" << *game.state << "\n" << std::endl; + auto const start = std::chrono::high_resolution_clock::now(); + probability_t const result = game.state->evaluate_state(); + auto const end = std::chrono::high_resolution_clock::now(); + + std::chrono::milliseconds const duration = std::chrono::duration_cast(end - start); + + // Output information now + std::cout << "Probability with optimal play: "; + print_probability(std::cout, result) << std::endl; + quiet_os << "Took " << duration.count() << "ms." << std::endl; + quiet_os << "Visited " << game.state->enumerated_states() << " states." << std::endl; + quiet_os << "Enumerated " << game.state->position_tablebase().size() << " unique game states. " << std::endl; + + // If specified, we can now launch the interactive shell + if (parms.interactive) + { + quiet_os << "\nDropping into interactive command line to explore result (type 'help'):" << std::endl; + auto game_shared = std::shared_ptr(game.state.release()); + cli(game_shared); + } + } + return 0; + }; + + std::optional parse_parms(int argc, char *argv[]) + { + CLIParms parms; + bpo::options_description desc("Allowed options"); + desc.add_options() + ("help", "print this help message") + ("game,g", bpo::value(), "Game ID from hanab.live") + ("file,f", bpo::value(), "Input file containing game in hanab.live json format") + ("turn,t", bpo::value(&parms.game_state_spec), "Turn number of state to analyze. Turn 1 means no actions have been taken.") + ("draw,d", bpo::value(&parms.game_state_spec), "Draw pile size of state to analyze.") + ("score,s", bpo::value>(&parms.score_goal), "Score that counts as a win, i.e. is optimized for achieving.") + ("clue-modifier,c", bpo::value(), "Modification to the number of clues applied to selected game state.") + ("all-clues", "Solve instance for all clue counts in all positions") + ("interactive,i", bpo::value>(&parms.interactive), "Drop into interactive shell to explore game") + ("recursive,r", "Print probabilities for all further sizes of draw pile.") + ("quiet,q", "Deactivate all non-essential prints") + ; + bpo::variables_map vm; + bpo::store(bpo::parse_command_line(argc, argv, desc), vm); + bpo::notify(vm); + + [[maybe_unused]] auto count = vm.count("clue-modifier"); + + if (vm.count("help")) { + std::cout << desc << std::endl; + std::cout << "You may not specify both --turn and --draw at the same time.\n"; + std::cout << "If none of them is specified, it is assumed that --draw 5 was given." << std::endl; + return std::nullopt; + } + + 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; + } + if (vm.count("file")) + { + parms.game = vm["file"].as(); + } + if (vm.count("game")) + { + parms.game = vm["game"].as(); + } + + // Parse game state options + if (vm.count("draw") + 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; + } + if (vm.count("turn")) + { + parms.game_state_spec_type = GameStateSpecType::turn; + } + if (vm.count("draw")) + { + parms.game_state_spec_type = GameStateSpecType::draw_pile_size; + } + + // Parse clue change options + if (vm.count("clue-modifier") + vm.count("all-clues") >= 2) + { + std::cout << "Conflicting options --clue-modifier and --all-clues." << std::endl; + std::cout << "Use '--help' to print a help message." << std::endl; + return std::nullopt; + } + if (vm.count("clue-modifier")) + { + [[maybe_unused]] auto c = vm.count("clue-modifier"); + parms.clue_spec = vm["clue-modifier"].as(); + } + if (vm.count("all-clues")) + { + parms.clue_spec = std::monostate(); + } + + // Parse opt-in bool options + parms.recursive = vm.count("recursive") > 0; + parms.quiet = vm.count("quiet") > 0; + + if (parms.recursive and std::holds_alternative(parms.clue_spec) and std::get(parms.clue_spec) != 0) + { + std::cout << "Conflicting options --recursive and --clue-modifier" << std::endl; + std::cout << "Use '--help' to print a help message." << std::endl; + return std::nullopt; + } + + // If nothing went wrong, we correctly parsed the parms now. + return parms; + } +} diff --git a/src/download.cpp b/src/download.cpp index 632c0c7..79c3a69 100644 --- a/src/download.cpp +++ b/src/download.cpp @@ -172,12 +172,12 @@ namespace Download { } } - Hanabi::Game get_game(std::variant game_spec, std::optional score_goal){ + Hanabi::Game get_game(std::variant game_spec, std::optional score_goal){ const std::optional game_json_opt = [&game_spec]() { if (game_spec.index() == 0) { return download_game_json(std::get(game_spec)); } else { - return open_game_json(std::get(game_spec)); + return open_game_json(std::get(game_spec).c_str()); } }(); diff --git a/src/game_state.cpp b/src/game_state.cpp index 2bd509c..a3ead9e 100644 --- a/src/game_state.cpp +++ b/src/game_state.cpp @@ -72,6 +72,11 @@ namespace Hanabi { return next_action + 1 == turn; } + bool Game::holds_state() + { + return state != nullptr; + } + bool Game::goto_draw_pile_size(size_t draw_pile_break) { while (state->draw_pile_size() > draw_pile_break and next_action < actions.size()) { diff --git a/src/main.cpp b/src/main.cpp index 7d4bb80..cc9e2cf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,154 +1,12 @@ -#include -#include -#include -#include - -#include - -#include "game_state.h" -#include "download.h" -#include "state_explorer.h" - - -namespace Hanabi { - void analyze_game_and_start_cli(std::variant game_id, int turn, int draw_pile_size, std::optional score_goal, - bool start_cli, bool print_remaining_states, bool quiet, clue_t clue_modifier, bool all_clues) { - auto game = Download::get_game(game_id, score_goal); - if (game.state == nullptr) { - if(game_id.index() == 0) { - std::cout << "Failed to download game " << std::get(game_id) << " from hanab.live." << std::endl; - } else { - std::cout << "Failed to open file " << std::get(game_id) << "." << std::endl; - } - return; - } - - game.goto_draw_pile_size(draw_pile_size); - if (draw_pile_size != 0 and game.state->draw_pile_size() != static_cast(draw_pile_size)) { - std::cout << "The given draw pile size (" << draw_pile_size << ") cannot be obtained with the specified replay." << std::endl; - return; - } - game.state->modify_clues(clue_modifier); - - if (not quiet) { - std::cout << "Analysing state: " << std::endl << std::endl << *game.state << std::endl; - std::cout << std::endl; - } - game.state->init_backtracking_information(); - auto start = std::chrono::high_resolution_clock::now(); - boost::rational result; - if (not print_remaining_states) { - result = game.state->evaluate_state(); - } - auto end = std::chrono::high_resolution_clock::now(); - - std::cout.precision(10); - - if (not print_remaining_states) { - std::cout << "Probability with optimal play: "; - print_probability(std::cout, result) << std::endl; - } - if (not quiet) { - std::cout << "Took " << std::chrono::duration_cast(end - start) << "." << std::endl; - std::cout << "Visited " << game.state->enumerated_states() << " states." << std::endl; - std::cout << "Enumerated " << game.state->position_tablebase().size() << " unique game states. " << std::endl; - } - - if (print_remaining_states) - { - size_t const max_draw_pile_size = game.state->draw_pile_size(); - for(size_t remaining_cards = 1; remaining_cards <= max_draw_pile_size; remaining_cards++) { - if (!game.goto_draw_pile_size(remaining_cards)) - { - std::cout << "The given draw pile size (" << remaining_cards << ") cannot be obtained with the specified replay." << std::endl; - continue; - }; - if (all_clues) { - clue_t original_num_clues = game.state->num_clues(); - for(clue_t num_clues = 0; num_clues <= 8; num_clues++) { - game.state->set_clues(num_clues); - 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; - } - game.state->set_clues(original_num_clues); - } else { - result = game.state->evaluate_state(); - std::cout << "Probability with " << remaining_cards << " cards left in deck: "; - print_probability(std::cout, result) << std::endl; - } - } - } - - if (start_cli) { - std::cout << std::endl; - std::cout << "Dropping into interactive command line to explore result (type 'help'):" << std::endl; - auto game_shared = std::shared_ptr(game.state.release()); - auto states = game_shared->possible_next_states(0, false); - cli(game_shared); - } - } -} - +#include +#include "command_line_interface.h" int main(int argc, char *argv[]) { - int turn = 100; - int draw_pile_size = 0; - std::optional score; - bool interactive_shell; - int clue_modifier = 0; - - boost::program_options::options_description desc("Allowed options"); - desc.add_options() - ("help", "print this help message") - ("id,g", boost::program_options::value(), "Game ID from hanab.live") - ("file,f", boost::program_options::value(), "Input file containing game in hanab.live json format") - ("turn,t", boost::program_options::value(&turn), "Turn number of state to analyze. Turn 1 means no actions have been taken.") - ("draw,d", boost::program_options::value(&draw_pile_size), "Draw pile size of state to analyze.") - ("score,s", boost::program_options::value(), "Score that counts as a win, i.e. is optimized for achieving.") - ("interactive,i", boost::program_options::value(&interactive_shell)->default_value(true), "Drop into interactive shell to explore game") - ("remaining-states,r", "Print probabilities for all further sizes of draw pile.") - ("clue-modifier,c", boost::program_options::value(&clue_modifier)->default_value(0), "Modification to the number of clues applied to selected game state.") - ("all-clues", "Solve instance for all clue counts in all positions") - ("quiet,q", "Deactivate all non-essential prints") - ; - boost::program_options::variables_map vm; - boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); - boost::program_options::notify(vm); - - if (vm.count("help")) { - std::cout << desc << std::endl; - return EXIT_SUCCESS; - } - - if (vm.count("file") + vm.count("id") != 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 EXIT_SUCCESS; - } - - if (vm.count("remaining-states") and clue_modifier != 0) { - std::cout << "You cannot use a clue modifier and solve the remaining states, the further game progress might be impossible with modified clue count." << std::endl; - std::cout << "Use '--help' to print a help message." << std::endl; - return EXIT_SUCCESS; - } - - if (vm.count("draw") + vm.count("turn") != 1) { - std::cout << "Exactly one option of 'draw' and 'turn' has to be given." << std::endl; - std::cout << "Use '--help' to print a help message." << std::endl; - return EXIT_SUCCESS; - } - - if (vm.count("score")) { - score = vm["score"].as(); - } - - if (vm.count("file")) { - Hanabi::analyze_game_and_start_cli(vm["file"].as().c_str(), turn, draw_pile_size, score, interactive_shell, vm.count("remaining-states"), vm.count("quiet"), clue_modifier, vm.count("all-clues")); - } else { - Hanabi::analyze_game_and_start_cli(vm["id"].as(), turn, draw_pile_size, score, interactive_shell, vm.count("remaining-states"), vm.count("quiet"), clue_modifier, vm.count("all-clues")); + std::optional parms = Hanabi::parse_parms(argc, argv); + if (parms.has_value()) + { + return Hanabi::run_cli(parms.value()); } return EXIT_SUCCESS; }