Tkinter – Trò chơi rắn săn mồi

5/5 - (3 votes)

Trong phần này chúng ta viết lại một game rất nổi tiếng đó là game rắn săn mồi (Snake).

Snake

Game này được phát hành vào những năm 70 trên các hệ máy cầm tay, một thời gian sau được đưa lên PC. Trong game người chơi sẽ điều khiển một con rắn. Mục tiêu của người chơi là cố gắng cho con rắn ăn được càng nhiều mồi càng tốt. Mỗi lần con rắn ăn mồi, cơ thể nó sẽ dài ra. Game kết thúc khi người chơi cho rắn “ăn” phải tường hoặc cơ thể chính mình.

Mô tả game

Kích thước của mỗi đốt trên con rắn là 10px. Con rắn sẽ được điều khiển bằng các phím mũi tên. Khi game kết thúc, dòng chữ “Game Over” sẽ hiện lên giữa màn hình.

import sys
import random
from PIL import Image, ImageTk
from tkinter import Tk, Frame, Canvas, ALL, NW

WIDTH      = 300
HEIGHT     = 300
DELAY      = 100
DOT_SIZE   = 10
ALL_DOTS   = 900 #WIDTH * HEIGHT / (DOT_SIZE * DOT_SIZE)
RAND_POS   = 27

x = [0] * ALL_DOTS
y = [0] * ALL_DOTS

class Board(Canvas):
   def __init__(self, parent):
       Canvas.__init__(self, width=WIDTH, height=HEIGHT, 
                       background="black", highlightthickness=0)
       self.parent = parent
       self.initGame()
       self.pack()
 
   def initGame(self):
       self.right = True
       self.left = False
       self.up = False
       self.down = False
 
       self.inGame = True
       self.dots = 3
 
       self.apple_x = 100
       self.apple_y = 190
 
       for i in range(self.dots):
           x[i] = 50 - i * 10
           y[i] = 50 
 
       try: 
           self.dotImage = Image.open("dot.png")
           self.dotImage.thumbnail((10, 10), Image.ANTIALIAS)
           self.dot = ImageTk.PhotoImage(self.dotImage)
 
           self.headImage = Image.open("head.png")
           self.headImage.thumbnail((10, 10), Image.ANTIALIAS) 
           self.head = ImageTk.PhotoImage(self.headImage)
 
           self.appleImage = Image.open("apple.png")
           self.appleImage.thumbnail((10, 10), Image.ANTIALIAS) 
           self.apple = ImageTk.PhotoImage(self.appleImage)
 
       except IOError as e:
           print (e)
           sys.exit(1)
 
       self.createObjects()
       self.locateApple()
       self.bind_all("<Key>", self.onKeyPressed)
       self.after(DELAY, self.onTimer)
 
   def createObjects(self):
       self.create_image(self.apple_x, self.apple_y, image=self.apple,
       anchor=NW, tag="apple")
       self.create_image(x[0], y[0], image=self.head, anchor=NW, tag="head")
       self.create_image(x[1], y[1], image=self.dot, anchor=NW, tag="dot")
       self.create_image(x[2], y[2], image=self.dot, anchor=NW, tag="dot") 
 
   def locateApple(self):
       apple = self.find_withtag("apple")
       self.delete(apple[0])
 
       r = random.randint(0, RAND_POS)
       self.apple_x = r * DOT_SIZE
       r = random.randint(0, RAND_POS)
       self.apple_y = r * DOT_SIZE
 
       self.create_image(self.apple_x, self.apple_y, image=self.apple,
                         anchor=NW, tag="apple") 
 
   def doMove(self):
       dots = self.find_withtag("dot")
       head = self.find_withtag("head")
 
       items = dots + head
 
       z = 0
 
       while z < len(items) - 1:
           c1 = self.coords(items[z])
           c2 = self.coords(items[z + 1])
           self.move(items[z], c2[0]-c1[0], c2[1]-c1[1])
           z += 1
 
       if self.left:
           self.move(head, -DOT_SIZE, 0)
 
       if self.right:
           self.move(head, DOT_SIZE, 0)
 
       if self.up:
           self.move(head, 0, -DOT_SIZE)
 
       if self.down:
           self.move(head, 0, DOT_SIZE)
 
   def checkCollisions(self):
       dots = self.find_withtag("dot")
       head = self.find_withtag("head")
 
       x1, y1, x2, y2 = self.bbox(head)
       overlap = self.find_overlapping(x1, y1, x2, y2)
 
       for dot in dots:
           for over in overlap:
               if over == dot:
                   self.inGame = False
 
       if x1 < 0: 
           self.inGame = False 
       if x1 > WIDTH - DOT_SIZE:
           self.inGame = False
       if y1 < 0: 
           self.inGame = False 
       if y1 > HEIGHT - DOT_SIZE:
           self.inGame = False
 
   def checkApple(self):
       apple = self.find_withtag("apple")
       head = self.find_withtag("head")
 
       x1, y1, x2, y2 = self.bbox(head)
       overlap = self.find_overlapping(x1, y1, x2, y2)
 
       for ovr in overlap:
           if apple[0] == ovr:
               x, y = self.coords(apple)
               self.create_image(x, y, image=self.dot, anchor=NW, tag="dot")
               self.locateApple()
 
   def onTimer(self): 
       if self.inGame:
           self.checkCollisions()
           self.checkApple()
           self.doMove()
           self.after(DELAY, self.onTimer)
       else:
           self.gameOver()
 
   def onKeyPressed(self, e):
       key = e.keysym
 
       if key == "Left" and not self.right:
          self.left = True
          self.up = False
          self.down = False
 
       if key == "Right" and not self.left:
           self.right = True
           self.up = False
           self.down = False
 
       if key == "Up" and not self.down:
           self.up = True
           self.right = False
           self.left = False
 
       if key == "Down" and not self.up:
           self.down = True
           self.right = False
           self.left = False
 
      if key == "Escape":
          self.quit()
 
   def gameOver(self):
      self.delete(ALL)
      self.create_text(self.winfo_width() / 2, self.winfo_height() / 2, 
                       text="Game Over", fill="white")
 
