1.实现方式的确定
扫雷这款游戏相信大家已经了如指掌,这里就不再对其进行进一步的介绍。观察扫雷游戏的特点,我们可以用多个“*”号来表示扫雷的界面,输入坐标后,就将对应位置的“*”号换成检测到的地雷数,如下图:
为了实现这一点,我们可以先创建一个char类型的二维数组(由于这个二维数组的内容会展示在屏幕上,我将其命名为display),并使其包含全部的“*”号。由于在整个游戏过程中,扫雷地图的行和列是不变的,为了避免在后续写各种函数时不停的传参,我们可以先将行和列的长度用如下的方式进行声明,这样我们在函数里面就可以直接使用,同时,也避免了使用变长数组导致在VS2022环境下会报错。
在初始化这个二维数组(display)之前,我们还有一个问题要解决。扫雷要有雷才能扫,那么这个雷要怎么表示呢。很简单,我们可以再创建一个行和列与display一样的二维数组,用’0‘表示空地,用’1‘表示地雷(由于这个数组不会展示出来,我将其命名为inside)。在玩家输入坐标后,就检查数组inside相应位置周围的’1‘的个数(当然,如果数组inside的对应位置的元素是’1‘,就说明踩到地雷了),然后将检查到的地雷个数赋值给数组display的相应位置的元素,那么当我们再次将数组display的内容打印出来时,就可以达到上图的效果。这里说的元素就是指数组内一个值或字符,就如数组 int num[2]={1,4},它有两个元素,一个是1,一个是4。前面提到的操作的具体的代码实现会在下文讲解,我们现在先完成数组的初始化。
因为地雷的位置是随机的,我们先将数组inside的元素全部初始化为’0‘。有一点需要注意的是,我们在检查某一位置的地雷个数时,会把周围8格都“扫描”一遍,但是如果这个位置在边界的位置,那么就会出现“越界”的情况,为了方便完成检测,我们将两个二维数组都往外拓宽一圈,也就是多两行两列,但展示时只输出中间的部分。这样在检测周围地雷数时就不用考虑处于边界的情况,当然,这样做也会有其它的弊端,同样会在下文提及。
需要初始化的二维数组inside和display的元素个数相同,行和列也相同,而且同样都是将内容全部初始化为一种字符,区别仅在于一个全都初始化为’0‘,而另一个则为’*‘那么我们可以定义一个函数turn用于数组的初始化,将’0‘或’*‘作为参数,与这两个二维数组一并传入函数turn,这样就不需要打两段相似的代码,完成代码的简化。具体实现较为简单,不再详细说明。
前面提到display的实际大小要比玩家看到的多两行两列,为了方便后续的操作,我们同样进行声明。
这样可以使我们之后更改行数和列数时直接更改 HANG 和LIE 的声明即可。我们接着创建数组
因为我在使用for循环时习惯用 i 和 j 作为控制循环的变量,所以我习惯将 i 和 j 设为全局变量,这样使用for循环时就不用重复定义,但我不建议大家这么做,很容易出现bug
以下即为turn函数的具体实现,利用简单的循环嵌套完成初始化
使用turn函数时将数组和初始化的内容传入即可,如下
2.定义show函数完成“扫雷图”的输出
确定扫雷游戏的实现方式后,我们就可以着手完成各种需要用到的函数了,之后再在main函数中进行组装。先从最简单的输出开始。
由于每次输入坐标后,输出的部分(玩家看到的扫雷图,即数组display的内容)都会发生变化,为了不重复地写用于输出数组display内容的代码,我们定义一个新函数show,使得将数组传入后即可完成数组内容的输出。
display的实际大小要比玩家看到的多两行两列,那么我们在输出时实际上是从第二行第二列开始到倒数第二行、倒数第二列的,即display[1][1]到display[HANGS-2][LIES-2],我们不难写出相应的代码。如下图,其中的 i 和 j 是全局变量,所以没有定义,之后不再说明。
(记得加空格和回车将*号隔开,这里加了两个空格)
使用时直接将数组输入即可
预计效果大致如下
由上图我们不难发现,这样不利于玩家观察坐标,所以我们最好顺便打印坐标轴,用数字表示即可。同时,为了让玩家将扫雷图和坐标区分开来,我们可以用 | 号和 _ 号进行提示。
横坐标可以放在第一行,这样较为简单,直接用循环打印即可。值得注意的是,如果直接将数字打印上去,当行数和列数为两位数时会发生错位,如图
所以我们将占位符%d改写为%-2d进行左对齐,当数字为两位数时会自动替换右边的空格,这样就可以齐了。纵坐标也是类似的道理,不过纵坐标使用%2d进行右对齐会更便于观察,具体代码如下。
纵坐标在每一行的开头,我们在打印每一行的第一个 * 号之前打印一下纵坐标即可。观察到 i 为1时刚好要打印输出部分的第一行,之后也是同理,我们可以利用 i 打印纵坐标。
最终效果如下
3.定义check函数完成对坐标周围地雷个数的计算
前面我们提到,检查地雷个数的本质就是计算二维数组inside的相应坐标周围有几个’1‘,那么我们可以在check函数中完成计算,然后返回周围的’1‘的个数,当然,坐标对应位置的元素就是’1‘的话就返回踩中地雷的信息,之后再进行下一步的操作。
那么具体要怎么实现呢?其实也不难,就比如当玩家输入坐标5 5 时,对应的元素是 inside[5][5],我们就检测它以及它的周围八格,也就是
inside[5+(-1)][5+(-1)]、inside[5+(-1)][5+0]、inside[5+(-1)][5+1],,
inside[5+0][5+(-1)]、inside[5+0][5+0]、inside[5+0][5+1],
inside[5+1][5+(-1)]、inside[5+1]][5+0]、inside[5+1][5+1],
相信大家不难看出规律,我们可以使用for循环嵌套,并将控制循环的变量 i 和 j 设为-1,终止条件设为 i<=1 和 j<=1, 用整形变量c记录地雷数,再依次与输入的横坐标与总坐标相加,检查数组inside在新坐标下的的元素,如果为’1‘就执行语句c++;如下,为了便于区分,在check函数中我用字符数组num接收传入的数组inside,所以这里的num 就相当于inside
注意!这里如果直接把计算式写进数组的方括号虽然不会报错,但有时会出现各种bug,甚至出现程序意外终止的情况,恕我才疏学浅,不知道具体原因,留给读者探究。
就如我们前面提到的,数组inside在原坐标下的对应的元素要先判断,也就是这里说的inside[5][5]的值,如果是’1‘就让check函数返回-1,表示踩中地雷(由于check函数返回0~8的整数表示的是周围的地雷数,所以用返回-1表示踩中地雷)。所以我们在开始计算地雷数之前先判断一下坐标对应的位置是不是地雷。那么我们就完成了check函数的定义,代码如下
使用时传入数组inside和玩家输入的横坐标和纵坐标即可
4.定义bomb函数实现“埋雷”操作
在开始定义函数之前,我们需要弄清几个细节。一是我们前面说了,数组inside和数组display一样,实际大小比玩家看到的大了一圈,但是对玩家而言这多出来的一圈并没有意义,简而言之就是我们不能把雷埋在玩家视野之外,不然就排不了雷了;二是玩家输入的第一个坐标周围八格不能有地雷,否则可能会出现两种情况,玩家直接踩中地雷,游戏刚开始就结束了,或者玩家输入坐标后只得到一个周围有多少雷的信息,也就是类似下图这样的情况
那么玩家就只能再随便选几个坐标直到可以确定地雷位置,在这个过程中很容易就会踩到雷,所以我们在埋雷的时候避开玩家输入的坐标以及其周围八格,这样就能进一步保证游戏的可玩性。
第三个细节就是注意避免在重复的位置埋雷,这个比较简单,埋雷之前判断一下即可,但是千万不能忽略。
细节弄清楚之后我们就可以开始埋雷了,埋雷的本质就是将数组inside中的一部分‘0’改成‘1’。要实现这个功能,我们可以生成两个大小合适的随机数,分别作为行数和列数,并将数组inside相应位置的‘0’改成‘1’,更改前先判断一下要更改的位置是不是‘0’,同时要判断这个位置是不是在玩家输入的坐标周围,以及有没有超出玩家视野范围,然后记录更改的次数,次数达到后就停止埋雷操作。下面我们开始完成相应的代码。
由于在整个游戏过程地雷的个数都不变,为方便函数使用,我们同样进行声明
为了避免重复地生成随机函数的种子,我们在main函数中使用srand函数,记得包含头文件stdlib.h和time.h即可
在bomb函数中我用数组num接收传入的数组inside,所以这里的num就是inside,整形 h 接收的是横坐标, l 则是纵坐标,即行和列,之后的其它函数也是如此,不再赘述
bomb函数的实现较为简单,只需注意埋雷前判断条件的编写以及记录埋雷个数终止循环即可
5.定义place函数完成对指定坐标对应位置的清理与自动扩展
前面我们提到,玩家输入坐标后,要检查坐标对应位置是否有雷,以及计算周围的雷数(这一步我们以及在check函数中实现),如果该坐标下有雷,就结束游戏,显示地雷位置,如果该坐标下没有雷,我们要把它周围的地雷数打印在该坐标对应的位置上,替换掉原有的 * 号,我将此操作称为清理,特别的,如果周围的地雷数为0,也就是没有雷,我们要将周围八格都清理掉,如果又出现周围没有雷的格子,就把它的周围八格也清理掉,实现自动扩展,预计效果如下
不难看出这些操作都是针对数组display的,我们定义函数place完成这些操作,之后再用show函数进行输出。不过因为place函数完成了这些操作,所以main函数里面用不到check函数,我们同样需要将check函数传递的踩中地雷的信息传递出去,同样返回-1即可,同时虽然这些操作都是针对display的,但我们需要将地雷的位置标出来,所以一样需要将含有地雷位置信息的数组inside传入,用数组num接收数组inside,用数组arr接收数组display,整形 h 和 l 接受玩家输入的横坐标和纵坐标,这样我们就完成了place函数的声明(完整的place函数在这小节的最后)
接下来的第一步肯定是先用check函数进行检查以及计算周围地雷数并接收check的返回值
那么这时候就有三种情况,第一种情况是这个坐标下是雷,check返回-1,按照前面说的,这时我们要将地雷的位置标出来,并传递游戏结束的信息。如何将地雷的位置标出来呢?我们知道数组display和数组inside上的每一格都是一 一对应的,我们可以用双循环嵌套对数组inside的内容一 一进行检测,若inside的某一坐标为地雷,我们可以用相同的坐标定位display的同一位置,这样我们就可以轻松地完成标出地雷的操作了。具体实现如下
剩下的两种情况便是check返回了不同的地雷数,一种是需要特殊处理的‘0’,另一种则是整数1~8后者的处理较为简单,我们先行完成,便于后续自动扩展的思路理解。由于 a 是整形,如果直接将 a 赋值给字符数组arr(也就是传入的数组display)的一个元素的话,实际上是将ASCII码表中整形a的数值对应的符号赋值给字符数组arr中的元素,而在ASCII码表中,字符0对应的是48,1则是49,以此类推,所以我们只需将整形a加上48,即可在赋值给字符数组arr中的元素时将其转化为数值大小相同的字符。如下
掌握这一点后,我们就可以轻松地写出相应的代码
那么当check返回的值为0时我们应该怎么实现自动扩展呢。首先扩展的原理就是清理check返回值为0的位置的周围八格,如果出现新的周围没有雷的格子,就再扩展一次,直到所有新扩展的格子附近都存在地雷,也就是类似下图的效果。
这里又引出了一个需要注意的点,我们知道数组inside和数组display实际要比玩家视野大上一圈,如果我们将处于边界的玩家看不到的格子也清理了,那么我们扩展的区域就会沿着边界不断延伸,将不该清理的位置也清理了,类似下图
原图 标注
所以我们在清理格子之前,要先判断是不是位于玩家视野之内
还需要注意的是在清理格子之前判断一下这一格有没有被清理过了,也就是判断display的相应位置是不是 * 号,这样在之后自动扩展时就不会出现在两个相邻的标记为0的区域死循环地重复清理。第一步的扩展并不难,用类似check函数的方式,即用起始值为-1的for循环嵌套逐一锁定周围的格子并进行操作即可,注意判断条件的编写,如下
这里的for循环就不能再用之前定义的全局变量 i 和 j 了,因为check函数用的也是这两个,check函数跑完后 i 和 j 就变了,这就是将 i 和 j 设为全局变量的弊端。
那么如果出现新的附近没有地雷的格子怎么办呢?我们仔细想想,这时我们是不是要将这个新出现的格子周围八格都清理掉,而我们正在定义的place函数不就是为了这个功能设计的吗?所以我们可以直接将这个新格子的坐标代入place函数,利用函数递归完成进一步的操作。
这样一来,当再次清理出周围没有地雷的格子时,place函数就会再次将其坐标代入并运行,这样我们就完成了自动扩展。至此,我们完成了place函数的定义,加上return语句返回k1即可,下图便是完整的place函数
6.游戏的终止
扫雷游戏的终止方式有两种,一种是踩到地雷游戏失败,这种已在place函数中实现,另一种则是清理了地雷以外的所有区域游戏胜利。我们可以想象一下,当玩家将除了地雷以外的所有区域都清理后,屏幕上剩下的 * 号是不是一定为地雷,而这些 * 号都是由show函数打印的,那么我们将show函数进行加工,使它记录下打印的 * 号个数,并让show函数返回 * 号的个数,如果打印的 * 号个数和地雷数BOMBS相同,就说明玩家清除了地雷以外的所有区域,我们就可以判断出玩家成功了。有了思路后,我们就能轻松地完成show函数的改写,创造结束游戏的条件。
记得更改show函数的返回类型
完成后,我们在main函数中接收show函数的返回值,并作为终止循环的变量即可,具体会在下一节中讲到
7.流程设计与各类函数的使用
完成各类函数的定义后,我们就可以开始逐步完成扫雷游戏的流程设计。首先,我们不能让玩家一运行程序就直接开始游戏,这会让人感到很突然。为此我们可以先设计以个简单的菜单,再让玩家进行选择。我们同样将其设计为函数
为了让游戏能够多次运行,我们将玩家输入的选择(1或0)作为控制循环的变量即可,同时在游戏结束后再次打印菜单供玩家选择。这样就能实现要玩就运行,不玩就退出的效果
接着便是用turn函数完成数组的创建及初始化以及随机函数种子的生成,记得使用turn函数需要传入初始化的内容
我们前面提到,玩家输入的第一个坐标及其附近不能有地雷,否则影响游戏可玩性,所有我们要先让玩家输入要清理的区域的坐标,再用bomb函数进行埋雷,记得使用place函数清理相应区域后用show函数输出,将信息反馈给玩家。这里用system(”cls“)清空输出窗口,使玩家看到的信息更简洁
之后便是让玩家逐步输入坐标并进行排雷,由于不需要特殊的埋雷操作,所以可以写成循环。用k来接受place函数的返回值,如果为-1就输出踩中雷的信息,由于踩中雷后place会将地雷的位置在数组display内标出,我们要用show函数打印出来。用q来接受show函数返回的数,即屏幕上打印的 * 号个数,如果与地雷数相等就传出扫雷成功的信息。记得在玩家输入坐标后用system(”cls“)清空输出窗口。
整体的main函数如下
至此,我们就完成了扫雷游戏的全部内容,如果说法有误欢迎指出,希望我的文章能对你有所帮助,感谢观看。