基于C++的AI五子棋游戏项目开发教程 您所在的位置:网站首页 java实训五子棋代码 基于C++的AI五子棋游戏项目开发教程

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

2023-03-22 12:17| 来源: 网络整理| 查看: 265

项目资源下载 基于C++的AI五子棋游戏项目源码压缩包下载地址基于C++的AI五子棋游戏项目源码Github下载地址基于C++的AI五子棋游戏项目所需素材基于C++的AI五子棋游戏项目所需要的EasyX 项目简介

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

项目开发软件环境 Windows11VS2017EasyX 项目开发硬件环境 CPU:Intel® Core™ i7-8750H CPU @ 2.20GHz 2.20 GHzRAM:24GBGPU:NVIDIA GeForce GTX 1060

文章目录 项目资源下载项目简介项目开发软件环境项目开发硬件环境前言一、创建项目二、导入素材三、项目框架设计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五子棋,当然,读者也可以根据自己的喜好调整游戏项目素材,以便达到最佳效果。下面就是本文的全部内容了!

一、创建项目

打开Microsoft Visual Studio(以下简称VS)后,点击“新建”->“项目” 请添加图片描述

然后输入项目名称与项目位置,然后点击“确定” 请添加图片描述

二、导入素材

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

将项目所用素材导入项目中的“resource”文件夹中,读者可以用自己的素材,也可以用我的素材,我的素材的下载链接已经放在上面的博客中了 请添加图片描述

三、项目框架设计 3.1 设计项目框架 整个项目的框架如下图所示,所有的代码都是根据以下四个类进行编写的: 请添加图片描述 Man(棋手):下棋的人Chess(棋盘):下棋的地方AI(人工智能):和棋手对弈的AIChessGame(游戏控制):控制游戏的基本逻辑 3.2 根据项目框架设计类

根据刚才设计好的项目框架,我们就要将其一一建立起来。首先创建Man(棋手)类,在“源文件”上右键,点击“添加”中的“类”: 请添加图片描述

在“类名”中输入“Man”,点击“确定”就可以了,其余的文件自动生成 请添加图片描述

可以发现,已经成功生成了 请添加图片描述

按照创建Man(棋手)类同样的方法,创建其他三个类,最终效果如下图所示: 请添加图片描述

四、设计游戏主要接口 4.1 设计Chess(棋盘)类主要接口 我们在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(人工智能)类主要接口

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

#pragma once #include "Chess.h" class AI { public: // 初始化 void init(Chess *chess); // AI下棋 void go(); }; 4.3 设计Man(棋手)类主要接口

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

#pragma once #include "Chess.h" class Man { public: // 初始化 void init(Chess *chess); // 下棋动作 void go(); }; 4.4 设计ChessGame(游戏控制)类主要接口

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

#pragma once class ChessGame { public: // 开始对局 void play(); }; 4.5 设计各个接口的具体实现

我们现在已经把我们项目的基本主要接口生成了,但是我们还需要将这些接口实现一下,以助于后面项目开发的使用。此时我们可以看到,刚刚创建好的接口函数下面有一个绿色的波浪线: 请添加图片描述

这个绿色的波浪线就是VS在提示我们还没有生成该接口的具体实现,所以我们需要实现这个接口。我们只需要将鼠标放在绿色波浪线上,然后点击“显示可能的修补程序”: 请添加图片描述

然后选择我红框标注的选项即可: 请添加图片描述

此时VS就帮我们自动地完成了接口的具体实现,当然,里面的具体内容需要根据不同项目需求自己填写。此时接口函数下面的绿色波浪线已经不存在了,那么我们只需要按“Ctrl+s”保存,然后关闭即可,此时VS就已经帮我们完成了: 请添加图片描述

其余的所有接口函数都按照上面的步骤完成接口的具体实现,不再一一赘述。具体的接口函数实现后的项目结构如下图所示: 请添加图片描述

五、设计游戏基本框架

此时我们已经创建好了整个游戏的基本接口并进行了初步的实现,但是游戏的框架我们目前还没有创建,所以下面的工作应该创建游戏的基本框架。因为游戏要由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; };

当游戏的基本内容创建好后,我们就要完成游戏的基本逻辑了,当然,此时只是简单的面向对象的逻辑实现,并不涉及具体的开发,具体的开发要到后面才具体实现。我们此时只需要在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; } } }

此时就完成了整个游戏的基本框架,下面我们就要往这个框架中添加具体内容了。当然,在这之前我们还需要使用一个主函数将刚刚创建的框架串联起来。首先创建main.cpp,这里面就是游戏的整体逻辑,具体内容后面再写,在“源文件”上右键,选择“添加”->“新建项”: 请添加图片描述

选择C++文件(.cpp)后输入名字,最后点击“添加”即可: 请添加图片描述

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

#include #include "ChessGame.h" int main(void) { Man man; Chess chess; AI ai; ChessGame game(&man, &ai, &chess); game.play(); return 0; }

此时我们可以运行测试一下,点击“调试”中的“开始执行(不调试)(H)”: 请添加图片描述

可以发现到目前为止,我们的程序没有问题: 请添加图片描述

