基于C++的AI五子棋游戏项目开发教程

项目资源下载

  1. 基于C++的AI五子棋游戏项目源码压缩包下载地址
  2. 基于C++的AI五子棋游戏项目源码Github下载地址
  3. 基于C++的AI五子棋游戏项目所需素材
  4. 基于C++的AI五子棋游戏项目所需要的EasyX

项目简介

  本项目基于C++开发,整体来说比较简单,实现了人与AI之间的五子棋对弈,并且可以判定胜负以及音效添加等等,按照我博客的详细教程一步一步做下去肯定没问题!


项目开发软件环境

  • Windows11
  • VS2017
  • EasyX

项目开发硬件环境

  • CPU:Intel® Core™ i7-8750H CPU @ 2.20GHz 2.20 GHz
  • RAM:24GB
  • GPU:NVIDIA GeForce GTX 1060

文章目录

  • 项目资源下载
  • 项目简介
  • 项目开发软件环境
  • 项目开发硬件环境
  • 前言
  • 零、项目演示
    • 0.1 人机五子棋对弈
    • 0.2 黑棋(棋手)胜利
    • 2.3 白棋(AI)胜利
  • 一、创建项目
  • 二、导入素材
  • 三、项目框架设计
    • 3.1 设计项目框架
    • 3.2 根据项目框架设计类
  • 四、设计游戏主要接口
    • 4.1 设计Chess(棋盘)类主要接口
    • 4.2 设计AI(人工智能)类主要接口
    • 4.3 设计Man(棋手)类主要接口
    • 4.4 设计ChessGame(游戏控制)类主要接口
    • 4.5 设计各个接口的具体实现
  • 五、设计游戏基本框架
  • 六、棋盘初始化
    • 6.1 EasyX的使用
    • 6.2 设计棋盘的数据成员
    • 6.3 构造棋盘
    • 6.4 棋盘初始化
  • 七、棋手下棋实现
    • 7.1 棋手初始化
    • 7.2 棋手下棋功能初始化
    • 7.3 判断棋手下棋位置是否有效
    • 7.4 实现棋手下棋
  • 八、AI下棋实现
    • 8.1 AI初始化
    • 8.2 AI下棋原理
    • 8.3 AI对棋局进行评分计算
    • 8.4 实现AI下棋
  • 九、胜负判定实现
    • 9.1 对胜负进行处理
    • 9.2 胜负判定原理
    • 9.3 胜负判定实现
  • 总结


前言

  以下就是基于C++的AI五子棋游戏项目的详细开发教程,我对于每一步都进行了详细的注释以及图解,相信读者只要按照我的步骤一步一步做下去肯定也能实现自己的AI五子棋,当然,读者也可以根据自己的喜好调整游戏项目素材,以便达到最佳效果。下面就是本文的全部内容了!


零、项目演示

0.1 人机五子棋对弈

基于C++的AI五子棋游戏项目开发教程_第1张图片

0.2 黑棋(棋手)胜利

  • 黑棋(棋手)胜利棋面:
    基于C++的AI五子棋游戏项目开发教程_第2张图片

  • 黑棋(棋手)胜利判定结果:
    基于C++的AI五子棋游戏项目开发教程_第3张图片

2.3 白棋(AI)胜利

  • 白棋(AI)胜利棋面:
    基于C++的AI五子棋游戏项目开发教程_第4张图片

  • 白棋(AI)胜利判定结果:
    基于C++的AI五子棋游戏项目开发教程_第5张图片

一、创建项目

  1. 打开Microsoft Visual Studio(以下简称VS)后,点击“新建”->“项目”
    基于C++的AI五子棋游戏项目开发教程_第6张图片

  2. 然后输入项目名称与项目位置,然后点击“确定”
    基于C++的AI五子棋游戏项目开发教程_第7张图片

二、导入素材

  1. 在项目内新建“resource”文件夹,准备存放项目的素材文件
    请添加图片描述

  2. 将项目所用素材导入项目中的“resource”文件夹中,读者可以用自己的素材,也可以用我的素材,我的素材的下载链接已经放在上面的博客中了
    基于C++的AI五子棋游戏项目开发教程_第8张图片

三、项目框架设计

3.1 设计项目框架

  1. 整个项目的框架如下图所示,所有的代码都是根据以下四个类进行编写的:
    基于C++的AI五子棋游戏项目开发教程_第9张图片
    • Man(棋手):下棋的人
    • Chess(棋盘):下棋的地方
    • AI(人工智能):和棋手对弈的AI
    • ChessGame(游戏控制):控制游戏的基本逻辑

3.2 根据项目框架设计类

  1. 根据刚才设计好的项目框架,我们就要将其一一建立起来。首先创建Man(棋手)类,在“源文件”上右键,点击“添加”中的“类”:
    基于C++的AI五子棋游戏项目开发教程_第10张图片

  2. 在“类名”中输入“Man”,点击“确定”就可以了,其余的文件自动生成
    基于C++的AI五子棋游戏项目开发教程_第11张图片

  3. 可以发现,已经成功生成了
    基于C++的AI五子棋游戏项目开发教程_第12张图片

  4. 按照创建Man(棋手)类同样的方法,创建其他三个类,最终效果如下图所示:
    基于C++的AI五子棋游戏项目开发教程_第13张图片

四、设计游戏主要接口

