C++五子棋人机对战_c++五子棋人机对战

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

 本教程配套视频

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白子-1
	vector<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里的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, 测试效果如下

如上黑色背景已经被去除。

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 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. 判断胜负

判断五子棋游戏是否结束。

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.

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++