不間斷 Python 挑戰 Day 26 - 專題:貪食蛇(下)

不間斷 Python 挑戰 Day 26 - 專題:貪食蛇(下)

更新於 發佈於 閱讀時間約 15 分鐘

接續前一篇文章,說明貪食蛇遊戲架構的後半部份,來完成整個遊戲的程式設計。

遊戲架構(續)

控制方向

要透過鍵盤來控制貪食蛇移動的方向,需要使用到Turtle模組的按鍵事件方法,也就是透過鍵盤按鍵的按下或釋放來觸發某一段程式執行。在這裡,我們使用鍵盤的上、下、左、右鍵做為觸發事件的按鍵,當這些按鍵按下後,觸發某一段程式,其語法如下:

turtle.listen()
turtle.onkeypress(fun, key)
  • fun – 按鍵按下後執行的函數
  • key – 觸發事件的按鍵

當向上的按鍵按下後,如果當下貪食蛇的方向為向左或向右,在下一步即轉為向上移動,也就是說,如果當下貪食蛇的方向為向上或向下,該事件無效,因為蛇身已經同向,或是不能直接反轉,其它方向的事件以此類推。

首先我們在Snake類別的初始化函數內新增一個移動方向的串列。

# snake.py
self.move_angle_list = []

當上、下、左、右鍵按下後,立即在move_angle_list中新增一個元素,代表下一個要轉向的角度。

# snake.py
def move_up(self):
  self.move_angle_list.append(90)

def move_down(self):
  self.move_angle_list.append(270)

def turn_right(self):
  self.move_angle_list.append(0)

def turn_left(self):
  self.move_angle_list.append(180)

在move()方法中,新增一段程式檢查move_angle_list是否為空串列,若有元素存在,即執行轉向的動作,在下一步改變移動的方向。

# snake.py
def move(self):
  # 檢查是否要轉彎
  while self.move_angle_list:
    next_move_angle = self.move_angle_list.pop(0)
    if next_move_angle != self.head.heading() and \
      next_move_angle != (self.head.heading() + 180) % 360:
      self.head.setheading(next_move_angle)
      break;
    else:
      continue;

# 往前移動
  for body_seg in range(len(self.snake_body) - 1, 0, -1):
    new_x = self.snake_body[body_seg - 1].xcor()
    new_y = self.snake_body[body_seg - 1].ycor()
    self.snake_body[body_seg].goto(new_x, new_y)
  self.head.forward(MOVE_DISTANCE)

在主程式中,則利用listen()及onkeypress()方法,接受上、下、左、右鍵按下的事件。

# 主程式
# 按鍵設定
snake_screen.listen()
snake_screen.onkeypress(snake.move_up, "Up")
snake_screen.onkeypress(snake.move_down, "Down")
snake_screen.onkeypress(snake.turn_left, "Left")
snake_screen.onkeypress(snake.turn_right, "Right")

如此一來便可用鍵盤來控制貪食蛇移動的方向,下一步就可以開始讓貪食蛇吃食物並延長蛇身。

raw-image

檢查是否吃到食物

在遊戲中吃食物必定是用蛇頭去吃,我們可以在每一步去檢查蛇頭和食物的距離,當距離足夠小即表示有吃到食物,這是和蛇相關的行為,因此在Snake類別中新增一個方法來執行這個任務。

# snake.py
def is_collision_with_food(self, food):
  if self.head.distance(food) < 5:
    return True
  return False

在吃食物的同時也會增加身體的長度,同樣在Snake類別中新增一個方法,複製貪食蛇末端的元素到snake_body串列的最後方,當蛇身下一次移動時就會完成延長蛇身的動作。

# snake.py
def extend_snake(self):
  self.add_snake_body(self.snake_body[-1].position())

最後,在主程式的while迴圈中,每一步移動之後即檢查是否吃到食物,若有,則呼叫food.random_food()隨機產生下一個食物,並執行snake.extend_snake()延長蛇身。

# 主程式
# 是否吃到食物
if snake.is_collision_with_food(food):
  food.random_food()
  snake.extend_snake()

執行結果:

raw-image

建立記分板

上一篇文章畫出圍牆的時候,我們有在畫布的上方預留比較大的空間,做為顯示記分板文字的空間,在貪食蛇吃到食物的同時更新分數,例如一次加一分。做為一個相對獨立的功能,我們在snake資料夾內另開一個scoreboard.py檔案,建立Scoreboard類別,繼承自Turtle模組。在初始化函數中移動至適當的位置,設定初始分數為0,並用write()方法寫到畫布上。

# scoreboard.py
from turtle import Turtle

SCORE_COLOR = "white"
SCORE_POSITION = (0, 265)

class Scoreboard(Turtle):

  def __init__(self):
    super().__init__()
    self.score = 0
    self.hideturtle()
    self.penup()
    self.color(SCORE_COLOR)
    self.speed("fastest")
    self.goto(SCORE_POSITION)
    self.write(f"score: {self.score}", False, align="center", font=("Arial", 20, "normal"))

  def get_score(self):
    self.score += 1
    self.clear()
    self.write(f"score: {self.score}", False, align="center", font=("Arial", 20, "normal"))

主程式中建立Scoreboard類別的物件,並在while迴圈中當偵測到蛇頭吃到食物時呼叫scoreboard.get_score()方法更新分數。

# 主程式
# 建立遊戲相關物件
scoreboard = Scoreboard()

# 遊戲主程式
is_game_on = True
while is_game_on:
  # 遊戲畫面更新
  snake_screen.update()
  time.sleep(time_delay)
  snake.move()

  # 是否吃到食物
  if snake.is_collision_with_food(food):
    food.random_food()
    snake.extend_snake()
    scoreboard.get_score()