4.1 设计Chess(棋盘)类主要接口

  1. 我们在Chess.h中设计Chess(棋盘)类的主要接口,这些主要接口不需要具体实现,只是暴露给外部的接口,等待外部使用的时候在进行个性化的实现即可。Chess.h中的代码如下:
    #pragma once
    
    // 表示落子位置
    struct ChessPos
    {
    	int row;
    	int col;
    };
    
    // 表示棋子的种类
    typedef enum
    {
    	CHESS_WHITE = -1, // 白棋
    	CHESS_BLACK = 1 // 黑棋
    }chess_kind;
    
    class Chess
    {
    public:
    
    	// 棋盘初始化:加载棋盘的图片资源,初始化棋盘的相关数据
    	void init();
    
    	/*
    		判断在指定坐标(x,y)位置,是否是有效点击,
    		如果是有效点击,把有效点击的位置(行,列)保存在参数pos中
    	*/
    	bool clickBoard(int x, int y, ChessPos *pos);
    
    	// 在棋盘的指定位置(pos), 落子(chess)
    	void chessDown(ChessPos *pos, chess_kind chess);
    
    	// 获取棋盘的大小(13线、15线、19线)
    	int getGradeSize();
    
    	// 获取指定位置是黑棋,还是白棋,还是空白
    	int getChessData(ChessPos *pos);
    int getChessData(int row, int col);
    
    	// 检查棋局是否结束
    	bool checkOver();
    
    };
    

4.2 设计AI(人工智能)类主要接口

  1. 同理,AI.h中的代码如下:

    #pragma once
    #include "Chess.h"
    
    class AI
    {
    public:
    
    	// 初始化
    	void init(Chess *chess);
    
    	// AI下棋
    	void go();
    
    };
    

4.3 设计Man(棋手)类主要接口

  1. 同理,Man.h中的代码如下:

    #pragma once
    #include "Chess.h"
    
    class Man
    {
    
    public:
    
    	// 初始化
    	void init(Chess *chess);
    
    	// 下棋动作
    	void go();
    
    };
    

4.4 设计ChessGame(游戏控制)类主要接口

  1. 同理,ChessGame.h中的代码如下:

    #pragma once
    
    class ChessGame
    {
    
    public:
    
    	// 开始对局
    	void play();
    
    };
    

4.5 设计各个接口的具体实现

  1. 我们现在已经把我们项目的基本主要接口生成了,但是我们还需要将这些接口实现一下,以助于后面项目开发的使用。此时我们可以看到,刚刚创建好的接口函数下面有一个绿色的波浪线:
    基于C++的AI五子棋游戏项目开发教程_第14张图片

  2. 这个绿色的波浪线就是VS在提示我们还没有生成该接口的具体实现,所以我们需要实现这个接口。我们只需要将鼠标放在绿色波浪线上,然后点击“显示可能的修补程序”:
    基于C++的AI五子棋游戏项目开发教程_第15张图片

  3. 然后选择我红框标注的选项即可:
    基于C++的AI五子棋游戏项目开发教程_第16张图片

  4. 此时VS就帮我们自动地完成了接口的具体实现,当然,里面的具体内容需要根据不同项目需求自己填写。此时接口函数下面的绿色波浪线已经不存在了,那么我们只需要按“Ctrl+s”保存,然后关闭即可,此时VS就已经帮我们完成了:
    基于C++的AI五子棋游戏项目开发教程_第17张图片

  5. 其余的所有接口函数都按照上面的步骤完成接口的具体实现,不再一一赘述。具体的接口函数实现后的项目结构如下图所示:
    基于C++的AI五子棋游戏项目开发教程_第18张图片

五、设计游戏基本框架

  1. 此时我们已经创建好了整个游戏的基本接口并进行了初步的实现,但是游戏的框架我们目前还没有创建,所以下面的工作应该创建游戏的基本框架。因为游戏要由ChessGame类控制,所以应该由ChessGame类调用各个类的功能,所以首先在ChessGame.h加入如下代码,此时就完成了整个游戏的基本内容创建:

    #pragma once
    #include "Man.h"
    #include "AI.h"
    #include "Chess.h"
    
    class ChessGame
    {
    
    public:
    
    	ChessGame(Man*, AI*, Chess*);
    
    	// 开始对局
    	void play();
    
    // 添加数据成员
    private:
    	Man* man;
    	AI* ai;
    	Chess* chess;
    };
    
  2. 当游戏的基本内容创建好后,我们就要完成游戏的基本逻辑了,当然,此时只是简单的面向对象的逻辑实现,并不涉及具体的开发,具体的开发要到后面才具体实现。我们此时只需要在ChessGame.cpp中加入如下代码即可:

    #include "ChessGame.h"
    
    ChessGame::ChessGame(Man* man, AI* ai, Chess* chess)
    {
    
    	this->man = man;
    	this->ai = ai;
    	this->chess = chess;
    
    	ai->init(chess);
    	man->init(chess);
    
    }
    
    // 对局(开始五子棋游戏)
    void ChessGame::play()
    {
    
    	// 棋盘初始化
    	chess->init();
    
    	// 开始对局
    	while (1)
    	{
    		// 首先由棋手走
    		man->go();
    		if (chess->checkOver())
    		{
    			chess->init();
    			continue;
    		}
    
    		// 再由AI走
    		ai->go();
    		if (chess->checkOver())
    		{
    			chess->init();
    			continue;
    		}
    	}
    
    }
    
  3. 此时就完成了整个游戏的基本框架,下面我们就要往这个框架中添加具体内容了。当然,在这之前我们还需要使用一个主函数将刚刚创建的框架串联起来。首先创建main.cpp,这里面就是游戏的整体逻辑,具体内容后面再写,在“源文件”上右键,选择“添加”->“新建项”:
    基于C++的AI五子棋游戏项目开发教程_第19张图片

  4. 选择C++文件(.cpp)后输入名字,最后点击“添加”即可:
    基于C++的AI五子棋游戏项目开发教程_第20张图片

  5. 在main.cpp中加入如下代码:

    #include 
    #include "ChessGame.h"
    
    int main(void)
    {
    	Man man;
    	Chess chess;
    	AI ai;
    	ChessGame game(&man, &ai, &chess);
    
    	game.play();
    
    	return 0;
    
    }
    
  6. 此时我们可以运行测试一下,点击“调试”中的“开始执行(不调试)(H)”:
    基于C++的AI五子棋游戏项目开发教程_第21张图片

  7. 可以发现到目前为止,我们的程序没有问题:
    基于C++的AI五子棋游戏项目开发教程_第22张图片

六、棋盘初始化

6.1 EasyX的使用

  1. 因为游戏要进行绘图,所以我们使用EasyX来完成游戏的绘图接口,可以帮助我们编写图形程序,EasyX的下载链接也在博客的上方。下载后双击打开:
    基于C++的AI五子棋游戏项目开发教程_第23张图片

  2. 点击“下一步”:
    基于C++的AI五子棋游戏项目开发教程_第24张图片

  3. 然后选择你对应版本的编译器进行“安装”:
    基于C++的AI五子棋游戏项目开发教程_第25张图片

  4. 然后会提示你安装成功:
    基于C++的AI五子棋游戏项目开发教程_第26张图片

6.2 设计棋盘的数据成员

  1. 当我们安装好EasyX图形库后,就要在Chess.h中引入一些我们所需要的头文件了:
    基于C++的AI五子棋游戏项目开发教程_第27张图片

  2. 然后需要添加一些棋盘初始化所需要的数据,我们只需要在Chess.h中加入如下代码:

    private:
    	IMAGE chessBlackImg; // 黑棋棋子
    	IMAGE chessWhiteImg; // 白棋棋子
    	int gradeSize; // 棋盘的大小(13线、15线、17线、19线)
    	int margin_x; // 棋盘的左侧边界
    	int margin_y; // 棋盘的顶部边界
    	float chessSize; // 棋子的大小(棋盘的小方格的大小)
    	/*
    		存储当前棋局的棋子分布数据
    		例如:chessMap[3][5]表示棋盘的第3行第5列的落子情况(0:空白;1:黑子;-1:白子)
    	*/
    	vector<vector<int>> chessMap;
    	/*
    		表示现在该谁下棋(落子)
    		true:该黑子走;false:该白子走
    	*/
    	bool playerFlag;
    

6.3 构造棋盘

  1. 我们需要利用刚才创建的棋盘类的数据进行棋盘的创建,首先就需要写一个函数来创建棋盘,所以我们在Chess.h中加入如下代码:

    Chess(int gradeSize, int maiginX, int marginY, float chessSize);
    
  2. 然后鼠标放在刚刚创建的函数上,点击“显示可能的修补程序”:
    基于C++的AI五子棋游戏项目开发教程_第28张图片

  3. 然后选择红框中的内容:
    基于C++的AI五子棋游戏项目开发教程_第29张图片

  4. 然后按“Ctrl+S”保存:
    基于C++的AI五子棋游戏项目开发教程_第30张图片

  5. 下面就要利用刚才创建的数据构造棋盘了,只需要在Chess.cpp中加入如下代码:

    // 构造棋盘
    Chess::Chess(int gradeSize, int marginX, int marginY, float chessSize)
    {
    	this->gradeSize = gradeSize;
    	this->margin_x = marginX;
    	this->margin_y = marginY;
    	this->chessSize = chessSize;
    	playerFlag = CHESS_BLACK;
    	for (int i = 0; i < gradeSize; i++)
    	{
    		vector<int> row;
    		for (int j = 0; j < gradeSize; j++)
    		{
    			row.push_back(0);
    		}
    		chessMap.push_back(row);
    	}
    }
    
  6. 然后来到main.cpp中,使用我们刚刚创建的构造函数,传入参数就构造好棋盘了:
    基于C++的AI五子棋游戏项目开发教程_第31张图片

  7. 然后我们还是来测试一下,点击“调试”中的“开始执行(不调试)(H)”:
    基于C++的AI五子棋游戏项目开发教程_第32张图片

  8. 可以发现,到目前为止,我们的程序没有任何问题:
    基于C++的AI五子棋游戏项目开发教程_第33张图片

6.4 棋盘初始化

  1. 在项目上右键后,点击“属性”:
    基于C++的AI五子棋游戏项目开发教程_第34张图片

  2. 在“常规”的“字符集”中,选择“使用多字节字符集”:
    基于C++的AI五子棋游戏项目开发教程_第35张图片

  3. 在Chess.cpp中加入如下头文件和相关库,目的是可以播放音乐:

    #include 
    #pragma comment(lib,"winmm.lib")
    
  4. 在Chess.cpp中加入如下代码,目的是看到实际的棋盘并播放音乐:

    // 棋盘初始化
    void Chess::init()
    {
    	// 创建游戏窗口
    	initgraph(897, 895);
    	// 显示棋盘图片
    	loadimage(0, "resource/棋盘2.jpg");
    	// 播放开始提示音
    	mciSendString("play resource/start.wav", 0, 0, 0);
    	// 加载黑棋和白棋棋子的图片
    	loadimage(&chessBlackImg, "resource/black.png", chessSize, chessSize, true);
    	loadimage(&chessWhiteImg, "resource/white.png", chessSize, chessSize, true);
    	// 棋盘清零
    	for (int i = 0; i < gradeSize; i++)
    	{
    		for (int j = 0; j < gradeSize; j++)
    		{
    			chessMap[i][j] = 0;
    		}
    	}
    	// 确定谁先下棋
    	playerFlag = true;
    }
    
  5. 然后我们测试一下:
    基于C++的AI五子棋游戏项目开发教程_第36张图片

  6. 发现已经成功显示棋盘,并成功播放音乐了:
    基于C++的AI五子棋游戏项目开发教程_第37张图片

七、棋手下棋实现

7.1 棋手初始化

  1. 给棋手类添加棋盘数据成员,在Man.h中加入如下代码:

    private:
    	Chess* chess;
    

