Rework CLI

This should cover all use cases / exceptions now and is in a reasonably
good code state.
This commit is contained in:
Maximilian Keßler 2023-11-12 17:30:44 +01:00
parent 3f69d0ac71
commit 32af52ae9e
Signed by: max
GPG key ID: BCC5A619923C0BA5
9 changed files with 344 additions and 152 deletions

View file

@ -16,7 +16,10 @@ include_directories(.)
include_directories(${Boost_INCLUDE_DIR}) include_directories(${Boost_INCLUDE_DIR})
add_executable(endgame-analyzer src/main.cpp src/state_explorer.cpp src/download.cpp 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 cpr)
target_link_libraries(endgame-analyzer Boost::program_options) target_link_libraries(endgame-analyzer Boost::program_options)

View file

@ -0,0 +1,35 @@
#ifndef DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H
#define DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H
#include <variant>
#include <boost/optional.hpp>
#include "game_state.h"
namespace Hanabi {
enum class GameStateSpecType {
turn = 0,
draw_pile_size = 1,
};
struct CLIParms {
std::variant<int, std::string> game {};
boost::optional<uint8_t> score_goal {};
GameStateSpecType game_state_spec_type { GameStateSpecType::draw_pile_size };
unsigned game_state_spec { 5 };
boost::optional<bool> interactive {};
bool quiet { false };
// If this holds std::monostate, then all clue numbers are evaluated
std::variant<std::monostate, clue_t> clue_spec;
bool recursive { false };
};
std::ostream & quiet_ostream(bool quiet);
constexpr int download_failed = 1;
constexpr int state_unreachable = 2;
std::optional<CLIParms> parse_parms(int argc, char *argv[]);
int run_cli(CLIParms parms);
}
#endif //DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H

View file

@ -43,7 +43,7 @@ namespace Download {
* draw pile hits the given size, whichever comes first * draw pile hits the given size, whichever comes first
* *
*/ */
Hanabi::Game get_game(std::variant<int, const char*> game_spec, std::optional<uint8_t> score_goal); Hanabi::Game get_game(std::variant<int, std::string> game_spec, std::optional<uint8_t> score_goal);
} // namespace Download } // namespace Download

View file

@ -266,6 +266,8 @@ struct Game {
bool goto_draw_pile_size(size_t draw_pile_break); bool goto_draw_pile_size(size_t draw_pile_break);
bool goto_turn(size_t turn); bool goto_turn(size_t turn);
bool holds_state();
std::unique_ptr<HanabiStateIF> state; std::unique_ptr<HanabiStateIF> state;
std::vector<Action> actions; std::vector<Action> actions;
std::vector<Card> deck; std::vector<Card> deck;

25
include/null_buffer.h Normal file
View file

@ -0,0 +1,25 @@
#ifndef DYNAMIC_PROGRAM_NULL_BUFFER_H
#define DYNAMIC_PROGRAM_NULL_BUFFER_H
#include <ostream>
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

View file

@ -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<class T>
std::optional<T> convert_optional(boost::optional<T> 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<clue_t>(parms.clue_spec) and std::get<clue_t>(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<int>(parms.game)) {
std::cout << "Failed to download game " << std::get<int>(parms.game) << " from hanab.live." << std::endl;
} else {
std::cout << "Failed to open file " << std::get<std::string>(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<clue_t>(parms.clue_spec))
{
game.state->modify_clues(std::get<clue_t>(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<std::monostate>(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<std::chrono::milliseconds>(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<HanabiStateIF>(game.state.release());
cli(game_shared);
}
}
return 0;
};
std::optional<CLIParms> 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<int>(), "Game ID from hanab.live")
("file,f", bpo::value<std::string>(), "Input file containing game in hanab.live json format")
("turn,t", bpo::value<unsigned>(&parms.game_state_spec), "Turn number of state to analyze. Turn 1 means no actions have been taken.")
("draw,d", bpo::value<unsigned>(&parms.game_state_spec), "Draw pile size of state to analyze.")
("score,s", bpo::value<boost::optional<uint8_t>>(&parms.score_goal), "Score that counts as a win, i.e. is optimized for achieving.")
("clue-modifier,c", bpo::value<int>(), "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<boost::optional<bool>>(&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<std::string>();
}
if (vm.count("game"))
{
parms.game = vm["game"].as<int>();
}
// 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<clue_t>();
}
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<clue_t>(parms.clue_spec) and std::get<clue_t>(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;
}
}

View file

@ -172,12 +172,12 @@ namespace Download {
} }
} }
Hanabi::Game get_game(std::variant<int, const char*> game_spec, std::optional<uint8_t> score_goal){ Hanabi::Game get_game(std::variant<int, std::string> game_spec, std::optional<uint8_t> score_goal){
const std::optional<boost::json::object> game_json_opt = [&game_spec]() { const std::optional<boost::json::object> game_json_opt = [&game_spec]() {
if (game_spec.index() == 0) { if (game_spec.index() == 0) {
return download_game_json(std::get<int>(game_spec)); return download_game_json(std::get<int>(game_spec));
} else { } else {
return open_game_json(std::get<const char *>(game_spec)); return open_game_json(std::get<std::string>(game_spec).c_str());
} }
}(); }();

View file

@ -72,6 +72,11 @@ namespace Hanabi {
return next_action + 1 == turn; return next_action + 1 == turn;
} }
bool Game::holds_state()
{
return state != nullptr;
}
bool Game::goto_draw_pile_size(size_t draw_pile_break) 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()) {

View file

@ -1,154 +1,12 @@
#include <iostream> #include <optional>
#include <vector> #include "command_line_interface.h"
#include <variant>
#include <chrono>
#include <boost/program_options.hpp>
#include "game_state.h"
#include "download.h"
#include "state_explorer.h"
namespace Hanabi {
void analyze_game_and_start_cli(std::variant<int, const char*> game_id, int turn, int draw_pile_size, std::optional<uint8_t> 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<int>(game_id) << " from hanab.live." << std::endl;
} else {
std::cout << "Failed to open file " << std::get<const char*>(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<size_t>(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<probability_base_type> 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<std::chrono::milliseconds>(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<HanabiStateIF>(game.state.release());
auto states = game_shared->possible_next_states(0, false);
cli(game_shared);
}
}
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
int turn = 100; std::optional<Hanabi::CLIParms> parms = Hanabi::parse_parms(argc, argv);
int draw_pile_size = 0; if (parms.has_value())
std::optional<int> score; {
bool interactive_shell; return Hanabi::run_cli(parms.value());
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<int>(), "Game ID from hanab.live")
("file,f", boost::program_options::value<std::string>(), "Input file containing game in hanab.live json format")
("turn,t", boost::program_options::value<int>(&turn), "Turn number of state to analyze. Turn 1 means no actions have been taken.")
("draw,d", boost::program_options::value<int>(&draw_pile_size), "Draw pile size of state to analyze.")
("score,s", boost::program_options::value<int>(), "Score that counts as a win, i.e. is optimized for achieving.")
("interactive,i", boost::program_options::value<bool>(&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<int>(&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<int>();
}
if (vm.count("file")) {
Hanabi::analyze_game_and_start_cli(vm["file"].as<std::string>().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<int>(), turn, draw_pile_size, score, interactive_shell, vm.count("remaining-states"), vm.count("quiet"), clue_modifier, vm.count("all-clues"));
} }
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }