详解 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

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

时间: 2024-08-09 23:53:39

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

[转]详解C#组件开发的来龙去脉

C#组件开发首先要了解组件的功能,以及组件为什么会存在.在Visual Studio .NET环境下,将会有新形式的C#组件开发. 组件的功能 微软即将发布的 Visual Studio .NET 将使程序开发人员获得一个集成开发环境,它不但为开发传统的 C/C++ 应用程序,而且也为令人振奋的Microsoft .NET 组件提供了丰富的工具.这些以管理代码编写.在通用语言运行时构建的组件向开发人员提供了一个全新的混合开发环境,即象 Microsoft Visual Basic 一样容易,而同

详解go语言的array和slice 【二】

上一篇  详解go语言的array和slice [一]已经讲解过,array和slice的一些基本用法,使用array和slice时需要注意的地方,特别是slice需要注意的地方比较多.上一篇的最后讲解到创建新的slice时使用第三个索引来限制slice的容量,在操作新slice时,如果新slice的容量大于长度时,添加新元素依然后使源的相应元素改变.这一篇里我会讲解到如何避免这些问题,以及迭代.和做为方法参数方面的知识点. slice的长度和容量设置为同一个值 如果在创建新的slice时我们把

供应商导入的API补充(详解EBS接口开发之供应商导入)(转)

原文地址  供应商导入的API补充(详解EBS接口开发之供应商导入) --供应商 --创建 AP_VENDOR_PUB_PKG.Create_Vendor ( p_api_version IN NUMBER, p_init_msg_list IN VARCHAR2 := FND_API.G_FALSE, p_commit IN VARCHAR2 := FND_API.G_FALSE, p_validation_level IN NUMBER := FND_API.G_VALID_LEVEL_FU

PullScrollView详解(六)——延伸拓展(listview中getScrollY()一直等于0、ScrollView中的overScrollBy)

前言:经常说follow your heart.但等到真到这么一天的时候,却很艰难 相关文章: 1.<PullScrollView详解(一)--自定义控件属性>2.<PullScrollView详解(二)--Animation.Layout与下拉回弹>3.<PullScrollView详解(三)--PullScrollView实现>4.<PullScrollView详解(四)--完全使用listview实现下拉回弹(方法一)>5.<PullScroll

功能表单字段、树形选择数据类型的配置详解——JEPLUS快速开发平台

功能表单字段之下拉框.单选框.多选框.树形选择数据类型的配置详解 JEPLUS平台的表单支持有多种不同的数据类型,这些不同的数据类型在展示不同类型的数据时能有很好的效果,今天这篇笔记就讲解一下下拉框.单选框.复选框.树形选择这四种数据类型的配置详解以及效果展示. 一.下拉框 打开表单数据录入界面,打开具体的目标字段的配置信息 第二种打开目标字段的配置信息方法是打开功能配置选项---->打开表单配置---->双击目标字段,即可打开 打开字段的表单配置信息界面,选择数据类型是"下拉框&q

底层战详解使用Java开发Spark程序(DT大数据梦工厂)

Scala开发Spark很多,为什么还要用Java开发原因:1.一般Spark作为数据处理引擎,一般会跟IT其它系统配合,现在业界里面处于霸主地位的是Java,有利于团队的组建,易于移交:2.Scala学习角度讲,比Java难.找Scala的高手比Java难,项目的维护和二次开发比较困难:3.很多人员有Java的基础,确保对Scala不是很熟悉的人可以编写课程中的案例预测:2016年Spark取代Map Reduce,拯救HadoopHadoop+Spark = A winning combat

【详解】嵌入式开发中固件的烧录方式

版本:v1.2 Crifan Li 摘要 本文主要介绍了嵌入式开发过程中,将固件从PC端下载到开发板中的各种方式,主要包括NFS挂载,Nand Flash和Nor Flash,USB,RS232,网卡NIC等方式. 本文提供多种格式供: 在线阅读 HTML HTMLs PDF CHM TXT RTF WEBHELP 下载(7zip压缩包) HTML HTMLs PDF CHM TXT RTF WEBHELP HTML版本的在线地址为: http://www.crifan.com/files/do

Dedesql数据库类详解(二次开发必备教程)(转)

http://www.dedecms.com/help/development/2009/1028/1076.html 织梦DedeCMS的二次开发不仅仅是会写写织梦的标签,会制作织梦的模板.很多时候,我们需要对织梦DedeCMS的数据库进行查询.插入.删除等等之类的操作,进行这一类的操作之前,我们必须知道织梦DedeCMS的数据库类,No牛网整理了织梦天涯版主关于DedeCMS程序的dedesql类常见的用法讲解的文章,希望有朋友用的上. 1.创建数据表 为了让讲解更加的贴合实际,天涯版主创建

详解C语言的main函数

如图所示:#include<stdio.h>这是一个头文件,包含的是C程序运行的C语言的库函数,只有包含了相关的头文件,在程序中才能调用.stdio表示输入输出控制.printf():就是来自这个头文件. int main(int argc ,const char *argv[]){...} :int 表示函数的返回值类型,main是函数名, ()里面的是参数,前面的关键字标示的是参数的类型 ,argc是命令行中的参数的个数,argv[]对应每一个参数. return是一个函数的返回值,当函数