7.2 棋手下棋功能初始化

  1. 在棋手类初始化时,传入棋盘类指针,只需要将Man.cpp中的init函数替换为如下代码:

    // 棋手初始化
    void Man::init(Chess * chess)
    {
    	this->chess = chess;
    }
    
  2. 为了实现棋手下棋功能,将Man.cpp中的go函数替换为如下代码:

    // 棋手下棋
    void Man::go()
    {
    	// 鼠标函数
    	MOUSEMSG msg;
    	// 落子位置
    	ChessPos pos;
    	while (1)
    	{
    		// 获取鼠标点击消息
    		msg = GetMouseMsg();
    		// 通过chess对象,来判断落子位置是否有效
    		if (msg.uMsg == WM_LBUTTONDOWN && chess->clickBoard(msg.x, msg.y, &pos))
    		{
    			break;
    		}
    	}
    	// 落子
    	chess->chessDown(&pos, CHESS_BLACK);
    }
    

7.3 判断棋手下棋位置是否有效

  1. 下棋最重要的一点就是让计算机知道棋下在了哪里,如何解决这个问题呢?我们可以看下面的图示:
    基于C++的AI五子棋游戏项目开发教程_第38张图片

  2. 棋子肯定要落在两条线的交界处,一共四个点,所以我们首先要计算落子位置距离四个点的距离。这里我们需要设置一个“阈值”,如果落子位置距离某个点的距离小于此“阈值”,就认为这个点就是真正的落子位置,否则就不落子,这个“阈值”的大小要小于棋子大小的一半,还要注意棋盘在计算机中存储的二维数组下标从0开始。此时我们只需要将如下代码加入Chess.cpp中:

    #include 
    
    // 判断落子是否有效
    bool Chess::clickBoard(int x, int y, ChessPos * pos)
    {
    	// 真实的落子列坐标
    	int col = (x - margin_x) / chessSize;
    	// 真实的落子行坐标
    	int row = (y - margin_y) / chessSize;
    	// 落子的左上角列坐标
    	int leftTopPosX = margin_x + chessSize * col;
    	// 落子的左上角行坐标
    	int leftTopPosY = margin_y + chessSize * row;
    	// 鼠标点击位置距离真实落子位置的阈值
    	int offset = chessSize * 0.4;
    	// 落子距离四个角的距离
    	int len;
    	// 落子是否有效
    	bool res = false;
    	do
    	{
    		// 落子距离左上角的距离
    		len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY) * (y - leftTopPosY));
    		// 如果落子距离左上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效
    		if (len < offset)
    		{
    			pos->row = row;
    			pos->col = col;
    			if (chessMap[pos->row][pos->col] == 0)
    			{
    				res = true;
    			}
    			break;
    		}
    		// 落子距离右上角的距离
    		int x2 = leftTopPosX + chessSize;
    		int y2 = leftTopPosY;
    		len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
    		// 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效
    		if (len < offset)
    		{
    			pos->row = row;
    			pos->col = col + 1;
    			if (chessMap[pos->row][pos->col] == 0)
    			{
    				res = true;
    			}
    			break;
    		}
    		// 落子距离左下角的距离
    		x2 = leftTopPosX;
    		y2 = leftTopPosY + chessSize;
    		len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
    		// 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效
    		if (len < offset)
    		{
    			pos->row = row + 1;
    			pos->col = col;
    			if (chessMap[pos->row][pos->col] == 0)
    			{
    				res = true;
    			}
    			break;
    		}
    		// 落子距离右下角的距离
    		x2 = leftTopPosX + chessSize;
    		y2 = leftTopPosY + chessSize;
    		len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
    		// 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效
    		if (len < offset)
    		{
    			pos->row = row + 1;
    			pos->col = col + 1;
    			if (chessMap[pos->row][pos->col] == 0)
    			{
    				res = true;
    			}
    			break;
    		}
    	} while (0);
    	// 返回落子是否有效的判断结果
    	return res;
    }
    
  3. 此时就可以判断落子的位置是否有效了,为了验证我们的代码没有问题,我们需要验证一下,我们在Chess.cpp中加入如下代码,测试成功后可以删除加入的代码:
    基于C++的AI五子棋游戏项目开发教程_第39张图片

  4. 在Man.cpp中加入如下代码,打印落子位置,同样,测试成功之后也可以删除加入的代码:
    基于C++的AI五子棋游戏项目开发教程_第40张图片

  5. 此时我们就可以来到main.cpp中进行测试了:
    基于C++的AI五子棋游戏项目开发教程_第41张图片

  6. 可以发现正确获取落子位置了,这说明我们的代码没有任何问题。测试成功之后,要把上面加的两处代码删掉:
    基于C++的AI五子棋游戏项目开发教程_第42张图片

