首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >与AI玩家玩UNO牌游戏

与AI玩家玩UNO牌游戏
EN

Code Review用户
提问于 2021-12-17 19:14:29
回答 2查看 1.2K关注 0票数 4

我失去了我的UNO牌的许多卡片,并感到沮丧,这正是这个想法在我的脑海里。我选择了C++,因为我知道这很难,只是为了提高我对语言的理解。自豪的是,我能够在C++中制作一个基本功能的UNO游戏!

乌诺

我的执行是非常基本的,没有惩罚,也没有任何内务规则。目前,你可以玩任何数量的机器人,而机器人不是很聪明,他们只打第一张牌,他们看到可玩。

我希望对我目前的实现有任何改进或建议,无论是性能还是可读性。

目录结构:

代码语言:javascript
复制
UNO++ - card.h
      - deck.h
      - player.h
      - cycle.h
      - game.h
      - main.cpp

card.h

代码语言:javascript
复制
#pragma once

#include 
#include 
#include 
#include 

namespace UNO
{
    const std::array COMMON_COLORS =
        {"BLUE", "GREEN", "RED", "YELLOW"};
    const std::array SPECIAL_COLORS =
        {"BLACK"};

    const std::array NUMBERS = 
        {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"};
    const std::array ACTION_TYPES =
        {"REVERSE", "SKIP", "2 PLUS"};
    const std::array WILD_TYPES =
        {"COLOR CHANGE", "4 PLUS"};

    std::map COLOR_MAP = {
        {"BLUE", "\033[34m"},
        {"GREEN", "\033[32m"},
        {"RED", "\033[31m"},
        {"YELLOW", "\033[93m"},
        {"BLACK", "\033[30m"},
    };
    const std::string RESET_COLOR = "\033[0m";

    class Card
    {
    public:
        std::string color;
        std::string type;

        Card(): color{"BLUE"}, type{"0"}{};
        Card(std::string color_, std::string type_):
            color{color_}, type{type_}{};

        operator std::string() const
        {
            return COLOR_MAP[color] + type + RESET_COLOR;
        }

        friend std::ostream& operator<<(std::ostream& os, const Card& card)
        {
            os << COLOR_MAP[card.color] << card.type << RESET_COLOR;
            return os;
        }
    };

    bool can_play_card(const Card& play_card, const Card& top_card)
    {
        if (
            top_card.color == play_card.color || 
            top_card.type == play_card.type || 
            play_card.color == "BLACK"
        )
        {
            return true;
        }
        return false;
    };

    std::string cards_to_str(const std::vector& cards)
    {
        int len_cards = cards.size();
        std::string str_cards = "[";

        for (int i = 0; i < len_cards; ++i)
        {
            str_cards += std::string(cards[i]);

            if (i != len_cards - 1)
                str_cards += ", ";
        }
        str_cards += "]";

        return str_cards;
    }
};

deck.h

代码语言:javascript
复制
#pragma once

#include 
#include "card.h"

namespace UNO
{
    std::vector create_cards(
        int cards_num=2,
        int cards_action=1,
        int cards_wild=2
    )
    {
        std::vector cards;

        for (auto color : COMMON_COLORS)
        {
            for (auto num_type : NUMBERS)
            {
                for (int i = 0; i < cards_num; ++i)
                    cards.push_back(Card(color, num_type));
            }

            for (auto action_type : ACTION_TYPES)
            {
                for (int i = 0; i < cards_action; ++i)
                    cards.push_back(Card(color, action_type));
            }
        }

        for (auto wild_type : WILD_TYPES)
        {
            for (int i = 0; i < cards_wild; ++i)
                cards.push_back(Card("BLACK", wild_type));
        }

        return cards;
    }

    class Deck
    {
    public:
        std::vector cards;

        Deck(): cards{create_cards()}{};
        Deck(std::vector cards_): cards{cards_}{};

        friend std::ostream& operator<<(std::ostream& os, const Deck& deck)
        {
            os << cards_to_str(deck.cards);
            return os;
        }

        void shuffle()
        {
            std::random_shuffle(cards.begin(), cards.end());
        }

        std::vector deal_cards(int amount)
        {
            if (cards.size() < amount)
            {
                for (const Card& card : create_cards())
                    cards.push_back(card);
            }

            std::vector dealt_cards;
            for (int i = 0; i < amount; ++i)
            {
                dealt_cards.push_back(cards.back());
                cards.pop_back();
            }

            return dealt_cards;
        }
    };
}

player.h.h

代码语言:javascript
复制
#pragma once

#include 
#include "deck.h"

namespace UNO
{
    class Player
    {
    public:
        std::string name;
        std::vector cards;