執行結果:

raw-image

檢查是否撞到牆

程式來到了最後的步驟,要決定遊戲何時該結束,可以歸納出兩個條件:

  1. 蛇頭撞到牆。
  2. 蛇頭撞到身體。

先處理撞到牆的部份,判斷撞到牆的條件是蛇頭超出了圍牆的範圍,前篇文章已知圍牆內緣的座標為:左上(-270, 250)、右上(270, 250)、左下(-270, -270)、右下(270, -270),我們便以此為條件在Snake類別新增is_collision_with_wall()方法來判斷。

# snake.py
def is_collision_with_wall(self, width, height):
  if self.head.xcor() > (width / 2 - 30) or \
    self.head.xcor() < -(width / 2 - 30) or \
    self.head.ycor() > (height / 2 - 50) or \
    self.head.ycor() < -(height / 2 - 30):
    return True
  return False

主程式的while迴圈中,每一次更新位置後呼叫snake.is_collision_with_wall()判斷是否撞牆。

# 主程式
# 是否撞牆
if snake.is_collision_with_wall(width=SCREEN_WIDTH, height=SCREEN_HEIGHT):
  is_game_on = False

執行結果:

raw-image

檢查是否撞到蛇身

要判斷蛇頭是否撞到蛇身,我們將蛇頭和蛇身每一節的距離一一比對,當距離小於20,也就是每前進一次的距離,就表示撞到自己了,當然設定更小也是沒有問題的。

# snake.py
def is_collision_with_body(self):
  for body_seg in self.snake_body[1:]:
    if self.head.distance(body_seg) < 5:
      return True
  return False

在主程式的while迴圈中,同樣在每一次更新位置後呼叫snake.is_collision_with_body(),來判斷是否相撞。

# 主程式
# 是否撞身體
if snake.is_collision_with_body():
  is_game_on = False

執行結果:

raw-image

結束遊戲

為了讓遊戲結束時有明確的標示,讓遊戲看起來更完整,我們讓前面兩個條件成立時進一步在畫布上顯示出"Game Over"字樣以提示遊戲結束,因為性質和記分板類似,我們也在Scoreboard類別中新增此功能。

# scoreboard.py
def game_over(self):
  self.color(GAMEOVER_COLOR)
  self.goto(GAMEOVER_POSITION)
  self.write("Game Over", False, align="center", font=("Arial", 40, "normal"))

最後,在判斷是否相撞的條件內呼叫scoreboard.game_over()方法,整個遊戲便大功告成。

# 主程式
# 遊戲主程式
is_game_on = True
while is_game_on:
  # 遊戲畫面更新
  snake_screen.update()
  time.sleep(time_delay)
  snake.move()

  # 是否吃到食物
  if snake.is_collision_with_food(food):
    food.random_food()
    snake.extend_snake()
    scoreboard.get_score()

  # 是否撞牆
  if snake.is_collision_with_wall(width=SCREEN_WIDTH, height=SCREEN_HEIGHT):
    is_game_on = False
    scoreboard.game_over()

  # 是否撞身體
  if snake.is_collision_with_body():
    is_game_on = False
    scoreboard.game_over()

"Game Over"顯示畫面:

raw-image

程式範例

主程式:https://github.com/wjweng/marathon_python/blob/master/Day1_to_25/marathon_python_day25.py

Snake類別:
https://github.com/wjweng/marathon_python/blob/master/Day1_to_25/snake/snake.py

Food類別:
https://github.com/wjweng/marathon_python/blob/master/Day1_to_25/snake/food.py

Wall類別:
https://github.com/wjweng/marathon_python/blob/master/Day1_to_25/snake/wall.py

Scoreboard類別:
https://github.com/wjweng/marathon_python/blob/master/Day1_to_25/snake/scoreboard.py

avatar-img
Wei-Jie Weng的沙龍
48會員
36內容數
留言
avatar-img
留言分享你的想法!
Wei-Jie Weng的沙龍 的其他內容
對於程式的初學者而言,理解程式的流程、迴圈的進行、或是變數的變化會需要一定程度將程式在腦中進行運算的能力,要一段時間熟悉與適應,尤其是當程式執行的結果不如預期時,往往是計算的過程和自己所想像的不同,這時又更難靠自己的能力找出錯誤。因此,這邊要介紹的這個工具可以將程式執行的過程逐行將變數的變化視覺化地
在上一節介紹了 JSON 資料的基本架構後,我們將改寫並擴充密碼產生器程式,讓它能夠藉由 JSON 的資料結構完成帳密搜尋的功能。
JSON的全名叫JavaScript Object Notation,是由Douglas Crockford所設計的一種資料格式,最初應用在JavaScript程式語言中,做為一種資料交換的格式,而後被廣泛運用在Web開發與NoSQL資料庫,現今已成為一種重要的資料格式。
對於程式的初學者而言,理解程式的流程、迴圈的進行、或是變數的變化會需要一定程度將程式在腦中進行運算的能力,要一段時間熟悉與適應,尤其是當程式執行的結果不如預期時,往往是計算的過程和自己所想像的不同,這時又更難靠自己的能力找出錯誤。因此,這邊要介紹的這個工具可以將程式執行的過程逐行將變數的變化視覺化地
在上一節介紹了 JSON 資料的基本架構後,我們將改寫並擴充密碼產生器程式,讓它能夠藉由 JSON 的資料結構完成帳密搜尋的功能。
JSON的全名叫JavaScript Object Notation,是由Douglas Crockford所設計的一種資料格式,最初應用在JavaScript程式語言中,做為一種資料交換的格式,而後被廣泛運用在Web開發與NoSQL資料庫,現今已成為一種重要的資料格式。