7.4 实现棋手下棋

  1. 为了实现棋盘落子,首先在Chess.cpp中加入如下代码,需要注意绘图的左边是左上角,所以为了让棋子的中心点在行线和列线的交界处,棋子的行和列坐标都需要减0.5倍的棋格大小,这一点需要格外关注:

    // 棋盘落子
    void Chess::chessDown(ChessPos * pos, chess_kind chess)
    {
    	// 加载落子音效
    	mciSendString("play resource/down7.wav", 0, 0, 0);
    	// 获取棋子的落子位置,需要注意绘图的左边是左上角,所以为了让棋子的中心点在行线和列线的交界处,棋子的行和列坐标都需要减0.5倍的棋格大小
    	int x = margin_x + chessSize * pos->col - 0.5 * chessSize;
    	int y = margin_y + chessSize * pos->row - 0.5 * chessSize;
    	// 根据棋子类型在对应位置生成棋子图片
    	if (chess == CHESS_WHITE)
    	{
    		putimage(x, y, &chessWhiteImg);
    	}
    	else
    	{
    		putimage(x, y, &chessBlackImg);
    	}
    }
    
  2. 然后我们来测试一下,发现可以成功落子,而且音效也没问题,但是每个棋子周围都有黑边,这些黑边肯定是不应该存在的:
    基于C++的AI五子棋游戏项目开发教程_第43张图片

  3. 落子后的棋子出现黑边是因为Easyx不支持png格式图片,为了解决这个问题,我们只需要在Chess.cpp中加入如下函数:

    // 解决Easyx不支持png格式图片的函数
    void putimagePNG(int x, int y, IMAGE* picture) //x为载入图片的X坐标,y为Y坐标
    {
    	// 变量初始化
    	DWORD* dst = GetImageBuffer();    // GetImageBuffer()函数,用于获取绘图设备的显存指针,EASYX自带
    	DWORD* draw = GetImageBuffer();
    	DWORD* src = GetImageBuffer(picture); // 获取picture的显存指针
    	int picture_width = picture->getwidth(); // 获取picture的宽度,EASYX自带
    	int picture_height = picture->getheight(); // 获取picture的高度,EASYX自带
    	int graphWidth = getwidth();       // 获取绘图区的宽度,EASYX自带
    	int graphHeight = getheight();     // 获取绘图区的高度,EASYX自带
    	int dstX = 0;    // 在显存里像素的角标
    
    	// 实现透明贴图 公式: Cp=αp*FP+(1-αp)*BP , 贝叶斯定理来进行点颜色的概率计算
    	for (int iy = 0; iy < picture_height; iy++)
    	{
    		for (int ix = 0; ix < picture_width; ix++)
    		{
    			int srcX = ix + iy * picture_width; // 在显存里像素的角标
    			int sa = ((src[srcX] & 0xff000000) >> 24); // 0xAArrggbb;AA是透明度
    			int sr = ((src[srcX] & 0xff0000) >> 16); // 获取RGB里的R
    			int sg = ((src[srcX] & 0xff00) >> 8);   // G
    			int sb = src[srcX] & 0xff;              // B
    			if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight)
    			{
    				dstX = (ix + x) + (iy + y) * graphWidth; // 在显存里像素的角标
    				int dr = ((dst[dstX] & 0xff0000) >> 16);
    				int dg = ((dst[dstX] & 0xff00) >> 8);
    				int db = dst[dstX] & 0xff;
    				draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16)  // 公式: Cp=αp*FP+(1-αp)*BP  ; αp=sa/255 , FP=sr , BP=dr
    					| ((sg * sa / 255 + dg * (255 - sa) / 255) << 8)         // αp=sa/255 , FP=sg , BP=dg
    					| (sb * sa / 255 + db * (255 - sa) / 255);              // αp=sa/255 , FP=sb , BP=db
    			}
    		}
    	}
    }
    
  4. 然后修改Chess.cpp中的Chess::chessDown函数为如下图示:
    基于C++的AI五子棋游戏项目开发教程_第44张图片

  5. 此时我们再来测试一下,可以发现黑边没有了,音效也没问题:
    基于C++的AI五子棋游戏项目开发教程_第45张图片

  6. 现在虽然已经实现了落子效果,但是只是表现了出来,并没有将落子数据存储在计算机中,我们之前创建了二维数组,就是为了存储落子数据的,所以我们应该将我们的落子信息存储在二维数组中。首先在Chess.h的private中加入如下函数:

    // 将落子信息存储到二维数组中
    void updateGameMap(ChessPos* pos);
    
  7. 然后在Chess.cpp中加入如下函数:

    // 将落子信息存储在二维数组中
    void Chess::updateGameMap(ChessPos * pos)
    {
    	// 存储落子信息
    	chessMap[pos->row][pos->col] = playerFlag ? CHESS_BLACK : CHESS_WHITE;
    	// 黑白方交换行棋
    	playerFlag = !playerFlag;
    }
    
  8. 然后在Chess.cpp中的Chess::chessDown函数中调用Chess::updateGameMap:
    基于C++的AI五子棋游戏项目开发教程_第46张图片

  9. 此时就已经将棋手下棋的落子信息存储在了计算机的二维数组中,这样就方便我们后续操作了

八、AI下棋实现

8.1 AI初始化

  1. 在进行AI初始化时,我们要考虑两个数据成员:

    • 棋盘对象:表示对哪个棋盘下棋
    • 评分数组:存储AI对棋盘所有落点的价值评估,以便AI做出最优决策
  2. 基于以上分析,我们首先在AI.h中加入两个数据成员:

    private:
    	// 棋盘对象
    	Chess* chess;
    	// 评分数组
    	vector<vector<int>> scoreMap;
    
  3. 然后在AI.cpp中加入如下代码:

    // AI初始化
    void AI::init(Chess * chess)
    {
    	this->chess = chess;
    	int size = chess->getGradeSize();
    	for (int i = 0; i < size; i++)
    	{
    		vector<int> row;
    		for (int j = 0; j < size; j++)
    		{
    			row.push_back(0);
    		}
    		scoreMap.push_back(row);
    	}
    }
    

