扫雷之递归

2,054 阅读5分钟
原文链接: zhuanlan.zhihu.com

前言

假前端这货又来了。

上一篇文章,我们已经生成了一张空白的雷区地图,并且将一定数量的地雷随机分布在整个雷区中,最后将环境标识计算出来。生成了一个较完整的雷区地图。

换个样子

嗯,地图有点丑,还不直观。

我们利用 vue 的 filters 对地图渲染做一些处理,用 emoji 来展示整张地图。

判断逻辑是:

  • 格子的数字是 0 ,显示空
  • 格子的数字是 9 ,显示炸弹
  • 格子是其他数字,仍然显示数字

好看很多啊~

也许你也发现问题了,扫雷一开始是所有格子都翻过来的。

对,我们目前仅仅是给每个格子赋值了一个数字代表当前格子是否是地雷以及周围地雷的数目,但是这样是远远不够的。

稍加思考会发现我们的每个格子还缺少一些状态,除了每个格子所代表的意义,还需要:

  • 格子当前翻开的状态
  • 每个格子的坐标

我们是用一个二维数组来表示雷区,其中每个元素是一个数字来表示地雷。现在我们需要改一改了。

我们仍然用一个二维数组来表示雷区,但其中每个元素不再是一个 `number`,而是一个 `object`,进而存放更多关于每个格子额状态:

同时,我们需要改一下 filter,不再单单判断 `value`,还需要同时判断`status`:

  • 翻开状态下
    • 数字 0 ,显示空
    • 数字 9 ,显示炸弹
    • 其他数字,仍然显示数字
  • 未翻开状态下,显示空,深色
  • 插旗状态下,显示旗子
  • 待定状态下,显示问号

事件绑定

也许会有疑问,为什么突然多出来旗子和问号?

这里就要梳理下整个扫雷游戏的交互了。

在整个游戏过程中,我们涉及到的交互主要分为三种

  • 单击鼠标左键:翻开格子
    • 插旗状态无法翻开
  • 单击鼠标右键:插旗,标识此处是地雷
    • 再次右击,取消插旗并切换至待定状态
    • 再次右击,切换至未翻开状态
    • 循环以上
  • 双击鼠标左键:如果已经找出格子周围所有地雷(插旗状态),则翻开周围其他格子

后文我们会一一解释和实现。

单击鼠标左键(递归)

单击鼠标左键的操作很简单,就是把格子翻开,看看格子下面是不是地雷,如果是地雷游戏结束 -- 失败;如果不是地雷游戏继续。

单击逻辑非常简单,就是把格子的状态改为 `FRONT`

但是,扫雷游戏里面有一个隐藏的规则。如果你之前也玩过扫雷游戏会发现,有时候点一个格子会打开一片。

这个是因为,如果你点开的格子周围没有地雷,也就是数字 0 ,会自动将周围的格子翻开,以此类推,如果自动翻开的格子周围仍然没有地雷,继续翻。

也就是说,如果地图中没有地雷,随便点击一个格子,地图就会完全打开。

动图中,点击第一下的时候,得知该格子周围存在一个地雷,所以正常翻开;当点击第二个格子的时候,这个格子周围没有地雷,就会自动扩散翻开。

如何像前面动图一样自动打开周围的格子?没错递归。退出条件就是当前翻开的格子不是 `SAFE_CELL`。

这也是著名算法网站 leetCode 第 529 题的解题思路,中等难度:扫雷。感兴趣的同学可以去试试。

单击鼠标右键

单击鼠标右键为切换格子状态。

我们给定格子有四种状态:

  • FRONT:翻开状态
  • BACK:未翻开状态
  • FLAG:插旗状态
  • NOT_SURE:疑问状态

如果这个格子已经被翻开 `FRONT`,那么再也无法切换,其他状态点击右键都会切换到下一个状态。

由于浏览器有个默认的右击弹出菜单事件,我们需要做一个 `preventDefault` 处理。

双击事件

双击事件是一个能帮助你快速完成扫雷的一个非常有用的时间,我很喜欢用。他的作用就是一个格子周围所有的累已经标出来了,双击这个格子,就会把未标记的格子一次性翻开,来提升你的操作速度。

看下操作结果。

知道了怎么回事,实现起来就简单了。需要注意的是,千万不要忘了自动打开。双击打开的区域如果存在空白区域,同样需要自动打开的。

这里的实现,就是将双击模拟成用户点击,这样,就拥有了一次性鼠标单击时自动打开的功能:

小总结

在这篇文章中,我们对每个格子的交互进行了一系列事件绑定。同时利用了递归,将一个看似复杂的自动打开功能完美实现了。

现在,我们的扫雷游戏终于可以进行操作了,距离我们完成扫雷游戏只差很小的一步。

下一步

还缺什么?

如何判定游戏输赢?怎样进行游戏计时以及如何保存记录。这些我们将在下一篇中讲述。同时,完完整整实现我们的扫雷游戏。