diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a29e0e..b19b41c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,8 @@ find_package(Boost 1.81 COMPONENTS program_options REQUIRED) include_directories(.) include_directories(${Boost_INCLUDE_DIR}) -add_executable(endgame-analyzer src/main.cpp src/cli_interface.cpp src/download.cpp) +add_executable(endgame-analyzer src/main.cpp src/cli_interface.cpp src/download.cpp + src/game_state.cpp) target_link_libraries(endgame-analyzer cpr) target_link_libraries(endgame-analyzer Boost::program_options) diff --git a/include/download.h b/include/download.h index c7b68a4..f9764e0 100644 --- a/include/download.h +++ b/include/download.h @@ -36,16 +36,14 @@ namespace Download { /** * @brief Create game object from given source * @param game_spec Either an id to download from hanab.live or a filename with a json specification - * @param turn Turn to skip to - * @param draw_pile_break Minimum draw pile size of produced game + * @param score_goal What score counts as a win for this game. If left empty, the maximum score is inserted. * @return Game state * * If both turn and draw_pile_break are specified, the game skips until the specified turn or the first time the * draw pile hits the given size, whichever comes first * - * @note Turns start counting at 1, since this is also the way hanab.live does it. */ - std::unique_ptr get_game(std::variant game_spec, unsigned turn = 1, size_t draw_pile_break = 0, std::optional score_goal = std::nullopt); + Hanabi::Game get_game(std::variant game_spec, std::optional score_goal); } // namespace Download diff --git a/include/game_state.h b/include/game_state.h index 5888e10..850f154 100644 --- a/include/game_state.h +++ b/include/game_state.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -251,6 +252,19 @@ protected: friend std::ostream& operator<<(std::ostream&, HanabiStateIF const&); }; +// A game mimics a game state together with a list of actions and allows to traverse the game +// history by making and reverting the stored actions. +struct Game { + void make_turn(); + void revert_turn(); + void forward_until(size_t turn = 100, size_t draw_pile_break = 0); + + + std::unique_ptr state; + std::vector actions; + unsigned next_action; +}; + inline std::ostream &operator<<(std::ostream &os, HanabiStateIF const &hanabi_state); template diff --git a/src/download.cpp b/src/download.cpp index 04c86dd..8240872 100644 --- a/src/download.cpp +++ b/src/download.cpp @@ -95,128 +95,75 @@ namespace Download { return boost::json::parse(game_json).as_object(); } - template - std::unique_ptr produce_state( - const std::vector& deck, - const std::vector& actions, - size_t start_turn, - size_t draw_pile_break = 0, - std::optional score_goal = std::nullopt - ) { + std::unique_ptr get_base_state( + std::size_t num_suits, + Hanabi::player_t num_players, + std::vector const & deck, + std::optional score_goal) { uint8_t actual_score_goal = score_goal.value_or(5 * num_suits); - auto game = std::unique_ptr(new Hanabi::HanabiState(deck, actual_score_goal)); - std::uint8_t index; - for (size_t i = 0; i < std::min(start_turn - 1, actions.size()); i++) { - if (game->draw_pile_size() == draw_pile_break) { - break; - } - switch(actions[i].type) { - case Hanabi::ActionType::color_clue: - case Hanabi::ActionType::rank_clue: - game->give_clue(); - break; - case Hanabi::ActionType::discard: - index = game->find_card_in_hand(deck[actions[i].target]); - ASSERT(index != std::uint8_t(-1)); - game->discard(index); - break; - case Hanabi::ActionType::play: - index = game->find_card_in_hand(deck[actions[i].target]); - ASSERT(index != std::uint8_t(-1)); - game->play(index); - break; - case Hanabi::ActionType::vote_terminate_players: - case Hanabi::ActionType::vote_terminate: - case Hanabi::ActionType::end_game: - return game; - } - } - game->init_backtracking_information(); - return game; - } - - std::unique_ptr get_game(std::variant game_spec, unsigned turn, size_t draw_pile_break, std::optional score_goal) { - const std::optional game_json_opt = [&game_spec]() { - if (game_spec.index() == 0) { - return download_game_json(std::get(game_spec)); - } else { - return open_game_json(std::get(game_spec)); - } - }(); - - if (!game_json_opt.has_value() or game_json_opt.value().empty()) { - return nullptr; - } - - const boost::json::object& game_json = game_json_opt.value(); - - const auto [deck, num_suits] = parse_deck(game_json.at("deck")); - const std::vector actions = parse_actions(game_json.at("actions")); - const size_t num_players = game_json.at("players").as_array().size(); - switch(num_players) { case 2: switch(num_suits) { case 3: - return produce_state<3,2,5>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<3,2,5>(deck, actual_score_goal)); case 4: - return produce_state<4,2,5>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<4,2,5>(deck, actual_score_goal)); case 5: - return produce_state<5,2,5>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<5,2,5>(deck, actual_score_goal)); case 6: - return produce_state<6,2,5>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<6,2,5>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } case 3: switch(num_suits) { case 3: - return produce_state<3,3,5>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<3,3,5>(deck, actual_score_goal)); case 4: - return produce_state<4,3,5>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<4,3,5>(deck, actual_score_goal)); case 5: - return produce_state<5,3,5>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<5,3,5>(deck, actual_score_goal)); case 6: - return produce_state<6,3,5>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<6,3,5>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } case 4: switch(num_suits) { case 3: - return produce_state<3,4,4>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<3,4,4>(deck, actual_score_goal)); case 4: - return produce_state<4,4,4>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<4,4,4>(deck, actual_score_goal)); case 5: - return produce_state<5,4,4>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<5,4,4>(deck, actual_score_goal)); case 6: - return produce_state<6,4,4>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<6,4,4>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } case 5: switch(num_suits) { case 3: - return produce_state<3,5,4>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<3,5,4>(deck, actual_score_goal)); case 4: - return produce_state<4,5,4>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<4,5,4>(deck, actual_score_goal)); case 5: - return produce_state<5,5,4>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<5,5,4>(deck, actual_score_goal)); case 6: - return produce_state<6,5,4>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<6,5,4>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } case 6: switch(num_suits) { case 3: - return produce_state<3,6,3>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<3,6,3>(deck, actual_score_goal)); case 4: - return produce_state<4,6,3>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<4,6,3>(deck, actual_score_goal)); case 5: - return produce_state<5,6,3>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<5,6,3>(deck, actual_score_goal)); case 6: - return produce_state<6,6,3>(deck, actions, turn, draw_pile_break, score_goal); + return std::unique_ptr(new Hanabi::HanabiState<6,6,3>(deck, actual_score_goal)); default: throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); } @@ -225,4 +172,37 @@ namespace Download { } } + Hanabi::Game get_game(std::variant game_spec, std::optional score_goal){ + const std::optional game_json_opt = [&game_spec]() { + if (game_spec.index() == 0) { + return download_game_json(std::get(game_spec)); + } else { + return open_game_json(std::get(game_spec)); + } + }(); + + if (!game_json_opt.has_value() or game_json_opt.value().empty()) { + return {nullptr, {}, 0}; + } + + const boost::json::object& game_json = game_json_opt.value(); + + const auto [deck, num_suits] = parse_deck(game_json.at("deck")); + const size_t num_players = game_json.at("players").as_array().size(); + + // Convert the actions from hanab.live format into local format used + const std::vector hanab_live_actions = parse_actions(game_json.at("actions")); + std::vector actions; + std::transform( + hanab_live_actions.begin(), + hanab_live_actions.end(), + std::back_inserter(actions), + [&deck](Action const & action){ + return Hanabi::Action {action.type, deck[action.target]}; + } + ); + + return {get_base_state(num_suits, num_players, deck, score_goal), actions, 0}; + } + } // namespace Download diff --git a/src/main.cpp b/src/main.cpp index 9b83e10..365eb00 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,8 +13,8 @@ namespace Hanabi { void analyze_game_and_start_cli(std::variant game_id, int turn, int draw_pile_size, std::optional score_goal, bool start_cli) { - auto game = Download::get_game(game_id, turn, draw_pile_size, score_goal); - if (game == nullptr) { + 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(game_id) << " from hanab.live." << std::endl; } else { @@ -23,9 +23,12 @@ namespace Hanabi { return; } - std::cout << "Analysing state: " << std::endl << std::endl << *game << std::endl; + game.forward_until(turn, draw_pile_size); + + std::cout << "Analysing state: " << std::endl << std::endl << *game.state << std::endl; + game.state->init_backtracking_information(); auto start = std::chrono::high_resolution_clock::now(); - auto res = game->evaluate_state(); + auto res = game.state->evaluate_state(); auto end = std::chrono::high_resolution_clock::now(); std::cout.precision(10); @@ -33,13 +36,13 @@ namespace Hanabi { std::cout << "Probability with optimal play: "; print_probability(std::cout, res) << std::endl; std::cout << "Took " << std::chrono::duration_cast(end - start) << "." << std::endl; - std::cout << "Visited " << game->enumerated_states() << " states." << std::endl; - std::cout << "Enumerated " << game->position_tablebase().size() << " unique game states. " << 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 (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(game.release()); + auto game_shared = std::shared_ptr(game.state.release()); auto states = game_shared->possible_next_states(0, false); cli(game_shared); }