六、棋盘初始化 6.1 EasyX的使用

因为游戏要进行绘图,所以我们使用EasyX来完成游戏的绘图接口,可以帮助我们编写图形程序,EasyX的下载链接也在博客的上方。下载后双击打开: 请添加图片描述

点击“下一步”: 请添加图片描述

然后选择你对应版本的编译器进行“安装”: 请添加图片描述

然后会提示你安装成功: 请添加图片描述

6.2 设计棋盘的数据成员

当我们安装好EasyX图形库后,就要在Chess.h中引入一些我们所需要的头文件了: 请添加图片描述

然后需要添加一些棋盘初始化所需要的数据,我们只需要在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 chessMap; /* 表示现在该谁下棋(落子) true:该黑子走;false:该白子走 */ bool playerFlag; 6.3 构造棋盘

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

Chess(int gradeSize, int maiginX, int marginY, float chessSize);

然后鼠标放在刚刚创建的函数上,点击“显示可能的修补程序”: 请添加图片描述

然后选择红框中的内容: 请添加图片描述

然后按“Ctrl+S”保存: 请添加图片描述

下面就要利用刚才创建的数据构造棋盘了,只需要在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 row.push_back(0); } chessMap.push_back(row); } }

然后来到main.cpp中,使用我们刚刚创建的构造函数,传入参数就构造好棋盘了: 请添加图片描述

然后我们还是来测试一下,点击“调试”中的“开始执行(不调试)(H)”: 请添加图片描述

可以发现,到目前为止,我们的程序没有任何问题: 请添加图片描述

6.4 棋盘初始化

在项目上右键后,点击“属性”: 请添加图片描述

在“常规”的“字符集”中,选择“使用多字节字符集”: 请添加图片描述

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

#include #pragma comment(lib,"winmm.lib")

在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 chessMap[i][j] = 0; } } // 确定谁先下棋 playerFlag = true; }

然后我们测试一下: 请添加图片描述

发现已经成功显示棋盘,并成功播放音乐了: 请添加图片描述

七、棋手下棋实现 7.1 棋手初始化

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

private: Chess* chess; 7.2 棋手下棋功能初始化

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

// 棋手初始化 void Man::init(Chess * chess) { this->chess = chess; }

为了实现棋手下棋功能,将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 判断棋手下棋位置是否有效

下棋最重要的一点就是让计算机知道棋下在了哪里,如何解决这个问题呢?我们可以看下面的图示: 请添加图片描述

棋子肯定要落在两条线的交界处,一共四个点,所以我们首先要计算落子位置距离四个点的距离。这里我们需要设置一个“阈值”,如果落子位置距离某个点的距离小于此“阈值”,就认为这个点就是真正的落子位置,否则就不落子,这个“阈值”的大小要小于棋子大小的一半,还要注意棋盘在计算机中存储的二维数组下标从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 res = true; } break; } // 落子距离右上角的距离 int x2 = leftTopPosX + chessSize; int y2 = leftTopPosY; len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); // 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效 if (len res = true; } break; } // 落子距离左下角的距离 x2 = leftTopPosX; y2 = leftTopPosY + chessSize; len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); // 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效 if (len res = true; } break; } // 落子距离右下角的距离 x2 = leftTopPosX + chessSize; y2 = leftTopPosY + chessSize; len = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); // 如果落子距离右上角的距离小于阈值并且当前位置没有棋子,就保存当前落子位置,并设置落子有效 if (len res = true; } break; } } while (0); // 返回落子是否有效的判断结果 return res; }

此时就可以判断落子的位置是否有效了,为了验证我们的代码没有问题,我们需要验证一下,我们在Chess.cpp中加入如下代码,测试成功后可以删除加入的代码: 请添加图片描述

在Man.cpp中加入如下代码,打印落子位置,同样,测试成功之后也可以删除加入的代码: 请添加图片描述

此时我们就可以来到main.cpp中进行测试了: 请添加图片描述

可以发现正确获取落子位置了,这说明我们的代码没有任何问题。测试成功之后,要把上面加的两处代码删掉: 请添加图片描述

7.4 实现棋手下棋

为了实现棋盘落子,首先在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); } }

然后我们来测试一下,发现可以成功落子,而且音效也没问题,但是每个棋子周围都有黑边,这些黑边肯定是不应该存在的: 请添加图片描述

落子后的棋子出现黑边是因为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 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 = 0 && iy // 存储落子信息 chessMap[pos->row][pos->col] = playerFlag ? CHESS_BLACK : CHESS_WHITE; // 黑白方交换行棋 playerFlag = !playerFlag; }

然后在Chess.cpp中的Chess::chessDown函数中调用Chess::updateGameMap: 请添加图片描述

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

八、AI下棋实现 8.1 AI初始化

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

棋盘对象:表示对哪个棋盘下棋评分数组:存储AI对棋盘所有落点的价值评估,以便AI做出最优决策

基于以上分析,我们首先在AI.h中加入两个数据成员:

private: // 棋盘对象 Chess* chess; // 评分数组 vector scoreMap;

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

