2023-08-12 00:04:02 +02:00
|
|
|
#include <cstdio>
|
|
|
|
#include <readline/readline.h>
|
2023-08-12 09:36:06 +02:00
|
|
|
#include <readline/history.h>
|
2023-08-12 00:04:02 +02:00
|
|
|
#include <iostream>
|
|
|
|
#include <memory>
|
|
|
|
#include <cmath>
|
|
|
|
#include "game_state.h"
|
|
|
|
|
|
|
|
namespace Hanabi {
|
|
|
|
|
|
|
|
std::ostream& operator<<(std::ostream& os, const std::optional<probability_t>& prob) {
|
|
|
|
if (prob.has_value()) {
|
|
|
|
os << prob.value() << " ~ " << std::setprecision(5) << boost::rational_cast<double>(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;
|
|
|
|
}
|
|
|
|
|
2023-08-12 10:19:04 +02:00
|
|
|
constexpr static std::array<std::string, 12> cli_commands = {
|
2023-08-12 09:36:06 +02:00
|
|
|
"play",
|
|
|
|
"clue",
|
|
|
|
"discard",
|
|
|
|
"id",
|
|
|
|
"state",
|
|
|
|
"revert",
|
|
|
|
"actions",
|
|
|
|
"evaluate",
|
|
|
|
"help",
|
|
|
|
"quit",
|
2023-08-12 10:19:04 +02:00
|
|
|
"initials",
|
|
|
|
"opt",
|
2023-08-12 09:36:06 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-08-12 00:04:02 +02:00
|
|
|
Card parse_card(std::string card_str) {
|
2023-08-12 10:19:04 +02:00
|
|
|
if (card_str == "trash" or card_str == "kt") {
|
2023-08-12 08:50:28 +02:00
|
|
|
return Cards::trash;
|
|
|
|
}
|
2023-08-12 00:04:02 +02:00
|
|
|
if(card_str.size() != 2) {
|
2023-08-12 08:50:28 +02:00
|
|
|
return Cards::unknown;
|
2023-08-12 00:04:02 +02:00
|
|
|
}
|
2023-08-12 10:19:04 +02:00
|
|
|
auto it = std::find(suit_initials.begin(), suit_initials.end(), card_str[0]);
|
|
|
|
if (it == suit_initials.end()) {
|
2023-08-12 08:50:28 +02:00
|
|
|
return Cards::unknown;
|
2023-08-12 00:04:02 +02:00
|
|
|
}
|
2023-08-12 10:19:04 +02:00
|
|
|
const suit_t suit = std::distance(suit_initials.begin(), it);
|
2023-08-12 00:04:02 +02:00
|
|
|
try {
|
|
|
|
const rank_t rank = 5 - std::stoi(card_str.substr(1, 1));
|
|
|
|
return Card {suit, rank};
|
|
|
|
} catch(std::invalid_argument&) {
|
2023-08-12 08:50:28 +02:00
|
|
|
return Cards::unknown;
|
2023-08-12 00:04:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ask_for_card_and_rotate_draw(const std::shared_ptr<HanabiStateIF>& 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;
|
2023-08-12 08:50:28 +02:00
|
|
|
unsigned num_trash = 0;
|
2023-08-12 09:36:06 +02:00
|
|
|
std::optional<probability_t> trash_discard_prob = 0;
|
2023-08-12 00:04:02 +02:00
|
|
|
for(const auto &[card_multiplicity, probability]: next_states) {
|
|
|
|
if (game->is_trash(card_multiplicity.card)) {
|
2023-08-12 08:50:28 +02:00
|
|
|
num_trash += card_multiplicity.multiplicity;
|
2023-08-12 09:36:06 +02:00
|
|
|
trash_discard_prob = probability;
|
2023-08-12 00:04:02 +02:00
|
|
|
} else {
|
2023-08-12 08:50:28 +02:00
|
|
|
std::cout << card_multiplicity.card << " (" << card_multiplicity.multiplicity;
|
|
|
|
std::cout << " copie(s) in draw) " << probability << std::endl;
|
2023-08-12 00:04:02 +02:00
|
|
|
}
|
|
|
|
}
|
2023-08-12 10:19:04 +02:00
|
|
|
|
2023-08-12 08:50:28 +02:00
|
|
|
if (num_trash > 0) {
|
2023-08-12 09:36:06 +02:00
|
|
|
std::cout << Cards::trash << " (" << num_trash << " copie(s) in draw) " << trash_discard_prob << std::endl;
|
2023-08-12 08:50:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2023-08-12 00:04:02 +02:00
|
|
|
std::cout << "Could not parse card " << card_str << std::endl;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-08-12 08:50:28 +02:00
|
|
|
auto selected_draw_it = std::find_if(next_states.begin(), next_states.end(), [&drawn_card, &game](const std::pair<CardMultiplicity, std::optional<probability_t>>& 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;
|
2023-08-12 00:04:02 +02:00
|
|
|
};
|
2023-08-12 08:50:28 +02:00
|
|
|
game->rotate_next_draw(selected_draw_it->first.card);
|
2023-08-12 00:04:02 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-08-12 09:36:06 +02:00
|
|
|
void cli(const std::shared_ptr<HanabiStateIF>& game) {
|
|
|
|
rl_attempted_completion_function = cli_command_completion;
|
|
|
|
using_history();
|
|
|
|
unsigned depth = 0;
|
2023-08-12 00:04:02 +02:00
|
|
|
while (true) {
|
|
|
|
const std::string prompt = read_line_memory_safe("> ");
|
2023-08-12 09:36:06 +02:00
|
|
|
add_history(prompt.c_str());
|
2023-08-12 00:04:02 +02:00
|
|
|
|
|
|
|
if (prompt.starts_with("help")) {
|
2023-08-12 09:36:06 +02:00
|
|
|
std::cout << "state: print information on current game state." << std::endl;
|
|
|
|
std::cout << "clue: give a clue." << std::endl;
|
2023-08-12 08:50:28 +02:00
|
|
|
std::cout << "play <card>: play specified card." << std::endl;
|
2023-08-12 09:36:06 +02:00
|
|
|
std::cout << "discard: discard trash from hand." << std::endl;
|
2023-08-12 10:19:04 +02:00
|
|
|
std::cout << "opt: take optimal action. In case of ties, prefers plays and discards in that order." << std::endl;
|
2023-08-12 09:36:06 +02:00
|
|
|
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;
|
2023-08-12 00:04:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-08-12 09:36:06 +02:00
|
|
|
if (prompt.starts_with("quit")) {
|
|
|
|
std::cout << "Quitting." << std::endl;
|
|
|
|
clear_history();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-08-12 10:19:04 +02:00
|
|
|
if (prompt.starts_with("initials")) {
|
|
|
|
if (prompt.length() < 12) {
|
|
|
|
std::cout << "At least 3 initials need to be specified" << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const std::string new_initials = prompt.substr(9);
|
|
|
|
for(size_t i = 0; i < std::min(size_t(6), new_initials.length()); i++) {
|
|
|
|
suit_initials[i] = new_initials[i];
|
|
|
|
}
|
|
|
|
std::cout << "Updated initials to ";
|
|
|
|
for(const char c: suit_initials) {
|
|
|
|
std::cout << c;
|
|
|
|
}
|
|
|
|
std::cout << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-08-12 00:04:02 +02:00
|
|
|
if (prompt.starts_with("state")) {
|
|
|
|
std::cout << *game << std::endl;
|
|
|
|
const std::optional<probability_t> prob = game->lookup();
|
|
|
|
std::cout << "Winning chance: " << prob << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-08-12 08:50:28 +02:00
|
|
|
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;
|
2023-08-12 09:36:06 +02:00
|
|
|
continue;
|
2023-08-12 08:50:28 +02:00
|
|
|
}
|
|
|
|
|
2023-08-12 00:04:02 +02:00
|
|
|
if (prompt.starts_with("revert")) {
|
2023-08-12 09:36:06 +02:00
|
|
|
if (depth == 0) {
|
|
|
|
std::cout << "Cannot revert more than base state." << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
2023-08-12 00:04:02 +02:00
|
|
|
std::cout << "Reverting one turn" << std::endl;
|
|
|
|
game->revert();
|
2023-08-12 09:36:06 +02:00
|
|
|
depth--;
|
2023-08-12 00:04:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-08-12 08:50:28 +02:00
|
|
|
if (prompt.starts_with("id")) {
|
|
|
|
std::cout << game->unique_id() << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-08-12 00:04:02 +02:00
|
|
|
if (prompt.starts_with("play")) {
|
|
|
|
const Card card = parse_card(prompt.substr(5,2));
|
2023-08-12 08:50:28 +02:00
|
|
|
if (prompt.length() < 7) {
|
|
|
|
std::cout << "No card specified." << std::endl;
|
2023-08-12 09:36:06 +02:00
|
|
|
continue;
|
2023-08-12 08:50:28 +02:00
|
|
|
}
|
|
|
|
if (card == Cards::unknown) {
|
2023-08-12 00:04:02 +02:00
|
|
|
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;
|
2023-08-12 09:36:06 +02:00
|
|
|
continue;
|
2023-08-12 00:04:02 +02:00
|
|
|
}
|
|
|
|
if (!ask_for_card_and_rotate_draw(game, index, true)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
game->play(index);
|
2023-08-12 09:36:06 +02:00
|
|
|
depth++;
|
2023-08-12 00:04:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (prompt.starts_with("discard")) {
|
2023-08-12 08:50:28 +02:00
|
|
|
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;
|
2023-08-12 00:04:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
2023-08-12 08:50:28 +02:00
|
|
|
if (game->num_clues() == max_num_clues) {
|
|
|
|
std::cout << "You cannot discard at " << max_num_clues << " clues." << std::endl;
|
|
|
|
continue;
|
2023-08-12 00:04:02 +02:00
|
|
|
}
|
2023-08-12 08:50:28 +02:00
|
|
|
if (!ask_for_card_and_rotate_draw(game, trash_index, false)) {
|
2023-08-12 00:04:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
2023-08-12 08:50:28 +02:00
|
|
|
game->discard(trash_index);
|
2023-08-12 09:36:06 +02:00
|
|
|
depth++;
|
2023-08-12 00:04:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (prompt.starts_with("clue")) {
|
2023-08-12 09:36:06 +02:00
|
|
|
if (game->num_clues() == 0) {
|
|
|
|
std::cout << "You cannot give a clue at 0 clues." << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
2023-08-12 08:50:28 +02:00
|
|
|
game->give_clue();
|
2023-08-12 09:36:06 +02:00
|
|
|
depth++;
|
2023-08-12 00:04:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (prompt.starts_with("actions")) {
|
|
|
|
for (const auto &[action, probability] : game->get_reasonable_actions()) {
|
|
|
|
std::cout << action << ": " << probability << std::endl;
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-08-12 10:19:04 +02:00
|
|
|
if (prompt.starts_with("opt")) {
|
|
|
|
const auto reasonable_actions = game->get_reasonable_actions();
|
|
|
|
if(reasonable_actions.empty()) {
|
|
|
|
std::cout << "Game is over, no actions to take." << std::endl;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Action best_action;
|
|
|
|
std::optional<probability_t> best_probability;
|
|
|
|
for (const auto &[action, probability] : game->get_reasonable_actions()) {
|
|
|
|
if (!best_probability.has_value() or (probability.has_value() and probability.value() > best_probability.value())) {
|
|
|
|
best_action = action;
|
|
|
|
best_probability = probability;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
switch(best_action.type) {
|
|
|
|
case ActionType::play:
|
|
|
|
std::cout << "Playing " << best_action.card << std::endl;
|
|
|
|
game->play(game->find_card_in_hand(best_action.card));
|
|
|
|
break;
|
|
|
|
case ActionType::discard:
|
|
|
|
std::cout << "Discarding" << std::endl;
|
|
|
|
game->discard(game->find_card_in_hand(best_action.card));
|
|
|
|
break;
|
|
|
|
case ActionType::clue:
|
|
|
|
std::cout << "Giving a clue" << std::endl;
|
|
|
|
game->give_clue();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-12 00:04:02 +02:00
|
|
|
std::cout << "Unrecognized command. Type 'help' for a list of available commands." << std::endl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|