8.2 AI下棋原理

  1. AI的下棋原理要比棋手下棋原理复杂得多,因为棋手是人工下棋,不需要计算机程序计算,而AI下棋需要根据棋手下棋的落子位置来找到下棋落子的最优策略,也就是说,AI需要对棋盘的所有可能落子点进行评分计算,然后选择一个评分最高的点落子,对于某个可能的落子点的评分,我们可以这么理解:此位置既可能是黑棋落子,也可能是白棋落子,将此位置想象为一个兵家必争之地,我们要做的就是判断黑棋夺取此位置获取的价值多,还是白棋夺取此位置获得的价值多,如果黑棋夺取此位置获取的价值更多,那么我们就应该让白棋落在这里,也就是要让白棋破坏黑棋夺得更多的价值;如果是白棋夺取此位置获取的价值更多,那么就让白棋下载此位置,以获取更多的价值,因为此时是AI执白棋,所以我们要尽可能地让AI获取更多的价值

  2. 对于AI来说,每一次落子后,此落子周围共有八个方向,对于每一个落子点,应该向该点的八个方向分别进行评分计算,评分计算的标准就是确定每个方向已经有几颗连续的棋子了。假设现在有一个可能的落子点如下图黑点所示:
    基于C++的AI五子棋游戏项目开发教程_第47张图片

  3. 根据上图可以发现,此次落子周围一共有八个方向,AI首先计算如果棋手在这个可能的位置落子,会有多大的价值,然后再计算AI在同样的位置落子,有多大的价值。那么如何评判价值的大小呢?我们可以将连续的落子数量作为评判的标准,如果黑棋或者白棋在这个位置落子,那么在这个位置的八个方向的某个方向上,一共有多少个连续的黑棋或白棋就是我们评判的标准,如果连续的黑棋或者白棋数量越多,那么在此位置落子的价值就越大

  4. 既然要根据连续的黑棋或者白棋数量进行价值的评判,所以我们应该对五子棋中常见的棋形有一个基本的了解,这样有助于我们对不同的情况进行价值的评判。五子棋中的常见棋形如下所示:

    • 连二:

      第一种情况 第二种情况
      请添加图片描述 基于C++的AI五子棋游戏项目开发教程_第48张图片
    • 活三:

      第一种情况 第二种情况
      基于C++的AI五子棋游戏项目开发教程_第49张图片 基于C++的AI五子棋游戏项目开发教程_第50张图片
    • 死三:

      第一种情况 第二种情况
      基于C++的AI五子棋游戏项目开发教程_第51张图片 基于C++的AI五子棋游戏项目开发教程_第52张图片
    • 活四

      第一种情况 第二种情况
      基于C++的AI五子棋游戏项目开发教程_第53张图片 基于C++的AI五子棋游戏项目开发教程_第54张图片
    • 死四

      第一种情况 第二种情况
      基于C++的AI五子棋游戏项目开发教程_第55张图片 基于C++的AI五子棋游戏项目开发教程_第56张图片
    • 连五(获胜)

      第一种情况
      基于C++的AI五子棋游戏项目开发教程_第57张图片 基于C++的AI五子棋游戏项目开发教程_第58张图片
  5. 对于每种不同的落子情况导致的不同棋形,我们要给予对应评分,方便AI做出判断,从而选取最优落子点落子。不同棋色以及不同棋形的评分标准如下图所示,此评分标准可能不是最优的,但是根据此评分标准设计的AI下五子棋的水平已经超过了大部分棋手的水平,如果需要挑战更难度的五子棋棋手水平,后续可以进行迭代优化。另外需要注意,在我们的游戏中,棋手执黑棋,AI执白棋:

    目标棋形 黑棋 白棋
    连二 10 10
    死三 30 25
    活三 40 50
    死四 60 55
    活四 200 10000
    连五(获胜) 20000 30000

8.3 AI对棋局进行评分计算

  1. 有了以上对AI下棋原理的分析,下面我们就要根据分析的结果进行代码编写了,首先我们要定义一个函数,用来处理AI在下棋过程中落子点的价值评分计算,那么我们在AI.h中加入如下函数:

    private:
    	// AI对棋局进行评分
    	void calculateScore();
    
  2. 在AI.cpp中加入如下代码:

    // AI对棋局进行评分计算
    void AI::calculateScore()
    {
    	// 棋手方(黑棋)有多少个连续的棋子
    	int personNum = 0;
    	// AI方(白棋)有多少个连续的棋子
    	int aiNum = 0;
    	// 该方向上空白位的个数
    	int emptyNum = 0;
    	// 将评分向量数组清零
    	for (int i = 0; i < scoreMap.size(); i++)	
    	{
    		for (int j = 0; j < scoreMap[i].size(); j++)
    		{
    			scoreMap[i][j] = 0;
    		}
    	}
    	// 获取棋盘大小
    	int size = chess->getGradeSize();
    	// 对可能的落子点的八个方向进行价值评分计算
    	for (int row = 0; row < size; row++)
    	{
    		for (int col = 0; col < size; col++)
    		{
    			// 只有当前位置没有棋子才是可能的落子点
    			if (chess->getChessData(row, col) == 0)
    			{
    				// 控制八个方向
    				for (int y = -1; y <= 0; y++)
    				{
    					for (int x = -1; x <= 1; x++)
    					{
    						// 重置棋手方(黑棋)有多少个连续的棋子
    						personNum = 0;
    						// 重置AI方(白棋)有多少个连续的棋子
    						aiNum = 0;
    						// 重置该方向上空白位的个数
    						emptyNum = 0;
    						// 消除重复计算
    						if (y == 0 && x != 1)
    						{
    							continue;
    						}
    						// 原坐标不计算在内
    						if (!(y == 0 && x == 0))
    						{
    							// 假设黑棋在该位置落子,会构成什么棋形?此时是黑棋的正向计算
    							for (int i = 1; i <= 4; i++)
    							{
    								int curRow = row + i * y;
    								int curCol = col + i * x;
    								if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 1)
    								{
    									personNum++;
    								}
    								else if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 0)
    								{
    									emptyNum++;
    									break;
    								}
    								else
    								{
    									break;
    								}
    							}
    							// 黑棋的反向计算
    							for (int i = 1; i <= 4; i++)
    							{
    								int curRow = row - i * y;
    								int curCol = col - i * x;
    								if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 1)
    								{
    									personNum++;
    								}
    								else if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 0)
    								{
    									emptyNum++;
    									break;
    								}
    								else
    								{
    									break;
    								}
    							}
    							// 连二
    							if (personNum == 1)
    							{
    								scoreMap[row][col] += 10;
    							}
    							// 连三
    							else if (personNum == 3)
    							{
    								// 死三
    								if (emptyNum == 1)
    								{
    									scoreMap[row][col] += 30;
    								}
    								// 活三
    								else if (emptyNum == 2)
    								{
    									scoreMap[row][col] += 40;
    								}
    							}
    							// 连四
    							else if (personNum == 3)
    							{
    								// 死四
    								if (emptyNum == 1)
    								{
    									scoreMap[row][col] += 60;
    								}
    								// 活四
    								else if (emptyNum == 2)
    								{
    									scoreMap[row][col] += 200;
    								}
    							}
    							// 连五
    							else if (personNum == 4)
    							{
    								scoreMap[row][col] += 20000;
    							}
    							// 清空空白棋子个数
    							emptyNum = 0;
    							// 假设白棋在该位置落子,会构成什么棋形?此时是白棋的正向计算
    							for (int i = 1; i <= 4; i++)
    							{
    								int curRow = row + i * y;
    								int curCol = col + i * x;
    								if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == -1)
    								{
    									aiNum++;
    								}
    								else if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 0)
    								{
    									emptyNum++;
    									break;
    								}
    								else
    								{
    									break;
    								}
    							}
    							// 白棋的反向计算
    							for (int i = 1; i <= 4; i++)
    							{
    								int curRow = row - i * y;
    								int curCol = col - i * x;
    								if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == -1)
    								{
    									aiNum++;
    								}
    								else if (curRow >= 0 && curRow < size && curCol >= 0 && curCol < size && chess->getChessData(curRow, curCol) == 0)
    								{
    									emptyNum++;
    									break;
    								}
    								else
    								{
    									break;
    								}
    							}
    							// 白色棋子无处可下
    							if (aiNum == 0)
    							{
    								scoreMap[row][col] += 5;
    							}
    							// 连二
    							else if (aiNum == 1)
    							{
    								scoreMap[row][col] += 10;
    							}
    							// 连三
    							else if (aiNum == 3)
    							{
    								// 死三
    								if (emptyNum == 1)
    								{
    									scoreMap[row][col] += 25;
    								}
    								// 活三
    								else if (emptyNum == 2)
    								{
    									scoreMap[row][col] += 50;
    								}
    							}
    							// 连四
    							else if (aiNum == 3)
    							{
    								// 死四
    								if (emptyNum == 1)
    								{
    									scoreMap[row][col] += 55;
    								}
    								// 活四
    								else if (emptyNum == 2)
    								{
    									scoreMap[row][col] += 10000;
    								}
    							}
    							// 连五
    							else if (aiNum >= 4)
    							{
    								scoreMap[row][col] += 30000;
    							}
    						}
    					}
    				}
    			}
    		}
    	}
    }
    

