#include #include #include #include #include #include #include "game_state.h" namespace Hanabi { std::ostream& operator<<(std::ostream& os, const std::optional& prob) { if (prob.has_value()) { os << prob.value() << " ~ " << std::setprecision(5) << boost::rational_cast(prob.value()) * 100 << "%"; } else { os << "unknown"; } return os; } std::string read_line_memory_safe(const char *prompt) { char *line = readline(prompt); std::string ret; if (line == nullptr) { ret = ""; } else { ret = std::string(line); } free(line); return ret; } constexpr static std::array cli_commands = { "play", "clue", "discard", "id", "state", "revert", "actions", "evaluate", "help", "quit", }; char * cli_commands_generator(const char *text, int state) { std::string text_str (text); for(auto& command : cli_commands) { if (command.starts_with(text_str) && state-- <= 0) { return strdup(command.c_str()); } } return nullptr; } char ** cli_command_completion(const char *text, int start, int end) { rl_attempted_completion_over = 1; return rl_completion_matches(text, cli_commands_generator); } Card parse_card(std::string card_str) { if (card_str == "trash") { return Cards::trash; } if(card_str.size() != 2) { return Cards::unknown; } constexpr std::array color_initials = {'r', 'y', 'g', 'b', 'p', 't'}; auto it = std::find(color_initials.begin(), color_initials.end(), card_str[0]); if (it == color_initials.end()) { return Cards::unknown; } const suit_t suit = std::distance(color_initials.begin(), it); try { const rank_t rank = 5 - std::stoi(card_str.substr(1, 1)); return Card {suit, rank}; } catch(std::invalid_argument&) { return Cards::unknown; } } bool ask_for_card_and_rotate_draw(const std::shared_ptr& game, hand_index_t index, bool play) { const auto next_states = game->possible_next_states(index, play); if (next_states.size() <= 1) { return true; } std::cout << "Choose drawn card: " << std::endl; unsigned num_trash = 0; std::optional trash_discard_prob = 0; for(const auto &[card_multiplicity, probability]: next_states) { if (game->is_trash(card_multiplicity.card)) { num_trash += card_multiplicity.multiplicity; trash_discard_prob = probability; } else { std::cout << card_multiplicity.card << " (" << card_multiplicity.multiplicity; std::cout << " copie(s) in draw) " << probability << std::endl; } } if (num_trash > 0) { std::cout << Cards::trash << " (" << num_trash << " copie(s) in draw) " << trash_discard_prob << std::endl; } std::stringstream prompt; prompt << "draw? ["; if (num_trash > 0) { prompt << Cards::trash; } else { prompt << next_states.front().first.card; } prompt << "] "; const std::string card_str = read_line_memory_safe(prompt.str().c_str()); const Card drawn_card = [&card_str, &num_trash, &next_states](){ if (card_str.empty()) { if (num_trash > 0) { return Cards::trash; } return next_states.front().first.card; } return parse_card(card_str); }(); if (drawn_card == Cards::unknown) { std::cout << "Could not parse card " << card_str << std::endl; return false; } auto selected_draw_it = std::find_if(next_states.begin(), next_states.end(), [&drawn_card, &game](const std::pair>& pair) { return (game->is_trash(pair.first.card) and drawn_card == Cards::trash) or pair.first.card == drawn_card; }); if (selected_draw_it == next_states.end()){ std::cout << "That card is not in the draw pile, aborting." << std::endl; return false; }; game->rotate_next_draw(selected_draw_it->first.card); return true; } void cli(const std::shared_ptr& game) { rl_attempted_completion_function = cli_command_completion; using_history(); unsigned depth = 0; while (true) { const std::string prompt = read_line_memory_safe("> "); add_history(prompt.c_str()); if (prompt.starts_with("help")) { std::cout << "state: print information on current game state." << std::endl; std::cout << "clue: give a clue." << std::endl; std::cout << "play : play specified card." << std::endl; std::cout << "discard: discard trash from hand." << std::endl; std::cout << "revert: revert last turn of game." << std::endl; std::cout << "actions: display list of reasonable actions to take and their winning chances." << std::endl; std::cout << "evaluate: evaluate current game state recursively. Potentially runtime-expensive." << std::endl; std::cout << "id: display id of state. Has no inherent meaning, useful for debugging." << std::endl; std::cout << "quit: Quit this interactive shell." << std::endl; continue; } if (prompt.starts_with("quit")) { std::cout << "Quitting." << std::endl; clear_history(); break; } if (prompt.starts_with("state")) { std::cout << *game << std::endl; const std::optional prob = game->lookup(); std::cout << "Winning chance: " << prob << std::endl; continue; } if (prompt.starts_with("evaluate")) { std::cout << "Evaluating current game state, this might take a while." << std::endl; game->evaluate_state(); std::cout << "Evaluated state." << std::endl; continue; } if (prompt.starts_with("revert")) { if (depth == 0) { std::cout << "Cannot revert more than base state." << std::endl; continue; } std::cout << "Reverting one turn" << std::endl; game->revert(); depth--; continue; } if (prompt.starts_with("id")) { std::cout << game->unique_id() << std::endl; continue; } if (prompt.starts_with("play")) { const Card card = parse_card(prompt.substr(5,2)); if (prompt.length() < 7) { std::cout << "No card specified." << std::endl; continue; } if (card == Cards::unknown) { std::cout << "Could not parse card " << prompt.substr(5,2) << std::endl; continue; } const hand_index_t index = game->find_card_in_hand(card); if (index == hand_index_t(-1)) { std::cout << "This card is not in the current players hand, aborting." << std::endl; continue; } if (!ask_for_card_and_rotate_draw(game, index, true)) { continue; } game->play(index); depth++; continue; } if (prompt.starts_with("discard")) { const auto hand = game->cur_hand(); hand_index_t trash_index = invalid_hand_idx; for(hand_index_t index = 0; index < hand.size(); index++) { if (game->is_trash(hand[index])) { trash_index = index; break; } } if (trash_index == invalid_hand_idx) { std::cout << "No trash in hand found, discarding not supported." << std::endl; continue; } if (game->num_clues() == max_num_clues) { std::cout << "You cannot discard at " << max_num_clues << " clues." << std::endl; continue; } if (!ask_for_card_and_rotate_draw(game, trash_index, false)) { continue; } game->discard(trash_index); depth++; continue; } if (prompt.starts_with("clue")) { if (game->num_clues() == 0) { std::cout << "You cannot give a clue at 0 clues." << std::endl; continue; } game->give_clue(); depth++; continue; } if (prompt.starts_with("actions")) { for (const auto &[action, probability] : game->get_reasonable_actions()) { std::cout << action << ": " << probability << std::endl; } continue; } std::cout << "Unrecognized command. Type 'help' for a list of available commands." << std::endl; } } }