目录
本教程配套视频
1. 项目目标
2. 效果演示
3. 创建项目
4. 项目框架设计
4.1 设计项目框架
4.2 根据设计框架创建类
5. 给类添加主要接口
5.1 设计棋盘类Chess的主要接口
5.2 设计AI类的主要接口
5.3 设计Man类的主要接口
5.4 设计ChessGame的主要接口
5.5 添加各个接口的具体实现
6. 实现游戏控制
6.1 添加数据成员
6.2 实现游戏控制啊
7. 创建游戏
8. 棋盘的“数据成员”设计
9. 使用棋盘类的“构造函数” 对棋盘进行构造
10. 棋盘的“初始化”
11. 实现棋手走棋
11.1 棋手的初始化
11.2 棋手走棋
11.3 判断落子点击位置是否有效
原理分析
代码实现
12. 实现棋盘落子
12.1 实现Chess类的chessDown成员函数
12.2 修改棋盘的棋子数据
13. 实现AI走棋
13.1 设计AI的数据成员
13.2 对AI进行初始化
13.3 AI“思考”怎样走棋
13.3.1 AI对落子点进行评分
13.3.2 AI根据评分进行“思考”
12.3.3 AI走棋
12.3.4 测试
14. AI的BUG
15. 判断胜负
15.1 对胜负进行处理
15.2 胜负判定原理
15. 3 实现胜负判定
15. 4 测试效果
16. AI进一步优化
AI提升
17. 开发拓展
五子棋人机对战,已经有很版本。但是使用纯C++,严格按照C++面向对象思想开发的,却还是很少的,所以准备使用C++面向对象的思想,开发一个完整的五子棋人机对战,对于C++初学者,是很有帮助的哦!
本教程配套视频
1. 项目目标
掌握C++的核心技术掌握C++开发项目的方法和流程掌握AI算法的基础应用2. 效果演示
开局头像,没有看错,就是我哈,棋魂附体 :-)
准备好 了吗?直接上代码!
3. 创建项目
使用VS2019+easyx图形库开发,也可以使用VS的其他版本。
参考:VS2019安装教程 easyx图形库入门教程
使用VS2019(或VS2022)创建一个新项目,选择空项目模板。
然后再导入图片素材res目录。因网盘链接不稳定,在评论中回复邮件地址,即发送完整素材。也可以使用自己的素材。
4. 项目框架设计
4.1 设计项目框架
使用C语言开发的初学者,往往直接就在main函数中写详细的过程。使用C++面向对象,就需要“脱胎换骨”,改变开发思路了!不写过程,直接写需要几个类!
这里,设计了4个类,分别表示棋手,AI, 棋盘,游戏控制。这应该是最符合现实情况的简单设计了,如果是做网络对战版,就还需要添加其它模块。
4.2 根据设计框架创建类
创建项目框架中描述的4个类。可以使用如下方式创建类:
填写类名,再单击确定即可。
按照这个方式,一共创建4个类:Man, AI, Chess, ChessGame. 创建完后,项目的目录结构如:
5. 给类添加主要接口
5.1 设计棋盘类Chess的主要接口
注意:在给类设计接口时,建议先只考虑对外暴露的“接口”,可以先不用考虑数据成员,对外(public)提供的接口(函数)才是最重要的。
Chess.h
typedef enum {CHESS_WHITE = -1, // 白方CHESS_BLACK = 1 // 黑方} chess_kind_t;struct ChessPos {int row;int col;};class Chess{public:// 棋盘的初始化:加载棋盘的图片资源,初始化棋盘的相关数据void init();// 判断在指定坐标(x,y)位置,是否是有效点击// 如果是有效点击,把有效点击的位置(行,列)保存在参数pos中bool clickBoard(int x, int y, ChessPos* pos);// 在棋盘的指定位置(pos), 落子(kind)void chessDown(ChessPos* pos, chess_kind_t kind);// 获取棋盘的大小(13线、15线、19线)int getGradeSize();// 获取指定位置是黑棋,还是白棋,还是空白int getChessData(ChessPos* pos);int getChessData(int row, int col);// 判断棋局是否结束bool checkOver();};
5.2 设计AI类的主要接口
AI.h
#include "Chess.h"class AI{public:void init(Chess* chess);void go();};
5.3 设计Man类的主要接口
Man.h
#include "Chess.h"class Man{public:void init(Chess* chess);void go();};
5.4 设计ChessGame的主要接口
ChessGame.h
class ChessGame{public:void play();};
5.5 添加各个接口的具体实现
可以使用如下方式自动生成各接口的具体实现。先不用考虑各个接口的真正实现,直接使用空函数体代替。
6. 实现游戏控制
直接调用各个类定义的接口,实现游戏的主体控制。
6.1 添加数据成员
为了便于调用各个类的功能,在ChessGame中,添加3各数据成员,并再构造函数中初始化这三个数据成员。
#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::ChessGame(Man* man, AI* ai, Chess* chess){this->man = man;this->ai = ai;this->chess = chess;ai->init(chess);man->init(chess);}
6.2 实现游戏控制啊
void ChessGame::play(){chess->init();while (1) {man->go();if (chess->checkOver()) {chess->init();;continue;}ai->go();if (chess->checkOver()) {chess->init();continue;}}}
7. 创建游戏
在main函数中,创建游戏。
#include <iostream>#include "ChessGame.h"int main(void) {Chess chess;Man man;AI ai;ChessGame game(&man, &ai, &chess);game.play();return 0;}
8. 棋盘的“数据成员”设计
为棋盘类,添加private权限的“数据成员”。
private:// 棋盘尺寸int gradeSize;float margin_x;//49;int margin_y;// 49;float chessSize; //棋子大小(棋盘方格大小)IMAGE chessBlackImg;IMAGE chessWhiteImg;// 存储当前游戏棋盘和棋子的情况,空白为0,黑子1,白子-1vector<vector<int>> chessMap;// 标示下棋方, true:黑棋方 false: AI 白棋方(AI方)bool playerFlag;
再补充一下头文件。
#include <graphics.h>#include <vector>using namespace std;
9. 使用棋盘类的“构造函数” 对棋盘进行构造
添加棋盘类的构造函数的定义以及实现。
Chess.h
Chess(int gradeSize, int marginX, 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);}}
同时修改main函数的Chess对象的创建。、
//Chess chess;Chess chess(13, 44, 43, 67.4);
10. 棋盘的“初始化”
对棋盘进行数据初始化,使得能够看到实际的棋盘。
void Chess::init(){initgraph(897, 895);loadimage(0, "res/棋盘2.jpg");mciSendString("play res/start.wav", 0, 0, 0); //需要修改字符集为多字节字符集loadimage(&chessBlackImg, "res/black.png", chessSize, chessSize, true);loadimage(&chessWhiteImg, "res/white.png", chessSize, chessSize, true);for (int i = 0; i < chessMap.size(); i++) {for (int j = 0; j < chessMap[i].size(); j++) {chessMap[i][j] = 0;}}playerFlag = true;}
添加头文件和相关库,使得能够播放落子音效。
Chess.cpp
#include <mmsystem.h>#pragma comment(lib, "winmm.lib")
修改项目的字符集为“多字节字符集”。
测试效果:
11. 实现棋手走棋
现在执行程序,除了弹出的棋盘,什么都不能干。因为,棋手的走棋函数,还没有实现哦!现在来实现棋手走棋功能。
11.1 棋手的初始化
为棋手类,添加数据成员,表示棋盘
Man.h
private:Chess* chess;
实现棋手对象的初始化。
Man.cpp
void Man::init(Chess* chess){this->chess = chess;}
在ChessGame的构造函数中,实现棋手的初始化。
ChessGame.cpp
ChessGame::ChessGame(Man* man, AI* ai, Chess* chess){this->man = man;this->ai = ai;this->chess = chess;man->init(chess); //初始化棋手}
11.2 棋手走棋
Man.cpp
void Man::go(){// 等待棋士有效落子MOUSEMSG msg;ChessPos pos;while (1) {msg = GetMouseMsg();if (msg.uMsg == WM_LBUTTONDOWN && chess->clickBoard(msg.x, msg.y, &pos)) {break;}}// 落子chess->chessDown(&pos, CHESS_BLACK);}
11.3 判断落子点击位置是否有效
执行程序后,还是没有任何效果,因为落子的有效性还没有判断。
原理分析
先计算点击位置附近的4个点的位置,然后再计算点击位置到这四个点之间的距离,如果离某个点的距离小于“阈值”,就认为这个点是落子位置。这个“阈值”, 小于棋子大小的一半即可。我们这里取棋子大小的0.4倍。
代码实现
Chess.cpp
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; // 20 鼠标点击的模糊距离上限int len;int selectPos = 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) {selectPos = true;}break;}// 距离右上角的距离len = sqrt((x - leftTopPosX - chessSize) * (x - leftTopPosX - chessSize) + (y - leftTopPosY) * (y - leftTopPosY));if (len < offset) {pos->row = row;pos->col = col + 1;if (chessMap[pos->row][pos->col] == 0) {selectPos = true;}break;}// 距离左下角的距离len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY - chessSize) * (y - leftTopPosY - chessSize));if (len < offset) {pos->row = row + 1;pos->col = col;if (chessMap[pos->row][pos->col] == 0) {selectPos = true;}break;}// 距离右下角的距离len = sqrt((x - leftTopPosX - chessSize) * (x - leftTopPosX - chessSize) + (y - leftTopPosY - chessSize) * (y - leftTopPosY - chessSize));if (len < offset) {pos->row = row + 1;pos->col = col + 1;if (chessMap[pos->row][pos->col] == 0) {selectPos = true;}break;}} while (0);return selectPos;}
可以通过打印语句,测试判断是否准确。
12. 实现棋盘落子
12.1 实现Chess类的chessDown成员函数
void Chess::chessDown(ChessPos *pos, chess_kind_t kind){mciSendString("play res/down7.WAV", 0, 0, 0);int x = margin_x + pos->col * chessSize - 0.5 * chessSize;int y = margin_y + pos->row * chessSize - 0.5 * chessSize;if (kind == CHESS_WHITE) {putimagePNG(x, y, &chessWhiteImg);}else {putimagePNG(x, y, &chessBlackImg);}}
检查落子效果:
棋子背后有黑色背景。这是因为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里的Rint sg = ((src[srcX] & 0xff00) >> 8); //Gint sb = src[srcX] & 0xff; //Bif (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}}}}
再把chessDown中的putimage更换为putimagePNG, 测试效果如下:
如上,黑色背景已经被去除。
12.2 修改棋盘的棋子数据
在界面上落子之后,还需要修改棋盘的棋子数据。为Chess类添加updateGameMap函数来修改棋子数据。这个方法,是给棋盘对象内部使用的,不需要开放给他人使用,所有把权限设置为private,设置为public也可以,但是从技术角度就不安全了。如果他人直接调用这个函数,就会导致棋盘的数据和界面上看到的数据不一样。
Chess.h
private:void updateGameMap(ChessPos *pos);
Chess.cpp
void Chess::updateGameMap(ChessPos* pos){ lastPos = *pos;chessMap[pos->row][pos->col] = playerFlag ? 1 : -1;playerFlag = !playerFlag; // 换手}
在落子后,调用updateGameMap更新棋子数据。
void Chess::chessDown(ChessPos *pos, chess_kind_t kind){// ......updateGameMap(pos);}
13. 实现AI走棋
终于可以设计我们的AI模块了!
13.1 设计AI的数据成员
添加棋盘数据成员,以表示对哪个棋盘下棋。添加评分数组, 用来存储AI对棋盘所有落点的价值评估。这也是人机对战最重要的部分。AI.h
private:Chess* chess;// 存储各个点位的评分情况,作为AI下棋依据vector<vector<int>> scoreMap;
13.2 对AI进行初始化
AI.cpp
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); }}
13.3 AI“思考”怎样走棋
AI的思考方法,就是对棋盘的所有可能落子点,做评分计算,然后选择一个评分最高的点落子。
13.3.1 AI对落子点进行评分
对每一个可能的落子点,从该点周围的八个方向,分别计算,确定出每个方向已经有几颗连续的棋子。
棋理格言:敌之好点,即我之好点。
就是说,每个点,都要考虑,如果敌方占领了这个点,会产生多大的价值,如果我方占领了这个点,又会产生多大的价值。如果我方占领这个点,价值只有1000,但是敌方要是占领了这个点,价值有2000,而在自己在其它位置没有价值更高的点,那么建议直接抢占这个敌方的好点。
兵家必争之地:荆州(隆中对的第一步,就是取荆州)
AI先计算棋手如果在这个位置落子,会有多大的价值。然后再计算自己如果在这个位置落子,有大大价值。具体计算方法,就是计算如果黑棋或者白棋在这个位置落子,那么在这个位置的某个方向上, 一共有连续几个黑子或者连续几个白子。连续的数量越多,价值越大。
常见棋形
连2:
活3
死3
活4
死4
连5(赢棋)
如果走这个点,产生的棋形以及对应评分:
用代码实现评分计算
AI.h
private:void calculateScore();
AI.cpp
void AI::calculateScore(){ // 统计玩家或者电脑连成的子 int personNum = 0; // 玩家连成子的个数 int botNum = 0; // AI连成子的个数 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 <= 1; y++) { for (int x = -1; x <= 1; x++) { // 重置 personNum = 0; botNum = 0; emptyNum = 0; // 原坐标不算 if (!(y == 0 && x == 0)) { // 每个方向延伸4个子 // 对黑棋评分(正反两个方向) 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 == 2) // 杀三 { 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) // 玩家的子 { botNum++; } 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) // 玩家的子 { botNum++; } else if (curRow > 0 && curRow < size && curCol > 0 && curCol < size && chess->getChessData(curRow, curCol) == 0) // 空白位 { emptyNum++; break; } else // 出边界 break; } if (botNum == 0) // 普通下子 scoreMap[row][col] += 5; else if (botNum == 1) // 活二 scoreMap[row][col] += 10; else if (botNum == 2) { if (emptyNum == 1) // 死三 scoreMap[row][col] += 25; else if (emptyNum == 2) scoreMap[row][col] += 50; // 活三 } else if (botNum == 3) { if (emptyNum == 1) // 死四 scoreMap[row][col] += 55; else if (emptyNum == 2) scoreMap[row][col] += 10000; // 活四 } else if (botNum >= 4) scoreMap[row][col] += 30000; // 活五,应该具有最高优先级 } } } } }}
13.3.2 AI根据评分进行“思考”
各个落子点的评分确定后,“思考”就很简单了,直接使用“遍历”,找出评分最高的点即可。
AI.h
ChessPos think(); //private权限
AI.cpp
ChessPos AI::think(){ // 计算评分 calculateScore(); // 从评分中找出最大分数的位置 int maxScore = 0; //std::vector<std::pair<int, int>> maxPoints; vector<ChessPos> maxPoints; int k = 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) { 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];}
对ChesPos类补充构造函数
Chess.h
ChessPos(int r=0, int c=0) :row(r), col(c){}
12.3.3 AI走棋
AI.cpp
void AI::go(){ChessPos pos = think();Sleep(1000); //假装思考 chess->chessDown(&pos, CHESS_WHITE);}
因为思考速度太快,使用Sleep休眠作为停顿,以提高棋手的“对局体验” :-)
12.3.4 测试
检查执行效果:
当AI在“思考”时,程序崩溃!设置断点后检查,发现ai对象的chess成员指向一个无效内存。因为可以判定,还没有对AI对象进行初始化。检查后发现,之前为AI对象定义了初始化init函数,但是没有调用这个函数。补充如下:
ChessGame.cpp
ChessGame::ChessGame(Man* man, AI* ai, Chess* chess){//...ai->init(chess);}
调试后还是发现,程序崩溃:
加断点检查发现Chess类的getGradeSize函数返回0. 修改如下:
int Chess::getGradeSize(){return gradeSize;}
测试运行后,发现AI很傻,落子很“臭”:
加断点调试,发现getChessData函数的返回值始终为0,原来是之前设计这个接口时,使用自动生产的,没有做真正的实现,需改如下:
int Chess::getChessData(ChessPos* pos){return chessMap[pos->row][pos->col];}int Chess::getChessData(int row, int col){return chessMap[row][col];}
测试后发现,AI的棋力,已经正常:
14. AI的BUG
现在的AI已经能够走棋了,而且还很不错,但是通过调试,发现AI在某些时候会下“昏招”, 成为“臭棋篓子”, 情况如下:
当下到这个局面时:
当棋手在第9行第9列落子时,形成冲4形态时,白棋应该进行阻挡防守,但是白棋却判断错误,在其它位置落子了!
通过加断点判断分析,原因是我们对8个方向做了判断,而在每个方向进行判断时,又对反方向进行了判断。最终导致AI在第行第5列的位置进行价值分析时,在正上方和正下方两次判断中,认为改点有“活三”价值,导致这点的价值被重复计算了一次,被累加到 20000,超过了黑棋冲四的价值!解决方法也很简单,就是8个方向,只要判断4次即可(如下图的绿色箭头
修改后的AI评分方法。
void AI::calculateScore(){int personNum = 0; //棋手方(黑棋)多少个连续的棋子int aiNum = 0; //AI方(白棋)连续有多少个连续的棋子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)) continue;for (int y = -1; y <= 0; y++) { //Y的范围还是-1, 0for (int x = -1; x <= 1; x++) { //X的范围是 -1,0,1if (y == 0 && x == 0) continue; if (y == 0 && x != 1) continue; //当y=0时,仅允许x=1personNum = 0;aiNum = 0;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) {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) { //连2//CSDN 程序员RockscoreMap[row][col] += 10;}else if (personNum == 2) {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] = 5000; //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 == 2) {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;}}}}}}
15. 判断胜负
判断五子棋游戏是否结束。
15.1 对胜负进行处理
Chess.cpp
bool Chess::checkOver(){if (checkWin()) {Sleep(1500);if (playerFlag == false) { //黑棋赢(玩家赢),此时标记已经反转,轮到白棋落子mciSendString("play res/不错.mp3", 0, 0, 0);loadimage(0, "res/胜利.jpg");}else {mciSendString("play res/失败.mp3", 0, 0, 0);loadimage(0, "res/失败.jpg");}_getch(); // 补充头文件 #include <conio.h>return true;}return false;}
补充头文件 conio.h, 并添加CheckWin的定义和实现。
15.2 胜负判定原理
具体的判定原理,就是对刚才的落子位置进行判断,判断该位置在4个方向上是否有5颗连续的同类棋子。
对于水平位置的判断:
其他方向的判断,原理类似。
15. 3 实现胜负判定
添加最近落子位置。
Chess.h
ChessPos lastPos; //最近落子位置, Chess的private数据成员
更新最近落子位置。
Chess.cpp
void Chess::updateGameMap(ChessPos* pos){lastPos = *pos;//...}
实现胜负判定。
Chess.cpp
bool Chess::checkWin(){// 横竖斜四种大情况,每种情况都根据当前落子往后遍历5个棋子,有一种符合就算赢// 水平方向int row = lastPos.row;int col = lastPos.col;for (int i = 0; i < 5; i++){// 往左5个,往右匹配4个子,20种情况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;}// 竖直方向(上下延伸4个)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 &&// 第[row+i]行,第[col-i]的棋子,与右上方连续4个棋子都相同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++){// 第[row+i]行,第[col-i]的棋子,与右下方连续4个棋子都相同if (row - i >= 0 &&row - i + 4 < gradeSize &&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;}
15. 4 测试效果
已经能够完美判定胜负了,并能自动开启下一局。
再把落子音效加上,用户体验就更好了。
Chess.cpp
void Chess::chessDown(ChessPos* pos, chess_kind_t kind){mciSendString("play res/down7.WAV", 0, 0, 0); //......}
16. AI进一步优化
现在AI的实力,对于一般的五子棋业余爱好者,已经能够秒杀,但是对于业余中的“大佬”,还是力不从心,甚至会屡战屡败,主要原因有两点:
1. 没有对跳三和跳四进行判断。实际上,跳三和跳四的价值与连三连四的价值,是完全相同的。而现在的AI只计算了连三和连四,没有考虑跳三跳四,所以就会错失“好棋”!
对于上图,在位置1和位置2,都会形成“跳三”。
对于上图在位置3和位置4,都会形成连三.
对于上图,在位置1对黑棋形成“跳四”,跳四的价值和“连四”或“冲四”的价值也是相同的!
2. 没有对黑棋设置“禁手”。因为五子棋已经发展到“黑棋先行必胜”的套路,所以职业五子棋比赛,会对黑棋设置以下“禁手”。
三三禁手四四禁手长连禁手三三禁手(如果在该位置主动落子或者被动落子,直接判黑方战败!)
四四禁手(如果在该位置主动落子或者被动落子,直接判黑方战败!)
长连禁手(如果在该位置主动落子或者被动落子,直接判黑方战败!)
AI提升
在计算落子点价值的时候,增加对跳三和跳四的价值判断在判断胜负时,增加对黑方禁手的判断。通过以上的优化后,业余高手也很难取胜了!但是对专业棋手,还是难以招架!原因在于,目前的AI只根据当前盘面进行判断,静态的最佳座子点。没有对后续步骤进行连续判断。可以使用“搜索树”,进行连续判定,搜索的深度越深,AI的棋力就越深。最终五子棋,就和象棋一样,彻底碾压人类棋手。
17. 开发拓展
计分以及棋力等级|
悔棋功能
棋力训练(充值送形势判断)
记棋谱功能
网络对战功能
邀请微信好友、QQ好友对战功能
移植到移动端(Android和IOS)
---END.