8.4 实现AI下棋

  1. 当每个可能的落子点各个方向的价值评分计算完成后,就可以让AI进行“思考”,选出价值评分最高的点进行落子。首先在AI.h中加入如下代码:

    private:
    	// 找出价值评分最高的点落子
    	ChessPos think();
    
  2. 然后在Chess.h中加入如下代码:
    基于C++的AI五子棋游戏项目开发教程_第59张图片

  3. 然后在AI.cpp中加入如下代码:

    // 找出价值评分最高的点落子
    ChessPos AI::think()
    {
    	// 计算各个方向的价值评分
    	calculateScore();
    	// 获取棋盘大小
    	int size = chess->getGradeSize();
    	// 存储多个价值最大值的点
    	vector<ChessPos> maxPoints;
    	// 初始价值最大值
    	int maxScore = 0;
    	// 遍历搜索价值评分最大的点
    	for (int row = 0; row < size; row++)
    	{
    		for (int col = 0; col < size; col++)
    		{
    			if (chess->getChessData(row, col) == 0)
    			{
    				if (scoreMap[row][col] > maxScore)
    				{
    					maxScore = scoreMap[row][col];
    					maxPoints.clear();
    					maxPoints.push_back(ChessPos(row, col));
    				}
    				else if (scoreMap[row][col] == maxScore)
    				{
    					maxPoints.push_back(ChessPos(row, col));
    				}
    			}
    		}
    	}
    	// 如果有多个价值最大值点,随机获取一个价值最大值点的下标
    	int index = rand() % maxPoints.size();
    	// 返回价值最大值点
    	return maxPoints[index];
    }
    
  4. 然后在AI.cpp中加入如下代码:

    // AI下棋
    void AI::go()
    {
    	// AI计算后的落子点
    	ChessPos pos = think();
    	// AI假装思考,给棋手缓冲时间
    	Sleep(1000);
    	// 在AI计算后的落子点落子
    	chess->chessDown(&pos, CHESS_WHITE);
    }
    
  5. 将Chess.cpp中的Chess::getGradeSize函数和两个Chess::getChessData函数进行如下修改:
    基于C++的AI五子棋游戏项目开发教程_第60张图片

  6. 然后测试一下,发现可以正常下棋了,而且智力还不错。读者可以根据自己的经验调整价值评分的赋值,从而让AI有更高的智力:

九、胜负判定实现

