用C语言写一个2048小游戏(用VS2013实现)

游戏简介

执行2048程序,进入游戏后屏幕打印如下图所示:
在这里插入图片描述
玩家通过udlrq来控制游戏:

u ————>> 上
d ————>> 下
l ————>> 左
r ————>> 右
q ————>> 退出 

最高分数用来记录游戏中的最大数,当最高分数达到2048的时候,屏幕显示“You Win!”,这个时候玩家可以通过“q”来退出游戏。如下图所示,我将玩家赢的条件设置为32.
在这里插入图片描述
当界面全部被数字填满且不可移动时如果还没有出现2048,则代表玩家输了,这个时候屏幕显示“Game Over!”,玩家同样可以通过“q”来退出游戏,如下图所示:
在这里插入图片描述

代码分析

1.建立3个文件,分别是game.c、game.h、main.c
game.c用来放一些自定义的函数;game.h用于放函数声明等内容;main.c用于放主函数,组织整个游戏框架。
在程序中,我们用一个NxN的二维数组gameData来存放游戏数据,0代表位置为空。我们用一个一维数组seedList来存放要随机生成的游戏数据,其中包含8个2和2个4,代表2的概率为80%,4的概率为20%。
2、在game.h中中声明如下一些函数:

/*
 * 函数名:initGame
 * 功能:对游戏界面数据进行初始化,并产生两个随机数
 * 参数:需要初始化的游戏数据
 * 返回值:无
*/
void initGame(int data[N][N]);

/*
 * 函数名:showGame
 * 功能:显示游戏界面
 * 参数:需要显示的游戏数据
 * 返回值:无
*/
void showGame(int data[N][N]);

/*
 * 函数名:getRand
 * 功能:在游戏数据中产生一个随机数,2的概率为80%,4的概率为20%
 * 参数:需要产生随机数的游戏数据
 * 返回值:无
*/
void getRand(int data[N][N]);

/*
 * 函数名:checkGameOver
 * 功能:检查游戏是否结束
 * 参数:需要检查的游戏数据
 * 返回值:游戏结束返回1,没有结束返回0
*/
int checkGameOver(int data[N][N]);

/*
 * 函数名:exitGame
 * 功能:退出游戏
 * 参数:无
 * 返回值:无
 */
void exitGame(void);

/*
 * 函数名:maxScore
 * 功能:获取游戏数据中的最大数
 * 参数:需要查找的游戏数据
 * 返回值:游戏数据中的最大值
*/
int maxScore(int data[N][N]);

/*
 * 函数名:moveUp
 * 功能:把游戏数据上移
 * 参数:需要上移的游戏数据
 * 返回值:无
 */
void moveUp(int data[N][N]);

/*
* 函数名:moveDown
* 功能:把游戏数据下移
* 参数:需要下移的游戏数据
* 返回值:无
*/
void moveDown(int data[N][N]);

/*
* 函数名:moveLeft
* 功能:把游戏数据左移
* 参数:需要左移的游戏数据
* 返回值:无
*/
void moveLeft(int data[N][N]);

/*
* 函数名:moveRight
* 功能:把游戏数据右移
* 参数:需要右移的游戏数据
* 返回值:无
*/
void moveRight(int data[N][N]);

/*
 * 函数名:getInput
 * 功能:获取用户的输入
 * 参数:无
 * 返回值:返回用户输入的按键
*/
int getInput(void);

/*
 * 函数名:checkGameWin
 * 功能:判断玩家是否赢得比赛
 * 参数:游戏数据中的最大值
 * 玩家赢则返回1,否则返回0
*/
int checkGameWin(int maxScore);

3、对moveUp()函数的分析
因为其他几个函数的逻辑都非常简单,看我的源代码就可以很容易理解,所以不必详细分析。2048小游戏主要的逻辑难点在上、下、左、右四个移动函数上,而这四个移动函数其实是一个思想,因此我们只需分析moveUp()函数。
moveUp()函数的代码如下:

/*
* 函数名:moveUp
* 功能:把游戏数据上移
* 参数:需要上移的游戏数据
* 返回值:无
*/
void moveUp(int data[N][N])
{
	int x = 0, y = 0;
	int idx;
	int isChange = 0;  //可移动标记位
	//先累加
	for (y = 0; y < N; ++y)
	{
		for (x = 0; x < N; ++x)
		{
			if (data[x][y] == 0)
			{
				continue;
			}
			//判断是否可加,能加则加
			for (idx = x + 1; idx <= N - 1; ++idx)
			{
				if (data[idx][y] == 0)
				{
					continue;
				}
				else if (data[idx][y] != data[x][y])
				{
					break;
				}
				else
				{
					data[x][y] += data[idx][y];//能加则加
					data[idx][y] = 0;//加完之后将其置0以便于不影响下次判断,置0后下次就不会判断改位了
					isChange = 1;
					break;
				}
			}
		}
	}

	//累加后移动
	for (y = 0; y < N; ++y)
	{
		for (x = 1; x < N; ++x)
		{
			if (data[x][y] == 0)
			{
				continue;
			}
			idx = x - 1;//从x-1位置开始查
			while (data[idx][y] == 0 && idx >= 0)//一直到非零或者超出范围结束
			{
				--idx;
			}
			if (data[idx + 1][y] == 0)
			{
				data[idx + 1][y] = data[x][y];
				data[x][y] = 0;
				isChange = 1;
			}
		}
	}

	//成功移动之后需要随机产生一个数
	if(isChange == 1)
	{
		getRand(data);
	}
}

