扫雷游戏中的算法:Union Find 和 Flood Fill
Contents
讲讲我在实现扫雷游戏时用到的算法,包括 Knuth shuffle、Union Find 和 Flood Fill。
可以在GitHub下载体验,Emoji Mines 是一年前就完成的作品。今天把雷生成的算法改了改,游戏体验变好许多,遂又向女胖友推销自己游戏(笑
在阅读之前假设你已可以熟悉扫雷的游戏规则。为了方便描述,将扫雷游戏中的方块分为:空白方块(即周围都没有雷方块的方块)、数字方块和雷方块。
雷的生成
雷的生成要保证随机,游戏体验才会好。游戏刚开始的阶段,也可以快速标记雷的位置。
替换法
替换法也就是把一个方块随机替换为雷,知道所有雷都放置完成。但是实际玩耍起来,体验不是很好,根本看不出哪个是雷,全靠盲猜。这样肯定要怀疑这个算法(特指下面实现方法)的随机性的,也不知道是不是Random
的锅。
|
|
洗牌法
然后一年过去,在知乎上偶然看到关于雷生成的算法,里面提到了洗牌法(shuffle)。使用的是 Knuth shuffle ,简单的说就是遍历数组,随机交换两个元素:
|
|
洗牌法分为两步:
- 首先将雷按序填入游戏地图
- 再使用 Knuth shuffle 打乱顺序
为了更好的游戏体验,下面算法可以实现第一次点击时,不会碰到雷。不过这也意味着雷的生成要推迟到第一次点击时。
|
|
空白区域
当点击空白方块,会连带出所有连通的空白方块和周围的数字方块。如下图所示:
这里有两种算法可以选择,也就是文章题目中的:Union Find 和 Flood Fill 。
Union Find
使用 Union Find 来实现连带空白方块是我最初的想法。在学习 Union Find 这个算法的时候,就想到:“诶,这个好像可以用在扫雷里吧”。之后,用了一晚上的时间写出了使用 Union Find 实现的扫雷。也是第一次觉得算法还是有用的(
Union Find API
初始化,每个节点各自成一个连通块:
1 2 3
for (int i = 0; i < n; i++) { parent[i] = i; }
union
:连接两个连通块。将两个节点设置为同一个root
。
|
|
connected
:判断两个连通块是否相连。相同root
的两个连通块就是连通的。
|
|
应用到扫雷游戏
我们可以利用union
操作将全部空白方块合成一个连通块,到我们点击到一个空白方块时,遍历游戏地图,将与其连通的空白方块都显示出来。因为游戏地图为二维数组,需要将二维坐标转化为一维坐标。
联合所有空白方块:
|
|
显示和当前空白方块相连的空白方块:
|
|
Flood Fill
Flood Fill(平铺算法?)其实就是个DFS,递归遍历四连通的方块,并将其标记。同样可以用来连带出所有连通的空白方块。
因为我的游戏已经使用 Union Find 实现,而且自己当时写的代码扩展性较差,如果要换算法,大概要全部重写。所以这里用一个简单的例子介绍 Flood Fill 算法。
假设有游戏地图如下:
当我们点击坐标(5, 4)
时,可以连带出5个空白方块组成的连通块(下方的T型)。
这个游戏地图用二维数组表示,把雷用“9”表示,因为一个方块周围最多有8个雷;已经展示的空白方块用“10”表示,防止重复遍历。
|
|
|
|
输出如下:
|
|
使用哪个算法
首先还是推荐使用 Flood Fill 算法,因为它更加适合这个场景,实现简单。
使用 Union Find 我们还要进行坐标转化、额外的存储空间、连带时需要遍历整个地图。所以整个连带操作的时间复杂度为O(row*col*lg(row*col))
。