        Player(std::string name_, std::vector cards_):
            name{name_}, cards{cards_}{};

        friend std::ostream& operator<<(std::ostream& os, const Player& player)
        {
            os << player.name;
            return os;
        }

        int get_play_card_index(const Card& top_card) const
        {
            for (int i = 0; i < cards.size(); ++i)
            {
                if (can_play_card(cards[i], top_card))
                    return i;
            }
            return -1;
        }

        Card pop_card(int index)
        {
            Card popped_card = cards[index];
            cards.erase(cards.begin() + index);
            return popped_card;
        }

        bool is_win() const
        {
            return (cards.size() == 0);
        }
    };
}

cycle.h

代码语言:javascript
复制
#pragma once

#include 

namespace UNO
{
    template 
    class Cycle
    {
    private:
        int index = -1;
        bool reversed = false;

    public:
        std::vector items;
        Cycle(){};
        Cycle(std::vector items_): items{items_}{};

        Type* next()
        {
            index += (reversed) ? -1 : 1;
            if (index == items.size())
                index = 0;
            else if (index == -1)
                index = items.size() - 1;

            return &items[index];
        }

        void reverse()
        {
            reversed = !reversed;
        }
    };
}

game.h

代码语言:javascript
复制
#pragma once

#include 
#include 
#include 
#include 

#include "card.h"
#include "deck.h"
#include "player.h"
#include "cycle.h"

namespace UNO
{
    class Game
    {
    public:
        Deck cards_deck;
        std::vector players;
        Cycle player_cycle;
        Card top_card;

        Game()
        {
            int no_of_players;
            std::cout << "Enter number of players: ";
            std::cin >> no_of_players;
            if (std::cin.fail())
                std::cerr << "Input Failure";

            std::string player_name;
            std::cout << "Enter your name: ";
            std::cin >> player_name;
            if (std::cin.fail())
                std::cerr << "Input Failure";

            cards_deck.shuffle();
            players.push_back(Player(player_name + " #1", cards_deck.deal_cards(7)));

            for (int i = 0; i < no_of_players - 1; ++i)
            {
                players.push_back(
                    Player(
                        "Player #" + std::to_string(i + 2),
                        cards_deck.deal_cards(7))
                    );
            }
            top_card = cards_deck.deal_cards(1)[0];
            player_cycle.items = players;
        };

        Game(Deck cards, Cycle player_cycle_):
            cards_deck{cards}, player_cycle{player_cycle_}{};

        bool human_player(const Player& player) const
        {
            return (player.name.back() == '1');
        }

        void play_game()
        {
            print_intro();
            while (true)
            {
                Sleep(3500);
                Player* player = next_player();
                print_turn(*player);
                handle_player(player);

                if (player->is_win())
                {
                    std::cout << *player << " wins the game!!\n";
                    break;
                }
            }
        };

        void handle_player(Player* player)
        {
            if (human_player(*player))
                handle_human_player(player);
            else
                handle_ai_player(player);
        }

        void print_intro() const
        {
            std::cout << "Welcome to UNO!\n";
            std::cout << "Finish your cards as fast as possible!\n\n";
        }

        void print_turn(Player& player) const
        {
            if (human_player(player))
            {
                std::cout << "You have " << player.cards.size()
                << " cards left - " << cards_to_str(player.cards) 
                << '\n';
            }
            else
            {
                std::cout << player << " has " << player.cards.size()
                    << " cards left...\n";
            }
            std::cout << "Top card - " << top_card << "\n\n";
        }

