完全基于C++ 实现的人机对战五子棋小游戏

1. 项目目标

  • 掌握C++的核心技术
  • 掌握C++开发项目的方法和流程
  • 掌握AI算法的基础应用

2. 效果演示

3. 创建项目

使用VS2019+easyx图形库开发,也可以使用VS的其他版本。

 使用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 

 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也可以,但是从技术角度就不安全了。如果他人直接调用这个函数,就会导致棋盘的数据和界面上看到的数据不一样。

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走棋

13.3 AI“思考”怎样走棋
AI的思考方法,就是对棋盘的所有可能落子点,做评分计算,然后选择一个评分最高的点落子。

13.3.1 AI对落子点进行评分
对每一个可能的落子点,从该点周围的八个方向,分别计算,确定出每个方向已经有几颗连续的棋子。

棋理格言:敌之好点,即我之好点。
就是说,每个点,都要考虑,如果敌方占领了这个点,会产生多大的价值,如果我方占领了这个点,又会产生多大的价值。如果我方占领这个点,价值只有1000,但是敌方要是占领了这个点,价值有2000,而在自己在其它位置没有价值更高的点,那么建议直接抢占这个敌方的好点。

 常见棋形

如果走这个点,产生的棋形以及对应评分:

 AI先计算棋手如果在这个位置落子,会有多大的价值。然后再计算自己如果在这个位置落子,有大大价值。具体计算方法,就是计算如果黑棋或者白棋在这个位置落子,那么在这个位置的某个方向上, 一共有连续几个黑子或者连续几个白子。连续的数量越多,价值越大

 用代码实现评分计算
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;   // 活五,应该具有最高优先级
                        }
                    }
                }
            }
        }
}

具体项目代码:dengchen1314/- (github.com)

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
五子棋是一种非常古老的游戏,也是一种非常常见的人机游戏。下面是一个简单的C++五子棋人机项目实现的示例: 1. 定义棋盘类 首先,我们需要定义一个棋盘类。在这个类中,我们需要定义棋盘的大小、棋子的状态(黑、白、空)、以及各种棋子的位置等信息。 ```cpp // 定义棋盘类 class ChessBoard { public: ChessBoard(int boardSize); // 构造函数,初始化棋盘 ~ChessBoard(); // 析构函数,释放棋盘内存 void reset(); // 重置棋盘 bool setChessman(int row, int col, ChessmanType type); // 在指定位置下子 ChessmanType getChessman(int row, int col) const; // 获取指定位置的棋子状态 bool isFull() const; // 判断棋盘是否已满 private: int m_boardSize; // 棋盘大小 ChessmanType** m_board; // 棋盘数组,存储各个位置的棋子状态 }; ``` 2. 定义AI类 接下来,我们需要定义一个AI类,用于计算机与人类玩家交互并生成下一步的走法。在这个类中,我们需要实现一个算法来判断哪一步最有可能赢得比赛。 ```cpp // 定义AI类 class AI { public: AI(ChessBoard* board, ChessmanType aiType); // 构造函数,初始化AI ~AI(); // 析构函数,释放AI内存 void getNextStep(int& row, int& col); // 获取下一步走法 private: ChessmanType m_aiType; // AI的棋子类型(黑或白) ChessBoard* m_board; // 棋盘 }; ``` 3. 定义游戏类 最后,我们需要定义一个游戏类,用于处理游戏的流程和逻辑。在这个类中,我们需要实现人机交互、判断胜负、判断游戏是否结束等功能。 ```cpp // 定义游戏类 class Game { public: Game(int boardSize); // 构造函数,初始化游戏 ~Game(); // 析构函数,释放游戏内存 void start(); // 开始游戏 private: ChessBoard* m_board; // 棋盘 AI* m_ai; // AI bool m_isPlayerTurn; // 当前是否是玩家回合 bool m_isGameOver; // 游戏是否结束 }; ``` 以上是一个简单的C++五子棋人机项目的实现示例。当然,这只是一个简单的示例,实际的实现可能会更加复杂,需要根据具体的需求进行调整和修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值