本项目基于C++开发,整体来说比较简单,实现了人与AI之间的五子棋对弈,并且可以判定胜负以及音效添加等等,按照我博客的详细教程一步一步做下去肯定没问题!
以下就是基于C++的AI五子棋游戏项目的详细开发教程,我对于每一步都进行了详细的注释以及图解,相信读者只要按照我的步骤一步一步做下去肯定也能实现自己的AI五子棋,当然,读者也可以根据自己的喜好调整游戏项目素材,以便达到最佳效果。下面就是本文的全部内容了!
在项目内新建“resource”文件夹,准备存放项目的素材文件
将项目所用素材导入项目中的“resource”文件夹中,读者可以用自己的素材,也可以用我的素材,我的素材的下载链接已经放在上面的博客中了
#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();
};
同理,AI.h中的代码如下:
#pragma once
#include "Chess.h"
class AI
{
public:
// 初始化
void init(Chess *chess);
// AI下棋
void go();
};
同理,Man.h中的代码如下:
#pragma once
#include "Chess.h"
class Man
{
public:
// 初始化
void init(Chess *chess);
// 下棋动作
void go();
};
同理,ChessGame.h中的代码如下:
#pragma once
class ChessGame
{
public:
// 开始对局
void play();
};
我们现在已经把我们项目的基本主要接口生成了,但是我们还需要将这些接口实现一下,以助于后面项目开发的使用。此时我们可以看到,刚刚创建好的接口函数下面有一个绿色的波浪线:
这个绿色的波浪线就是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,这里面就是游戏的整体逻辑,具体内容后面再写,在“源文件”上右键,选择“添加”->“新建项”:
在main.cpp中加入如下代码:
#include
#include "ChessGame.h"
int main(void)
{
Man man;
Chess chess;
AI ai;
ChessGame game(&man, &ai, &chess);
game.play();
return 0;
}
然后需要添加一些棋盘初始化所需要的数据,我们只需要在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;
我们需要利用刚才创建的棋盘类的数据进行棋盘的创建,首先就需要写一个函数来创建棋盘,所以我们在Chess.h中加入如下代码:
Chess(int gradeSize, int maiginX, int marginY, float chessSize);
下面就要利用刚才创建的数据构造棋盘了,只需要在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);
}
}
在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 < gradeSize; i++)
{
for (int j = 0; j < gradeSize; j++)
{
chessMap[i][j] = 0;
}
}
// 确定谁先下棋
playerFlag = true;
}
给棋手类添加棋盘数据成员,在Man.h中加入如下代码:
private:
Chess* chess;
在棋手类初始化时,传入棋盘类指针,只需要将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);
}
棋子肯定要落在两条线的交界处,一共四个点,所以我们首先要计算落子位置距离四个点的距离。这里我们需要设置一个“阈值”,如果落子位置距离某个点的距离小于此“阈值”,就认为这个点就是真正的落子位置,否则就不落子,这个“阈值”的大小要小于棋子大小的一半,还要注意棋盘在计算机中存储的二维数组下标从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;
}
此时就可以判断落子的位置是否有效了,为了验证我们的代码没有问题,我们需要验证一下,我们在Chess.cpp中加入如下代码,测试成功后可以删除加入的代码:
为了实现棋盘落子,首先在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 < 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
}
}
}
}
现在虽然已经实现了落子效果,但是只是表现了出来,并没有将落子数据存储在计算机中,我们之前创建了二维数组,就是为了存储落子数据的,所以我们应该将我们的落子信息存储在二维数组中。首先在Chess.h的private中加入如下函数:
// 将落子信息存储到二维数组中
void updateGameMap(ChessPos* pos);
然后在Chess.cpp中加入如下函数:
// 将落子信息存储在二维数组中
void Chess::updateGameMap(ChessPos * pos)
{
// 存储落子信息
chessMap[pos->row][pos->col] = playerFlag ? CHESS_BLACK : CHESS_WHITE;
// 黑白方交换行棋
playerFlag = !playerFlag;
}
此时就已经将棋手下棋的落子信息存储在了计算机的二维数组中,这样就方便我们后续操作了
在进行AI初始化时,我们要考虑两个数据成员:
基于以上分析,我们首先在AI.h中加入两个数据成员:
private:
// 棋盘对象
Chess* chess;
// 评分数组
vector<vector<int>> scoreMap;
然后在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);
}
}
AI的下棋原理要比棋手下棋原理复杂得多,因为棋手是人工下棋,不需要计算机程序计算,而AI下棋需要根据棋手下棋的落子位置来找到下棋落子的最优策略,也就是说,AI需要对棋盘的所有可能落子点进行评分计算,然后选择一个评分最高的点落子,对于某个可能的落子点的评分,我们可以这么理解:此位置既可能是黑棋落子,也可能是白棋落子,将此位置想象为一个兵家必争之地,我们要做的就是判断黑棋夺取此位置获取的价值多,还是白棋夺取此位置获得的价值多,如果黑棋夺取此位置获取的价值更多,那么我们就应该让白棋落在这里,也就是要让白棋破坏黑棋夺得更多的价值;如果是白棋夺取此位置获取的价值更多,那么就让白棋下载此位置,以获取更多的价值,因为此时是AI执白棋,所以我们要尽可能地让AI获取更多的价值
对于AI来说,每一次落子后,此落子周围共有八个方向,对于每一个落子点,应该向该点的八个方向分别进行评分计算,评分计算的标准就是确定每个方向已经有几颗连续的棋子了。假设现在有一个可能的落子点如下图黑点所示:
根据上图可以发现,此次落子周围一共有八个方向,AI首先计算如果棋手在这个可能的位置落子,会有多大的价值,然后再计算AI在同样的位置落子,有多大的价值。那么如何评判价值的大小呢?我们可以将连续的落子数量作为评判的标准,如果黑棋或者白棋在这个位置落子,那么在这个位置的八个方向的某个方向上,一共有多少个连续的黑棋或白棋就是我们评判的标准,如果连续的黑棋或者白棋数量越多,那么在此位置落子的价值就越大
既然要根据连续的黑棋或者白棋数量进行价值的评判,所以我们应该对五子棋中常见的棋形有一个基本的了解,这样有助于我们对不同的情况进行价值的评判。五子棋中的常见棋形如下所示:
对于每种不同的落子情况导致的不同棋形,我们要给予对应评分,方便AI做出判断,从而选取最优落子点落子。不同棋色以及不同棋形的评分标准如下图所示,此评分标准可能不是最优的,但是根据此评分标准设计的AI下五子棋的水平已经超过了大部分棋手的水平,如果需要挑战更难度的五子棋棋手水平,后续可以进行迭代优化。另外需要注意,在我们的游戏中,棋手执黑棋,AI执白棋:
目标棋形 | 黑棋 | 白棋 |
---|---|---|
连二 | 10 | 10 |
死三 | 30 | 25 |
活三 | 40 | 50 |
死四 | 60 | 55 |
活四 | 200 | 10000 |
连五(获胜) | 20000 | 30000 |
有了以上对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.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;
}
}
}
}
}
}
}
}
当每个可能的落子点各个方向的价值评分计算完成后,就可以让AI进行“思考”,选出价值评分最高的点进行落子。首先在AI.h中加入如下代码:
private:
// 找出价值评分最高的点落子
ChessPos think();
然后在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];
}
然后在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有更高的智力:
首先在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;
}
上面对胜负进行处理的过程是我们胜负判定实现的一个基本框架,其核心部分是checkWin函数,也就是我们如何判断当然谁赢谁输。我们可以这样想:对于某一个落子位置,我们要判断其八个方向是否连成五子,但是每次判断我们都可以同时根据偏移的落子位置,将其反方向是否连成五子进行判断,所以只需要判断四个大方向,八个小方向即可。假设我们首先判断水平方向,如下图所示:
可以看到,对于某一落子位置,我们首先判断从此位置向右的连续五个位置是否是相同颜色,然后将起始落子点分别向左偏移一、二、三、四、五个位置再判断是否有连续的五个相同颜色的棋子,如果满足,就获胜,否则就没获胜。这样我们就可以在一个大方向的判断上同时判断两个小方向,就完成了我们的胜负判断,其余方向的胜负判断同理
有了以上的原理分析后,我们就可以写代码了。首先在Chess.h中加入某一落子点位置的数据成员:
private:
// 某一落子点的位置
ChessPos lastPos;
然后在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;
}
写完之后我们可以测试一下:
可以看到,不管是黑棋(棋手)胜,还是白棋(AI)胜,都可以正常显示胜负的判定了。而且按回车(Enter)键还可以自动开启下一局
以上就是基于C++的AI五子棋游戏项目开发教程的全部内容了,可以看到我们已经实现了目标,但是后续仍旧可以进行一些优化,比如AI对于落子的价值评分的优化、悔棋功能、主界面菜单等等,后续如果我有时间仍会更新此博客,如果读者自己爱钻研、感兴趣也可以自己完成优化的部分,因为整体的思路都比较清晰,而且逻辑也没有什么太大变化,所以优化起来也比较简单。那这篇博客就暂时告一段落了,我们下篇博客见!