学完分支和循环一脸茫然?我写了个扫雷,把它们全串起来了

前言

上一篇写完分支和循环,我在结尾留了个点:那些 if、switch、while、break、continue、goto 单独学的时候都挺好懂,可它们凑一块儿能干嘛?

所以这篇来了。

Windows 控制台版扫雷。 9×9 的棋盘,输入坐标翻格子,踩雷就死,翻完就赢,还能选难度 (雷的数量喵) 。

先说清楚这是个很初阶的版本,免得看到一半骂我喵 :这不是 Windows 自带那个扫雷。没有鼠标,不能插旗,点开一片空白也不会自动展开,第一下就可能踩雷。但它是我从空白文件一个字一个字敲出来的,能跑,能输能赢------对我来说已经很有成就感了(当然后续会继续改进)。

很简单的菜单页...


一、先想明白扫雷

  1. 棋盘里随机藏一堆雷,玩家看不见。
  2. 你输入一个坐标,翻开那个格子。
  3. 是雷就结束;不是雷,就告诉你周围八个格子里有几颗雷。
  4. 所有没雷的格子都翻开了,你就赢了。

规则就这么点。顺手查了查游戏背景(因为我很喜欢玩扫雷呢),扫雷是微软两位工程师 Curt Johnson 和 Robert Donner 在 1990 年做的,1992 年跟着 Windows 3.1 一起发出去,把系统里原来的黑白棋给挤走了。记得我小学电脑课就偷偷玩扫雷 ,现在自己写一遍,感觉还挺爽。

来源:百度百科「扫雷」词条。

二、两张棋盘,but whay?

这是我写扫雷之前很久没有想到的地方。

我一开始想,一张二维数组不就装下了吗?后来才反应过来------程序知道雷在哪,玩家不能知道。 只有一张表的话,每次打印都得判断"这格能不能给他看"。想想就头大喵 。

所以是两个数组:

c 复制代码
char mine[ROWS][COLS];   // 后台:'0' 不是雷,'1' 是雷
char show[ROWS][COLS];   // 前台:一开始全是 '*',翻开后变成周围雷数

你可以把它想成上下两张纸:

markdown 复制代码
玩家看到的 show              程序保存的 mine

  *   *   *   *               0   0   1   0
  *   *   *   *               0   0   0   0
  *   2   *   *               1   0   0   0
  *   *   *   *               0   0   0   1

平时只给你看上面那张。只有两种时候才会把下面那张翻出来:你踩雷了 (让你死个明白喵) ,或者你赢了(让你看看雷都藏哪)。

运行扫雷游戏界面

三、棋盘应是 9×9,数组我却开了 11×11

这个小技巧是我这次学到的最聪明的东西 (好像让我开智了一样) ,也是我第一次体会到"多开点空间换代码简单"是什么感觉。

c 复制代码
#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2

数组实际 11×11,但你能输入的坐标还是只有 1~9。第 0 行、第 10 行、第 0 列、第 10 列都不是游戏区,全填 '0'。

橙色是辅助格(永远是 '0'),蓝色是真正的 9×9 游戏区域。

为什么要多一圈? 因为数一个格子周围的雷,要访问它周围 8 个位置。你选的是角落 (1,1),代码照样会去访问 (0,0)。多出来这一圈,保证了这些访问永远落在数组里,不会越界。

代价是几十个 char。好处是不用写八个 if 去判断"我是不是在边缘"。

(太划算了,属于那种"知道之前觉得很麻烦,知道之后再也回不去"的东西了喵)

四、难度选择是我自己加的 (顺便打了自己的脸喵)

说实话基础扫雷不一定要有难度选项。我加它的理由特别朴素------上一篇刚学完 switch,我想找个地方再用一次,就为了练习了。

三档只改雷数,棋盘大小不变 (懒了) :

难度 雷数 要翻开多少格才赢
Easy 10 71
medium 15 66
hard 20 61

那个"要翻开多少格"就是 9 × 9 - 雷数。Easy 是 81 减 10 等于 71,翻完这 71 格就赢。后面判断胜负用的就是这个。

然后是我打自己脸的地方:

上一篇讲 goto 的时候我写的是"能不用就别用,跳来跳去难维护"。然后这次......我用了,还用了两次:

c 复制代码
again:
    printf("请输入0或1或2:>");
    if (scanf("%d", &n) != 1)
    {
        printf("输入不是数字喵!只能输入0或1或2!\n");
        while (getchar() != '\n');
        goto again;
    }

原因是这里的场景就是"输错了,回去重输"。用 again: 加 goto 写起来最短,读起来也最清楚,比包一层 while 再配标志变量干净多了 (说白了还是懒) 。不是不能用,是不该拿它去搭主逻辑。 像这种"错了重来"的小范围回跳,反而最直白。

其次是这段一开始其实我还忘了做防止输入字母的防护 (scanf输入的是int类型),给别人玩出来bug了,后来紧急修复了,所以这一块儿也是为了方便,紧急写的 (流汗了喵)

c 复制代码
while (getchar() != '\n');

清理字母还是太好用了喵! ("输入不是数字喵!只能输入0或1或2!\n")

五、数周围有几颗雷 (绕圈圈来了)

就是这个:

c 复制代码
int AroundBoob(char mine[ROWS][COLS], int x, int y)
{
    return mine[x - 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1]
         + mine[x + 1][y] + mine[x + 1][y + 1] + mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0';
}

八个方向全加一遍,最后减 8 * '0'。

第一次看到末尾那个 - 8 * '0' 我是懵的,什么鬼。后来明白:我数组里存的是字符 '0' 和 '1',不是数字 0 和 1。字符 '0' 的 ASCII 是 48,'1' 是 49。

八个格子直接加,就算一个雷都没有,结果也是 48 × 8 = 384,不是 0。

所以每个格子都要减掉一个 '0':

  • '1' - '0' = 49 - 48 = 1(有一个雷)
  • '0' - '0' = 48 - 48 = 0(没雷)

八个的 '0' 一次性减掉,就是 - 8 * '0'。剩下的正好是雷数。

想通之后我还挺兴奋的喵 。字符和数字之间就差一个 '0'。 后面把数字放回棋盘给玩家看,也是同一个套路:

c 复制代码
show[x][y] = num + '0';   // 数字 3 变成字符 '3'

顺带说一句,这段代码能这么"粗暴"地直接访问八个方向而不用担心越界,靠的就是第三节那一圈辅助格。要是数组只有 9×9,这里得写八个 if 判断边界,光想想就烦。

深色是当前翻开的格子,浅色是要统计的 8 个邻居。

六、布雷:随机 + 判重复

c 复制代码
int Boob = count;
while (Boob != 0)
{
    int x = rand() % row + 1;
    int y = rand() % col + 1;

    if (board[x][y] == '0')
    {
        board[x][y] = '1';
        Boob--;
    }
}

思路就是摇号:随机摇个坐标,那格还是 '0'(没有雷喵) 就放雷,待放数量减一;摇到重复的就作废重摇。

有两个地方很重要喵:

** 首先+ 1 不能漏。** rand() % 9 出来的是 08,加 1 才是你能用的 1 9。少了它+ 1,雷就跑到辅助格子里去了喵,棋盘上就一个雷都没有了。

判重。 随机可能是会重复的。不判断的话 Boob-- 照样执行,表面上放了 10 颗,实际棋盘上可能只有 7 颗。这个 bug 特别阴------程序不报错,只是难度悄悄变简单了,你得自己调试一遍才发现喵。

另外 srand((unsigned int)time(NULL)) 只在 main 开头调一次。rand() 是伪随机,不播种的话每次运行都是同一串数,雷的位置永远一样,玩两局就没意思了。

来源:rand / srand 用法参考 cppreference。

七、清屏这件小事,得谢谢另外一位博主 (bug也是他玩出来的,谢谢喵)

我最早的版本没清屏,每翻一次格子就在下面再打一张棋盘。终端里堆一长串历史,翻三次你就不知道自己在哪了 体验非常糟糕了喵 。

后来看到 CSDN 上黑暗的丹 写的《从"一碰就碎"到"铜墙铁壁":大一新生的 C 语言扫雷实战复盘》,里面用了清屏,我照着试了下:

c 复制代码
void DisPlayBoard(char board[ROWS][COLS], int row, int col)
{
    system("cls");
    /* 然后才打印当前棋盘 */
}

加上之后画面就变成"同一块棋盘在原地更新"了,观感一下子好一大截。那篇写得挺细,还讲了递归展开,推荐去看看。不过我这篇讲的就是我自己这个基础版。人家做到那个确实很棒。

清屏有个坑(可能第一次用不太会喵) :cls 会把之前打印的提示也一起清掉。所以我把玩法提示放在每次要坐标的时候打印,而不是循环外面打一次。顺序弄反了就会出现"明明打印了提示,眼前却什么都没有"的诡异情况------程序没报错,但玩家会很迷惑。

