草庐IT

C++五子棋人机对战

程序员Rock 2023-04-12 原文

目录

 本教程配套视频

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.

有关C++五子棋人机对战的更多相关文章

  1. 【目标检测】TPH-YOLOv5:基于transformer的改进yolov5的无人机目标检测 - 2

    简介最近在使用VisDrone作为目标检测任务的数据集,看到了这个TPH-YOLOv5这个模型在VisDrone2021testset-challenge数据集上的检测效果排到了第五,mAP达到39.18%。于是开始阅读它的论文,并跑一跑的它的代码。论文地址:https://arxiv.org/pdf/2108.11539.pdf项目地址:https://github.com/cv516Buaa/tph-yolov5VisDrone数据集下载:https://pan.baidu.com/s/1JzRTeSi_LgdUVhwtbWhA_w?pwd=8888解决问题TPH-YOLOv5旨在解决无人

  2. 【无人机】基于遗传算法实现无人机编队位置规划附matlab代码 - 2

     1内容介绍现代社会的无人机成本造价低、不易损耗、轻巧灵便、易躲藏、能精确打击目标这些特点,使其在一些高危任务中发挥了不可替代的作用[5]。无人机的用处主要有两种:民用和军事。在民用方面,我们可以运用无人机对一些可能出现隐患的事物进行监控,比如对震后灾区的地面勘探、森林火灾的检测、风暴中心的气象数据等。在2014索契奥运会上,无人机携带的摄像拍摄的画面更贴近运动员,画质更为清晰,2018中国新年春晚上大量无人机组成的海豚造型惊艳了世界。在军事方面,我们可以运用无人机进行一些特殊任务的执行,比如对毒贩的监视工作,边境的巡防工作,无人机侦查、搜救、预警等。无人机的运用使我们在一些事情上实现了无人员

  3. DJI Pilot无人机航线规划-实景三维建模全流程 - 2

    目前很多网上推荐的无人机航线规划软件如Altizure、航测通等难以下载或为商用软件。该文章以大疆精灵4为例演示DJIPilot航线规划-CC实景建模-三维模型导入Cesiumlab3全流程。目录一、软件准备二、DJIPilot航线规划1、准备工作1.1了解测区环境1.2检查无人机2、航线规划2.1创建测绘区域2.2参数设置3、执行飞行任务三、CC实景建模1.1创建工程1.2添加影像1.3影像设置1.4提交空中三角测量1.5空间框架参数设置四、在cesiumlab3上导入三维模型2.1OSGB格式转为3Dtiles2.2导入3D模型附录:1、GSD2.不同区域像控点选取:3、奥维地图在测绘作业

  4. go - 当我在 Windows 10 中执行使用键盘控制 dji tello 无人机的 go 文件时发生错误 - 2

    我有一个名为drone_control.go的go文件,它通过点击键盘按钮来控制djitello无人机。当我尝试使用命令提示符执行此文件时,它显示错误*exec:"stty":executablefilenotfoundin%PATH%我正在使用windows10和gobot框架来控制无人机。以下是我的drone_control.go文件的内容。packagemainimport("time""gobot.io/x/gobot""gobot.io/x/gobot/platforms/dji/tello""gobot.io/x/gobot/platforms/keyboard")func

  5. 实验室无人机平台 Pixhawk 2.4.8 / PX4 v1.9.2 - 2

    实验室无人机平台及相关应用无人机平台目录实验室无人机平台及相关应用无人机平台1.硬件1.1无人机本体1.1.1四旋翼无人机机架1.1.2Pixhawk2.4.8飞控板1.1.3电调1.1.4分电板1.1.5锂电池1.1.6电机1.1.7遥控模块1.2机载电脑与传感器1.2.2激光雷达1.2.3双目相机1.2.4激光定高1.2.5GPS1.2.6光流计1.2.7单目相机2.软件2.1Pixhawk飞控板PX41.9.2飞控固件2.1.1固件编译2.1.2固件刷写2.1.4机架选择2.1.3参数配置2.1.4传感器校准2.1.5遥控器校准2.1.6飞行模式2.2JetsonNano机载电脑Ubun

  6. 6款常见的无人机仿真开发平台(附超详细特点功能对比) - 2

    随着无人机与无人集群的快速发展,开发者对于无人机系统仿真测试环境的需求也日渐显现。本文整理了几款常见的无人机仿真平台,旨在为开发者提供一款更为易用、通用且真实可靠的平台。无人机与无人集群的研制应用快速发展,无人机系统研制过程中试验成本高,空域申请难,测试稳定性低及危险性高等缺点严重限制了无人机集群算法验证的飞行测试工作。无人机系统仿真测试环境应运而生,研究者仅需将无人机研究工作中的实验和算法迭代部分放在仿真环境中,充分验证后再进行实际的飞行测试,可以很大程度上降低研制的成本和风险,有效缩短研制进程。本文将对比几款常见的无人机仿真平台,旨在为开发者提供一款更为易用、通用且真实可靠的平台,使其专注

  7. 基于EKF的四旋翼无人机姿态估计matlab仿真 - 2

    目录1.算法描述2.仿真效果预览3.MATLAB核心程序4.完整MATLAB1.算法描述    卡尔曼滤波是一种高效率的递归滤波器(自回归滤波器),它能够从一系列的不完全包含噪声的测量中,估计动态系统的状态。这种滤波方法以它的发明者鲁道夫·E·卡尔曼(RudolfE.Kalman)命名。卡尔曼最初提出的滤波理论只适用于线性系统。Bucy,Sunahara等人提出并研究了扩展卡尔曼滤波(EKF),将卡尔曼滤波理论进一步应用到非线性领域。    扩展卡尔曼滤波(ExtendedKalmanFilter,EKF)是标准卡尔曼滤波在非线性情形下的一种扩展形式,EKF算法是将非线性函数进行泰勒展开,省略

  8. TGK-Planner无人机运动规划算法解读 - 2

    高速移动无人机的在线路径规划一直是学界当前研究的难点,引起了大量机器人行业的研究人员与工程师的关注。然而无人机的计算资源有限,要在短时间内规划出一条安全可执行的路径,这就要求无人机的运动规划算法必须轻型而有效。本文将介绍一种无人机的在线路径规划算法TGK-Planner,希望能给开发者提供一些解决思路。TGK-Planner简介TGK-Planner为浙江大学FastLab提出的一种轻型有效的拓扑引导的无人机路径规划算法,用于具有有限机载计算资源的四旋翼无人机在线飞行。该算法结构遵循传统的前后端工作流程,采用新颖的设计来提高寻路和轨迹优化子模块的鲁棒性和效率。首先在前端部分使用拓扑引导图来粗略

  9. 基于simulink的无人机姿态飞行控制仿真 - 2

    目录1.算法描述2.仿真效果预览3.MATLAB核心程序4.完整MATLAB1.算法描述    无人机是无人驾驶飞机的简称(UnmannedAerialVehicle),是利用无线电遥控设备和自备的程序控制装置的不载人飞机,包括无人直升机、固定翼机、多旋翼飞行器、无人飞艇、无人伞翼机。广义地看也包括临近空间飞行器(20-100公里空域),如平流层飞艇、高空气球、太阳能无人机等。从某种角度来看,无人机可以在无人驾驶的条件下完成复杂空中飞行任务和各种负载任务,可以被看做是“空中机器人”。    飞控子系统是无人机完成起飞、空中飞行、执行任务和返场回收等整个飞行过程的核心系统,飞控对于无人机相当于驾

  10. Ubuntu20.04下基于ROS和PX4的无人机仿真平台的基础配置搭建(我所遇到的问题) - 2

    写在前面:我目前也处于学习阶段,当时按照ROS教程安装的20.04,随后搭建XTDrone阶段因为版本问题出现了很多问题,这是我根据问题,检索后汇总的一些解决措施。本文中提到的问题可能不是我遇到的所有问题,由于我整体配置过程比较混乱,所以我主要挑选了自己记忆比较深刻的问题及搜索到的解决方法进行了列举。大家遇到了其他问题都可以直接搜索报错信息,可能可以获得解决方法。(很多部分可能没有留存报错信息的截图)参考https://blog.csdn.net/sirobot/article/details/115521712https://blog.csdn.net/yinhangbin/article/

随机推荐