diff --git a/CMakeLists.txt b/CMakeLists.txt index f5c4b61..4f8402b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ 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) +add_executable(endgame-analyzer src/main.cpp src/cli_interface.cpp src/download.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 7890ec3..285790e 100644 --- a/include/download.h +++ b/include/download.h @@ -2,7 +2,6 @@ #define DYNAMIC_PROGRAM_DOWNLOAD_H #include -#include #include #include #include @@ -11,230 +10,42 @@ #include "game_state.h" #include "myassert.h" -// This helper function deduces the type and assigns the value with the matching key -template -void extract(boost::json::object const &obj, T &t, std::string_view key) { - t = value_to(obj.at(key)); -} - namespace Hanabi { - Card tag_invoke(boost::json::value_to_tag, - boost::json::value const &jv) { - Hanabi::Card card{}; - boost::json::object const &obj = jv.as_object(); - extract(obj, card.rank, "rank"); - extract(obj, card.suit, "suitIndex"); - card.rank = 5 - card.rank; - return card; - } - - void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, Hanabi::Card const &card) { - jv = {{"suitIndex", card.suit}, - {"rank", card.rank}}; - } + Card tag_invoke(boost::json::value_to_tag, boost::json::value const &jv); + void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, Hanabi::Card const &card); } namespace Download { struct Action { Hanabi::ActionType type{}; - uint8_t target; + uint8_t target{}; }; - Action tag_invoke(boost::json::value_to_tag, boost::json::value const &jv) { - Action action{}; - uint8_t type; - boost::json::object const &obj = jv.as_object(); + Action tag_invoke(boost::json::value_to_tag, boost::json::value const &jv); - extract(obj, action.target, "target"); - extract(obj, type, "type"); + std::pair, Hanabi::rank_t> parse_deck(const boost::json::value &deck_json); - action.type = static_cast(type); - switch (action.type) { - case Hanabi::ActionType::color_clue: - case Hanabi::ActionType::rank_clue: - action.type = Hanabi::ActionType::clue; - break; - case Hanabi::ActionType::end_game: - case Hanabi::ActionType::vote_terminate: - action.type = Hanabi::ActionType::end_game; - break; - case Hanabi::ActionType::play: - case Hanabi::ActionType::discard: - break; - default: - throw std::runtime_error( - "Invalid game format, could not parse action type " + std::to_string(type)); - } - return action; - } + std::vector parse_actions(const boost::json::value &action_json); - std::pair, Hanabi::rank_t> parse_deck(const boost::json::value &deck_json) { - auto deck = boost::json::value_to>(deck_json); - for (auto &card: deck) { - ASSERT(card.rank < 5); - ASSERT(card.rank >= 0); - ASSERT(card.suit < 6); - ASSERT(card.suit >= 0); - } - Hanabi::rank_t num_suits = 0; - for(const auto& card: deck) { - num_suits = std::max(num_suits, card.suit); - } - return {deck, num_suits + 1}; - } + std::optional download_game_json(int game_id); - std::vector parse_actions(const boost::json::value &action_json) { - return boost::json::value_to>(action_json); - } + std::optional open_game_json(const char *filename); - std::optional download_game_json(int game_id) { - std::string request_str = "https://hanab.live/export/" + std::to_string(game_id); - cpr::Response r = cpr::Get(cpr::Url(request_str)); - if (r.header["content-type"] != "application/json; charset=utf-8") { - return std::nullopt; - } - return boost::json::parse(r.text).as_object(); - } + /** + * @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 + * @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 open_game_json(const char *filename) { - std::ifstream file(filename); - if (!file.is_open()) { - return std::nullopt; - } - std::string game_json((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - return boost::json::parse(game_json).as_object(); - } - - template - std::unique_ptr produce_state( - const std::vector& deck, - const std::vector& actions, - size_t num_turns_to_replicate, - size_t draw_pile_break = 0 - ) { - auto game = std::unique_ptr(new Hanabi::HanabiState(deck)); - std::uint8_t index; - for (size_t i = 0; i < std::min(num_turns_to_replicate, 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: - 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 = 0) { - 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); - case 4: - return produce_state<4,2,5>(deck, actions, turn, draw_pile_break); - case 5: - return produce_state<5,2,5>(deck, actions, turn, draw_pile_break); - case 6: - return produce_state<6,2,5>(deck, actions, turn, draw_pile_break); - 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); - case 4: - return produce_state<4,3,5>(deck, actions, turn, draw_pile_break); - case 5: - return produce_state<5,3,5>(deck, actions, turn, draw_pile_break); - case 6: - return produce_state<6,3,5>(deck, actions, turn, draw_pile_break); - 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); - case 4: - return produce_state<4,4,4>(deck, actions, turn, draw_pile_break); - case 5: - return produce_state<5,4,4>(deck, actions, turn, draw_pile_break); - case 6: - return produce_state<6,4,4>(deck, actions, turn, draw_pile_break); - 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); - case 4: - return produce_state<4,5,4>(deck, actions, turn, draw_pile_break); - case 5: - return produce_state<5,5,4>(deck, actions, turn, draw_pile_break); - case 6: - return produce_state<6,5,4>(deck, actions, turn, draw_pile_break); - 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); - case 4: - return produce_state<4,6,3>(deck, actions, turn, draw_pile_break); - case 5: - return produce_state<5,6,3>(deck, actions, turn, draw_pile_break); - case 6: - return produce_state<6,6,3>(deck, actions, turn, draw_pile_break); - default: - throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); - } - default: - throw std::runtime_error("Invalid number of players: " + std::to_string(num_players)); - } - } - - - -} // namespacen Download +} // namespace Download #endif // DYNAMIC_PROGRAM_DOWNLOAD_H diff --git a/src/download.cpp b/src/download.cpp new file mode 100644 index 0000000..cbb7e76 --- /dev/null +++ b/src/download.cpp @@ -0,0 +1,222 @@ +#include "download.h" +#include + + +// This helper function deduces the type and assigns the value with the matching key +template +void extract(boost::json::object const &obj, T &t, std::string_view key) { + t = value_to(obj.at(key)); +} + +namespace Hanabi { + Card tag_invoke(boost::json::value_to_tag, + boost::json::value const &jv) { + Hanabi::Card card{}; + boost::json::object const &obj = jv.as_object(); + extract(obj, card.rank, "rank"); + extract(obj, card.suit, "suitIndex"); + card.rank = 5 - card.rank; + return card; + } + + void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, Hanabi::Card const &card) { + jv = {{"suitIndex", card.suit}, + {"rank", card.rank}}; + } +} + +namespace Download { + + Action tag_invoke(boost::json::value_to_tag, boost::json::value const &jv) { + Action action{}; + uint8_t type; + boost::json::object const &obj = jv.as_object(); + + extract(obj, action.target, "target"); + extract(obj, type, "type"); + + action.type = static_cast(type); + switch (action.type) { + case Hanabi::ActionType::color_clue: + case Hanabi::ActionType::rank_clue: + action.type = Hanabi::ActionType::clue; + break; + case Hanabi::ActionType::end_game: + case Hanabi::ActionType::vote_terminate: + action.type = Hanabi::ActionType::end_game; + break; + case Hanabi::ActionType::play: + case Hanabi::ActionType::discard: + break; + default: + throw std::runtime_error( + "Invalid game format, could not parse action type " + std::to_string(type)); + } + return action; + } + + std::pair, Hanabi::rank_t> parse_deck(const boost::json::value &deck_json) { + auto deck = boost::json::value_to>(deck_json); + for (auto &card: deck) { + ASSERT(card.rank < 5); + ASSERT(card.rank >= 0); + ASSERT(card.suit < 6); + ASSERT(card.suit >= 0); + } + Hanabi::rank_t num_suits = 0; + for(const auto& card: deck) { + num_suits = std::max(num_suits, card.suit); + } + return {deck, num_suits + 1}; + } + + std::vector parse_actions(const boost::json::value &action_json) { + return boost::json::value_to>(action_json); + } + + std::optional download_game_json(int game_id) { + std::string request_str = "https://hanab.live/export/" + std::to_string(game_id); + cpr::Response r = cpr::Get(cpr::Url(request_str)); + if (r.header["content-type"] != "application/json; charset=utf-8") { + return std::nullopt; + } + return boost::json::parse(r.text).as_object(); + } + + std::optional open_game_json(const char *filename) { + std::ifstream file(filename); + if (!file.is_open()) { + return std::nullopt; + } + std::string game_json((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return boost::json::parse(game_json).as_object(); + } + + template + std::unique_ptr produce_state( + const std::vector& deck, + const std::vector& actions, + size_t num_turns_to_replicate, + size_t draw_pile_break = 0 + ) { + auto game = std::unique_ptr(new Hanabi::HanabiState(deck)); + std::uint8_t index; + for (size_t i = 0; i < std::min(num_turns_to_replicate, 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: + 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) { + 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); + case 4: + return produce_state<4,2,5>(deck, actions, turn, draw_pile_break); + case 5: + return produce_state<5,2,5>(deck, actions, turn, draw_pile_break); + case 6: + return produce_state<6,2,5>(deck, actions, turn, draw_pile_break); + 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); + case 4: + return produce_state<4,3,5>(deck, actions, turn, draw_pile_break); + case 5: + return produce_state<5,3,5>(deck, actions, turn, draw_pile_break); + case 6: + return produce_state<6,3,5>(deck, actions, turn, draw_pile_break); + 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); + case 4: + return produce_state<4,4,4>(deck, actions, turn, draw_pile_break); + case 5: + return produce_state<5,4,4>(deck, actions, turn, draw_pile_break); + case 6: + return produce_state<6,4,4>(deck, actions, turn, draw_pile_break); + 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); + case 4: + return produce_state<4,5,4>(deck, actions, turn, draw_pile_break); + case 5: + return produce_state<5,5,4>(deck, actions, turn, draw_pile_break); + case 6: + return produce_state<6,5,4>(deck, actions, turn, draw_pile_break); + 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); + case 4: + return produce_state<4,6,3>(deck, actions, turn, draw_pile_break); + case 5: + return produce_state<5,6,3>(deck, actions, turn, draw_pile_break); + case 6: + return produce_state<6,6,3>(deck, actions, turn, draw_pile_break); + default: + throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits)); + } + default: + throw std::runtime_error("Invalid number of players: " + std::to_string(num_players)); + } + } + +} // namespace Download diff --git a/src/main.cpp b/src/main.cpp index 23bf641..ee9a624 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,7 +16,7 @@ namespace Hanabi { void analyze_game_and_start_cli(std::variant game_id, int turn) { - auto game = Download::get_game(game_id, turn - 1); + auto game = Download::get_game(game_id, turn); if (game == nullptr) { if(game_id.index() == 0) { std::cout << "Failed to download game " << std::get(game_id) << " from hanab.live." << std::endl;