(cls 和 Sleep 都是 Windows 的东西,所以这代码是 Windows 专属的,拿到 Linux 上得换成 clear 和 usleep。)

找雷流程

八、依旧我踩过的坑,和还可以改进的

踩过并解决了的:

  • ROWS 和 ROW 传反了(思路想了好久)。 初始化要覆盖整个 11×11,用 ROWS/COLS;布雷打印排雷只管 9×9,用 ROW/COL。我第一次写反,雷全布到辅助格子里,棋盘上一个雷都没有。
  • rand() 后面忘了 + 1。 上面说过了。
  • 布雷没判重。 也说过,但值得再说一遍,因为比较阴。
  • cls清屏吃掉提示。 上面也说过。
  • 一开始防呆忘做齐了。 让别人玩出来bug了,后续选择难度与坐标的时候要是输入字母就会卡死死循环。(在这里再次感谢一下黑暗的丹 喵) 我的问题喵,本来想多个功能,没想到多了个bug喵。。。

还没解决,可以改进的:

  1. int count; 写在头文件里,其实不是很规范喵。 两个 .c 都 include 了这个头文件 (太懒了喵) ,我现在 VS 的配置能编译,但正经写法应该是头文件里写 extern int count;,再在某个 .c 里定义一次。
  2. 没有插旗与递归炸开空白,其实这是我最想后续改进的(已经在学了喵),扫雷最爽的就是点一下可以炸开没雷的空白区域嘞。
  3. 没有首击保护。 第一下就可能踩雷,因为还没做。真正的扫雷会保证你第一次点一定安全,后续也将严肃改进。

九、代码放在 GitHub 上了,可以拉下来玩玩

完整代码我 push 了:

👉 github.com/S2MPLE273/s...

用 Visual Studio 打开 saolei.slnx,编译运行就行。三个文件都在 saolei/ 目录下:

css 复制代码
saolei/
├─ game.h      常量和函数声明
├─ saolei.c    菜单、难度选择、main
└─ dingyi.c    初始化、打印、布雷、排雷

只想看核心逻辑的话直接翻 dingyi.c,棋盘相关的函数都在那儿。README 我也写了,照着跑就行。

可以来撒泼我一下喵。 Easy 难度 71 格其实没那么难翻完,hard 那 20 颗雷就有点刺激了(棋盘不变)。踩雷了别骂我,雷是随机的,不关我事啊喵。

很快就踩雷了喵...

十、最后

做完最大的感受是:二维数组、循环、分支,单独学的时候就只是一堆零件。 你能背出语法,能做对课后题,但你不知道它们该怎么用。

等把它们接成一个能跑能玩能输能赢的东西,那些代码块才有了位置。上一篇学的 switch、do-while、嵌套 for、break、continue,还有那个我说"别用"结果自己用了的 goto(打脸了喵) ,这次全上场了。

下一步想加的是空白自动展开------点开一片空白自动铺开那种,扫雷最爽的机制。不过那个要用递归,学完回来补 (下一篇吧喵) ,到时候再写一篇 "给我的扫雷动手术"的大型DLC哈哈哈哈哈。

如果你也刚学完二维数组和循环,真的,动手写一个小游戏吧。那种,刷再多选择题都换不来喵。

最后还是哪里写得不对大佬们可以来评论区骂我喵。


参考与来源


相关推荐
fanruitian1 小时前
python 进程 线程
java·前端·python
ZoeLandia1 小时前
React03:useState 与不可变更新
前端·react.js·typescript·react
蒲公英内测分发2 小时前
智能手表卖出去后,包装、说明书和官网里的 App 下载入口怎么统一?
前端·人工智能·测试工具·智能家居·智能硬件·智能手表
溪语流沙2 小时前
【Web全栈进阶】监控与告警:线上系统的“体检报告”
前端·vue.js·python·fastapi
鬼手点金2 小时前
单轮对话:最简单的 API 调用
java·服务器·前端·javascript·react.js·servlet·tomcat
燕落南枝3 小时前
CSS 字体样式:颜色、字体族、倾斜、加粗与装饰
前端·css
鬼手点金3 小时前
消息的四种角色
java·前端·人工智能·git·深度学习
陈随易3 小时前
副业搞钱之代办永不过时
前端·后端·程序员
古夕3 小时前
同样的 font-weight,为什么这个项目的字体就是更细?
前端·javascript