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 实现胜负判定

1. 项目目标

实现玩家和电脑(AI)的下棋过程和游戏规则:玩家是黑棋,AI白棋,默认玩家先走棋。

通过这次项目站给C++语言的语法和知识点。
(1)掌握C++的核心技术。

(2)了解C++面向对象的思想。
(3)掌握C++开发的方法和流程。
(4)掌握AI算法(代价计算)。

2. 效果演示

c++五子棋开发

3. 创建项目

开发环境:Microsoft Visual Studio Enterprise 2022 (64位)+easyx图形库
也可以使用VS2019、2017版本均可,但建议用17版以上的版本。

在开发之前得先配置VS2022的easyx图形库的配置。自己找教程配置。也可以参考下面这个教程:

如何使用Visual Studio 2019配置EasyX环境_MagentaSS的博客-CSDN博客_easyx配置

使用VS2022(或VS2019)创建一个新项目,选择空项目模板。

然后再导入项目配置素材res文件夹,里面有图片、音效等配置文件。res素材评论区下留下邮箱我看到会第一时间把素材发给你。

如下:把res目录放到这个路径下。即和debug同一目录。

4. 项目框架设计
4.1 设计项目框架

根据游戏需要,我们可以设置4个类:分别表示棋手,AI, 棋盘,游戏控制。

4.2 根据设计框架创建类
创建项目框架中描述的4个类。可以使用如下方式创建类:

填写类名,再单击确定即可。以添加Man为例,如下:

这里下好类名不要改动,默认即可。点击确定。

按照这个方式,一共创建4个类:Man, AI, Chess, ChessGame. 创建完后,项目的目录结构如:

最后加一个main.cpp函数,是主函数。

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.hclass ChessGame
{
public:void play();
};

5.5 添加各个接口的具体实现
使用如下方式自动生成各接口的具体实现。先不用考虑各个接口的真正实现,直接使用空函数体代替。 先设计总体架构,最后再层层深入实现。

6. 实现游戏控制
直接调用各个类定义的接口,实现游戏的主体控制。

6.1 添加数据成员
为了便于调用各个类的功能,在ChessGame中,添加3各数据成员,并再构造函数中初始化这三个数据成员。

ChessGame.h

#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 实现游戏控制

ChessGame.cpp

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.cpp函数中,创建游戏。

#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权限的“数据成员”。

chess.h

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;

再在chess.h里面加上头文件:

#include <graphics.h>
#include <vector>
using namespace std;

9. 使用棋盘类的“构造函数” 对棋盘进行构造
添加棋盘类的构造函数的定义以及实现。

Chess.h

Chess(int gradeSize, int marginX, int marginY, float chessSize);
Chess.cppChess::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.cpp中

同时修改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倍。(不一定一定是0.4,在0.3—0.6均可)

代码实现
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的棋力就越深。最终五子棋,就和象棋一样,彻底碾压人类棋手。

项目总结:

1、本项目最核心的地方在于评分的计算和胜负的判定

2、项目得配置easyx图形库

3、本项目得AI可以挑战一般得棋手,对大师级别还是胜不了。可以自己优化

4、代码多的地方可以自己优化。

5、项目还有很多功能未能实现,如联网对战、数据库实现等。