9.1 对胜负进行处理

  1. 首先在Chess.h中加入如下函数,目的是检查当前谁嬴谁输,然后根据检查结果来进行胜负后的处理:

    private:
        // 检查当前谁嬴谁输,如果胜负已分就返回true,否则返回false
        bool checkWin();
    
  2. 在Chess.cpp中加入如下头文件

    #include 
    
  3. 将Chess.cpp中的Chess::checkOver函数修改为如下内容:

    // 胜负判定
    bool Chess::checkOver()
    {
    	// checkWin()函数来检查当前谁嬴谁输,如果胜负已分就返回true,否则返回false
    	if (checkWin())
    	{
            // 暂停
    		Sleep(1500);![请添加图片描述](https://img-blog.csdnimg.cn/4dbfb593cf904d2dbf7a140e2a4bbb9c.png)
    
    		// 说明黑棋(棋手)赢
    		if (playerFlag == false)
    		{
    			mciSendString("play resource/不错.mp3", 0, 0, 0);
    			loadimage(0, "resource/胜利.jpg");
    		}
    		// 说明白棋(AI)赢
    		else
    		{
    			mciSendString("play resource/失败.mp3", 0, 0, 0);
    			loadimage(0, "resource/失败.jpg");
    		}
    		// 暂停
    		_getch();
    		return true;
    	}
    	return false;
    }
    

9.2 胜负判定原理

  1. 上面对胜负进行处理的过程是我们胜负判定实现的一个基本框架,其核心部分是checkWin函数,也就是我们如何判断当然谁赢谁输。我们可以这样想:对于某一个落子位置,我们要判断其八个方向是否连成五子,但是每次判断我们都可以同时根据偏移的落子位置,将其反方向是否连成五子进行判断,所以只需要判断四个大方向,八个小方向即可。假设我们首先判断水平方向,如下图所示:
    基于C++的AI五子棋游戏项目开发教程_第61张图片

  2. 可以看到,对于某一落子位置,我们首先判断从此位置向右的连续五个位置是否是相同颜色,然后将起始落子点分别向左偏移一、二、三、四、五个位置再判断是否有连续的五个相同颜色的棋子,如果满足,就获胜,否则就没获胜。这样我们就可以在一个大方向的判断上同时判断两个小方向,就完成了我们的胜负判断,其余方向的胜负判断同理

9.3 胜负判定实现

  1. 有了以上的原理分析后,我们就可以写代码了。首先在Chess.h中加入某一落子点位置的数据成员:

    private:
    	// 某一落子点的位置
    	ChessPos lastPos;
    
  2. 然后在Chess.cpp中的Chess::updateGameMap函数中加入如下代码:
    基于C++的AI五子棋游戏项目开发教程_第62张图片

  3. 然后在Chess.cpp中加入如下代码:

    // 检查当前谁嬴谁输,如果胜负已分就返回true,否则返回false
    bool Chess::checkWin()
    {
    	// 某一落子点的位置
    	int row = lastPos.row;
    	int col = lastPos.col;
    	// 落子点的水平方向
    	for (int i = 0; i < 5; i++)
    	{
    		if (((col - i) >= 0) && ((col - i + 4) < gradeSize) && (chessMap[row][col - i] == chessMap[row][col - i + 1]) && (chessMap[row][col - i] == chessMap[row][col - i + 2]) && (chessMap[row][col - i] == chessMap[row][col - i + 3]) && (chessMap[row][col - i] == chessMap[row][col - i + 4]))
    		{
    			return true;
    		}
    	}
    	// 落子点的垂直方向
    	for (int i = 0; i < 5; i++)
    	{
    		if (((row - i) >= 0) && ((row - i + 4) < gradeSize) && (chessMap[row - i][col] == chessMap[row - i + 1][col]) && (chessMap[row - i][col] == chessMap[row - i + 2][col]) && (chessMap[row - i][col] == chessMap[row - i + 3][col]) && (chessMap[row - i][col] == chessMap[row - i + 4][col]))
    		{
    			return true;
    		}
    	}
    	// 落子点的右斜方向
    	for (int i = 0; i < 5; i++)
    	{
    		if (((row + i) < gradeSize) && (row + i - 4 >= 0) && (col - i >= 0) && ((col - i + 4) < gradeSize) && (chessMap[row + i][col - i] == chessMap[row + i - 1][col - i + 1]) && (chessMap[row + i][col - i] == chessMap[row + i - 2][col - i + 2]) && (chessMap[row + i][col - i] == chessMap[row + i - 3][col - i + 3]) && (chessMap[row + i][col - i] == chessMap[row + i - 4][col - i + 4]))
    		{
    			return true;
    		}
    	}
    	// 落子点的左斜方向
    	for (int i = 0; i < 5; i++)
    	{
    		if (((row - i + 4) < gradeSize) && (row - i >= 0) && (col - i >= 0) && ((col - i + 4) < gradeSize) && (chessMap[row - i][col - i] == chessMap[row - i + 1][col - i + 1]) && (chessMap[row - i][col - i] == chessMap[row - i + 2][col - i + 2]) && (chessMap[row - i][col - i] == chessMap[row - i + 3][col - i + 3]) && (chessMap[row - i][col - i] == chessMap[row - i + 4][col - i + 4]))
    		{
    			return true;
    		}
    	}
    	return false;
    }
    
  4. 写完之后我们可以测试一下:

    • 黑棋(棋手):
      • 黑棋(棋手)胜的棋面:基于C++的AI五子棋游戏项目开发教程_第63张图片
      • 黑棋(棋手)胜的判定:
        基于C++的AI五子棋游戏项目开发教程_第64张图片
    • 白棋(AI):
      • 白棋(AI)胜的棋面:
        基于C++的AI五子棋游戏项目开发教程_第65张图片
      • 白棋(AI)胜的判定:基于C++的AI五子棋游戏项目开发教程_第66张图片
  5. 可以看到,不管是黑棋(棋手)胜,还是白棋(AI)胜,都可以正常显示胜负的判定了。而且按回车(Enter)键还可以自动开启下一局


总结

  以上就是基于C++的AI五子棋游戏项目开发教程的全部内容了,可以看到我们已经实现了目标,但是后续仍旧可以进行一些优化,比如AI对于落子的价值评分的优化、悔棋功能、主界面菜单等等,后续如果我有时间仍会更新此博客,如果读者自己爱钻研、感兴趣也可以自己完成优化的部分,因为整体的思路都比较清晰,而且逻辑也没有什么太大变化,所以优化起来也比较简单。那这篇博客就暂时告一段落了,我们下篇博客见!

你可能感兴趣的:(C++,算法,人工智能,AI五子棋,C++,游戏)