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(总格数减一),这就大大缩小了我们的工作量。
结束语
那么关于扫雷的基本原理的讲解就到此为止了,希望我的思路可以帮助各位初学者完成自己的扫雷,也欢迎各位大佬指出我的不足。关于扫雷的其它拓展功能,我会再写一篇文章详细介绍。