Windows平台下C++五子棋项目实战开发相关推荐

  1. Windows平台下Mediasoup客户端开发指南

    操作系统:Windows 10 IDE: Visual Studio 2019 GitHub:https://github.com/versatica/libmediasoupclient/ 官网文档 ...

  2. Windows平台下Glade+GTK开发环境的搭建

    [@.1 MVVM设计模式与Glade] 做上层软件开发的程序员可能对于MVVM模式比较熟悉,这是一种经典的软件设计模式,很好的将用户界面与后台处理之间分层开,通过属性.事件绑定这种统一的" ...

  3. 《Kotlin项目实战开发》第1章 Kotlin是什么

    第1章 Kotlin是什么 当下互联网大数据云计算时代,数以百万计的应用程序在服务器.移动手机端上运行,其中的开发语言有很大一部分是用流行软件界20多年的.强大稳定的主力的编程语言Java编写. 如果 ...

  4. 零基础学习嵌入式入门以及项目实战开发【手把手教+国内独家+原创】

    零基础学习嵌入式入门以及项目实战开发[手把手教+国内独家+原创] 独家拥有,绝对经典                            创 科 之 龙 嵌入式开发经典系列教程 [第一期] 主讲人: ...

  5. Windows平台下Makefile学习笔记

    来源:http://blog.csdn.net/clever101 决心学习Makefile,一方面是为了解决编译开源代码时需要跨编译平台的问题(发现一些开源代码已经在使用VS2010开发,但我还没安 ...

  6. windows平台下vlc编译

    转自:http://jeremiah.blog.51cto.com/539865/114190     Jeremiah刚刚工作几个月,参与的第一个项目是与视频监控有关,分配给我的任务就是用开源的vl ...

  7. Windows平台下NS2网络仿真环境的搭建

    NS2(Network Simulator 2) 是一种针对网络技术的源代码公开的.免费的软件模拟平台,研究人员使用它可以很容易的进行网络技术的开发,而且发展到今天,它所包含的模块几乎涉及到了网络技术 ...

  8. windows平台下,有什么好的分屏软件推荐?3款让窗口布局更合理的App

    windows平台下,有什么好的分屏软件推荐?Windows 10 系统为例,系统自带功能支持二分屏/三分屏/四分屏的分屏方式.比如用户通过鼠标将应用窗口拖到屏幕边缘,窗口会自动以占据 1/2 屏幕大 ...

  9. windows平台下的mysql启动等基本操作

    一.windows下启动和停止mysql ======================= mysql安装好之后,需要启动mysql服务,否则无法访问到. 当我们在windows平台下,且使用二进制分发 ...

最新文章

  1. Michael Jordan、Sutton、Silver等人,刚刚入选英国皇家学会会士
  2. 《SAP CRM管理与实施指南》一一2.3 小结
  3. 三创比赛关于软件设计的策划书_关于大学生创业和电商创业大赛
  4. XML文档的简易增删查改
  5. linux 6.5桌面环境kde,CentOS 5/6 安装 GNOME 或 KDE 桌面
  6. Java 获取集合元素的值
  7. Python编程基础02:Python基本语法
  8. 误差函数拟合优缺点_欠拟合、过拟合及如何防止过拟合
  9. EBS_FORM_开发:关于切换不同BLOCK的时候弹出需要保存的窗口
  10. php中几个数组函数array_slice() array_filter array_unique() in_array()
  11. CSS3背景渐变。。。
  12. python中列表,元组,字典常用操作方法的总结
  13. 单片机开发板抗干扰(转载于51hei单片机)
  14. killer网卡ubantu下不能wifi联网的问题(据说就是intel网卡)(心酸血泪史)(不升级内核)
  15. wnidows查看电脑序列号命令
  16. pygame 实现 pong 小游戏
  17. windows10下安装choco
  18. 与引导文件系统/vmfs/devices..的备用设备之间的连接已丢失,主机配置更改将不会保存到持久存储中...
  19. 英语听力采用计算机化考试,高考英语复习资料及听力机考特点与应对建议
  20. MySQL read_only 与 super_read_only 之间的关系

热门文章

  1. 微信小程序代码修改无效
  2. 骨传导耳机是怎么传声的?骨传导耳机会伤害耳朵吗?
  3. android 分屏rom,原生ROM都有分屏 为啥MIUI做了那么久?
  4. Java标准的IO操作
  5. html5段落缩进,在Word 2010中缩进段落
  6. 【人物志2】威廉·肖克利(William B. Shockley)
  7. 如何设置在Apple Watch上优化的电池充电
  8. Invalid or Damaged Bootable Partition 虚惊一场
  9. JavaScript基础_10(BOM对象1)
  10. C/C++中算法运行时间的三种计算方式(By 虚怀若谷)