如何用Python寫一個貪吃蛇AI

原文出處: 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)

BFS+Wander

上一節的簡單版本跑起來後,咱們認識到,只教貪吃蛇一種策略是不行的。 它這麼笨一條蛇,你很少教它一點,它分分鐘就會掛掉的。 因此,我寫了個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,否則圖片是不會循環播放的)

Last but not least

若是對源代碼感興趣,請戳如下的連接: Code goes here

另外,本文的貪吃蛇程序使用了curses模塊, 類Unix系統都默認安裝的,使用Windows的童鞋須要安裝一下這個模塊, 送上地址: 須要curses請戳我

以上的代碼仍然能夠繼續改進(如今加註釋不到300行,優化一下能夠更少), 也可用pygame或是pyglet庫把界面作得更加漂亮,Enjoy!

QQ技術交流羣290551701 http://cxy.liuzhihengseo.com/541.html

相關文章
相關標籤/搜索