364 lines
15 KiB
C++
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;
|
|
}
|
|
}
|