Snake

貪食蛇(Snake)是一個起源於1976年的大型電玩遊戲 Blockade。此類遊戲在1990年代由於一些具有小型螢幕的行動電話的引入而再度流行起來,在現在的手機上基本都可安裝此小遊戲。版本亦有所不同。

img

1. 創建class和做準備

snake.pyde:

spot.py:

image-20230214184633629

2. 將畫面用spot分格

snake.pyde:

spot.py:

image-20230215104538477

這個步驟主要是將整個畫面都分成20格x20格。

setup()中,加入2d array去儲起Spot class,這個格式在第二個遊戲breakout時有做過。

我們在Spotclass的show()中,臨時加上框線來顯示,看看是否正常,debug後就可以不再畫框線,現在則暫時保留。

3. 為每個格加上鄰居

snake.pyde:

spot.py:

image-20230215111707440

由於貪食蛇的蛇身是連著的,而且一定是格的上下左右鄰居的其中一個,要將蛇越拖越長而且記著蛇走過的路,我們首先為每個格都加入鄰居。這也是用class的好處,class自己是可以包含自己的,你剛開一個class,class入面的變數就可以是這個class的本身。

spot.py中,

分別加入上、右、下和左四個鄰居,順序是按順時針的。但要留意,如果格剛好在邊介位置,例如最左。最右。最上和最下,就要加入限制,否則就會超出列表的範圍。

在主程式的draw()中,

特地為grid[3][3]為入鄰居,再將四個鄰居用不同顏色標記出來,用來debug檢查是否正確。值得留意的是,grid[3][3]中的3是array index,index是由0開始的,所以index 3,實際上不是第3格而是第4格。

3.1 特別處理邊介的鄰居

主程式沒有變,spot.py:

Screenshot 2023-02-15 115319Screenshot 2023-02-15 115341
Screenshot 2023-02-15 115408Screenshot 2023-02-15 115431

spotclass的addNeighbors()中,加入如果在邊位時的考量,由於貪食蛇是可以穿牆到畫面的另一邊,故此,邊位的鄰居就是另一邊的格。

addNeighbors()加入最底下的四個考量。之後去主頁,將之前測試的格座標改一改,試一試四個邊位是否正確。

3.2 測試完成後,為每個格加上鄰居

snake.pyde:

spot.py:

image-20230215104538477

為方便大家,我將全部程式一次過貼出來。前一步測試完成後,就可以在setup()中,加入:

為每個格都加入4個鄰居,這個步驟只要做一次就可以了,所以在setup()中完成就可以。在主程式頁面,將原本draw()中的鄰居顯示刪去。

4. 製作貪食蛇的class

snake.pyde:

snake.py

spot.py沒有改變,則不再重覆了。

image-20230215162926702

snake.pysnakeclass中,

將全個畫的的格匯入這個class中,所以開一個同樣叫grids的列表變數。蛇本身有body,而且會隨著吃蘋果越來越長,所有用列表snakeBody來裝起,最後就是蛇頭,只有一個,之後會用來裝起格仔的。

接著初始化,蛇頭預設在畫面中央,所以是grids[10][10],之後蛇身一開始時有3格(不計蛇頭,額外有3格)。值得注意是: 今次的for是次序相反的for i in range(3,0,-1):的意思是,由3開始計到0,每次-1,之所以要這樣逆次序,是因為snakeBody是用來紀錄蛇經過的路徑的,snakeBody[0]是蛇尾,也是蛇最舊的路徑。

5. 令蛇懂得行走

snake.pyd

snake.py:

spot.py`沒有變,不再重覆。

snake1

snakeclass中,

加上兩個變數分別為dirIDnext dirID是neighbors的方向,跟之前一樣上、右、下和左四個鄰居,順序是按順時針。而next則是蛇將要行的下一格。

在下面的show()中,顯示完之後,按照前進方向更新下一個蛇頭位置,之後將現有的蛇頭加入到蛇身,再將現在的蛇頭更新成一下個蛇頭,最後就用pop(),移除蛇身第一個內容(即蛇尾,這就是為何我們一開始要逆次序。

 

返回在主程式中,

setup()之中加入frameRate(),設定動畫影格為10 frame per second。

在程式最下,加上方向鍵控制,改變蛇的dirID就能改變其更新的方向。

6. 加入game over

snake.pyde:

snake.py:

其他兩個分頁spot.py`沒有變。

