我失去了我的UNO牌的许多卡片,并感到沮丧,这正是这个想法在我的脑海里。我选择了C++,因为我知道这很难,只是为了提高我对语言的理解。自豪的是,我能够在C++中制作一个基本功能的UNO游戏!
我的执行是非常基本的,没有惩罚,也没有任何内务规则。目前,你可以玩任何数量的机器人,而机器人不是很聪明,他们只打第一张牌,他们看到可玩。
我希望对我目前的实现有任何改进或建议,无论是性能还是可读性。
目录结构:
UNO++ - card.h
- deck.h
- player.h
- cycle.h
- game.h
- main.cpp#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;
}
};#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;
}
};
}#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);
}
};
}#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;
}
};
}#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;
};
};
}示例
#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',它们只是为了使控制台看起来干净。发布于 2021-12-18 01:07:03
。
class Card将其颜色和卡片类型存储为std::strings,这有几个问题。除了每个卡使用大量内存之外,如果有人创建一个Card,并将color和type设置为无效的UNO卡颜色和类型的名称,会发生什么?特别是在较大的程序中,容易出错,请考虑:
Card card1{"Red", "3"}; // wrong capitalization
Card card2{"COLOR CHANGE", "BLACK"}; // wrong order解决这些问题的方法是为颜色和卡片类型创建enum类型,或者更好的enum class类型:
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()现在看起来如下:
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()等等。一个可能的快捷方式是让Deck从std::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读取输入可能会有问题,因为这不能读取整行,它只读取单个数字(如果variable是int)或单个单词(如果是std::string)。考虑:
std::string player_name;
std::cout << "Enter your name: ";
std::cin >> player_name;如果我用我的名字和姓呢?这将导致player_name中填充我的名字,并且可能会在第一次调用handle_human_player()时将Invalid Input打印出来。要确保读取整行输入,请使用std::getline():
std::string player_name;
std::cout << "Enter your name: ";
if (!std::getline(std::cin, player_name)) {
std::cerr << "Input failure\n";
return;
}当要读取数字时,必须将包含刚读取的行的字符串转换为数字,例如使用std::stoi():
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是一个有效的指针值,或者因为您有虚拟类),否则更倾向于通过引用传递对象。
发布于 2021-12-18 15:06:17
如果您将所有定义放置在头文件中,您将在编译速度上受到很大影响,因为如果您对标头中的定义进行任何更改,任何包含这些头的文件都必须重新编译。异常可以是cycle.h,它是一个模板类。
Suggestion:将包含警卫和杂注一起使用一次。包含保护是在标准中定义的,而语用曾经有其优点,几乎所有现代编译器都支持它。更多的阅读。
理解
operator<<不应该是成员函数。而且它并不总是需要是friend。
operator<<应该是类之外的一个免费函数。如果并且只有当您需要来自右侧类的私有成员时,才可以在类中添加一个朋友声明(但不是像您所做的那样的定义),比如:friend std::ostream& operator<<(std::ostream& out, const Y& o);。
来自优先选择的示例:
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)看到这行中的问题了吗?
如果不是,想象一下它的另一种形式:
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 (与类相同,但所有成员都是公共的)。然而,大多数情况下,您希望将公共类和私有类混合使用(并为基类提供保护)。
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。当然,你不必这么做,这是个建议。
https://codereview.stackexchange.com/questions/272109
复制相似问题