improve program output and CLI documentation

This commit is contained in:
Maximilian Keßler 2023-11-15 16:53:49 +01:00
parent ac4cf5e797
commit 6ce692a06f
Signed by: max
GPG key ID: BCC5A619923C0BA5
2 changed files with 93 additions and 16 deletions

View file

@ -13,23 +13,81 @@ namespace Hanabi {
}; };
struct CLIParms { struct CLIParms {
/**
* The data source for the game to be analysed.
* If of type int, assumed to be a game ID from hanab.live
* If of type string, assumed to be a (relative) filename to a game in hanab.live json format,
* see https://raw.githubusercontent.com/Hanabi-Live/hanabi-live/main/misc/example_game_with_comments.jsonc
* for a format specification.
*/
std::variant<int, std::string> game {}; std::variant<int, std::string> game {};
/**
* Definition of a 'winning' game, i.e. what score (number of cards played) is considered
* to be winning.
* If std::nullopt, then the maximum score of the game will be used.
*/
boost::optional<uint8_t> score_goal {}; boost::optional<uint8_t> score_goal {};
/**
* Whether game_state_spec denotes a turn number or draw pile size.
*/
GameStateSpecType game_state_spec_type { GameStateSpecType::draw_pile_size }; GameStateSpecType game_state_spec_type { GameStateSpecType::draw_pile_size };
/**
* Either a turn number or a draw pile size, depending on game_state_spec_type.
* If this represents a draw pile size, we mean the first turn of the game where the draw pile
* had the given size, i.e. the turn immediately after drawing a card.
* If this represents a turn, we just mean the corresponding turn number, starting numbering at 1
* (since this is also the case on hanab.live)
* Thus, a turn number of 0 is undefined and a turn number of 1 corresponds to no actions taken in the game.
*/
unsigned game_state_spec { 5 }; unsigned game_state_spec { 5 };
/**
* Whether to launch an interactive exploration shell for the game state after performing analysis.
*/
boost::optional<bool> interactive {}; boost::optional<bool> interactive {};
/**
* If true, deactivates non-essential output (to cout).
*/
bool quiet { false }; bool quiet { false };
// If this holds std::monostate, then all clue numbers are evaluated
/**
* If this holds std::monostate, then all clue numbers are evaluated.
* Otherwise, the specified clue modifier is applied (relative to actual number of clues).
* Thus, setting this to 0 has no effect.
*/
std::variant<std::monostate, clue_t> clue_spec {static_cast<clue_t>(0)}; std::variant<std::monostate, clue_t> clue_spec {static_cast<clue_t>(0)};
/**
* If true, then all states corresponding to smaller draw pile sizes
*/
bool recursive { false }; bool recursive { false };
}; };
/**
* @brief Get an output stream that is std::cout or a Null-Stream.
* @param quiet If true, NullStream is returned, otherwise std::cout
*/
std::ostream & quiet_ostream(bool quiet); std::ostream & quiet_ostream(bool quiet);
constexpr int download_failed = 1; constexpr int download_failed = 1;
constexpr int state_unreachable = 2; constexpr int state_unreachable = 2;
/**
* Parse parameters from command-line arguments.
* @param argc Number of arguments
* @param argv Array of C-Style strings, where argv[0] is ignored (as the program name).
*
* @note This is exactly the signature of the main() method
*/
std::optional<CLIParms> parse_parms(int argc, char *argv[]); std::optional<CLIParms> parse_parms(int argc, char *argv[]);
/**
* @brief Execute parsed parameters.
*/
int run_cli(CLIParms parms); int run_cli(CLIParms parms);
} }
#endif //DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H #endif //DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H

View file

@ -187,33 +187,52 @@ namespace Hanabi {
std::optional<CLIParms> parse_parms(int argc, char *argv[]) std::optional<CLIParms> parse_parms(int argc, char *argv[])
{ {
CLIParms parms; CLIParms parms;
bpo::options_description desc("Allowed options"); bpo::options_description desc("Allowed program options");
desc.add_options() desc.add_options()
("help", "print this help message") ("help,h", "Print this help message.")
("game,g", bpo::value<int>(), "Game ID from hanab.live") ("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") ("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.") ("turn,t", bpo::value<unsigned>(&parms.game_state_spec), "Turn number of state to analyze. "
("draw,d", bpo::value<unsigned>(&parms.game_state_spec), "Draw pile size of state to analyze.") "Turn 1 means no actions have been taken. ")
("score,s", bpo::value<boost::optional<uint8_t>>(&parms.score_goal), "Score that counts as a win, i.e. is optimized for achieving.") ("draw-pile-size,d", bpo::value<unsigned>(&parms.game_state_spec), "Draw pile size of state to analyze.")
("clue-modifier,c", bpo::value<int>(), "Modification to the number of clues applied to selected game state.") ("score-goal,s", bpo::value<boost::optional<uint8_t>>(&parms.score_goal),
("all-clues", "Solve instance for all clue counts in all positions") "Score that counts as a win, i.e. is optimized for achieving. If unspecified, the maximum possible "
("interactive,i", bpo::value<boost::optional<bool>>(&parms.interactive), "Drop into interactive shell to explore game") "score will be used.")
("recursive,r", "Print probabilities for all further sizes of draw pile.") ("clue-modifier,c", bpo::value<int>(), "Optional relative modification to the number of clues applied to "
("quiet,q", "Deactivate all non-essential prints") "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")
("all-clues", "Whenever evaluating a game state, evaluate it with all clue counts and output their "
"probabilities.")
("quiet,q", "Deactivate all non-essential prints. Useful if output is parsed by another program.")
; ;
bpo::variables_map vm; bpo::variables_map vm;
bpo::store(bpo::parse_command_line(argc, argv, desc), vm); bpo::store(bpo::parse_command_line(argc, argv, desc), vm);
bpo::notify(vm); bpo::notify(vm);
[[maybe_unused]] auto count = vm.count("clue-modifier");
if (vm.count("help")) { 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 << 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 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; std::cout << "If none of them is specified, it is assumed that --draw 5 was given." << std::endl;
return std::nullopt; return std::nullopt;
} }
// Parse file or game id specification, ensuring at most one is given
if (vm.count("file") + vm.count("game") != 1) { 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 << "Exactly one option of 'file' and 'id' has to be given." << std::endl;
std::cout << "Use '--help' to print a help message." << std::endl; std::cout << "Use '--help' to print a help message." << std::endl;
@ -228,7 +247,7 @@ namespace Hanabi {
parms.game = vm["game"].as<int>(); parms.game = vm["game"].as<int>();
} }
// Parse game state options // Parse game state options (turn or draw), ensuring at most one is given.
if (vm.count("draw") + vm.count("turn") != 1) { if (vm.count("draw") + vm.count("turn") != 1) {
std::cout << "Conflicting options --draw and --turn." << std::endl; std::cout << "Conflicting options --draw and --turn." << std::endl;
std::cout << "Use '--help' to print a help message." << std::endl; std::cout << "Use '--help' to print a help message." << std::endl;