Windows平台下C++五子棋项目实战开发

news/2024/10/17 20:31:20/

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、项目还有很多功能未能实现,如联网对战、数据库实现等。


http://www.ppmy.cn/news/563043.html

相关文章

基于java的五子棋游戏设计

技术&#xff1a;Java、JSP等摘要&#xff1a;随着互联网迅速的发展&#xff0c;网络游戏已经成为人们普遍生活中不可或缺的一部分&#xff0c;它不仅能使人娱乐&#xff0c;也能够开发人的智力&#xff0c;就像本文所主要讲的五子棋游戏一样能挖掘人们聪明的才干与脑袋的机灵程…

用Java实现五子棋对弈

目录 题目展示 题目分析 代码实现 结果展示 题目展示 1.使用二维数组存储五子棋棋盘 如下图 2.在控制台通过Scanner输入黑白棋坐标( 表示二维数组坐标),使用实心五角星和空心五角星表示黑白棋子。 如下图: 输入后重新输出棋盘如下图: 白棋输入后如下图&#xff1a; 黑白棋…

C++实现基于博弈树的5x5一子棋人机对战

基于博弈树的5x5一子棋人机对战 919106840637实验2 这是智能计算三个课程实验的第二个实验&#xff0c;即博弈树搜索 。我之前对博弈树的了解不多&#xff0c;所以实现起来比较的简略&#xff0c;仅仅是基本达到了要求 实验语言 C 实验内容 实践博弈树搜索——“5x5格子的一…

Java五子棋(人机版),昨天买的棋子今天就用不上了

Java五子棋&#xff0c;老程序员也花了3天 作者简介 作者名&#xff1a;编程界明世隐 简介&#xff1a;CSDN博客专家&#xff0c;从事软件开发多年&#xff0c;精通Java、JavaScript&#xff0c;博主也是从零开始一步步把学习成长、深知学习和积累的重要性&#xff0c;喜欢跟广…

文科生从0学Python转数据分析学习建议避坑指南

我本科是财务管理&#xff0c;文科专业&#xff0c;零基础学习Python转行数分后&#xff0c;现在我的日常工作都离不开它。 接下来&#xff0c;给各位跟我一样无编程经验的朋友一些学习的建议 目标导向&#xff1a;先搞清楚为啥要学 Python几乎可以做任何事&#xff0c;但我…

数据库是如何工作的

数据库是如何工作的 注&#xff1a; 本文翻译自db_tutorial. 数据库计算机世界的一个基础软件&#xff0c;要想深入了解数据库&#xff0c;就不得不思考如下几个问题&#xff1a; 数据以什么格式保存&#xff1f;&#xff08;在内存和磁盘上&#xff09;它何时从内存移动到磁…

日立医疗影像诊断业务正式加入富士胶片集团

富士胶片株式会社宣布&#xff0c;于2021年3月31日完成对株式会社日立制作所&#xff08;以下简称“日立”&#xff09;旗下影像诊断相关业务的收购程序&#xff0c;标志着日立为继承相关业务而成立的新公司“富士胶片医疗健康”正式成为富士胶片集团全资子公司&#xff0c;全新…

云胶片(云影像)功能简介

1 首先&#xff0c;云胶片不能是这样的&#xff0c;这个只是换瓶不换药。这种图只是影像科老师在全序列图像中&#xff0c;选取的部分影像&#xff0c;患者没有拿到自己全部数据&#xff1b;另外&#xff0c;通过N-Print&#xff08;胶片打印标准&#xff09;打印的图像都是通过…