用 OpenCV 写一个 UNO 小游戏

本文介绍如何利用OpenCV开发一个UNO小游戏,包括准备卡牌资源、配置OpenCV库、设计游戏程序结构,如卡牌类、玩家类、UNO运行时类和AI类,并提供了游戏流程的详细步骤,包括人机交互和游戏状态控制。项目源代码已开源在GitHub上。
摘要由CSDN通过智能技术生成

源代码已在 GitHub 上公开。仓库地址:https://github.com/shiawasenahikari/UnoCard

第一步:准备卡牌图片资源

进入维基百科 UNO 词条,下载矢量图资源。地址:https://commons.wikimedia.org/wiki/File:UNO_cards_deck.svg

下载完成后,将其转为 PNG 位图,并对图像进行切割,提取出其中的每一张卡牌图像。然后对提取出的卡牌再做调整大小、调色等处理,最终得到以下位图资源。为了良好的屏幕显示效果,这里所有的卡牌图像大小均调整为 121x181。(可以到我的 github 仓库里提取已经做好的图片素材,地址:https://github.com/shiawasenahikari/UnoCard/tree/master/UnoCard/resource

我把除牌背外的每张牌都额外做了一次暗色处理,准备在画面显示中,用暗色牌表示“不可出的牌”,用亮色牌代表“可以出的牌”。另外,带颜色的黑牌也没有做暗色处理。这里带颜色的黑牌只用来指示打出这张牌的玩家指定的接下来的合法颜色是什么,只会出现在“最近打出的牌”里,而不会出现在玩家的手牌里。实际的 108 张牌里并没有真正意义上的“带颜色的黑牌”。

第二步:新建 VS 项目,导入 OpenCV 库

打开你的 Visual Studio 软件,新建一个 Win32 C++ 控制台应用程序。

在 Win32 应用程序向导里,勾选【空项目】复选框,然后点击【完成】。

在你的解决方案根目录里,建立如下的目录结构

UnoCard(解决方案根目录)

└─UnoCard(源代码根目录)

    ├─include(头文件根目录)

    ├─lib(库目录)

    ├─resource(卡牌资源目录)

    └─src(cpp 文件根目录)

然后,把你准备好的图片资源文件复制到 resource 目录里。

接下来导入 OpenCV 库。这里以 4.1.2 版本的 OpenCV 为例,其他版本(3.0 及以上)的配置方法大同小异。

1) 进入 OpenCV 官网(地址:https://opencv.org/releases)下载 Windows 版的 OpenCV。如图所示,点击那个 Windows 按钮即可开始下载。

2) 下载完成后会得到一个自解压文件,将其解压到任意目录(会在指定目录下自动创建 opencv 子目录),假设解压到了 D:\ 目录。然后我们依次复制以下文件到我们的解决方案目录里:

将 D:\opencv\build\include\opencv2 目录复制到解决方案根目录下的 UnoCard\include 目录里。

将 D:\opencv\build\x64\vc14\lib 目录中的 opencv_world412.lib 文件复制到解决方案根目录下的 UnoCard\lib 目录里。注意文件名里的数字,因为这里下载的是 4.1.2 版本的 OpenCV,所以文件名里的数字是 412。如果你下载了其他版本的 OpenCV,则文件名里的数字也会有所不同。下面复制的 dll 文件同理。(这里默认你用的是 Visual Studio 2015。如果你用的是 Visual Studio 2017,则改为从 D:\opencv\build\x64\vc15\lib 目录中复制。)

将 D:\opencv\build\x64\vc14\bin 目录中的 opencv_world412.dll 文件复制到解决方案根目录下的 UnoCard 目录里。(这里默认你用的是 Visual Studio 2015。如果你用的是 Visual Studio 2017,则改为从 D:\opencv\build\x64\vc15\bin 目录中复制。)

第三步:在项目中配置 OpenCV

1) 在 Visual Studio 的右侧【解决方案资源管理器】里,右击 UnoCard,选择【属性】。

2) 点击右上角的【配置管理器】,然后删除 Debug 配置和 Win32 平台,只保留 Release 配置和 x64 平台。

3) 关闭配置管理器后,点击项目属性对话框左侧的【VC++ 目录】,配置 include 目录和库目录。其中,在【包含目录】中添加 $(MSBuildProjectDirectory)\include 目录,在【库目录】中添加 $(MSBuildProjectDirectory)\lib 目录。

