实现玩家和电脑(AI)的下棋过程和游戏规则:玩家是黑棋,AI白棋,默认玩家先走棋。
通过这次项目站给C++语言的语法和知识点。
(1)掌握C++的核心技术。
(2)了解C++面向对象的思想。
(3)掌握C++开发的方法和流程。
(4)掌握AI算法(代价计算)。
c++五子棋开发
开发环境: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个类:分别表示棋手,AI, 棋盘,游戏控制。
4.2 根据设计框架创建类
创建项目框架中描述的4个类。可以使用如下方式创建类:

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

这里下好类名不要改动,默认即可。点击确定。
按照这个方式,一共创建4个类:Man, AI, Chess, ChessGame. 创建完后,项目的目录结构如:
最后加一个main.cpp函数,是主函数。

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();
- };
- #include "Chess.h"
- class AI
- {
- public:
- void init(Chess* chess);
- void go();
- };
- #include "Chess.h"
-
- class Man
- {
- public:
- void init(Chess* chess);
- void go();
- };
- 5.4 设计ChessGame的主要接口
- ChessGame.h
-
- class ChessGame
- {
- public:
- void play();
- };

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);
- }
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;
- }
- }
- }
- #include <iostream>
- #include "ChessGame.h"
-
- int main(void) {
- Chess chess;
- Man man;
- AI ai;
- ChessGame game(&man, &ai, &chess);
-
- game.play();
-
- return 0;
- }
chess.h
- private:
- // 棋盘尺寸
- int gradeSize;
- float margin_x;//49;
- int margin_y;// 49;
- float chessSize; //棋子大小(棋盘方格大小)
-
- IMAGE chessBlackImg;
- IMAGE chessWhiteImg;
-
- // 存储当前游戏棋盘和棋子的情况,空白为0,黑子1,白子-1
- vector<vector<int>> chessMap;
-
- // 标示下棋方, true:黑棋方 false: AI 白棋方(AI方)
- bool playerFlag;
再在chess.h里面加上头文件:
- #include <graphics.h>
- #include <vector>
- using namespace std;
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.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")
修改项目的字符集为“多字节字符集”。看下图最后一行。

测试效果:

发现棋盘出来了,现在就是实现走棋。
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); //初始化棋手
- }
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);
- }

原理分析:*****(五颗星)项目核心
先计算点击位置附近的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;
- }
- //可以通过打印语句,测试判断是否准确。
- 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里的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
- }
- }
- }
- }
//这个接口就是用来将图片的底边去掉的,当然也可以用其他方法,感兴趣的读者可以试试其他方法。
再把chessDown中的putimage更换为putimagePNG, 测试效果如下:

如上,黑色背景已经被去除。好的现在程序越来越像棋子的样子了。
继续开发。。。。
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, 0
- for (int x = -1; x <= 1; x++) { //X的范围是 -1,0,1
- if (y == 0 && x == 0) continue;
- if (y == 0 && x != 1) continue; //当y=0时,仅允许x=1
-
- personNum = 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 程序员Rock
- 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] = 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.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的定义和实现。
对于水平位置的判断:

其他方向的判断,原理类似。
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、项目还有很多功能未能实现,如联网对战、数据库实现等。