原文出处: Hawstein's Blog html
这两天在网上看到一张让人涨姿式的图片,图片中展现的是贪吃蛇游戏, 估计大部分人都玩过。但若是仅仅是贪吃蛇游戏,那么它就没有什么让人涨姿式的地方了。 问题的关键在于,图片中的贪吃蛇真的很贪吃XD,它把矩形中出现的食物吃了个遍, 而后华丽丽地把整个矩形填满,真心是看得赏心悦目。做为一个CSer, 第一个想到的是,这东西是写程序实现的(由于,通常人干不出这事。 果断是要让程序来干的)第二个想到的是,写程序该如何实现,该用什么算法? 既然开始想了,就开始作。由于Talk is cheap,要show me the code才行。 (从耗子叔那学来的)python
开始以前,让咱们再欣赏一下那只让人涨姿式的贪吃蛇吧:( 若是下面的动态图片浏览效果不佳的话,能够右键保存下来查看)c++
Life is short, use python! 因此,根本就没多想,直接上python。git
先让你的程序跑起来github
首先,咱们第一件要作的就是先不要去分析这个问题。 你好歹先写个能运行起来的贪吃蛇游戏,而后再去想AI部分。这个应该很简单, cc++也就百来行代码(若是我没记错的话。不弄复杂界面,直接在控制台下跑), python就更简单了,去掉注释和空行,五、60行代码就搞定了。并且,最最关键的, 这个东西网上确定写滥了,你没有必要重复造轮子, 去弄一份来按照你的意愿改造一下就好了。算法
简单版本安全
我以为直接写perfect版本不是什么好路子。由于perfect版本每每要考虑不少东西, 直接上来就写这个通常是bug百出的。因此, 一开始个人目标仅仅是让程序去控制贪吃蛇运动,让它去吃食物,仅此而已。 如今让咱们来陈述一下最初的问题:函数
1oop 2布局 |
在一个矩形中,每一时刻有一个食物,贪吃蛇要在不撞到本身的条件下, 找到一条路(未必要最优),而后沿着这条路运行,去享用它的美食 |
咱们先不去想蛇会愈来愈长这个事实,问题基本就是,给你一个起点(蛇头)和一个终点( 食物),要避开障碍物(蛇身),从起点找到一条可行路到达终点。 咱们能够用的方法有:
BFS
DFS
A*
只要有选择,就先选择最简单的方案,咱们如今的目标是要让程序先跑起来, 优化是后话。so,从BFS开始。咱们最初将蛇头位置放入队列,而后只要队列非空, 就将队头位置出队,而后把它四领域内的4个点放入队列,不断地循环操做, 直到到达食物的位置。这个过程当中,咱们须要注意几点:1.访问过的点再也不访问。 2.保存每一个点的父结点(即每一个位置是从哪一个位置走到它的, 这样咱们才能把可行路径找出来)。3.蛇身所在位置和四面墙不可访问。
经过BFS找到食物后,只须要让蛇沿着可行路径运动便可。这个简单版本写完后, 贪吃蛇就能够很欢快地运行一段时间了。看图吧:(不流畅的感受来自录屏软件@_@)
为了尽可能保持简单,我用的是curses模块,直接在终端进行绘图。 从上面的动态图片能够看出,每次都单纯地使用BFS,最终有一天, 贪吃蛇会由于这种不顾后果的短视行为而陷入困境。 并且,即便到了那个时候,它也只会BFS一种策略, 致使由于当前看不到目标(食物),认为本身这辈子就这样了,破罐子破摔, 最终停在它人生中的某一个点,再也不前进。(我好爱讲哲理XD)
上一节的简单版本跑起来后,咱们认识到,只教贪吃蛇一种策略是不行的。 它这么笨一条蛇,你很少教它一点,它分分钟就会挂掉的。 因此,我写了个Wander函数,顾名思义,当贪吃蛇陷入困境后, 就别让它再BFS了,而是让它随便四处走走,散散心,思考一下人生什么的。 这个就比如你困惑迷茫的时候还去工做,效率不佳不说,还可能阻碍你走出困境; 相反,这时候你若是放下手中的工做,停下来,出去旅个游什么的。回来时, 说不定就豁然开朗,土地平旷,屋舍俨然了。
Wander函数怎么写都行,可是确定有优劣之分。我写了两个版本,一个是在可行的范围内, 朝随机方向走随机步。也就是说,蛇每次运动的方向是随机出来的, 总共运动的步数也是随机的。Wander完以后,再去BFS一下,看可否吃到食物, 若是能够那就皆大欢喜了。若是不行,说明思考人生的时间还不够,再Wander一下。 这样过程不断地循环进行。但是就像“随机过程随机过”同样,你“随机Wander就随机挂”。 会Wander的蛇确实能多走好多步。但是有一天,它就会把本身给随机到一条死路上了。 陷入困境还能够Wander,进入死胡同,那可没有回滚机制。因此, 第二个版本的Wander函数,我就让贪吃蛇贪到底。在BFS无解后, 告诉蛇一个步数step(随机产生step),让它在空白区域以S形运动step步。 这回运动方向就不随机了,而是有组织有纪律地运动。先看图,而后再说说它的问题:
没错,最终仍是挂掉了。S形运动也是没法让贪吃蛇避免死亡的命运。 贪吃蛇能够靠S形运动多存活一段时间,但是因为它的策略是:
1 2 3 4 5 |
while 没有按下ESC键: if 蛇与食物间有路径: 走起,吃食物去 else: Wander一段时间 |
问题就出在蛇发现它本身和食物间有路径,就二话不说跑去吃食物了。 它没有考虑到,你这一去把食物给吃了后造成的局势(蛇身布局), 彻底就可能让你挂掉。(好比进入了一个本身蛇身围起来的封闭小空间)
so,为了能让蛇活得久一些,它还要更高瞻远瞩才行。
咱们如今已经有了一个比较低端的版本,并且对问题的认识也稍微深刻了一些。 如今能够进行一些比较慎密和严谨的分析了。首先,让咱们罗列一些问题: (像头脑风暴那样,想到什么就写下来便可)
蛇和食物间有路径直接就去吃,不可取。那该怎么办?
若是蛇去吃食物后,布局是安全的,是否就直接去吃?(这样最优吗?)
怎样定义布局是否安全?
蛇和食物之间若是没有路径,怎么办?
最短路径是否最优?(这个明显不是了)
那么,若是布局安全的状况下,最短路径是否最优?
除了最短路径,咱们还能够怎么走?S形?最长?
怎么应对蛇身愈来愈长这个问题?
食物是随机出现的,有没可能出现无解的布局?
暴力法(brute force)可否获得最优序列?(让贪吃蛇尽量地多吃食物)
只要去想,问题还挺多的。这时让咱们以面向过程的思想,带着上面的问题, 把思路理一理。一开始,蛇很短(初始化长度为1),它看到了一个食物, 使用BFS获得矩形中每一个位置到达食物的最短路径长度。在没有蛇身阻挡下, 就是曼哈顿距离。而后,我要先判断一下,贪吃蛇这一去是否安全。 因此我须要一条虚拟的蛇,它每次负责去探路。若是安全,才让真正的蛇去跑。 固然,虚拟的蛇是不会绘制出来的,它只负责模拟探路。那么, 怎么定义一个布局是安全的呢? 若是你把文章开头那张动态图片中蛇的销魂走位好好的看一下, 会发现即便到最后蛇身已经很长了,它仍然没事通常地走出了一条路。并且, 是跟着蛇尾走的!嗯,这个其实不难解释,蛇在运动的过程当中,消耗蛇身, 蛇尾后面老是不断地出现新的空间。蛇短的时候还无所谓,当蛇一长, 就会发现,要想活下来,基本就只能追着蛇尾跑了。在追着蛇尾跑的过程当中, 再去考虑可否安全地吃到食物。(下图是某次BFS后,获得的一个布局, 0表明食物,数字表明该位置到达食物的距离,+号表明蛇头,*号表明蛇身, -号表明蛇尾,#号表明空格,外面的一圈#号表明围墙)
1 2 3 4 5 6 7 |
# # # # # # # # 0 1 2 3 4 # # 1 2 3 # 5 # # 2 3 4 - 6 # # 3 + * * 7 # # 4 5 6 7 8 # # # # # # # # |
通过上面的分析,咱们能够将布局是否安全定义为蛇是否能够跟着蛇尾运动, 也就是蛇吃完食物后,蛇头和蛇尾间是否存在路径,若是存在,我就认为是安全的。
OK,继续。真蛇派出虚拟蛇去探路后,发现吃完食物后的布局是安全的。那么, 真蛇就直奔食物了。等等,这样的策略好吗?未必。由于蛇每运动一步, 布局就变化一次。布局一变就意味着可能存在更优解。好比由于蛇尾的消耗, 本来须要绕路才能吃到的食物,忽然就出如今蛇眼前了。因此,真蛇走一步后, 更好的作法是,从新作BFS。而后和上面同样进行安全判断,而后再走。
接下来咱们来考虑一下,若是蛇和食物之间不存在路径怎么办? 上文其实已经提到了作法了,跟着蛇尾走。只要蛇和食物间不存在路径, 蛇就一直跟着蛇尾走。一样的,因为每走一步布局就会改变, 因此每走一步就从新作BFS获得最新布局。
好了,问题又来了。若是蛇和食物间不存在路径且蛇和蛇尾间也不存在路径, 怎么办?这个我是没办法了,选一步可行的路径来走就是了。仍是一个道理, 每次只走一步,更新布局,而后再判断蛇和食物间是否有安全路径; 没有的话,蛇头和蛇尾间是否存在路径;尚未,再挑一步可行的来走。
上面列的好几个问题里都涉及到蛇的行走策略,通常而言, 咱们会让蛇每次都走最短路径。这是针对蛇去吃食物的时候, 但是蛇在追本身的尾巴的时候就不能这么考虑了。咱们但愿的是蛇头在追蛇尾的过程当中, 尽量地慢。这样蛇头和蛇尾间才能腾出更多的空间,空间多才有得发展。 因此蛇的行走策略主要分为两种:
1 2 |
1. 目标是食物时,走最短路径 2. 目标是蛇尾时,走最长路径 |
那第三种状况呢?与食物和蛇尾都没路径存在的状况下, 这个时候原本就只是挑一步可行的步子来走,最短最长关系都不大了。 至于人为地让蛇走S形,我以为这不是什么好策略,最第一版本中已经分析过它的问题了。 (固然,除非你想使用最最无懈可击的那个版本,就是彻底无论食物, 让蛇一直走S,而后在墙边留下一条过道便可。这样一来, 蛇老是能够完美地把全部食物吃完,而后占满整个空间,但是就很boring了。 没有任何的意思)
上面还提到一个问题:由于食物是随机出现的,有没可能出现无解的局面? 是:有。我运行了程序,而后把每一次布局都输出到log,发现会有这样的状况:
1 2 3 4 5 6 7 |
# # # # # # # # * * * * * # # * * - 0 * # # * * # + * # # * * * * * # # * * * * * # # # # # # # # |
其中,+号是蛇头,-号是蛇尾,*号是蛇身,0是食物,#号表明空格,外面一圈# 号表明墙。这个布局上,食物已经在蛇头面前了,但是它能吃吗?不能! 由于它吃完食物后,长度加1,蛇头就会把0的位置填上,布局就变成:
1 2 3 4 5 6 7 |
# # # # # # # # * * * * * # # * * - + * # # * * # * * # # * * * * * # # * * * * * # # # # # # # # |
此时,因为蛇的长度加1,蛇尾没有动,而蛇头被本身围着,挂掉了。但是, 咱们却还有一个空白的格子#没有填充。按照咱们以前教给蛇的策略, 面对这种状况,蛇头就只会一直追着蛇尾跑,每当它和食物有路径时, 它让虚拟的蛇跑一遍发现,获得的新布局是不安全的,因此不会去吃食物, 而是选择继续追着蛇尾跑。而后它就这样一直跑,一直跑。死循环, 直到你按ESC键为止。
因为食物是随机出现的,因此有可能出现上面这种无解的布局。固然了, 你也能够获得完满的结局,贪吃蛇把整个矩形都填充满。
上面的最后一个问题,暴力法是否能获得最优序列。从上面的分析看来, 能够获得,但不能保证必定获得。
最后,看看高瞻远瞩的蛇是怎么跑的吧:
矩形大小10*20,除去外面的边框,也就是8*18。Linux下录完屏再转成GIF格式的图片, 优化前40多M,真心是无法和Windows的比。用下面的命令优化时, 有一种系统在用生命作优化的感受:
Shell
1 |
convert output.gif -fuzz 10% -layers Optimize optimised.gif |
最后仍是拿到Windows下用AE,三下五除二用图片序列合成的动态图片 (记得要在format options里选looping,否则图片是不会循环播放的)
若是对源代码感兴趣,请戳如下的连接: Code goes here
另外,本文的贪吃蛇程序使用了curses模块, 类Unix系统都默认安装的,使用Windows的童鞋须要安装一下这个模块, 送上地址: 须要curses请戳我
以上的代码仍然能够继续改进(如今加注释不到300行,优化一下能够更少), 也可用pygame或是pyglet库把界面作得更加漂亮,Enjoy!
QQ技术交流群290551701 http://cxy.liuzhihengseo.com/541.html