4) 点击左侧的【链接器】→【输入】,然后在右侧的【附加依赖项】中添加 opencv_world412.lib。注意你导入的是哪个版本的 OpenCV,要和对应的库文件的文件名一致。这里我们下载的是 4.1.2 版本,所以文件名是 opencv_world412.lib。

5) 配置完成后点【确定】,然后我们来检查一下配置是否成功。在解决方案资源管理器中右击【源文件】,然后选择【添加】→【新建项】,然后在解决方案根目录下的 UnoCard\src 目录中新建 main.cpp。

6) 在 main.cpp 中输入如下代码:

#include <opencv2/highgui.hpp>

int main() {
	cv::Mat image = cv::imread("resource/front_r0.png");
	cv::namedWindow("Uno");
	cv::imshow("Uno", image);
	cv::waitKey(0);
	cv::destroyAllWindows();
	return 0;
} // main()

7) 按 Ctrl+F5 开始执行,如果弹出一个窗口显示了一张 Uno 卡牌,就说明 OpenCV 配置成功了。然后按任意键退出。

第四步:开始设计程序

这里使用面向对象的设计思想,将整个程序分为两部分:主程序部分、核心部分。其中主程序部分负责屏幕显示及人机交互,核心部分用于管理各玩家的卡牌资源,以及控制游戏实时状态,由四个子模块构成:卡牌类、玩家类、UNO 运行时类、AI 类。

4.1 卡牌类——Card class

顾名思义,一个卡牌类对象里存储的就是一张牌的基本信息。在设计卡牌类之前,我们需要先定义两个新的枚举类型,用于标识一张 UNO 牌里的两个元素——颜色、内容。其中颜色有红、黄、蓝、绿,以及无色(黑牌打出前视为无色,打出后视为打出此牌的玩家所指定的颜色)五种,内容有数字 0~9、反转、禁止、+2、变色、变色且 +4 共 15 种。对颜色和内容的枚举类型的声明分别放置在 include\Color.h 和 include\Content.h 头文件中。

// Color.h

#pragma once

typedef enum {
	NONE, RED, BLUE, GREEN, YELLOW
} Color;



// Content.h

#pragma once

typedef enum {
	NUM0, NUM1, NUM2, NUM3, NUM4,
	NUM5, NUM6, NUM7, NUM8, NUM9,
	REV, SKIP, DRAW2, WILD, WILD_DRAW4
} Content;

然后我们开始设计卡牌类。对该类的声明和定义分别放在 include\Card.h 头文件和 src\Card.cpp 实现文件里。(注意,本篇文章里只是实现一个最小模型,实际一个对象里不见得只能存储这些信息,完全可以进一步拓展。具体该如何拓展,可以参考我的 GitHub 仓库里的代码。)

// Card.h

#pragma once

#include <string>
#include <Color.h>
#include <Content.h>
#include <opencv2/core.hpp>

class Card {
public:
	const cv::Mat image;      // 卡牌高亮图案
	const cv::Mat darkImg;    // 卡牌暗色图案
	const Content content;    // 卡牌内容,例:NUM3
	const std::string name;   // 卡牌名字,例:"Blue 3"
	Color getColor();         // 返回卡牌颜色
	bool isWild();            // 该牌是否为黑牌

private:
	friend class Uno;         // 授权 UNO 运行时类访问本类私有字段
	const int order;          // 卡牌序号,排序时用
	Color color;              // 卡牌颜色,例:BLUE。该属性可写,但只能被 UNO 运行时类改写

	// 构造器,该类的实例仅可被 UNO 运行时类创建
	Card(const cv::Mat&, const cv::Mat&, Color, Content, const std::string&);
}; // Card Class
// Card.cpp

#include <Card.h>

// 构造器,该类的实例仅可被 UNO 运行时类创建
Card::Card(const cv::Mat& image, const cv::Mat& darkImg,
	Color color, Content content, const std::string& name) :
	name(name),
	image(image),
	color(color),
	content(content),
	darkImg(darkImg),
	order((color << 4) + content) {
} // Card(Mat&, Mat&, Color, Content, string&) (Class Constructor)

// 返回卡牌颜色
Color Card::getColor() {
	return color;
} // getColor()

// 该牌是否为黑牌
bool Card::isWild() {
	// 【变色】及【变色 +4】为黑牌,可以不受上一张打出的牌的限制,任意跟出。
	// 其他有颜色的牌只能按同色或同内容来跟。
	return content == WILD || content == WILD_DRAW4;
} // isWild()