class Snake(Frame):
   def __init__(self, parent):
       Frame.__init__(self, parent)
       parent.title("Snake")
       self.board = Board(parent)
       self.pack()
 
root = Tk()
snake = Snake(root)
root.mainloop()

Đầu tiên chúng ta định nghĩa một số hằng số dùng trong game:

  • WIDTHHEIGHT là kích thước cửa sổ chính.
  • DOT_SIZE là kích thước của mồi và mỗi đốt trên con rắn.
  • ALL_DOTS là số lượng đốt tối đa của rắn. Vì cửa sổ có kích thước 300 * 300, mỗi đốt rắn có kích thước 10 * 10 nến số lượng đốt tối đa là (300 * 300) / (10 * 10) = 900.
  • RAND_POS là hằng số để tính vị trí ngẫu nhiên của mồi.
  • DELAY là tốc độ của game.
x = [0] * ALL_DOTS
y = [0] * ALL_DOTS

Tiếp theo chúng ta tạo 2 mảng x và y, hai mảng này lưu trữ tọa độ của tất cả các đốt trên thân con rắn.

Kế tiếp là phương thức initGame(), nhiệm vụ của phương thức này là khởi tạo toàn bộ mọi thứ có trong game, từ khởi tạo biến, load ảnh đến khởi động timer…

try: 
    self.dotImage = Image.open("dot.png")
    self.dotImage.thumbnail((10, 10), Image.ANTIALIAS)
    self.dot = ImageTk.PhotoImage(self.dotImage)
 
    self.headImage = Image.open("head.png")
    self.headImage.thumbnail((10, 10), Image.ANTIALIAS) 
    self.head = ImageTk.PhotoImage(self.headImage)
 
    self.appleImage = Image.open("apple.png")
    self.appleImage.thumbnail((10, 10), Image.ANTIALIAS) 
    self.apple = ImageTk.PhotoImage(self.appleImage)
 
except (IOError, e):
    print (e)
    sys.exit(1)

Đoạn code trên thực hiện load ảnh dùng để hiển thị mồi và các đốt của con rắn. Do mỗi đốt của con rắn và mồi có kích thước 10×10 pixel nên chúng ta nên resize kích thước ảnh về 10×10 phòng trường hợp ảnh của chúng ta quá lớn bằng phương thức thumbnail().

self.createObjects()
self.locateApple()

Phương thức createObjects() có nhiệm vụ vẽ các đối tượng lên canvas. Phương thức locateApple() tạo tọa độ ngẫu nhiên của mồi và vẽ lên canvas.

self.bind_all("<Key>", self.onKeyPressed)

Tiếp theo chúng ta gọi phương thức bind_all() để gắn sự kiện bấm phím vào phương thức onKeyPressed().

def createObjects(self):

    self.create_image(self.apple_x, self.apple_y, image=self.apple,
        anchor=NW, tag="apple")
    self.create_image(50, 50, image=self.head, anchor=NW,  tag="head")
    self.create_image(30, 50, image=self.dot, anchor=NW, tag="dot")
    self.create_image(40, 50, image=self.dot, anchor=NW, tag="dot")

Bên trong phương thức createObjects() chúng ta vẽ các đối tượng lên canvas bao gồm mồi và 3 đốt đầu của con rắn. Ý nghĩa của các tham số đã được giải thích trong bài viết trước ngoại trừ tham số tag. Tham số tag đơn giản là giống như chúng ta đưa một ID cho ảnh vậy thôi, về sau chúng ta sẽ có các phương thức tìm các đối tượng này nhờ vào tag.

Bên trong phương thức checkApple() chúng ta kiểm tra xem con rắn có ăn trúng mồi hay không, nếu có thì thêm một đốt vào sau con rắn và gọi đến phương thức locateApple() để khởi tạo mồi mới.

apple = self.find_withtag("apple")
head = self.find_withtag("head")

Phương thức find_withtag() sẽ tìm đối tượng trên canvas dựa vào tag của đối tượng đó. Ở đây chúng ta cần tìm 2 đối tượng là mồi và đầu của con rắn – đốt đầu tiên.