        void handle_human_player(Player* player)
        {
            bool any_play_card = false;
            for (const Card& card : player->cards)
            {
                if (can_play_card(card, top_card))
                {
                    any_play_card = true;
                    break;
                }
            }

            char choice = 'd';
            if (any_play_card)
            {
                while (true)
                {
                    std::cout << "Play or Draw? (p/d): ";
                    std::cin >> choice;
                    if (std::cin.fail())
                        std::cerr << "Input Failure";

                    choice = tolower(choice);
                    if (choice == 'p' || choice == 'd')
                        break;
                    else
                        std::cout << "Invalid Input\n";
                }
            }

            if (choice == 'p')
            {
                int index;
                while (true)
                {    
                    std::cout << "Enter card index " << '(' << "1 - " << 
                        player->cards.size() << ')' << ": ";
                    std::cin >> index;
                    if (std::cin.fail())
                        std::cerr << "Input Failure";

                    if (index < 0 || index > player->cards.size())
                        std::cout << "Invalid index!\n";
                    else if (!can_play_card(player->cards[index - 1], top_card))
                        std::cout << "Can not play " << player->cards[index - 1] << "!\n";
                    else
                        break;
                }

                Card card = player->pop_card(index - 1);
                top_card = card;
                std::cout << *player << " plays " << card << '\n';
                handle_card_effect(&card, player);
            }
            else if (choice == 'd')
            {
                Card card = cards_deck.deal_cards(1)[0];
                player->cards.push_back(card);
                std::cout << "You got a " << card << "...\n";
                Sleep(2000);

                if (can_play_card(card, top_card))
                {
                    std::cout << "Do you want to play " << card << "? (y/n): ";
                    char choice;
                    std::cin >> choice;
                    if (std::cin.fail())
                        std::cerr << "Input Failure";
                    choice = tolower(choice);

                    if (choice == 'y')
                    {
                        top_card = card;
                        player->cards.pop_back();
                        std::cout << *player << " plays " << card << '\n';
                        handle_card_effect(&card, player);
                    }
                }
                std::cout << '\n';
            }
        }

        void handle_ai_player(Player* player)
        {
            int card_index = player->get_play_card_index(top_card);
            if (card_index != -1)
            {
                Card play_card = player->pop_card(card_index);
                std::cout << *player << " plays " << play_card << '\n';
                handle_card_effect(&play_card, player);
            }
            else
            {
                Card card = cards_deck.deal_cards(1)[0];
                std::cout << "Dealing 1 card to " << *player << "...\n";
                if (can_play_card(card, top_card))
                {
                    top_card = card;
                    Sleep(3500);
                    std::cout << *player << " plays " << card << '\n';
                    handle_card_effect(&card, player);
                }
                else
                    player->cards.push_back(card);
                std::cout << '\n';
            }
        }

        void handle_card_effect(Card* card, Player* card_player)
        {
            bool card_is_number = (std::find(
                NUMBERS.begin(), NUMBERS.end(), card->type) != NUMBERS.end());
            top_card = *card;

            if (card_is_number) {}
            else if (card->type == "REVERSE")
            {
                player_cycle.reverse();
                std::cout << "Cycle reversed...\n";
            }
            else if (card->type == "SKIP")
            {
                Player* skip_player = next_player();
                std::cout << *skip_player << " skipped...\n";
            }
            else if (card->type == "2 PLUS")
            {
                Player* player = next_player();
                std::vector deal_cards = cards_deck.deal_cards(2);
                for (int i = 0; i < 2; ++i)
                    player->cards.push_back(deal_cards[i]);

                std::cout << "Dealing 2 cards to " << *player << ", skipped...\n";              
            }
            else if (card->type == "COLOR CHANGE")
            {
                if (human_player(*card_player))
                    top_card.color = get_color_input();
                else
                    top_card.color = COMMON_COLORS[rand() % 4];
                std::cout << "Color changed to " << COLOR_MAP[top_card.color]
                    << top_card.color << RESET_COLOR << '\n';
            }
            else if (card->type == "4 PLUS")
            {
                if (human_player(*card_player))
                    top_card.color = get_color_input();
                else
                    top_card.color = COMMON_COLORS[rand() % 4];
                std::cout << "Color changed to " << COLOR_MAP[top_card.color]
                    << top_card.color << RESET_COLOR << '\n';

                Player* player = next_player();
                std::vector deal_cards = cards_deck.deal_cards(4);
                for (int i = 0; i < 4; ++i)
                    player->cards.push_back(deal_cards[i]);

                std::cout << "Dealing 4 cards to " << *player << ", skipped...\n";            
            }
            std::cout << '\n';
        }

        Player* next_player()
        {
            while (true)
            {
                Player* player = player_cycle.next();
                if (!player->is_win())
                    return player;
            }
        };

