本节要简单实现扫雷游戏
扫雷游戏实现逻辑:
1.在地图上布置10个雷
2.排查雷:如果是雷就炸死,如果不是,就统计周围雷的个数,并显示
3.如果把所有非雷的位置找到,游戏结束
1. 项目文件框架搭建
我需要建立3个文件,一个游戏主体的源文件 test.c ,一个包含游戏内实现某些功能的函数的源文件 game.c ,一个用来声明外部函数和变量的文件 game.h
2. 游戏主体构架搭建
首先我需要一个游戏菜单让玩家选择游玩还是退出,当玩完一轮后不让程序结束继续让玩家选择是否游玩。
这部分与之前的猜数字游戏是相似的。
值得注意的是我注释掉的play函数,这时游戏的内部实现逻辑,游玩游戏的整体操作都是通过调用这个函数引领起来的。
3. paly()函数的实现
3.1 棋盘的定义
首先,扫雷游戏需要一个棋盘存放雷,这个棋盘用数组来定义,在有雷的位置上放1,没有的位置放0,但是在判断周围是否有雷的时候会在棋盘上展示了雷的个数,这时很有可能就产生了歧义,展示出来的数字如果是1的话那下次判断的时候会不会将这里识别成雷,因此我要创建两个棋盘,一个用于存放雷的信息,一个用于展示雷。
第二,为了防止如下图出现的蓝色区域数组越界访问,我可以将棋盘的上下左右都延伸出去,变成绿色棋盘的大小,两个棋盘都如此,方便同时操作它们。
就像这样创建了两个棋盘,因为我将棋盘的上下左右都延伸出去了一格,所以虽然行数和列数都是11,但是实际上这是个 9×9 大小的棋盘。
如果说这么创建棋盘大小的话后期想要更改太过于麻烦,于是我们可以动用头文件进行全局变量的定义
因为在vs的编译器中数组定义不支持用变量,所以使用 #define 定义常变量,用常变量来定义数组的大小。
当然不要忘记引用自己定义的头文件。
3.2 棋盘的初始化和打印
3.2.1 棋盘初始化
棋盘的初始化就是将棋盘中的所有元素变成初始值,因为两个棋盘是一样的尺寸,只是要初始成的内容是不一样的,所以我只需要一个函数 initboard() 只要传不同的参数就可以控制我要初始化哪个棋盘,要将它的每个元素初始化成什么字符。
首先在test.c文件中设定好实现的大概框架,想好要传的参数
接着在game.h头文件中声明函数,设定好形参的类型
最后在game.c文件中实现函数的功能
值得注意的是不要忘记在game.c文件中也要声明头文件,用来获得 ROWS,COLS 的值
3.2.2 棋盘的打印
打印棋盘基本操作与初始化相似,不同在于不需要操作边缘那一圈,同时要打印出来行号和列号,这里只展示一下函数的内容和运行结果。其他步骤比如函数在头文件中的声明不再展示。
3.3 布置雷
刚才的打印只是浅试一下我们的函数是否可以正常运行,实际上要先设置好雷之后再打印棋盘,再让玩家排查雷。
值得注意的是,在编程过程中要写一点测试一点,不要等都写完了,一跑发现跑不通,那就尴尬了,找问题都找不到在哪。
首先在game.h头文件中设置雷数MAX_MINE
布置雷与打印棋盘相同,只需要操作边缘圈之内的数组。随机生成边缘圈之内的坐标,当坐标中没有雷的时候就布置一个雷。
用了rand()函数不要忘记给它设置种子,否则生成的就是伪随机数,在main()函数开始的时候设置种子。
同时,不要忘记引用rand函数和time函数的头文件,我将这些头文件都在 game.h 中引用,这样在其他源文件中用就只用引用一个 game.h 的头文件就够了,不需要再引用其他头文件了,这样极其的方便。
3.4 排查雷
排查雷函数的基本思路是在mine棋盘中判断玩家输入的坐标是否是雷,不是雷就在show棋盘中打印该坐标周围的雷数,是雷就提醒玩家被炸了,游戏结束。
因此排查雷的传参需要两个数组都用到。
首先将排查雷中需要玩家持续输入坐标的框架写出来
再将注释中的内容实现
count_mine函数是用来数玩家选择位置周围有多少雷
这里运用了字符数字减字符0等于这个字符对应的数字例如:'4' - '0' = 4 ,将周围8个格都加起来再减掉8个字符0就得到了周围的雷数。
此时我发现 find_mine 函数只有炸死的时候才能停止,所以再优化一下
在while判断的位置加上一个判断玩家是否获胜的函数,获胜了就不再继续循环下去了
if_win函数的实现
4. 结束
至此游戏已经可以玩了(bushi)
目前的游戏相当反人类,一次只能排查一个坐标,9*9的棋盘玩家要输入71次坐标才有可能获胜,传统的扫雷游戏是有连锁解开那些连着的周围没有雷的坐标的机制的,这是我接下来优化的方向。
5.优化
计划是这样的,如果玩家选择的这个位置周围没有雷,我就将它变成 空格 然后扫描这个位置四周的位置,再以四周的8个位置为中心,重复以下步骤:中心周围没雷就将中心变成空格然后以四周的8个格为新的中心展开,如果中心周围出现了雷就在show棋盘上将中心周围的雷数显示出来。
//没有雷的点位向四周递归展开,直到出现周围有雷的点
static void spread_out(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int row, int col)
{
if (x - 1 >= 1 && y - 1 >= 1) //左上角还在棋盘中
{
if (count_mine(mine, x-1, y-1) == 0) //左上周围没雷
{
show[x - 1][y - 1] = ' '; //没雷就变成空的
spread_out(mine, show, x - 1, y - 1, ROW, COL); //以左上角为中心递归出去
}
else //如果左上角出现雷了,就展示雷数,停止递归
{
show[x - 1][y - 1] = count_mine(mine, x - 1, y - 1) + '0';
}
}
if (x - 1 >= 1) //上在棋盘中
{
if (count_mine(mine, x - 1, y) == 0) //上周围没雷
{
show[x - 1][y] = ' ';
spread_out(mine, show, x - 1, y, ROW, COL);
}
else
{
show[x - 1][y] = count_mine(mine, x - 1, y) + '0';
}
}
if(x-1>=1&&y+1<=col) //右在棋盘中
{
if (count_mine(mine, x - 1, y + 1) == 0) //右上周围没雷
{
show[x - 1][y + 1] = ' ';
spread_out(mine, show, x - 1, y + 1, ROW, COL);
}
else
{
show[x - 1][y + 1] = count_mine(mine, x - 1, y + 1) + '0';
}
}
if (y - 1 >= 1) //左在棋盘中
{
if (count_mine(mine, x, y - 1) == 0) //左周围没雷
{
show[x][y - 1] = ' ';
spread_out(mine, show, x, y - 1, ROW, COL);
}
else
{
show[x][y - 1] = count_mine(mine, x, y - 1) + '0';
}
}
if (y + 1 <= row) //右在棋盘中
{
if (count_mine(mine, x, y + 1) == 0) //右周围没雷
{
show[x][y + 1] = ' ';
spread_out(mine, show, x, y + 1, ROW, COL);
}
else
{
show[x][y + 1] = count_mine(mine, x, y + 1) + '0';
}
}
if (x + 1 <= row && y - 1 >= 1) //左下在棋盘中
{
if (count_mine(mine, x + 1, y - 1) == 0) //左下周围没雷
{
show[x + 1][y - 1] = ' ';
spread_out(mine, show, x + 1, y - 1, ROW, COL);
}
else
{
show[x + 1][y - 1] = count_mine(mine, x + 1, y - 1) + '0';
}
}
if (x + 1 <= row) //下在棋盘中
{
if (count_mine(mine, x + 1, y) == 0) //下周围没雷
{
show[x + 1][y] = ' ';
spread_out(mine, show, x + 1, y, ROW, COL);
}
else
{
show[x + 1][y] = count_mine(mine, x + 1, y) + '0';
}
}
if (x + 1 <= row && y + 1 <= col) //右下在棋盘中
{
if (count_mine(mine, x + 1, y + 1) == 0) //右下周围没雷
{
show[x + 1][y + 1] = ' ';
spread_out(mine, show, x + 1, y + 1, ROW, COL);
}
else
{
show[x + 1][y + 1] = count_mine(mine, x + 1, y + 1) + '0';
}
}
}
好的,到目前为止,听起来很有道理是吧,那你就被骗了。这么多年扫雷都不是这么写的,根本就不是,有时候多想想自己的问题好不好,有没有好好思考,有没有认真敲代码(在说我自己,是我自己没有想明白QAQ)。仔细一想就会发现这种展开方式是不合理的,如果有两个连着的没有雷的点位出现程序就会陷入死循环:
就像这样两个周围没有雷的中心点会不停的互相激发,从而陷入死循环。
所以我们要保证,当过中心的位置不能再被激发,所以添加一个判断这个点是否激发过的判断
然后再把这个判断外边的所有会将show棋盘上的位置变成空格的语句都关掉,我们只需要在这一个位置将show棋盘某位置改成空格,其次是如果这个判断外面如果有相同的语句会让show棋盘变成空格的话会让这里的语句判断出现逻辑错误。
比如排查雷函数的这里
还有spread_out函数的其他位置。
都改好之后我们的spread_out函数就变成了这样
//没有雷的点位向四周递归展开,直到出现周围有雷的点
static void spread_out(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int row, int col)
{
if (show[x][y] == ' ')
{
return 0; //如果这个点被当作中心点被激发过,那就不再操作它
}
else //如果没激发过,能进来这个函数说明它周围没有雷,所以将这个位置变成空格
{
show[x][y] = ' ';
}
if (x - 1 >= 1 && y - 1 >= 1) //左上角还在棋盘中
{
if (count_mine(mine, x-1, y-1) == 0) //左上周围没雷
{
spread_out(mine, show, x - 1, y - 1, ROW, COL); //以左上角为中心递归出去
}
else //如果左上角出现雷了,就展示雷数,停止递归
{
show[x - 1][y - 1] = count_mine(mine, x - 1, y - 1) + '0';
}
}
if (x - 1 >= 1) //上在棋盘中
{
if (count_mine(mine, x - 1, y) == 0) //上周围没雷
{
spread_out(mine, show, x - 1, y, ROW, COL);
}
else
{
show[x - 1][y] = count_mine(mine, x - 1, y) + '0';
}
}
if(x-1>=1&&y+1<=col) //右在棋盘中
{
if (count_mine(mine, x - 1, y + 1) == 0) //右上周围没雷
{
spread_out(mine, show, x - 1, y + 1, ROW, COL);
}
else
{
show[x - 1][y + 1] = count_mine(mine, x - 1, y + 1) + '0';
}
}
if (y - 1 >= 1) //左在棋盘中
{
if (count_mine(mine, x, y - 1) == 0) //左周围没雷
{
spread_out(mine, show, x, y - 1, ROW, COL);
}
else
{
show[x][y - 1] = count_mine(mine, x, y - 1) + '0';
}
}
if (y + 1 <= row) //右在棋盘中
{
if (count_mine(mine, x, y + 1) == 0) //右周围没雷
{
spread_out(mine, show, x, y + 1, ROW, COL);
}
else
{
show[x][y + 1] = count_mine(mine, x, y + 1) + '0';
}
}
if (x + 1 <= row && y - 1 >= 1) //左下在棋盘中
{
if (count_mine(mine, x + 1, y - 1) == 0) //左下周围没雷
{
spread_out(mine, show, x + 1, y - 1, ROW, COL);
}
else
{
show[x + 1][y - 1] = count_mine(mine, x + 1, y - 1) + '0';
}
}
if (x + 1 <= row) //下在棋盘中
{
if (count_mine(mine, x + 1, y) == 0) //下周围没雷
{
spread_out(mine, show, x + 1, y, ROW, COL);
}
else
{
show[x + 1][y] = count_mine(mine, x + 1, y) + '0';
}
}
if (x + 1 <= row && y + 1 <= col) //右下在棋盘中
{
if (count_mine(mine, x + 1, y + 1) == 0) //右下周围没雷
{
spread_out(mine, show, x + 1, y + 1, ROW, COL);
}
else
{
show[x + 1][y + 1] = count_mine(mine, x + 1, y + 1) + '0';
}
}
}
6.结语
都改好之后现在我们来尝试运行一下这个扫雷游戏
ok,现在整个游戏就逐渐完美起来了,当然也许你会发现雷数似乎变少了,没有错!我为了展示一下这个递归函数的效果把雷数从10调成了5,不然雷区开的太慢了实在不方便展示。
现在游戏就完成了绝大部分内容,或许你还想添加,标记雷的功能,或者是,选择游戏难度的功能,这就交给诸位了,继续努力啦朋友们!!!
完整游戏代码: