improve cli interface: easier handling, more input validation

This commit is contained in:
Maximilian Keßler 2023-08-12 08:50:28 +02:00
parent a8818418e9
commit 74fe5513da
Signed by: max
GPG key ID: BCC5A619923C0BA5
4 changed files with 192 additions and 64 deletions

View file

@ -29,20 +29,23 @@ namespace Hanabi {
}
Card parse_card(std::string card_str) {
if (card_str == "trash") {
return Cards::trash;
}
if(card_str.size() != 2) {
return unknown_card;
return Cards::unknown;
}
constexpr std::array<char, 6> 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 unknown_card;
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 unknown_card;
return Cards::unknown;
}
}
@ -52,27 +55,51 @@ namespace Hanabi {
return true;
}
std::cout << "Choose drawn card: " << std::endl;
unsigned num_trash = 0;
for(const auto &[card_multiplicity, probability]: next_states) {
if (game->is_trash(card_multiplicity.card)) {
std::cout << "kt " << std::endl;
num_trash += card_multiplicity.multiplicity;
} else {
std::cout << card_multiplicity.card << " ";
std::cout << card_multiplicity.card << " (" << card_multiplicity.multiplicity;
std::cout << " copie(s) in draw) " << probability << std::endl;
}
std::cout << "(" << card_multiplicity.multiplicity << " copie(s) in draw) " << probability << std::endl;
}
const std::string card_str = read_line_memory_safe("draw? ");
const Card drawn_card = parse_card(card_str);
if (drawn_card == unknown_card) {
if (num_trash > 0) {
std::cout << Cards::trash << " (" << num_trash << " copie(s) in draw" << 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;
}
if (std::find_if(next_states.begin(), next_states.end(), [&drawn_card](const std::pair<CardMultiplicity, std::optional<probability_t>>& pair) {
return pair.first.card.suit == drawn_card.suit and pair.first.card.rank == drawn_card.rank;
}) == next_states.end()) {
std::cout << "That card is not in the draw pile" << std::endl;
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;
};
game->rotate_next_draw(drawn_card);
game->rotate_next_draw(selected_draw_it->first.card);
return true;
}
@ -81,12 +108,14 @@ namespace Hanabi {
const std::string prompt = read_line_memory_safe("> ");
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 <card>: play specified card" << std::endl;
std::cout << "discard <card>: discard specified card" << 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 << "state: print information on current game state." << std::endl;
std::cout << "clue: give a clue." << std::endl;
std::cout << "play <card>: 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 << "id: display id of state. Has no inherent meaning, useful for debugging." << std::endl;
std::cout << "evaluate: evaluate current game state recursively. Potentially runtime-expensive." << std::endl;
continue;
}
@ -97,15 +126,29 @@ namespace Hanabi {
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;
}
if (prompt.starts_with("revert")) {
std::cout << "Reverting one turn" << std::endl;
game->revert();
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 (card == unknown_card) {
if (prompt.length() < 7) {
std::cout << "No card specified." << std::endl;
}
if (card == Cards::unknown) {
std::cout << "Could not parse card " << prompt.substr(5,2) << std::endl;
continue;
}
@ -121,24 +164,31 @@ namespace Hanabi {
}
if (prompt.starts_with("discard")) {
const Card card = parse_card(prompt.substr(8,2));
if (card == unknown_card) {
std::cout << "Could not parse card " << prompt.substr(5,2) << std::endl;
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;
}
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;
}
if (!ask_for_card_and_rotate_draw(game, index, false)) {
if (game->num_clues() == max_num_clues) {
std::cout << "You cannot discard at " << max_num_clues << " clues." << std::endl;
continue;
}
game->discard(index);
if (!ask_for_card_and_rotate_draw(game, trash_index, false)) {
continue;
}
game->discard(trash_index);
continue;
}
if (prompt.starts_with("clue")) {
game->clue();
game->give_clue();
continue;
}

View file

@ -123,7 +123,7 @@ namespace Download {
switch(actions[i].type) {
case Hanabi::ActionType::color_clue:
case Hanabi::ActionType::rank_clue:
game->clue();
game->give_clue();
break;
case Hanabi::ActionType::discard:
index = game->find_card_in_hand(deck[actions[i].target]);

View file

@ -39,8 +39,11 @@ namespace Hanabi {
constexpr size_t max_card_duplicity = 3;
constexpr clue_t max_num_clues = 8;
constexpr uint8_t not_in_starting_hand = std::numeric_limits<uint8_t>::max();
constexpr hand_index_t invalid_hand_idx = std::numeric_limits<hand_index_t>::max();
constexpr std::array<std::string, 6> suit_initials{"r", "y", "g", "b", "p", "t"};
// We might want to change these at runtime to adapt to other variants.
// However, a global variable is used so that we can have an output operator for cards reading from here
static std::array<char, 6> suit_initials = {'r', 'y', 'g', 'b', 'p', 't'};
struct Card {
suit_t suit;
@ -53,7 +56,48 @@ namespace Hanabi {
inline const Card operator++(int);
auto operator<=>(const Card &) const = default;
inline bool operator==(const Card &other) const;
};
namespace Cards {
static constexpr Card r0 = {0, 5};
static constexpr Card r1 = {0, 4};
static constexpr Card r2 = {0, 3};
static constexpr Card r3 = {0, 2};
static constexpr Card r4 = {0, 1};
static constexpr Card r5 = {0, 0};
static constexpr Card y0 = {1, 5};
static constexpr Card y1 = {1, 4};
static constexpr Card y2 = {1, 3};
static constexpr Card y3 = {1, 2};
static constexpr Card y4 = {1, 1};
static constexpr Card y5 = {1, 0};
static constexpr Card g0 = {2, 5};
static constexpr Card g1 = {2, 4};
static constexpr Card g2 = {2, 3};
static constexpr Card g3 = {2, 2};
static constexpr Card g4 = {2, 1};
static constexpr Card g5 = {2, 0};
static constexpr Card b0 = {3, 5};
static constexpr Card b1 = {3, 4};
static constexpr Card b2 = {3, 3};
static constexpr Card b3 = {3, 2};
static constexpr Card b4 = {3, 1};
static constexpr Card b5 = {3, 0};
static constexpr Card p0 = {4, 5};
static constexpr Card p1 = {4, 4};
static constexpr Card p2 = {4, 3};
static constexpr Card p3 = {4, 2};
static constexpr Card p4 = {4, 1};
static constexpr Card p5 = {4, 0};
static constexpr Card t0 = {5, 5};
static constexpr Card t1 = {5, 4};
static constexpr Card t2 = {5, 3};
static constexpr Card t3 = {5, 2};
static constexpr Card t4 = {5, 1};
static constexpr Card t5 = {5, 0};
static constexpr Card unknown = {std::numeric_limits<suit_t>::max(), 0};
static constexpr Card trash = {std::numeric_limits<suit_t>::max(), 1};
};
}
@ -70,19 +114,6 @@ namespace Hanabi {
inline std::ostream &operator<<(std::ostream &os, const Card &card);
constexpr Card r0 = {0, 5};
constexpr Card r1 = {0, 4};
constexpr Card r2 = {0, 3};
constexpr Card r3 = {0, 2};
constexpr Card r4 = {0, 1};
constexpr Card r5 = {0, 0};
constexpr Card y0 = {1, 5};
constexpr Card y1 = {1, 4};
constexpr Card y2 = {1, 3};
constexpr Card y3 = {1, 2};
constexpr Card y4 = {1, 1};
constexpr Card y5 = {1, 0};
constexpr Card unknown_card = {0, 6};
/**
* To store:
@ -162,19 +193,23 @@ inline std::ostream& operator<<(std::ostream& os, const Action& action);
class HanabiStateIF {
public:
virtual void clue() = 0;
virtual void give_clue() = 0;
virtual void discard(hand_index_t index) = 0;
virtual void play(hand_index_t index) = 0;
virtual void rotate_next_draw(const Card& card) = 0;
virtual void revert() = 0;
[[nodiscard]] virtual hand_index_t find_card_in_hand(const Card& card) const = 0;
[[nodiscard]] virtual player_t turn() const = 0;
[[nodiscard]] virtual clue_t num_clues() const = 0;
[[nodiscard]] virtual std::vector<std::vector<Card>> hands() const = 0;
[[nodiscard]] virtual std::vector<Card> cur_hand() const = 0;
[[nodiscard]] virtual size_t draw_pile_size() const = 0;
[[nodiscard]] virtual bool is_trash(const Card& card) const = 0;
[[nodiscard]] virtual bool is_playable(const Card& card) const = 0;
[[nodiscard]] virtual size_t draw_pile_size() const = 0;
[[nodiscard]] virtual bool is_relative_state_initialized() const = 0;
[[nodiscard]] virtual hand_index_t find_card_in_hand(const Card& card) const = 0;
[[nodiscard]] virtual std::uint64_t enumerated_states() const = 0;
[[nodiscard]] virtual const std::unordered_map<unsigned long, probability_t>& position_tablebase() const = 0;
@ -182,8 +217,8 @@ public:
virtual void init_backtracking_information() = 0;
virtual probability_t evaluate_state() = 0;
virtual std::optional<probability_t> lookup() const = 0;
[[nodiscard]] virtual std::optional<probability_t> lookup() const = 0;
[[nodiscard]] virtual std::uint64_t unique_id() const = 0;
virtual std::vector<std::pair<Action, std::optional<probability_t>>> get_reasonable_actions() = 0;
virtual std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>> possible_next_states(hand_index_t index, bool play) = 0;
@ -203,7 +238,7 @@ public:
HanabiState() = default;
explicit HanabiState(const std::vector<Card>& deck);
void clue() final;
void give_clue() final;
void discard(hand_index_t index) final;
void play(hand_index_t index) final;
@ -211,10 +246,15 @@ public:
void revert() final;
[[nodiscard]] player_t turn() const final;
[[nodiscard]] clue_t num_clues() const final;
[[nodiscard]] std::vector<std::vector<Card>> hands() const final;
[[nodiscard]] std::vector<Card> cur_hand() const final;
[[nodiscard]] size_t draw_pile_size() const final;
[[nodiscard]] hand_index_t find_card_in_hand(const Card& card) const final;
[[nodiscard]] bool is_trash(const Card& card) const final;
[[nodiscard]] bool is_playable(const Card& card) const final;
[[nodiscard]] size_t draw_pile_size() const final;
[[nodiscard]] bool is_relative_state_initialized() const final;
[[nodiscard]] std::uint64_t enumerated_states() const final;
@ -224,6 +264,7 @@ public:
probability_t evaluate_state() final;
std::optional<probability_t> lookup() const;
std::uint64_t unique_id() const final;
std::vector<std::pair<Action, std::optional<probability_t>>> get_reasonable_actions() final;
std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>> possible_next_states(hand_index_t index, bool play) final;
@ -237,7 +278,7 @@ private:
struct BacktrackAction {
explicit BacktrackAction(
ActionType action_type,
Card discarded_or_played = unknown_card,
Card discarded_or_played = Cards::unknown,
hand_index_t index = 0,
bool was_on_8_clues = false
);
@ -299,8 +340,6 @@ private:
static constexpr player_t draw_pile = num_players;
static constexpr player_t trash_or_play_stack = num_players + 1;
std::uint64_t unique_id() const;
// Usual game state
player_t _turn{};
clue_t _num_clues{};

View file

@ -40,8 +40,16 @@ namespace Hanabi {
return ret;
}
bool Card::operator==(const Card &other) const {
return suit == other.suit and rank == other.rank;
}
std::ostream &operator<<(std::ostream &os, const Card &card) {
os << suit_initials[card.suit] << 5 - card.rank;
if (card == Cards::trash) {
os << "kt";
} else {
os << suit_initials[card.suit] << 5 - card.rank;
}
return os;
}
@ -118,11 +126,11 @@ namespace Hanabi {
}
template<suit_t num_suits, player_t num_players, hand_index_t hand_size>
void HanabiState<num_suits, num_players, hand_size>::clue() {
void HanabiState<num_suits, num_players, hand_size>::give_clue() {
ASSERT(_num_clues > 0);
--_num_clues;
_actions_log.emplace(ActionType::clue, unknown_card, 0);
_actions_log.emplace(ActionType::clue, Cards::unknown, 0);
incr_turn();
}
@ -469,6 +477,37 @@ namespace Hanabi {
}
}
template<suit_t num_suits, player_t num_players, hand_index_t hand_size>
player_t HanabiState<num_suits, num_players, hand_size>::turn() const {
return _turn;
}
template<suit_t num_suits, player_t num_players, hand_index_t hand_size>
clue_t HanabiState<num_suits, num_players, hand_size>::num_clues() const {
return _num_clues;
};
template<suit_t num_suits, player_t num_players, hand_index_t hand_size>
std::vector<std::vector<Card>> HanabiState<num_suits, num_players, hand_size>::hands() const {
std::vector<std::vector<Card>> hands;
for(player_t player = 0; player < num_players; player++) {
hands.push_back({});
for(const Card& card: _hands[player]) {
hands.back().push_back(card);
}
}
return hands;
}
template<suit_t num_suits, player_t num_players, hand_index_t hand_size>
std::vector<Card> HanabiState<num_suits, num_players, hand_size>::cur_hand() const {
std::vector<Card> hand;
for(const Card& card: _hands[_turn]) {
hand.push_back(card);
}
return hand;
}
template<suit_t num_suits, player_t num_players, hand_index_t hand_size>
std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>> HanabiState<num_suits, num_players, hand_size>::possible_next_states(hand_index_t index, bool play) {
std::vector<std::pair<CardMultiplicity, std::optional<probability_t>>> next_states;
@ -551,9 +590,9 @@ namespace Hanabi {
}
if(_num_clues > 0) {
clue();
give_clue();
const std::optional<probability_t> prob = lookup();
const Action action = {ActionType::clue, unknown_card};
const Action action = {ActionType::clue, Cards::unknown};
reasonable_actions.emplace_back(action, prob);
revert_clue();
}
@ -648,7 +687,7 @@ namespace Hanabi {
// Last option is to stall
if(_num_clues > 0) {
clue();
give_clue();
const probability_t probability_stall = evaluate_state();
revert_clue();
best_probability = std::max(best_probability, probability_stall);