C语言的简单运用——扫雷(基础篇)

前言

      相信大家小时候在windows游戏菜单里都看到过扫雷,大部分人应该都玩过扫雷。我们在学习了C语言后,也可以自己编写一个C语言的扫雷游戏,让我们来看看如何实现吧!

 


游戏规则介绍

       我们以经典的9*9雷盘,10个雷的布局为例。9*9雷盘即盘面上有9*9=81个格子,10个雷即计算机随机在十个格子上生成雷,如果玩家成功找到并打开非雷的71个格子则获得胜利,如果玩家打开了含雷的格子,则失败。这是游戏的基本规则,接下来是打开非雷格子后出现的数字的含义,打开后数字为n,则代表该格周围的8个格子中存在n个雷,例如

 我们看到,图片正中间的格子数字为2,那么它周围的八个格子中一定有两个格子是含雷格(即图中用旗子标出的两格)。那么如果一个格子周围没有含雷格,则该格显示为空格。


准备

       由于代码较长,我们采用分装不同函数实现不同功能的方式完成,这里我分为两个源文件和一个头文件进行编写

       把所有的函数都在头文件中声明,然后再源文件中包含头文件即可调用函数。我们先来看看库函数头文件"stdio.h"用于调用printf函数和scanf函数,"stdlib.h"用于调用srand函数,rand函数(用于生成随机数),system函数(用于优化界面,使用方法可以在我的三子棋<http://t.csdn.cn/8I9Op>界面优化方法中找到),"time.h"用于调用time函数(用于生成随机数)。接下来看宏常量,由于我以9*9雷盘10雷布局为例,故“COL”和“ROW”为9,“MINE”为雷数10,而“COLS”和“ROWS”为雷盘宽度加2,即11,具体作用在下文中详细解释。

一、游戏菜单

1.菜单界面

     菜单界面的设计可以仿照三子棋菜单,用多行printf函数打印出一个菜单,如图 

2.开始或退出游戏 

       这里我们分装一个函数,使玩家选择开始或退出游戏,为了在一局游戏结束后玩家能在不重启程序的情况下再次开始游戏,我们使用一个循环语句使玩家能重复游玩。

      

      对应到游戏菜单,我们可以将玩家输入的值分成三种,若为“1”,则开始游戏,进入game函数(即游戏本体);若为“2”,则跳出循环,退出游戏;若不为“1”也不为“2”,则输入错误,让玩家重新输入。

二、游戏内容

1.雷盘设计

设计思路

       首先我们来分析雷池(以9*9雷盘10雷为例),我们可以把雷池分为两个部分,第一部分是计算机随机生成的一个确定的雷池,这部分是玩家看不到的,相当于真实埋在地里的雷;第二部分则是玩家所看到的雷盘,会有数字提示其周围的含雷格数。那么在这里,我们可以定义两个二维数组,分别对应雷池的两个部分,如图中的数组mine和数组show

        这里需要注意的是,9*9的雷盘,我创建的数组大小为11*11(比雷盘多2行2列),即图中“COLS”和“ROWS”的值为11,这涉及到后期的扫雷判断,这里先留下一个小小的悬念

初始化雷池

       有了设计思路后,我们就可以开始制作了,首先我们需要做的就是初始化雷池。因为我们要初始化的是两个大小相同的数组,所以我们只需要分装一个init函数,接收所需要初始化的数组和初始化内容,即可分别初始化这两个数组

       由于mine数组代表的是雷的真实位置,那么这个数组里就会存在两种元素,一种是代表非雷的元素,另一种则是代表含雷的元素,这里我用字符'0'代表非雷,字符'1'代表含雷(后期判断方便),那么我只要将mine数组和字符'0'传入init函数即可完成初始化。同理,show数组为中的元素为玩家所看到的字符,我用'*'初始化show数组,那么游戏开始时玩家看到的即为9*9个'*'。

打印雷池

       初始化后我们要做的就是将游戏画面展示给玩家,由于mine数组为雷的具体位置,当然不能展示给玩家,所以我们只需要打印show数组即可,打印思路也很简单,使用嵌套循环打印数组中的每个元素即可(注意:由于我创建的数组大小为11*11,而雷盘大小为9*9,故应打印的元素下标范围应该在1~9之间)

void print_board(char arr[COLS][ROWS])
{
	int i = 0;
	int j = 0;
	int col = COL;
	int row = ROW;
	int cols = COLS;
	int rows = ROWS;
	for (j = 1; j < (rows + 1); j++)
	{
		if (j != rows)
		{
			printf("****");
		}
		else
		{
			printf("*\n");
		}
	}
	printf("|");
	for (j = 0; j < (row + 1); j++)
	{
		printf(" %d |", j);
	}
	printf("\n");
	for (i = 1; i < (col + 1); i++)
	{
		for (j = 1; j < (rows + 1); j++)
		{
			if (j != rows)
			{
				printf("|---");
			}
			else
			{
				printf("|\n");
			}
		}
		printf("| %d |", i);
		for (j = 1; j < (row + 1); j++)
		{
			printf(" %c |", arr[i][j]);
		}
		printf("\n");
	}
	for (j = 1; j < (rows + 1); j++)
	{
		if (j != rows)
		{
			printf("****");
		}
		else
		{
			printf("*\n");
		}
	}
}

       这里我用print_board函数实现打印,并优化了界面,打印效果如下(最左和最上的数列用于方便玩家确定坐标)

布雷

        布雷的设计思路很简单,可以使用srand函数、rand函数和time函数随机生成布雷坐标,将对应坐标的mine数组元素替换为雷(字符'1')即可,如此处的set_mine函数

void set_mine(char mine[COLS][ROWS])
{
	int col = COL;
	int row = ROW;
	int Mine = MINE;
	srand((unsigned int)time(NULL));
	int i = 0;
	while (i < Mine)
	{
		int x = rand() % col + 1;
		int y = rand() % row + 1;
		if (mine[x][y] == '1')
		{
			continue;
		}
		if (mine[x][y] != '1')
		{
			mine[x][y] = '1';
		}
		i++;
	}
}

       注意,由于雷盘大小为9*9,我们需要在mine数组正中间的9*9个元素中布雷(即边缘不布雷),那么布雷坐标范围就限制在(1,1)到(9,9)之间,上方代码生成的x,y的范围正好在1~9之间。

2.扫雷过程

       布雷完成后,我们就要进入扫雷步骤了。我这里分装了一个find函数来实现。

首先,我们需要让玩家输入需要打开的格子的坐标

然后我们需要判断输入的坐标是否合法,即是否在1~9范围之间

扫雷时分成两种情况,第一是所选坐标为含雷格,那么我们直接判断游戏结束即可

若为非雷格,此时我们需要统计该格周围八格中含雷格的数量,并将该格对应的数组元素替换为该数字,那么这时候用字符'1'和'0'分别代表含雷格和非雷格的好处就体现出来了,我们来看这个计算方法

首先判断show数组中该坐标对应的元素是否为'*',即是否已经打开,若未打开则进入计算。由于含雷格和非雷格在mine数组中分别以'1'和'0'表示,那么我们可以将该坐标周围八格在mine数组中对应的元素减去一个字符'0',那么字符'0'减去字符'0'结果为0,字符'1'减去字符'0'结果为1,此时我们把这些减法的结果全部加起来,即为该格周围含雷格的个数,再将该数字加上字符'0',即可得到该数字对应的字符(这里解释一下,10以内的整型数字n加上字符'0'得到的结果为字符'n')。

接下来解释一下前面的悬念,为什么我们创建的数组大小为11*11而不是9*9。当我们统计指定格子周围八格的含雷格个数时,周围八格的坐标是否应该对应数组中的元素?那么如果我们创建的数组大小为9*9,当我们判断雷盘边缘格子的时候,会出现数组越界。所以我们创建11*11大小的数组,并在初始化时将雷盘边界外的数组元素初始化为'0',即非雷,这样在统计雷盘边缘格的周围含雷格时就非常方便了。

3.判断胜利

       判断胜利的思路是统计打开格子的个数,例如,一个9*9的雷盘共有81格,其中包含10个含雷格,那么我们只需要是否已经打开了71(81-10)格即可。

这里我通过重复统计雷盘上的'*'的个数实现,这个思路就比较简单了。

4.游戏内容拼接

       到这里,扫雷的基本原理我们就已经实现了,只需要将各个功能的函数拼接起来并放入game函数,就可以开始一局紧张刺激的扫雷了!

三、测试技巧

       对于初学者来说,或者说对所有程序员来说,想一次性就写出成功的代码几乎不可能(除非代码过于简单)。在编写代码的过程中我们需要一段一段进行测试,这里我分享两点我测试扫雷时发现的小技巧。

1.打印雷池

       仔细的小伙伴会发现,我的game函数中屏蔽了一段print_board函数,传入的是mine数组,这其实相当于直接将雷池分布打印出来,即开了透视,这可以让我们在测试的时候更轻易地找出bug。

2.修改雷数 

       在我们测试判断胜利的代码时,我们总需要完成一局游戏,而这意味着我们需要找到71个非雷格,这就增加了我们的工作量,这时我们可以把雷数改为80(总格数减一),这就大大缩小了我们的工作量。

 


结束语

       那么关于扫雷的基本原理的讲解就到此为止了,希望我的思路可以帮助各位初学者完成自己的扫雷,也欢迎各位大佬指出我的不足。关于扫雷的其它拓展功能,我会再写一篇文章详细介绍。