rework downloading interface: return game consisting of state and actions
This commit is contained in:
parent
89bab62032
commit
9af6ef3368
5 changed files with 86 additions and 90 deletions
|
@ -15,7 +15,8 @@ find_package(Boost 1.81 COMPONENTS program_options REQUIRED)
|
||||||
include_directories(.)
|
include_directories(.)
|
||||||
include_directories(${Boost_INCLUDE_DIR})
|
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 cpr)
|
||||||
target_link_libraries(endgame-analyzer Boost::program_options)
|
target_link_libraries(endgame-analyzer Boost::program_options)
|
||||||
|
|
|
@ -36,16 +36,14 @@ namespace Download {
|
||||||
/**
|
/**
|
||||||
* @brief Create game object from given source
|
* @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 game_spec Either an id to download from hanab.live or a filename with a json specification
|
||||||
* @param turn Turn to skip to
|
* @param score_goal What score counts as a win for this game. If left empty, the maximum score is inserted.
|
||||||
* @param draw_pile_break Minimum draw pile size of produced game
|
|
||||||
* @return Game state
|
* @return Game state
|
||||||
*
|
*
|
||||||
* If both turn and draw_pile_break are specified, the game skips until the specified turn or the first time the
|
* 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
|
* 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<Hanabi::HanabiStateIF> get_game(std::variant<int, const char*> game_spec, unsigned turn = 1, size_t draw_pile_break = 0, std::optional<uint8_t> score_goal = std::nullopt);
|
Hanabi::Game get_game(std::variant<int, const char*> game_spec, std::optional<uint8_t> score_goal);
|
||||||
|
|
||||||
} // namespace Download
|
} // namespace Download
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include <stack>
|
#include <stack>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <boost/container/static_vector.hpp>
|
#include <boost/container/static_vector.hpp>
|
||||||
#include <boost/rational.hpp>
|
#include <boost/rational.hpp>
|
||||||
|
@ -251,6 +252,19 @@ protected:
|
||||||
friend std::ostream& operator<<(std::ostream&, HanabiStateIF const&);
|
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<HanabiStateIF> state;
|
||||||
|
std::vector<Action> actions;
|
||||||
|
unsigned next_action;
|
||||||
|
};
|
||||||
|
|
||||||
inline std::ostream &operator<<(std::ostream &os, HanabiStateIF const &hanabi_state);
|
inline std::ostream &operator<<(std::ostream &os, HanabiStateIF const &hanabi_state);
|
||||||
|
|
||||||
template <suit_t num_suits, player_t num_players, hand_index_t hand_size>
|
template <suit_t num_suits, player_t num_players, hand_index_t hand_size>
|
||||||
|
|
190
src/download.cpp
190
src/download.cpp
|
@ -95,47 +95,84 @@ namespace Download {
|
||||||
return boost::json::parse(game_json).as_object();
|
return boost::json::parse(game_json).as_object();
|
||||||
}
|
}
|
||||||
|
|
||||||
template<std::size_t num_suits, Hanabi::player_t num_players, std::size_t hand_size>
|
std::unique_ptr<Hanabi::HanabiStateIF> get_base_state(
|
||||||
std::unique_ptr<Hanabi::HanabiStateIF> produce_state(
|
std::size_t num_suits,
|
||||||
const std::vector<Hanabi::Card>& deck,
|
Hanabi::player_t num_players,
|
||||||
const std::vector<Action>& actions,
|
std::vector<Hanabi::Card> const & deck,
|
||||||
size_t start_turn,
|
std::optional<uint8_t> score_goal) {
|
||||||
size_t draw_pile_break = 0,
|
|
||||||
std::optional<uint8_t> score_goal = std::nullopt
|
|
||||||
) {
|
|
||||||
uint8_t actual_score_goal = score_goal.value_or(5 * num_suits);
|
uint8_t actual_score_goal = score_goal.value_or(5 * num_suits);
|
||||||
auto game = std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<num_suits, num_players, hand_size>(deck, actual_score_goal));
|
switch(num_players) {
|
||||||
std::uint8_t index;
|
case 2:
|
||||||
for (size_t i = 0; i < std::min(start_turn - 1, actions.size()); i++) {
|
switch(num_suits) {
|
||||||
if (game->draw_pile_size() == draw_pile_break) {
|
case 3:
|
||||||
break;
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,2,5>(deck, actual_score_goal));
|
||||||
|
case 4:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,2,5>(deck, actual_score_goal));
|
||||||
|
case 5:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,2,5>(deck, actual_score_goal));
|
||||||
|
case 6:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6,2,5>(deck, actual_score_goal));
|
||||||
|
default:
|
||||||
|
throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits));
|
||||||
}
|
}
|
||||||
switch(actions[i].type) {
|
case 3:
|
||||||
case Hanabi::ActionType::color_clue:
|
switch(num_suits) {
|
||||||
case Hanabi::ActionType::rank_clue:
|
case 3:
|
||||||
game->give_clue();
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,3,5>(deck, actual_score_goal));
|
||||||
break;
|
case 4:
|
||||||
case Hanabi::ActionType::discard:
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,3,5>(deck, actual_score_goal));
|
||||||
index = game->find_card_in_hand(deck[actions[i].target]);
|
case 5:
|
||||||
ASSERT(index != std::uint8_t(-1));
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,3,5>(deck, actual_score_goal));
|
||||||
game->discard(index);
|
case 6:
|
||||||
break;
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6,3,5>(deck, actual_score_goal));
|
||||||
case Hanabi::ActionType::play:
|
default:
|
||||||
index = game->find_card_in_hand(deck[actions[i].target]);
|
throw std::runtime_error("Invalid number of suits: " + std::to_string(num_suits));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
case 4:
|
||||||
|
switch(num_suits) {
|
||||||
|
case 3:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,4,4>(deck, actual_score_goal));
|
||||||
|
case 4:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,4,4>(deck, actual_score_goal));
|
||||||
|
case 5:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,4,4>(deck, actual_score_goal));
|
||||||
|
case 6:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(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 std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,5,4>(deck, actual_score_goal));
|
||||||
|
case 4:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,5,4>(deck, actual_score_goal));
|
||||||
|
case 5:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,5,4>(deck, actual_score_goal));
|
||||||
|
case 6:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(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 std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<3,6,3>(deck, actual_score_goal));
|
||||||
|
case 4:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<4,6,3>(deck, actual_score_goal));
|
||||||
|
case 5:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<5,6,3>(deck, actual_score_goal));
|
||||||
|
case 6:
|
||||||
|
return std::unique_ptr<Hanabi::HanabiStateIF>(new Hanabi::HanabiState<6,6,3>(deck, actual_score_goal));
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
game->init_backtracking_information();
|
|
||||||
return game;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Hanabi::HanabiStateIF> get_game(std::variant<int, const char*> game_spec, unsigned turn, size_t draw_pile_break, std::optional<uint8_t> score_goal) {
|
Hanabi::Game get_game(std::variant<int, const char*> game_spec, std::optional<uint8_t> score_goal){
|
||||||
const std::optional<boost::json::object> game_json_opt = [&game_spec]() {
|
const std::optional<boost::json::object> game_json_opt = [&game_spec]() {
|
||||||
if (game_spec.index() == 0) {
|
if (game_spec.index() == 0) {
|
||||||
return download_game_json(std::get<int>(game_spec));
|
return download_game_json(std::get<int>(game_spec));
|
||||||
|
@ -145,84 +182,27 @@ namespace Download {
|
||||||
}();
|
}();
|
||||||
|
|
||||||
if (!game_json_opt.has_value() or game_json_opt.value().empty()) {
|
if (!game_json_opt.has_value() or game_json_opt.value().empty()) {
|
||||||
return nullptr;
|
return {nullptr, {}, 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
const boost::json::object& game_json = game_json_opt.value();
|
const boost::json::object& game_json = game_json_opt.value();
|
||||||
|
|
||||||
const auto [deck, num_suits] = parse_deck(game_json.at("deck"));
|
const auto [deck, num_suits] = parse_deck(game_json.at("deck"));
|
||||||
const std::vector<Action> actions = parse_actions(game_json.at("actions"));
|
|
||||||
const size_t num_players = game_json.at("players").as_array().size();
|
const size_t num_players = game_json.at("players").as_array().size();
|
||||||
|
|
||||||
switch(num_players) {
|
// Convert the actions from hanab.live format into local format used
|
||||||
case 2:
|
const std::vector<Action> hanab_live_actions = parse_actions(game_json.at("actions"));
|
||||||
switch(num_suits) {
|
std::vector<Hanabi::Action> actions;
|
||||||
case 3:
|
std::transform(
|
||||||
return produce_state<3,2,5>(deck, actions, turn, draw_pile_break, score_goal);
|
hanab_live_actions.begin(),
|
||||||
case 4:
|
hanab_live_actions.end(),
|
||||||
return produce_state<4,2,5>(deck, actions, turn, draw_pile_break, score_goal);
|
std::back_inserter(actions),
|
||||||
case 5:
|
[&deck](Action const & action){
|
||||||
return produce_state<5,2,5>(deck, actions, turn, draw_pile_break, score_goal);
|
return Hanabi::Action {action.type, deck[action.target]};
|
||||||
case 6:
|
|
||||||
return produce_state<6,2,5>(deck, actions, turn, draw_pile_break, 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);
|
|
||||||
case 4:
|
|
||||||
return produce_state<4,3,5>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
case 5:
|
|
||||||
return produce_state<5,3,5>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
case 6:
|
|
||||||
return produce_state<6,3,5>(deck, actions, turn, draw_pile_break, 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);
|
|
||||||
case 4:
|
|
||||||
return produce_state<4,4,4>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
case 5:
|
|
||||||
return produce_state<5,4,4>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
case 6:
|
|
||||||
return produce_state<6,4,4>(deck, actions, turn, draw_pile_break, 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);
|
|
||||||
case 4:
|
|
||||||
return produce_state<4,5,4>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
case 5:
|
|
||||||
return produce_state<5,5,4>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
case 6:
|
|
||||||
return produce_state<6,5,4>(deck, actions, turn, draw_pile_break, 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);
|
|
||||||
case 4:
|
|
||||||
return produce_state<4,6,3>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
case 5:
|
|
||||||
return produce_state<5,6,3>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
case 6:
|
|
||||||
return produce_state<6,6,3>(deck, actions, turn, draw_pile_break, score_goal);
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {get_base_state(num_suits, num_players, deck, score_goal), actions, 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Download
|
} // namespace Download
|
||||||
|
|
17
src/main.cpp
17
src/main.cpp
|
@ -13,8 +13,8 @@
|
||||||
namespace Hanabi {
|
namespace Hanabi {
|
||||||
void analyze_game_and_start_cli(std::variant<int, const char*> game_id, int turn, int draw_pile_size, std::optional<uint8_t> score_goal,
|
void analyze_game_and_start_cli(std::variant<int, const char*> game_id, int turn, int draw_pile_size, std::optional<uint8_t> score_goal,
|
||||||
bool start_cli) {
|
bool start_cli) {
|
||||||
auto game = Download::get_game(game_id, turn, draw_pile_size, score_goal);
|
auto game = Download::get_game(game_id, score_goal);
|
||||||
if (game == nullptr) {
|
if (game.state == nullptr) {
|
||||||
if(game_id.index() == 0) {
|
if(game_id.index() == 0) {
|
||||||
std::cout << "Failed to download game " << std::get<int>(game_id) << " from hanab.live." << std::endl;
|
std::cout << "Failed to download game " << std::get<int>(game_id) << " from hanab.live." << std::endl;
|
||||||
} else {
|
} else {
|
||||||
|
@ -23,9 +23,12 @@ namespace Hanabi {
|
||||||
return;
|
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 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();
|
auto end = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
std::cout.precision(10);
|
std::cout.precision(10);
|
||||||
|
@ -33,13 +36,13 @@ namespace Hanabi {
|
||||||
std::cout << "Probability with optimal play: ";
|
std::cout << "Probability with optimal play: ";
|
||||||
print_probability(std::cout, res) << std::endl;
|
print_probability(std::cout, res) << std::endl;
|
||||||
std::cout << "Took " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start) << "." << std::endl;
|
std::cout << "Took " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start) << "." << std::endl;
|
||||||
std::cout << "Visited " << game->enumerated_states() << " states." << std::endl;
|
std::cout << "Visited " << game.state->enumerated_states() << " states." << std::endl;
|
||||||
std::cout << "Enumerated " << game->position_tablebase().size() << " unique game states. " << std::endl;
|
std::cout << "Enumerated " << game.state->position_tablebase().size() << " unique game states. " << std::endl;
|
||||||
|
|
||||||
if (start_cli) {
|
if (start_cli) {
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
std::cout << "Dropping into interactive command line to explore result (type 'help'):" << std::endl;
|
std::cout << "Dropping into interactive command line to explore result (type 'help'):" << std::endl;
|
||||||
auto game_shared = std::shared_ptr<HanabiStateIF>(game.release());
|
auto game_shared = std::shared_ptr<HanabiStateIF>(game.state.release());
|
||||||
auto states = game_shared->possible_next_states(0, false);
|
auto states = game_shared->possible_next_states(0, false);
|
||||||
cli(game_shared);
|
cli(game_shared);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue