前言
本文章源代码完全公开
在上篇文章中我们对扫雷游戏的基础版本已经有了一个详细且全面的了解,但是基础版本的功能十分有限,游玩体验也不够好,因此,我们来拓展它。
一、拓展一:展开一片
有玩过扫雷的人都知道,当我们的鼠标点击到四周没有地雷的格子时会一下子展开一大片无地雷的空白区域,效果如下:
看起来十分的炫酷,并且这种展开一片的效果也可以避免我们游玩自制扫雷程序的时候的大量重复操作,大大的提升了游戏体验。问题来了,怎么实现这种效果呢?
展开条件
1.只有当我们输入的坐标位置四周都没有雷(周围8格都是‘0’)时才触发展开效果。
2.已经展开过的坐标不能重复展开。
其实,这样看,展开效果的触发条件和限制条件就很明确了,为了程序的可读性,我们把这个实现函数展开效果封装在一个函数spread里面,显然它不需要返回值,需要传递两个棋盘,和横纵坐标
函数定义如下
void Spread(char mine[ROWS][LINES], char show[ROWS][LINES], int x, int y)
{
}
代码实现
由一及彼,这需要用到递归思想,同学们第一次见,可能一开始会有些理解困难,不过相信自己,我后面也会单独出一篇文章来介绍函数递归的
选定一个坐标,如果这个为止不是雷的话,我们就用到Spread这个函数,且看我一步一步分析:
先获取该点周围雷的数量,若有雷,不展开,改下展示棋盘就行
void Spread(char mine[ROWS][LINES], char show[ROWS][LINES], int x, int y)
{
int count = GetMindCount(mine, x, y);
if (!count)
{
// …
}
else show[x][y] = count + ‘0’;
}
如果周围为零,那么该点自身就可以设置为空字符,周围八个位置,想想怎么遍历一遍?
相信循环是很多人的第一想法,关键是怎么循环?请注意,我们对数组一定要有一个行列的意识,所谓循环,不过是行跑一遍,列跑一遍,向左向上,相应坐标减一,向右向下,坐标加一,于是我们进一步完善:
void Spread(char mine[ROWS][LINES], char show[ROWS][LINES], int x, int y)
{
int count = GetMindCount(mine, x, y);
if (!count)
{
show[x][y] = ’ ';
for (int i = x - 1 ; i <= x + 1 ; i++){
for (int j = y - 1 ; j <= y + 1 ; j++){
// …
}
}
}
else show[x][y] = count + ‘0’;
}
可是,这样循环3 * 3,我们自身那一个点也包括进去了,这是我们不愿意的,怎么处理?
很简单,加个if语句判断就行,如果新坐标字符值为‘*’的话,那就Spread,除此之外,我们也要考虑坐标不能越界,当我们选定坐标在棋盘内的时候,才能展开,也就是说,当行或者列为10 或 0(数组大小为11,下标最大值为10)的时候,就不能再展开了,也就是说,当x 和 y小于等于9 或者 大于等于1的时候,那都可以拓展:
void Spread(char mine[ROWS][LINES], char show[ROWS][LINES], int x, int y)
{
int count = GetMindCount(mine, x, y);
if (!count)
{
show[x][y] = ’ ';
for (int i = x - 1 ; i <= x + 1 ; i++){
for (int j = y - 1 ; j <= y + 1 ; j++){
if (show[x][y] = ‘*’ && x >= 1 && x <= 9 && y >= 1 && y <= 9);
Spread(mine, show, i, j);
}
}
}
else show[x][y] = count + ‘0’;
}
然后就是打印棋盘了
二、扩展二:标记地雷
程序如何结束?
事实上,这个拓展跟上面是相辅相成的,因为我们原先的判断游戏胜利方式是排查了多少非雷,而使用展开后,这种判断方式就作废了,所以,我们也来想想怎么实现地雷的标记吧
穷则变,变则通,通则久!
这不困难,既然排查了多少个雷,行不通,那我们就以还剩下多少个雷为条件控制循环
即show棋盘上还有刚好* 和 @(标记符号)两者加起来刚好等于地雷数的时候,判定为游戏胜利
代码实现
这个代码逻辑相当简单,标记的时候,if判断是否是正确待标记点’ * ‘,取消标记的时候,if判断是否是正确待取消标记点’ @ ',如果非正确输入提示一下即可
void Mark(char show[ROWS][COLS], int x, int y)
{
if (show[x][y] == ‘*’) {
show[x][y] = ‘@’;
DisplayBoard(show, ROW, COL);
}
else printf(“该点不该标记,请重新选点\n”);
}
DelMark取消标记函数同理,不再赘述
三、模块整合(铠甲合体!)
前文里,我们提到了展开函数和标记取消标记函数,我们现在要组合它们
请注意,这个思想及其重要!我一开始就说C语言是一门模块化、组织化的语言
因此,化大为小,分而治之是C语言的核心思想!!!
这是我test.c文件中的主函数,是不是很惊讶,但这就是C语言最美的体现,main函数能简则简
system(“…”);是用来处理控制台的名字,能一眼给人看出来我们这个程序是用来做什么的
我们再看test函数,同基础版本一样,采用switch形式来决定游戏是否游玩,加了时间戳改变随机数种子,为的是每次藏雷位置的不同
这时候,我们再看game函数,来看看游戏的核心逻辑
加油,我们越来越具象了!
可以看出来,一开始是先初始化两个棋盘,在布置雷,接下来就是找雷了
其他都很好理解,就是这个找雷比较抽象,分为好几种可能,我们来研究它
找雷,首先肯定是循环找雷,我们前面说循环条件是场上的* 和 @两符号数大于雷的数量,这样当循环结束的时候,就是成功找到所有雷的时候,因为万一之前踩雷了,那早就输了,退出
所以,我们用star来记录* 和 @的数量,并且在这里输入要操作的坐标
选好坐标后,我们有三种操作,分别是排雷、标记、取消标记,我们把它封装为一个函数Choose
至于为什么要考虑返回,来决定是否break,这是因为万一踩雷,我们不需要考虑循环了,直接判定为游戏失败
可这样看,我们对Choose函数感觉还是抽象,没事,我继续给你们讲解
如果执行后两个操作,那我们返回回去还是正常循环,因此,ret默认为true
而排雷,若我们幸运的查找到了非雷,就还是返回true,反之返回false
但是这个TestMine函数还是有点抽象,不急,我们再看
到了这里,就比较直接看出思路了
是雷,爆,结束游戏!
不是,那就展开(可以的前提下)
什么,你说Spread函数还是有点抽象,好!再看!
你会发现,这跟我们前面说的好像不太一样
事实上,因为各个模块之间也不是相互独立的,我们这里的Spread是考虑了其他函数的版本
并且,我还做了小优化
比如,之前采用双重for循环,我在这里直接用两个数组辅助,成功改为了一层for循环,这样不管从效率还是美观层面上都更进一步了
至于我们这边的临界条件等递归层面的思考,请不要紧张,我后期再详细介绍
我保证这是最后一次有深到浅了,真的!
我们再看这个GetMineCount函数,大家可以想一下,怎么可以获取已知点周围的雷数?
我们考虑到雷与非雷是以’0’ 、 '1’显示的,而1 = ‘1’ - ‘0’,是的,靠差
遍历周围所有点,所有字符与字符0之差的和即为所求雷数
这次真没了,真的,大家看完上面一连串分析,此时此刻是什么感受呢,哈哈!
由浅入深,由深又到浅,来去自如!
至于几个小函数,比如说:设置雷函数SetMine、展示棋盘函数DisplayBoard、初始化棋盘函数InitBoard等,就交由大家自己解决了~
总结
行至中江,举帆!
感觉这个进阶篇写的比基础篇还好,可能是更用心的缘故吧,继续加油!