moveUp()函数主要由三部分来实现:先累加、累加后移动、成功移动之后需要随机产生一个数。
(1)先累加

for (y = 0; y < N; ++y)
	{
		for (x = 0; x < N; ++x)
		{
			if (data[x][y] == 0)//如果行元素是零则跳过该元素
			{
				continue;
			}
			//判断是否可加,能加则加
			for (idx = x + 1; idx <= N - 1; ++idx)
			{
				if (data[idx][y] == 0)
				{
					continue;
				}
				else if (data[idx][y] != data[x][y])
				{
					break;
				}
				else
				{
					data[x][y] += data[idx][y];//能加则加
					data[idx][y] = 0;//加完之后将其置0以便于不影响下次判断,置0后下次就不会判断改位了
					isChange = 1;
					break;
				}
			}
		}
	}

在这一部分中,我们先固定列不变,对行进行累加。比如说y=0的时候相当于固定累加列为第一列,这个时候再固定行通过行遍历来进行累加。如果行元素是零则跳过该元素,如果行元素非零则需要判断是否可加。比如说第一个行元素非零(x=0),判断是否可加的时候需要从该元素的下一个元素(x=1)开始判断,如果下面的元素为零则跳过,继续往下判断,直到遇到非零元素或者超出范围停止。如果超出范围则说明下面的元素都是零,不可加。如果遇到非零元素,判断和原来的元素(x=0)是否相等,如果不相等表示不可加,如果相等(比如说x=2的位置),则将(x=0)的位置赋值为累加和,另一个位置(x=2)赋值为零,以便于不影响下次判断,置0后下次就不会判断改位了。将(x=1)这个位置累加完之后,将(x=1, x=2, x=3 … x=N-2)的位置用上面的方法依次累加。
在第一列累加完成后,将(y=1, y=2, y=3… y=N-2)依次用上面的方法进行累加。
累加示意图:
在这里插入图片描述
图中(1)是上移之前的数据,在y=0时(固定第一列,对第一列进行累加),通过对x=0遍历累加得到(2),对x=1和x=2遍历累加得到(3)和(4)。在y=1时(固定第二列,对第二列进行累加),通过对x=0遍历累加得到(5),通过对x=1和x=2遍历累加得到(6)和(7)。同样的方法,经过多次迭代累加得到(8)。

(2)累加后移动

//累加后移动
	for (y = 0; y < N; ++y)
	{
		for (x = 1; x < N; ++x)
		{
			if (data[x][y] == 0)
			{
				continue;
			}
			idx = x - 1;//从x-1位置开始查
			while (data[idx][y] == 0 && idx >= 0)//一直到非零或者超出范围结束
			{
				--idx;
			}
			if (data[idx + 1][y] == 0)
			{
				data[idx + 1][y] = data[x][y];
				data[x][y] = 0;
				isChange = 1;
			}
		}
	}

这部分和上以部分相似。先固定列,对行进行遍历。从第二行(x=1)开始,如果数据为零则跳过(不需要移动),如果不为零则往上找,一直到找到非零数据或者超出范围为止(程序中用idx来记录停止的位置)。最后对停止位置的下一个位置(idx+1)判断,如果非零则不需要移动,如果为零则将(idx+1)位置赋值为打算移动的位置的值,同时将打算移动的位置赋零。
移动示意图:
在这里插入图片描述

(3)成功移动之后需要随机产生一个数

//成功移动之后需要随机产生一个数
	if(isChange == 1)
	{
		getRand(data);
	}

在第一步和第二步中,我们用isChange变量来标记是否移动了。当玩家的操作使得累加成功执行或移动成功执行时,就说明操作使得移动成功了,这时isChange=1,程序要随机产生一个数。否则代表没有移动,isChange=0,程序不会随机产生一个数。

对代码的一些优化

如下面代码所示,在getInput()函数中,一开始采用的是ch = getchar()来获取玩家输入的字符,这样做首先是用户体验不太好,每次都需要用户输入回车才会做出反应;除此之外,当玩家赢了或者输了的时候输入“q”进行退出的时候,还需要考虑回车被当做玩家输入的字符被读取的情况(需要在showGame()函数中添加fflush(stdio)来将缓冲区清空一下)。

/*
* 函数名:getInput
* 功能:获取用户的输入
* 参数:无
* 返回值:返回用户输入的按键
*/
int getInput(void)
{
	char ch;
	int key;
	ch = getchar();
	switch (ch)
	{
	case 'u': key = UP; break;
	case 'd': key = DOWN; break;
	case 'l': key = LEFT; break;
	case 'r': key = RIGHT; break;
	case 'q': key = EXIT; break;
	default: key = OTHER; break;
	}
	return key;
}

后来我做了如下改进,将ch = getchar()改为了ch = _getch(),就这样的一个简单改进,玩家就可以不用考虑输入回车的问题,同时回车被误读入的问题也解决了。
在windows下的编译器,如果支持头文件conio.h,那么就可以使用_getch()函数,来实现不按回车就输入的效果。
源代码链接:https://github.com/xiao-hao-hao/2048

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值