Endgame-Analyzer/src/command_line_interface.cpp

364 lines
15 KiB
C++

#include <boost/program_options.hpp>
#include "download.h"
#include "null_buffer.h"
#include "state_explorer.h"
#include <version.h>
#include "command_line_interface.h"
#include "myassert.h"
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 const & parms)
{
if (parms.version_info) {
std::cout << "endgame-analyzer " << Version::git_description << " (commit ";
std::cout.write(Version::git_sha1.data(), 8) << ")" << std::endl;
if (Version::is_dirty())
{
std::cout << "Warning: The repository contains local changes that are not part of the build." << std::endl;
}
return EXIT_SUCCESS;
}
// 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) != clue_t(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);
// Load game, either from file or from hanab.live
Game game = [&parms] {
if (std::holds_alternative<int>(parms.game))
{
return Download::get_game(std::get<int>(parms.game), convert_optional(parms.score_goal), parms.save_memory);
}
else
{
return Download::get_game(std::get<std::string>(parms.game), convert_optional(parms.score_goal), parms.save_memory);
}
}();
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;
std::cout << "Replay ends at turn " << game.cur_turn() << " with score of " << game.state->score() << "."
<< 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();
quiet_os << "Base state specified:\n" << *game.state << std::endl;
if (parms.list_actions) {
unsigned max_turn = game.cur_turn();
// Note this loop is safe even for unsigned max_turn, since turn() is at least 1.
for (unsigned turn = game.num_turns(); turn >= max_turn; turn--) {
bool reached = game.goto_turn(turn);
game.state->evaluate_state();
ASSERT(reached);
for (auto const & [action, probability] : game.state->get_reasonable_actions(true, false)) {
std::cout << "Turn " << turn << ", " << action << ": ";
print_probability(std::cout, probability) << std::endl;
}
}
quiet_os << "Enumerated " << game.state->position_tablebase().size() << " states." << std::endl;
return EXIT_SUCCESS;
}
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();
bool printed_replay_end_msg = false;
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 << "Specified draw pile size of " << game.state->draw_pile_size() - 1
<< " 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;
printed_replay_end_msg = true;
}
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 <= clue_t(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;
std::cout << *game.state << std::endl;
auto const [a, b] = game.state->dump_unique_id_parts();
for (auto elem: a)
{
std::cout << elem << ", ";
}
std::cout << "-> " << game.state->unique_id() << 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 << "\nAnalysing 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.value_or(!parms.quiet))
{
quiet_os << "\nDropping into interactive command line to explore result (type 'help'):" << std::endl;
cli(game);
}
return EXIT_SUCCESS;
}
std::optional<CLIParms> parse_parms(int argc, char *argv[])
{
CLIParms parms;
bpo::options_description desc("Allowed program options");
desc.add_options()
("help,h", "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-pile-size,d", bpo::value<unsigned>(&parms.game_state_spec), "Draw pile size of state to analyze. Must be at most 20.")
("score-goal,s"
, bpo::value<boost::optional<unsigned int>>(&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 "
"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")
("list-actions,l","List all actions (including suboptimal ones) of all turns after specified state.")
("all-clues", "Whenever evaluating a game state, evaluate it with all clue counts and output their "
"probabilities.")
("save-memory", "Reduce memory consumption by roughly 50%. This results in roughly 5 times as much execution time, so use this only if you are really short on memory.")
("quiet,q", "Deactivate all non-essential prints. Useful if output is parsed by another program.")
("version,v", "Print version information and exit.");
bpo::variables_map vm;
bpo::store(bpo::parse_command_line(argc, argv, desc), vm);
bpo::notify(vm);
if (vm.count("version"))
{
parms.version_info = true;
return parms;
}
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"
"Note that this is thus not a legal Hanabi strategy, but provides an upper bound on\n"
"any legal strategy. Experience shows that in real-world games (with easy variants),\n"
"the computed winning percentages are often not far from what humans (or programs)\n"
"can achieve when playing well, though there are certainly counterexamples.\n"
"Therefore, treat the displayed winning percentages with care, since they might not\n"
"be realistic for real-world play in some cases.\n" << std::endl;
std::cout << desc << std::endl;
std::cout << "You have to either specify --game or --file as a data source.\n";
std::cout << "You may not specify both --turn and --draw at the same time.\n";
std::cout << "You may not specifiy both --recursive and --list-actions at the same time.\n";
return std::nullopt;
}
// Parse file or game id specification, ensuring at most one is given
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 (turn or draw), ensuring at most one is given.
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;
}
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 = static_cast<clue_t>(vm["clue-modifier"].as<int>());
}
if (vm.count("all-clues"))
{
parms.clue_spec = std::monostate();
}
if (vm.count("recursive") + vm.count("list-actions") >= 2)
{
std::cout << "Conflicting options --recursive and --list-actions specified." << std::endl;
std::cout << "Use '--help' to print a help message." << std::endl;
return std::nullopt;
}
// Parse opt-in bool options
parms.recursive = vm.count("recursive") > 0;
parms.list_actions = vm.count("list-actions") > 0;
parms.quiet = vm.count("quiet") > 0;
parms.save_memory = vm.count("save-memory") > 0;
if (parms.recursive and std::holds_alternative<clue_t>(parms.clue_spec) and std::get<clue_t>(parms.clue_spec) != clue_t(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;
}
}