详解 C 语言开发五子棋游戏以及游戏中的重要算法与思路

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解 C 语言开发五子棋游戏以及游戏中的重要算法与思路相关的知识,希望对你有一定的参考价值。

重拾 C 语言之后发现,原来 C 语言是那么的简洁,对于写小项目来讲,C 语言是那么的合适,然后,博主自己写了一个五子棋游戏,同样是基于博主自己封装的 nkCEngine 代码库编写,其实整个游戏里面大部分代码都用在逻辑处理上了,图形处理以及窗口创建的部分,因为有高度封装的 nkCEngine,基本上可以忽略不计,这篇博文来讲一讲 C 语言开发一个简单的五子棋游戏,这个游戏不包含人机对战的逻辑,所以唯一的难点估计就是在于如何判断下棋的一方在下棋时候是否获胜了,同时博主也会介绍一个游戏开发中最经常用到的一个技术 —— 有限状态机;

在开始之前,我们先来看看成品图:

技术分享 

技术分享

技术分享

首先说一说棋子与棋盘是怎么保存与处理的,首先,我们定义一个枚举值,用来表示棋盘中每个格子放置的是一号玩家的棋子,还是二号玩家的棋子,还是没有放置任何棋子:

typedef enum _emPlayerTurn
{
	ePT_NoPlayer,
	ePT_Player1,
	ePT_Player2,
}
emPlayerTurn;

同时,游戏中的棋盘,是用一个数组表示的,这个数组的大小就是棋盘在纵横两个方向上格子的数量,博主这里用了 20 x 20 的棋盘大小,所以定义这么一个数组:

emPlayerTurn Grid[GRID_NUM * GRID_NUM];

数组的类型为之前定义的枚举型,这样子,我们就可以根据数组中任何一个元素的值,来判断对应的格子的状态了,同时,这个二维数组在游戏初始化的时候,会用 ePT_NoPlayer 枚举值来完全填充,说明棋盘中没有放置任何棋子,以后,如果有玩家在其中放置棋子,就会把对应的数组元素设置为对应的枚举值;

然后,在接受到窗口鼠标点击消息的时候,我们可以得到鼠标点击在窗口客户区中的坐标值,可以通过下面公式,将这个值转换为棋盘上格子的索引值:

void CalcGridIndex(const u32 x, const u32 y, u32 * pRow, u32 * pCol)
{
	if (0 != pRow && 0 != pCol)
	{
		(*pRow) = y / (SCREEN_W / GRID_NUM);
		(*pCol) = x / (SCREEN_W / GRID_NUM);
	}
}

上述的代码中,宏 SCREEN_W 是棋盘的总宽度,单位为像素,宏 GRID_NUM 为棋盘在纵横两个方向上的格子数,之所以用宏而不是写死一个常量值,是因为日后可以通过修改对应的宏来改变棋盘大小,函数中的参数 x 与 y 是鼠标点击在窗口客户区中的坐标值,参数 pRow 与 pCol 用来返回计算好的格子行列索引;

uRow = 0;
uCol = 0;

CalcGridIndex(Msg.nCursorX, Msg.nCursorY, &uRow, &uCol);

if (uRow < GRID_NUM && uCol < GRID_NUM)
{
	uIndex = uRow * GRID_NUM + uCol;

	if (ePT_NoPlayer == g_GameData.Grid[uIndex])
	{
		if (&g_GameData.Player1 == g_GameData.pCurPlayer)
		{
			g_GameData.Grid[uIndex] = ePT_Player1;
			g_GameData.pCurPlayer->uTurns += 1;
			g_GameData.pCurPlayer = &g_GameData.Player2;
		}
		else if (&g_GameData.Player2 == g_GameData.pCurPlayer)
		{
			g_GameData.Grid[uIndex] = ePT_Player2;
			g_GameData.pCurPlayer->uTurns += 1;
			g_GameData.pCurPlayer = &g_GameData.Player1;
		}
	}
}