        std::string get_color_input()
        {
            std::string color;
            do
            {
                std::cout << "Enter color: ";
                std::cin >> color;
                if (std::cin.fail())
                    std::cerr << "Input Failure";
                for (int i = 0; i < color.length(); ++i)
                    color[i] = toupper(color[i]);
            } while (
                std::find(COMMON_COLORS.begin(),
                COMMON_COLORS.end(), color) == COMMON_COLORS.end()
            );

            return color;
        };
    };
}

main.cpp --一个最小的

示例

代码语言:javascript
复制
#include 
#include 
#include "game.h"

using namespace UNO;

int main()
{
    srand(time(0));
    /* Can also handle creating players and cards deck
    Deck cards;
    std::vector players = {
        Player("Taha #1", cards.deal_cards(7)),
        Player("player #2", cards.deal_cards(7)),
        Player("player #3", cards.deal_cards(7)),
        Player("player #4", cards.deal_cards(7)),
    };
    Cycle player_cycle(players);
    Game my_game(cards, player_cycle);
    */

    // Or use the default constructor
    Game my_game;
    my_game.play_game();
    return 0;
}

在审查前要知道的几点:

  • 我接触过一种面向对象的风格。
  • 我已经创建了一个模板类Cycle,因此它也可以用于其他目的。
  • 您可能会在代码块之间看到许多随机的'\n',它们只是为了使控制台看起来干净。
  • 我是这么说的,但是机器人不是很聪明。
  • 在这一点上,我花了很多时间和错误,所以可能有一些bug,但我观察到的是,我已经修复了它们。
EN

回答 2

Code Review用户

回答已采纳

发布于 2021-12-18 01:07:03

避免使用字符串类型的

class Card将其颜色和卡片类型存储为std::strings,这有几个问题。除了每个卡使用大量内存之外,如果有人创建一个Card,并将colortype设置为无效的UNO卡颜色和类型的名称,会发生什么?特别是在较大的程序中,容易出错,请考虑:

代码语言:javascript
复制
Card card1{"Red", "3"};              // wrong capitalization
Card card2{"COLOR CHANGE", "BLACK"}; // wrong order

解决这些问题的方法是为颜色和卡片类型创建enum类型,或者更好的enum class类型:

代码语言:javascript
复制
enum class Color {
    BLUE, GREEN, RED, YELLOW, BLACK
};

enum class Type {
    _0, _1, _2, ..., _9,
    REVERSE, SKIP, ...
};

class Card {
public:
    Color color;
    Type type;

