From 6ce692a06f43791b06e8ff08c5925530e7737178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Wed, 15 Nov 2023 16:53:49 +0100 Subject: [PATCH] improve program output and CLI documentation --- include/command_line_interface.h | 60 +++++++++++++++++++++++++++++++- src/command_line_interface.cpp | 49 ++++++++++++++++++-------- 2 files changed, 93 insertions(+), 16 deletions(-) diff --git a/include/command_line_interface.h b/include/command_line_interface.h index b990a7b..9ab9f87 100644 --- a/include/command_line_interface.h +++ b/include/command_line_interface.h @@ -13,23 +13,81 @@ namespace Hanabi { }; 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 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 score_goal {}; + + /** + * Whether game_state_spec denotes a turn number or 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 }; + + /** + * Whether to launch an interactive exploration shell for the game state after performing analysis. + */ boost::optional interactive {}; + + /** + * If true, deactivates non-essential output (to cout). + */ 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 clue_spec {static_cast(0)}; + + /** + * If true, then all states corresponding to smaller draw pile sizes + */ 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); constexpr int download_failed = 1; 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 parse_parms(int argc, char *argv[]); + + /** + * @brief Execute parsed parameters. + */ int run_cli(CLIParms parms); } #endif //DYNAMIC_PROGRAM_COMMAND_LINE_INTERFACE_H diff --git a/src/command_line_interface.cpp b/src/command_line_interface.cpp index a6e465c..d0cc284 100644 --- a/src/command_line_interface.cpp +++ b/src/command_line_interface.cpp @@ -187,33 +187,52 @@ namespace Hanabi { std::optional parse_parms(int argc, char *argv[]) { CLIParms parms; - bpo::options_description desc("Allowed options"); + bpo::options_description desc("Allowed program 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") + ("help,h", "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-pile-size,d", bpo::value(&parms.game_state_spec), "Draw pile size of state to analyze.") + ("score-goal,s", bpo::value>(&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(), "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>(&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::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 << "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 << "If none of them is specified, it is assumed that --draw 5 was given." << std::endl; 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; @@ -228,7 +247,7 @@ namespace Hanabi { parms.game = vm["game"].as(); } - // 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) { std::cout << "Conflicting options --draw and --turn." << std::endl; std::cout << "Use '--help' to print a help message." << std::endl;