image-20230216171021480

snakeclass中,

最初始化的最下加入一個變數叫gameOver,設定成False

之後加入一個函數叫check(),用來檢查蛇是否撞到自己。由於我們蛇身都是用一個list去裝起的,所以只要蛇的下一步next包含在snakeBody的陣列中,即蛇撞到自己的蛇身,遊戲結束。

跟之前一樣,將原先draw()中的內容都用if (snake.gameOver == False):包裹,之後加入else:如果輸了就在畫面中央大大隻字顯示game over。

最後在keyPressed()中,加入按下r鍵就會重新遊戲。

7. 加入目標物蘋果

snake.pyde:

其餘的程式沒有變。

image-20230216175619766

下一個步驟是加入貪食蛇的目標物蘋果。

一開始宣告一個列表變數叫apples,在setup()中,為其加入一個新的內容。加入新內容時用一個叫addApple()的函數去抽出新的蘋果,下文會詳述。

在主頁的最下,加入一個自訂函數叫addApple()。一開始抽出全部grids裡面全部格的一個,接著就要對比一下,這個抽出來的格不能是蛇身,不能不蛇頭,也不能是原本apples已經有的內容,遇到這情況直接call addApple()再抽多一次,否則就return抽到的格。

8. 食到蘋果後得分

snake.pyde:

snake.py:

image-20230217100749990

在主程式上,

在遊戲的後,在snake.show()snake.check()之後,加入snake.ate()的函數,這個函數會在另一版加入,但現在先做用,這個函數的功能就是告訴你蛇是否有食到蘋果,如果是的話就回傳True,所以如果吃到的話,我們就要找出那一個蘋果是蛇吃到的(初階段只會有一個蘋果,但遊戲之後可以加入多個蘋果)。

上句的功能,index()是用來找出snake.next的索引,之後就用apples.pop(removeID)將該個索引的蘋果移除,再補一個新的。

之後再加上蛇的分數score,用來告訴蛇吃了多少蘋果。

在另一個分頁,snakeclass中,

gameOver外,再加入另外兩個變數,reward是一個boolean變數,用來蛇當下的收狀態是否正在吃蘋果,而score就是蛇吃了多少蘋果。

show()中,比較特別的是,在最後將蛇身長度減少的部分,之前每一次執行show()時,都會更新將蛇前進一格而扣減最後一格,但今次加入rewardboolean變數,如果是False的話,即現階段沒有吃到蘋果,所以照舊要移除最後一格,但如果是吃到蘋果的話,就不用移除最後一格,而且score加一,再將reward變回False,那這個動作就只會執行一次。(這個技術在第四章Flappy Bird時都有介紹過)

在最後,加入另一個函數ate(),要將所有的apple匯入,所以中間要加入_apple,因為class是不能使用和招喚global變數的,所以要用這方法去匯入。內容方面,如果匯入的蘋果當中,包含蛇頭(或者nextshow()之後兩者是同一格的),即蛇吃得到蘋果,就回傳True和將reward轉成True。(你也可以在這裡直接回傳需要刪除的蘋果,那在主程式就可以簡潔一點。)

9. 考考你

  1. 蛇的行走方向如果是左手邊的話,玩家按右鍵,就會即時game over,同樣情況也出現在其他 方向鍵,加入條件式,防止這個情況,蛇向左行,玩家按右鍵的話不會game over,只會甚麼都沒發生。

  2. score是5的倍數的話,那5n+1關時同一時間就會出現兩顆蘋果,例如第6關, 第11關都會同一時間有2個蘋果。

  3. 今次的程式是刻意將所有蛇的行為都包圍在snakeclass中的,你可以試試增加多一條蛇,這條蛇由w,s,a,d鍵作為方向鍵來控制。之後有機會再教大家用AI同一時間有很多條蛇在爭蘋果。