x1, y1, x2, y2 = self.bbox(head)
overlap = self.find_overlapping(x1, y1, x2, y2)

Phương thức bbox() trả về một tuple là tọa độ điểm trái-trên và phải-dưới của một đối tượng. Phương thức find_overlapping() sẽ tìm các đối tượng nằm đè lên nhau. Tức là ở đây chúng ta kiểm tra xem đầu con rắn có chạm vào mồi hay không.

for ovr in overlap:
  
    if apple[0] == ovr:
        x, y = self.coords(apple)
        self.create_image(x, y, image=self.dot, anchor=NW, tag="dot")
        self.locateApple()

Nếu đầu con rắn chạm vào mồi thì chúng ta vẽ một đốt mới tại vị trí của mồi sau đó gọi đến phương thức locateApple() để khởi tạo mồi mới.

Bên trong phương thức doMove() chúng ta kiểm tra hướng đi hiện tại của con rắn và cập nhật tọa độ của tất cả các đốt trên con rắn.

z = 0
while z < len(items)-1:
    c1 = self.coords(items[z])
    c2 = self.coords(items[z+1])
    self.move(items[z], c2[0]-c1[0], c2[1]-c1[1])
    z += 1

Tọa độ của các đốt phía sau sẽ bằng tọa độ của đốt liền trước đó. Riêng tọa độ của đầu thì dựa trên hướng đi hiện tại.

Trong phương thức checkCollisions() chúng ta kiểm tra xem con rắn có cắn phải mình hay đụng tường hay không.

x1, y1, x2, y2 = self.bbox(head)
overlap = self.find_overlapping(x1, y1, x2, y2)

for dot in dots:
    for over in overlap:
        if over == dot:
          self.inGame = False

Nếu có thì chúng ta cho thuộc tính inGameFalse tức là kết thúc game.

if y1 > HEIGHT - DOT_SIZE:
    self.inGame = False

Phương thức locateApple() sẽ tạo ngẫu nhiên mồi mới trên màn hình.

apple = self.find_withtag("apple")
self.delete(apple[0])

Trước hết chúng ta xóa mồi cũ đi.

r = random.randint(0, RAND_POS)

Sau đó dùng phương thức randint() để lấy một số ngẫu nhiên từ 0 đến RAND_POS – 1.

self.apple_x = r * DOT_SIZE
...
self.apple_y = r * DOT_SIZE

Hai dòng trên set tọa độ của mồi.

Trong phương thức onKeyPressed() chúng ta kiểm tra sự kiện bấm phím.

if key == "Left" and not self.right: 
    self.left = True
    self.up = False
    self.down = False

Trong trường hợp trên chúng ta kiểm tra nếu người dùng bấm phim mũi trên trái và hướng đi hiện tại của con rắn không phải hướng bên phải thì set các thuộc tính leftTrue, các thuộc tính còn lại là False. Tương tự với các hướng còn lại.

def onTimer(self):

    if self.inGame:
        self.checkCollisions()
        self.checkApple()
        self.doMove()
        self.after(DELAY, self.onTimer)
    else:
        self.gameOver() 

Bên trong phương thức onTimer(), chúng ta thực hiện một số công việc cứ sau 100 mili giây (lưu trong hằng số DELAY). Chúng ta kiểm tra sự va chạm của con rắn với mồi hay tường… cập nhật tọa độ con rắn rồi gọi phương thức after() một lần nữa để đồng hồ chạy tiếp 100ms và lại tiếp tục kiểm tra. Nếu vì một lý do gì đó mà thuộc tính inGame là False thì chúng ta gọi phuonwg thức gameOver().

def gameOver(self):

    self.delete(ALL)
    self.create_text(self.winfo_width()/2, self.winfo_height()/2, 
        text="Game Over", fill="white")     

Bên trong phương thức gameOver() chúng ta xóa các đối tượng ra khỏi bộ nhớ và in dòng chữ “Game Over” lên giữa màn hình.

Untitled
4.8 5 votes
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

11 Comments
Inline Feedbacks
View all comments
minh
minh
7 năm trước

code sai rồi

tuấn
tuấn
7 năm trước

sao em ko chạy được nhỉ

Minh
Minh
6 năm trước

bạn ơi có thể giải thích thêm cho mình về 2 hàm find_withtag và find_overlapping đc không? cách dùng thế nào? 2 hàm đó trả về giá trị cụ thể là gì? Mình cảm ơn.

Đỗ Linh
Đỗ Linh
4 năm trước

PIL này ở python 3. có cần cài k admin

Đỗ Linh
Đỗ Linh
4 năm trước

PIL này ở python 3. có cần cài k ad

Đỗ Linh
Đỗ Linh
4 năm trước

cho e hỏi sao của e nó bảo là k tìm thấy file ‘dot.png’ ạ

Đỗ Linh
Đỗ Linh
4 năm trước
Reply to  Đỗ Linh

Vâng e đã tìm đc nguyên nhân xl vì hỏi ngu ạ:)))

Jordan
3 năm trước

Source code đâu ad?