这里,卡牌的颜色属性字段 color 设置为了私有,同时提供了一个公有函数 getColor() 来获取卡牌颜色。首先,color 属性不能设置为 const,因为黑牌的颜色是可变的——打出前视为无色,打出后视为打出该牌的玩家所指定的颜色。其次,color 属性不能暴露给外部,否则有被篡改的可能。所以就这样封装了一下,在类内部以及 UNO 运行时类里,color 字段是可改写的;在其他地方看来,color 字段是只读的。

4.2 玩家类——Player class

用于存储每位玩家的实时信息,如玩家的手牌,玩家最近一轮的动作(是摸牌还是出牌,如果是出牌,则出了哪张牌)。对该类的声明和定义分别放在 include\Player.h 头文件和 src\Player.cpp 实现文件里。

// Player.h

#pragma once

#include <vector>
#include <Card.h>

class Player {
public:
	static const int YOU = 0;            // 你的 ID
	static const int COM1 = 1;           // 西家的 ID
	static const int COM2 = 2;           // 北家的 ID
	static const int COM3 = 3;           // 东家的 ID
	const std::vector<Card*>& getHand(); // 返回所有手牌
	Card* getRecent();                   // 返回最近一轮出的牌,或 nullptr 表最近一轮摸了牌

private:
	friend class Uno;                   // 授权 UNO 运行时类访问本类私有字段
	std::vector<Card*> hand;            // 手牌列表
	Card* recent;                       // 最近一轮的出牌,或 nullptr(表最近一轮摸了牌)
	Player();                           // 构造器,该类的实例仅可被 UNO 运行时类创建
}; // Player Class
// Player.cpp

#include <Player.h>

// 构造器,该类的实例仅可被 UNO 运行时类创建
Player::Player() {
	recent = nullptr;
} // Player() (Class Constructor)

// 返回所有手牌
const std::vector<Card*>& Player::getHand() {
	return hand;
} // getHand()

// 返回最近一轮出的牌,或 nullptr 表最近一轮摸了牌
Card* Player::getRecent() {
	return recent;
} // getRecent()

同样,该类对两个字段 hand、recent 也做了一层只读封装处理,在类内部以及 UNO 运行时类里,这两个字段是可改写的(用于实时更新玩家的状态);在其他地方看来,这两个字段是只读的(防止玩家的状态被非法篡改)。另外注意,getHand() 方法返回的是相应字段的常引用,避免了不必要的复制所带来的开销

4.3 Uno 运行时类——Uno class

重头戏来了。这个类要做的事很多,游戏里的一切动作,不论是加载卡牌资源,还是洗牌、发牌、摸牌、出牌,都是委托这个类完成的。这里先简要说明一下该类由哪些内容构成,然后再上代码。

类成员:

  • cv::Mat backImage:牌背图案。
  • cv::Mat wildImage[5]:无色的、指定红色的、指定蓝色的、指定绿色的、指定黄色的变色牌图案。
  • cv::Mat wd4Image[5]:无色的、指定红色的、指定蓝色的、指定绿色的、指定黄色的变色牌图案。
  • int now:指示当前是谁的回合。合法值只能是 Player::YOU, Player::COM1, Player::COM2, Player::COM3 之一。
  • int direction:指示玩家的行动顺序是顺时针还是逆时针。其中 1 表示顺时针,3 表示逆时针,相当于每次顺时针跳 3 个人。
  • Player player[4]:玩家。存储并实时更新四个玩家的状态信息。
  • std::vector<Card> table:牌库。一个包含了 108 个 Card 对象的列表。
  • std::vector<Card*> deck:发牌堆。游戏开局时,以随机顺序获得牌库(table)列表中所有 Card 对象的指针,生成发牌堆列表;玩家抓牌时,从发牌堆顶端取一个 Card* 对象加入到相应的 player 对象的手牌(hand)列表中。
  • std::vector<Card*> recent:展示最近打出过的牌,最多 5 张。玩家出牌时,将玩家手牌(hand)中相应的 Card* 对象移动到本列表中。当本列表超过 5 个元素时,将本列表头部的元素移动到下面说的【弃牌堆】列表中。
  • std::vector<Card*> used:弃牌堆。最近出牌列表超过 5 个元素时,将其头部的元素移动到本列表中,不再向玩家展示。当发牌堆没有余牌时,将本列表里的所有牌以乱序移回发牌堆(洗牌)使游戏正常进行下去,直到分出胜负为止。

