博客搬家:最爱午后红茶
诺基亚手机经典游戏,规则不多说~
先上一个多年前火了一段时间的 gif 动态图以表达敬意:
听说这是一个俄罗斯人用程序实现的。
所以这里想谈一下 贪吃蛇如何才能把地图填满?
我们用把复杂问题简单化的方法一步步分析用计算机求解的过程:
1)蛇的移动
蛇的移动是一个有趣的问题,在不碰墙的情况下,蛇头可以有 3 个可移动的格子,蛇的任意一次移动都表示要把原蛇头变成蛇身,把要移动到的格子变成蛇头;如果要移动到的格子是食物,就不需要把蛇尾删除,否则删除蛇尾;这就完成了蛇的移动。所以用双链表来储存蛇的信息是很方便的。在逻辑设计的时候有一点是需要注意的:
像这种情况(上左图蛇颈是在蛇头上面)如果蛇头下一步是往下走,是合理的(我是这样想的);所以这里我是先把尾巴删了后再移动蛇头。如果反过来就会造成游戏结束。这还有一个设计上的问题;像上面的左图,如果我没说出蛇颈在哪,你都分不出方向;所以蛇身之间是合着还是分开最好表现出来,以便观察。
2)吃到食物
一般就看到哪出现食物就往哪跑,所以可以使用 A* 或单纯的 BFS 来搜索最短路径。这里要注意的是:在搜索到路径之后,不能直接按照路径走;因为蛇一移动,整个格局就改变了,即地图是动态的。所以在搜索到路径之后,只让蛇走路径的第一步,然后重新搜索,以此类推。
然而用这种方法吃食物会出现什么问题?
1、有可能找不到食物:(紫色为蛇头,橘黄色为食物)
虽然找不到食物并不意味着死亡,但这是计算机在玩,在搜索失败的时候必须给出个方案,这个后面再讲。
2、吃了食物后必死:
这种情况毫无疑问无法补救了,所以程序是绝不允许出现这样的情况的。
3)保持不死
没错,这个游戏只要操作得当,是不会挂掉的(除非地图被填满)。随着蛇的移动,尾巴走过的路线一定是空出来的;所以只要蛇头跟着尾巴跑就绝对不会死。所以就有了一个很无耻的必胜法,非常无耻:
此法只对空白地图有效,如果有障碍物很明显就不适用,这种无脑操作不在讨论范围。。。
因此蛇要填满地图就必须保持不死的状态吃食物。那如何做到这点?
从上面可以知道,在任意状态下,只要蛇头与蛇尾之间有路径,蛇就不会死。所以我们搜索出蛇头到食物的最短路后,先模拟走一下这条路线(这里也是每走一步重新搜索一遍),不必绘图;如果走完后蛇头与蛇尾是连通的,就说明按这条路线吃食物是不会挂的;也就可以大胆过去吃。
这里又出现了一个问题:
(图1) (图2) (图3)
上面 (图1) 在搜索路径时可能会出现 (图2) 和 (图3) 的状况(两者都是最短路);问题是如果是 (图2) 这样的话吃完食物后蛇头与蛇尾是连通的,但 (图3) 明显就不行了。这是个棘手的问题(我觉得是),因为我们无法判断哪条路才不会导致吃完食物后挂掉(除非把所有路径找出然后逐条判断),这里我的态度是随意(因为还没找到合适的方案):如果它搜出的是 (图3) 的路线,就把它当作不合理路线;如果搜出 (图2) 的路线那就最好了。
接着,如果找不到安全的路线去吃食物怎么办?比如这样:
这样很明显就不要鲁莽地去吃了,本来遇到这样的情况就需要找一条蛇头到蛇尾的最长路(首尾距离越大就越有可能腾出空间吃食物),然后取最长路的第一步走,之后又开始判断能否安全地吃到食物;最长路可不好算,可以通过枚举路径长度得到,也可以用 A* 近似得到。这里我想:反正就取一步,我直接从蛇头下一步可以走的格子取一个离蛇尾最远的走就行了,比如上图就选 1号格子;实践效果还是不错的。
做到这里就有不死之身了,然而会出现两种情况:
(1)
这个例子里吃完 39 个食物后出现死循环。主要还是上面提到的当出现多个最短路时没有选对路线导致的,有点棘手。
(2)
没错!填满了
如果仔细分析,由于食物出现的随机性,是否一定可以填满地图呢(无脑那个例外~)?其实除了上面的由于设计不当导致死循环之外,还有一种可能就是,当地图快要填满时,食物出现在一个无论蛇身如何移动都吃不到的地方;由于蛇不会死,所以这也是个死循环,后面障碍物就是一个这样的例子。
还有一个现象就是,上面那个填满了地图的动图看似风光,其实是非常愚昧的;它采取的方案是:只要一出现安全路线,就跑过去吃。这样没有问题,只是会使地图各处出现大大小小的空洞,也即食物会出现在各个空洞里,这将导致贪吃蛇有时要移动很长一段距离才能吃到食物。因此,在后期蛇比较长的时候可以采取这样的策略:以最大间距追着蛇尾跑,这个过程可以吃到食物就吃。这样蛇也是不会挂的,而且可以得到优雅一点的路线。像下面那样,我在蛇吃了 50 个食物(地图的一半)后开始采取这样的策略,可以尽可能地填补空格,可以感受一下;虽然无法跟那个俄罗斯人的相比。
没有障碍物时可以随意填满,那有障碍物是什么情况?实践得到:复杂很多
反正我觉得在有障碍物的情况下要把地图填满需要一定的运气(可能是我能力只有这么点),所以也没深究下去。直接用上面的搜索方案可以跑 2/3 左右(这个例子在吃完 68 个食物后出现无解死循环)。
心得:
这是一个非常适合练习搜索算法的题目,难度适中;我用 qt 开发,总共 500 多行代码,界面与基本逻辑设计占 3/5 ,AI 占 2/5。整个思路如上面的内容,并不复杂,但是细节非常多,要成功地填满地图需要非常细心;这也是对自己思维严谨性的磨练。以 Ubuntu 为平台,gif 动图录制使用了 byzanz ,由于是命令行操作,可以配合 xdotool 获取鼠标坐标以定位录制起点。不过我下载的 byzanz 好像没有循环播放 gif 动图的可选项,即 gif 只播放一次;可以使用这个在线图片修改网修复:动态图片循环播放修复工具-修复动态图片只播放一次 (这也是个修图的好东西,还是在线的)
qt 源代码地址:
https://github.com/QYPan/qt-examples/tree/master/snake
参考文章:
貪吃蛇 AI 人工智慧 C++ 實現 (这个要翻墙 or VPN)