上面代码有很多没有见过的变量,首先,Msg 是一个结构体变量,保存了窗口消息的相关参数,里面的数据成员 nCursorX 与 nCursorY 保存了此时鼠标在窗口客户区中的坐标值,uRow 与 uCol 用于保存鼠标点击了棋盘中的格子的行列索引值,g_GameData 是一个全局结构体的实例变量,保存了所有游戏数据,Grid 是我们上面介绍过的棋盘数组,pCurPlayer 指针指向当前玩家,在这个游戏中,有两个玩家,每个玩家用一个结构体 PlayerData 来表示,结构体的定义如下:

// 玩家数据
typedef struct _PlayerData
{
	// 本局思考时间
	u32 uThinkTime;

	// 本局已使用回合数
	u32 uTurns;

	// 已胜利局数
	u32 uWins;
}
PlayerData;

里面都是一些与游戏逻辑没有太大关系的数据,这里就不做介绍了,然后 g_GameData 中创建了两个 PlayerData 结构体的实例,分别是 Player1 和 Player2,用来保存两个玩家的数据,我们可以通过 pCurPlayer 指针来操作当前进行下棋的玩家,然后,在下完棋之后,通过把 pCurPlayer 指向另外一个玩家,以达到互相切换玩家,轮流下棋的目的,公式 uIndex = uRow * GRID_NUM + uCol 为我们计算出鼠标点击的棋盘格子,在棋盘数组中的下标索引值,然后用这个索引值来读取棋盘数组对应元素的值,以判断格子是否可以下棋,如果可以下棋,根据当前玩家,把数组元素设置为对应的枚举值;

介绍完棋盘之后,我们来看看最重要的部分,如何判断下棋的一方有没有赢,这里楼主用了一个最暴力,也是非常高效的方法,就是通过硬编码来判断所下棋子的八个方向上,是否有五个连续相同的棋子存在,如果是,说明赢了,如果不是,说明还没赢,可以继续下棋,我们来看看下图:

技术分享

上图中,白色圆圈是下棋的玩家所下的棋子,要判断有没有赢,只需要一白色棋子为中心,向上图中的八个方向遍历一次,如果有五个连续的棋子在一起,说明赢了;

思路就是这么简单,因为代码量比较大,这里就不把代码贴出来了,这个算法唯一需要注意的地方,就是有可能棋子落在了五个连续的棋子的中间的某个区段,导致判断失误,我们来看看下图:

技术分享

上图中,五个白色棋子已经连在一起了,但是,最后下的那个棋子,是放置在中间的那个白色棋子,如果按照上述说法,向上和向下两个方向遍历之后,两个方向均只有三个棋子连在一起,而实际上,已经有五个棋子连在一起了,导致误判,要修复这个问题很简单,就是除了单独判断上方向与下方向是否有五个棋子连在一起之外,还要再判断两个方向上连在一起的棋子的总和,就可以知道是否赢棋了,这里要注意的是,必须是两个相反方向上连在一起的相同颜色的棋子达到五个,而不是两个相反方向上相同颜色的棋子达到五个,这点很重要;

好了,判断输赢的方法已经说了,剩下的就是游戏中的状态,我们可以从文章一开始的三张图中看到,这个游戏是有三种状态的,不同状态下,游戏的表现会不一样,分别是:开场、游戏中、结束,三个游戏状态,游戏在任何一个时刻,都只能处于一个状态,并且在满足一定的条件之后,会切换到下一个合适的状态,比方说在游戏开始的时候,游戏处于开场状态,如果点击鼠标左键后,会切换到游戏中状态,如果赢家产生了,或者平局了,就会切换到结束状态,如果点击鼠标左键,又会切换到开场状态,如此无限循环,直到关闭游戏程序,下面是表示有状态的枚举体:

// 游戏状态
typedef enum _emGameState
{
	// 开局
	eGS_Open,

	// 游戏中
	eGS_Play,

	// 结局
	eGS_End,
}
emGameState;

然后在全局游戏数据的 g_GameData 中,用变量 GameState 保存当前游戏状态,接下来看看在窗口消息响应函数中,如果用户点击了鼠标左键,我们会如何处理:

void OnWndMsgCallback(nkWndMsg Msg)
{
	u32 uRow;
	u32 uCol;
	u32 uIndex;

	// 如果玩家点击了鼠标左键

	if (WM_LBUTTONDOWN == Msg.uMsg)
	{
		// 如果现在处于开场状态

		if (eGS_Open == g_GameData.GameState)
		{
			// 切换到游戏中状态

			g_GameData.GameState = eGS_Play;
		}
		else if (eGS_End == g_GameData.GameState)
		{
			// 如果现在处于结束状态,切换到开场状态

			g_GameData.GameState = eGS_Open;
		}
		else if (eGS_Play == g_GameData.GameState)
		{
			// 如果现在处于游戏中状态,进行游戏逻辑处理

			uRow = 0;
			uCol = 0;

			// 计算用户的鼠标点击了棋盘上的哪个格子

			CalcGridIndex(Msg.nCursorX, Msg.nCursorY, &uRow, &uCol);

			// 要保证点击在棋盘内部,才算有效

			if (uRow < GRID_NUM && uCol < GRID_NUM)
			{
				// 计算被点击的棋盘格子在棋盘数组中的索引值

				uIndex = uRow * GRID_NUM + uCol;

				// 根据索引值查看棋盘上被点击的格子现在是处于什么状态

				if (ePT_NoPlayer == g_GameData.Grid[uIndex])
				{
					// 如果被点击的棋盘格子没有放置任何棋子

					if (&g_GameData.Player1 == g_GameData.pCurPlayer)
					{
						// 如果下棋的是一号玩家,则放置一号玩家的棋子

						g_GameData.Grid[uIndex] = ePT_Player1;
						g_GameData.pCurPlayer->uTurns += 1;

						// 将下棋玩家切换到二号玩家

						g_GameData.pCurPlayer = &g_GameData.Player2;
					}
					else if (&g_GameData.Player2 == g_GameData.pCurPlayer)
					{
						// 如果下棋的是二号玩家,则放置二号玩家的棋子

						g_GameData.Grid[uIndex] = ePT_Player2;
						g_GameData.pCurPlayer->uTurns += 1;

						// 将下棋玩家切换到一号玩家

						g_GameData.pCurPlayer = &g_GameData.Player1;
					}

					g_GameData.uTurns += 1;
				}

				// 查看是否有赢家产生

				g_GameData.Winner = IsWinnerFound(uRow, uCol);

				// 如果有赢家产生,或者平局,则切换到结束状态

				if (ePT_Player1 == g_GameData.Winner)
				{
					// 一号玩家赢了

					GameReset();
					g_GameData.GameState = eGS_End;
					g_GameData.Player1.uWins += 1;
				}
				else if (ePT_Player2 == g_GameData.Winner)
				{
					// 二号玩家赢了

					GameReset();
					g_GameData.GameState = eGS_End;
					g_GameData.Player2.uWins += 1;
				}
				else
				{
					// 平局了

					if ((GRID_NUM * GRID_NUM) == g_GameData.uTurns)
					{
						GameReset();
						g_GameData.GameState = eGS_End;
					}
				}
			}
		}
	}
}

代码中的注释已经写得足够详细了,希望大家仔细阅读;

最后,博主放出完整的可执行游戏程序,下载的压缩包中,有个 Release 目录,双击运行里面的 exe 就可以运行五子棋游戏了:

http://pan.baidu.com/s/1eSftiz4 

-- 2016-10-12 By NekoDev cnblogs

-- 原创技术文章,转载必须保持本文的完整性,并注明出处

 

以上是关于详解 C 语言开发五子棋游戏以及游戏中的重要算法与思路的主要内容,如果未能解决你的问题,请参考以下文章

项目实战轻松实现C/C++大作业:五子棋游戏

C语言 打地鼠游戏 超级详解,各个函数与算法,设计思路与流程。

双人小游戏—五子棋(c语言)

五子棋人机博弈游戏(cocos creator)

C语言游戏超详解扫雷游戏完整版,细节满满!!

C语言游戏超详解扫雷游戏完整版,细节满满!!