类函数:

  • Uno():构造器,用于初始化运行时类。
  • static Uno* getInstance():为减少资源浪费,该类被设计为单例模式。该函数为获取运行时实例的唯一入口。
  • const cv::Mat& getBackImage():获取牌背图案。
  • const cv::Mat& getColoredWildImage(Color color):获取指定过颜色的变色牌图案。
  • const cv::Mat& getColoredWildDraw4Image(Color color):获取指定过颜色的变色 +4 牌图案。
  • int getNow():获取当前回合的玩家 ID。
  • int switchNow():切换到下一个玩家的回合,并返回其 ID。
  • int getNext():获取下家的 ID。
  • int getOppo():获取对家的 ID。
  • int getPrev():获取上家的 ID。
  • int getDirection():获取当前行动顺序,1 为顺时针,3 为逆时针。
  • int switchDirection():反转行动顺序,并返回反转后的行动顺序。
  • Player* getPlayer(int id):获取给定 ID 所对应的玩家对象。
  • int getDeckCount():获取发牌堆剩余牌数量。
  • int getUsedCount():获取弃牌堆剩余牌数量。
  • const std::vector<Card*>& getRecent():获取最近出过的牌。
  • void start():开局。
  • Card* draw(int who):指定 ID 的玩家从发牌堆抓一张牌。
  • bool isLegalToPlay(Card* card):指定的卡牌在当前游戏场景下能否合法打出。
  • Card* play(int who, int index, Color color = NONE):指定 ID 的玩家打出手中指定位置的手牌。其中第三个参数是一个可选参数,仅当打出的牌是黑牌时才需要指定这个参数,用于告知玩家用黑牌指定的接下来的合法颜色是哪个。

对该类的声明和定义分别放在 include\Uno.h 头文件和 src\Uno.cpp 实现文件里。

// Uno.h

#pragma once

#include <vector>
#include <Card.h>
#include <Color.h>
#include <Player.h>
#include <Content.h>
#include <opencv2/core.hpp>

class Uno {
public:
	static const int DIR_LEFT = 1;
	static const int DIR_RIGHT = 3;
	static const int MAX_HOLD_CARDS = 14; // 因画面大小限制,每位玩家最多持有 14 张手牌
	static Uno* getInstance();
	const cv::Mat& getBackImage();
	const cv::Mat& getColoredWildImage(Color color);
	const cv::Mat& getColoredWildDraw4Image(Color color);
	int getNow();
	int switchNow();
	int getNext();
	int getOppo();
	int getPrev();
	int getDirection();
	int switchDirection();
	Player* getPlayer(int id);
	int getDeckCount();
	int getUsedCount();
	const std::vector<Card*>& getRecent();
	void start();
	Card* draw(int who);
	bool isLegalToPlay(Card* card);
	Card* play(int who, int index, Color color = NONE);

private:
	cv::Mat backImage;
	cv::Mat wildImage[5];
	cv::Mat wildDraw4Image[5];
	int now;
	int direction;
	Player player[4];
	std::vector<Card> table;
	std::vector<Card*> deck;
	std::vector<Card*> recent;
	std::vector<Card*> used;
	Uno();
}; // Uno Class
// Uno.cpp

#include <Uno.h>
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <opencv2/imgcodecs.hpp>

// 图片资源缺失时在控制台中显示的错误信息
static const char* BROKEN_IMAGE_RESOURCES_EXCEPTION =
"One or more image resources are broken. Re-install this application.";

// 单一实例,对外隐藏构造器
Uno::Uno() {
	int i;
	cv::Mat br[54], dk[54]; // 54 种亮色/暗色牌面图案

	// 读取牌背图案
	backImage = cv::imread("resource/back.png");
	if (backImage.empty() ||
		backImage.rows != 181 ||
		backImage.cols != 121) {
		std::cout << BROKEN_IMAGE_RESOURCES_EXCEPTION << std::endl;
		exit(1);
	} // if (backImage.empty() || ...)

	// 读取牌面图案
	br[0] = cv::imread("resource/front_r0.png");
	br[1] = cv::imread("resource/front_r1.png");
	br[2] = cv::imread("resource/front_r2.png");
	br[3] = cv::imread("resource/front_r3.png");
	br[4] &
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值