    Card(): color{Color::BLUE}, type{Type::_0} {}
    Card(Color color, Type type): color{color}, type{type} {}
    ...
};

例如,can_play_card()现在看起来如下:

代码语言:javascript
复制
bool can_play_card(const Card& play_card, const Card& top_card) {
    return top_card.color == play_card.color ||
           top_card.type == play_card.type ||
           play_card.color == Color::BLACK;
}

你仍然需要一些方法来在std::strings,Colors和TypeD15之间绘制地图,不幸的是,enum并不能帮到你。

使用Deck everywhere而不是std::vector

您正在使用Decks和std::vectors在您的一组卡的代码。我建议您尝试在任何地方使用Deck (当然,在class Deck内部,您仍然必须使用std::vector )。要使这种工作正常进行,您应该向Deck添加成员函数,使其像std::vector那样工作,例如size()begin()end()push_back()erase()等等。一个可能的快捷方式是让Deckstd::vector继承。

您还可以考虑将代码从create_cards()移到Deck的构造函数中。

Cycle

中的可能的越界访问

有可能让Cycle::next()超出界限读取。例如,如果items为空,它将始终尝试返回指向不存在项的指针。但是也可以用多个参与者构造一个Cycle,但是在第一次调用next()之前调用reverse(),在这种情况下,对next()的第一次调用将导致index变成-2

虽然这种情况在UNO游戏中可能是不可能的(因为您总是从前向开始,并且在next_player()至少被调用一次之前是无法逆转的),但是一个类的正确性不太依赖于它的用户的行为是一个好主意。

即使第一件事是调用next(),您也可以轻松地使reverse()行为正确。

至于在空周期中调用next(),您可以认为调用方不应该这样做。它可能仍然是好的assert(!items.empty()),所以调试程序更容易,以防这种情况发生。您还可以考虑禁止构造一个空的Cycle,方法是删除默认的构造函数,并创建一个获取条目向量的构造函数(如果是throw )。

不要随意睡觉

在玩家之间睡3.5秒很快就会变得烦人。另外,Sleep()不是可移植的C++,如果您真的想使用它,请使用std::this_thread::sleep_for()。但更好的办法是,等待用户按下键盘上的Enter键,进入下一个播放器;这样人类玩家就可以随意控制速度。

考虑使用std::getline()读取输入

std::cin >> variable读取输入可能会有问题,因为这不能读取整行,它只读取单个数字(如果variableint)或单个单词(如果是std::string)。考虑:

代码语言:javascript
复制
std::string player_name;
std::cout << "Enter your name: ";
std::cin >> player_name;

如果我用我的名字和姓呢?这将导致player_name中填充我的名字,并且可能会在第一次调用handle_human_player()时将Invalid Input打印出来。要确保读取整行输入,请使用std::getline()

代码语言:javascript
复制
std::string player_name;
std::cout << "Enter your name: ";
if (!std::getline(std::cin, player_name)) {
    std::cerr << "Input failure\n";
    return;
}

当要读取数字时,必须将包含刚读取的行的字符串转换为数字,例如使用std::stoi()

代码语言:javascript
复制
std::string line;
std::cout << "Enter number of players: ";
if (!std::getline(std::cin, line)) {
    std::cerr << "Input failure\n";
    return;
}
int no_of_players = std::stoi(line);

但是请注意,std::stoi()std::cin >> number一样,在遇到第一个不是数字的有效部分的字符时就停止了解析。

避免使用std::random_shuffle()

std::random_shuffle()在C++14中被废弃,在C++17中被删除。避免使用它,而使用std::shuffle()

通过(const)引用

传递大型对象

虽然在某些地方确实使用(const)引用,但也有一些地方不必要地按值传递参数。例如,Player()的构造函数应该将卡片的名称和牌套作为const引用。

通过引用而不是指针

传递Player

Game::handle_player()中,您通过指针传递播放机,但在print_turn()中则是引用传递。除非有理由将它作为指针传递(比如nullptr是一个有效的指针值,或者因为您有虚拟类),否则更倾向于通过引用传递对象。

票数 1
EN

Code Review用户

发布于 2021-12-18 15:06:17

在.cpp文件中放置定义

如果您将所有定义放置在头文件中,您将在编译速度上受到很大影响,因为如果您对标头中的定义进行任何更改,任何包含这些头的文件都必须重新编译。异常可以是cycle.h,它是一个模板类。

Suggestion:将包含警卫和杂注一起使用一次。包含保护是在标准中定义的,而语用曾经有其优点,几乎所有现代编译器都支持它。更多的阅读

理解

operator<<

operator<<不应该是成员函数。而且它并不总是需要是friend

operator<<应该是类之外的一个免费函数。如果并且只有当您需要来自右侧类的私有成员时,才可以在类中添加一个朋友声明(但不是像您所做的那样的定义),比如:friend std::ostream& operator<<(std::ostream& out, const Y& o);

来自优先选择的示例:

代码语言:javascript
复制
class Y {
    int data; // private member
    // the non-member function operator<< will have access to Y's private members
    friend std::ostream& operator<<(std::ostream& out, const Y& o);
};
// friend declaration does not declare a member function
// this operator<< still needs to be defined, as a non-member
std::ostream& operator<<(std::ostream& out, const Y& y)
{
    return out << y.data; // can access private member Y::data
}

避免冗余副本

另一个答案建议您使用枚举而不是字符串(这是您应该使用的)。但这是我要指出的一个缺陷,以防将来需要迭代字符串数组:for (auto color : COMMON_COLORS)看到这行中的问题了吗?

如果不是,想象一下它的另一种形式:

代码语言:javascript
复制
for (int i = 0; i < COMMON_COLORS.length(); i++) {
    auto color = COMMON_COLORS[i];
}

没错。当color可能是const std::string&时,它是一个D18

解决方案:for (const auto& color : COMMON_COLORS).您不会看到特定程序的运行时速度差异,但这避免了过早的悲观。没有必要复制颜色,修复很简单,所以我们修复它。

固定封装

其他类不使用的Functions应该是 private**.**此外,您的所有类都是class,默认情况下,它的所有成员都是私有的,您可以使用public将它们全部公有。如果您确实需要所有public成员,不妨使用一个struct (与类相同,但所有成员都是公共的)。然而,大多数情况下,您希望将公共类和私有类混合使用(并为基类提供保护)。

代码语言:javascript
复制
class Foo {
    int f; // f is private
public:
    float a; // a is public
    std::string b; // b is public
private:
    bool c; // c is private
}

考虑使用Getters和Setters而不是公共成员变量。好的做法是只有函数应该是public,变量应该是private。当然,你不必这么做,这是个建议。

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/272109

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档