// AI初始化 void AI::init(Chess * chess) { this->chess = chess; int size = chess->getGradeSize(); for (int i = 0; i row.push_back(0); } scoreMap.push_back(row); } } 8.2 AI下棋原理

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

对于AI来说,每一次落子后,此落子周围共有八个方向,对于每一个落子点,应该向该点的八个方向分别进行评分计算,评分计算的标准就是确定每个方向已经有几颗连续的棋子了。假设现在有一个可能的落子点如下图黑点所示: 请添加图片描述

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

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

连二:

第一种情况第二种情况请添加图片描述请添加图片描述

活三:

第一种情况第二种情况请添加图片描述请添加图片描述

死三:

第一种情况第二种情况请添加图片描述请添加图片描述

活四

第一种情况第二种情况请添加图片描述请添加图片描述

死四

第一种情况第二种情况请添加图片描述请添加图片描述

连五(获胜)

第一种情况请添加图片描述请添加图片描述

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

目标棋形黑棋白棋连二1010死三3025活三4050死四6055活四20010000连五(获胜)2000030000 8.3 AI对棋局进行评分计算

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

private: // AI对棋局进行评分 void calculateScore();

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

// AI对棋局进行评分计算 void AI::calculateScore() { // 棋手方(黑棋)有多少个连续的棋子 int personNum = 0; // AI方(白棋)有多少个连续的棋子 int aiNum = 0; // 该方向上空白位的个数 int emptyNum = 0; // 将评分向量数组清零 for (int i = 0; i scoreMap[i][j] = 0; } } // 获取棋盘大小 int size = chess->getGradeSize(); // 对可能的落子点的八个方向进行价值评分计算 for (int row = 0; row // 只有当前位置没有棋子才是可能的落子点 if (chess->getChessData(row, col) == 0) { // 控制八个方向 for (int y = -1; y // 重置棋手方(黑棋)有多少个连续的棋子 personNum = 0; // 重置AI方(白棋)有多少个连续的棋子 aiNum = 0; // 重置该方向上空白位的个数 emptyNum = 0; // 消除重复计算 if (y == 0 && x != 1) { continue; } // 原坐标不计算在内 if (!(y == 0 && x == 0)) { // 假设黑棋在该位置落子,会构成什么棋形?此时是黑棋的正向计算 for (int i = 1; i personNum++; } else if (curRow >= 0 && curRow = 0 && curCol getChessData(curRow, curCol) == 0) { emptyNum++; break; } else { break; } } // 黑棋的反向计算 for (int i = 1; i personNum++; } else if (curRow >= 0 && curRow = 0 && curCol 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 aiNum++; } else if (curRow >= 0 && curRow = 0 && curCol getChessData(curRow, curCol) == 0) { emptyNum++; break; } else { break; } } // 白棋的反向计算 for (int i = 1; i aiNum++; } else if (curRow >= 0 && curRow = 0 && curCol 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下棋

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

private: // 找出价值评分最高的点落子 ChessPos think();

然后在Chess.h中加入如下代码: 请添加图片描述

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

// 找出价值评分最高的点落子 ChessPos AI::think() { // 计算各个方向的价值评分 calculateScore(); // 获取棋盘大小 int size = chess->getGradeSize(); // 存储多个价值最大值的点 vector maxPoints; // 初始价值最大值 int maxScore = 0; // 遍历搜索价值评分最大的点 for (int row = 0; row 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]; }

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

// AI下棋 void AI::go() { // AI计算后的落子点 ChessPos pos = think(); // AI假装思考,给棋手缓冲时间 Sleep(1000); // 在AI计算后的落子点落子 chess->chessDown(&pos, CHESS_WHITE); }

将Chess.cpp中的Chess::getGradeSize函数和两个Chess::getChessData函数进行如下修改: 请添加图片描述

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

九、胜负判定实现 9.1 对胜负进行处理

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

private: // 检查当前谁嬴谁输,如果胜负已分就返回true,否则返回false bool checkWin();

在Chess.cpp中加入如下头文件

#include

将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 胜负判定原理

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

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

9.3 胜负判定实现

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

private: // 某一落子点的位置 ChessPos lastPos;

然后在Chess.cpp中的Chess::updateGameMap函数中加入如下代码: 请添加图片描述

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

// 检查当前谁嬴谁输,如果胜负已分就返回true,否则返回false bool Chess::checkWin() { // 某一落子点的位置 int row = lastPos.row; int col = lastPos.col; // 落子点的水平方向 for (int i = 0; i return true; } } // 落子点的垂直方向 for (int i = 0; i return true; } } // 落子点的右斜方向 for (int i = 0; i return true; } } // 落子点的左斜方向 for (int i = 0; i return true; } } return false; }

写完之后我们可以测试一下:

黑棋(棋手): 黑棋(棋手)胜的棋面:在这里插入图片描述黑棋(棋手)胜的判定: 请添加图片描述 白棋(AI): 白棋(AI)胜的棋面: 在这里插入图片描述白棋(AI)胜的判定:请添加图片描述

可以看到,不管是黑棋(棋手)胜,还是白棋(AI)胜,都可以正常显示胜负的判定了。而且按回车(Enter)键还可以自动开启下一局

总结

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



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有