這一節要用物件導向的方式來寫「生命遊戲」。
既然先前沒有用物件導向的方式來寫都可以處理得很好,那為什麼要用物件導向的方式來寫呢?簡單來說,那是因為使用物件導向式的寫法,可以創造更多的可能性!
當細胞只有「狀態」這個屬性時,一個list的元素,就足以描述一個細胞的所有性質了;這也就是先前非物件導向式的寫法不會過於難寫的原因。可是,如果我們希望細胞除了「死」和「活」之外,也能夠有其他更複雜的行為或屬性,像是會到處亂跑、隨著環境而改變顏色,甚或本身就有一定長度的壽命,歷經一定數量的世代之後,就再也無法活過來等等,這些如果不用物件導向的方式來寫,那就真的是自找麻煩了。要用物件導向的方式來寫「生命遊戲」,第一步當然就是要先造出能夠描述細胞的類別。假設類別的名稱是Cell
,而屬性有細胞的狀態、位置、尺寸等,則Cell
類別可以這樣設計:
class Cell:
def __init__(self, state, x, y, cell_size):
self.screen = pygame.display.get_surface()
# 當前狀態
self.state = state
# 前一世代時的狀態
self.previous = state
self.x = x
self.y = y
self.cell_size = cell_size
在非物件導向式的寫法中,我們是用兩個2D的list來記錄細胞在當前及下一世代時的狀態。在使用物件導向式寫法時,我們稍微改變一下,改成記錄細胞在當前及前一世代時的狀態。要注意的是,因應這樣子的改變,計算細胞當前狀態值的寫法也必須跟著調整;至於要怎麼個調整法,後面再來詳細說明。
先前提到過,利用物件導向式的寫法,可以創造出更多的可能性。例如,我們可以讓細胞在改變狀態時,呈現出有別於黑、白這兩種分別代表生、死的顏色。假設我們要讓細胞在活過來時變成藍色,而由生轉死的時候變成紅色,只要在Cell
類別中加入下面這個方法就可以了:
def show(self):
rect = pygame.Rect(self.x, self.y, self.cell_size, self.cell_size)
if self.previous == 0 and self.state == 1:
# 活過來的細胞塗上藍色
pygame.draw.rect(screen, (0, 0, 255), rect)
elif self.previous == 1 and self.state == 0:
# 由生轉死的細胞塗上紅色
pygame.draw.rect(screen, (255, 0, 0), rect)
elif self.state == 1:
pygame.draw.rect(screen, (0, 0, 0), rect)
else:
pygame.draw.rect(screen, (0, 0, 0), rect, 1)
有了Cell
這個類別之後,就可以造出一個個的細胞物件,並讓它們排列成棋盤狀。不過,這一個個的細胞物件,要用什麼樣的資料結構來處理呢?答案很簡單,就把它們放進一個2D的list中就可以了;這跟前面非物件導向式的寫法其實也沒什麼太大的不同。非物件導向式的寫法是把細胞的狀態放到2D的list中,而物件導向式的寫法,則是把細胞本身,也就是細胞物件,放到2D的list中。要把細胞物件放到2D的list中,程式可以這樣寫:
board = [[Cell(0, j*cell_size, i*cell_size, cell_size) for j in range(columns)] for i in range(rows)]
這樣子,board
中的每個元素,就都是當前及前一世代的狀態值皆為0
的細胞,而其排列的方式,也會對應到細胞實際上棋盤式的排列方式。
造好board
這個2D list之後,要怎麼設定「生命遊戲」的初始樣式呢?假設邊緣細胞自始至終都是死的,而其他細胞的初始狀態則是隨機的;根據這樣子的設定,程式可以這樣寫:
for i in range(1, rows-1):
for j in range(1, columns-1):
board[i][j].previous = board[i][j].state = random.randint(0, 1)
這裡要注意的是,必須把previous
這個屬性的值,設定成和初始狀態值一樣,這樣子在接下來進行第一次迭代的時候,才能夠利用它來計算細胞新的狀態值。
計算細胞新狀態值的寫法,其實和非物件導向式的寫法差不多;重點在於,用於計算的狀態值現在是存放在previous
這個屬性中。假設現在要計算board[i][j]
這個細胞的新狀態值,程式可以這樣寫:
# 計算活著的鄰居數量
neighbor_sum = sum(cell.previous for cell in board[i-1][j-1:j+2]) + sum(cell.previous for cell in board[i][j-1:j+2:2]) + sum(cell.previous for cell in board[i+1][j-1:j+2])
# 依據規則決定細胞的新狀態
if board[i][j].previous == 1 and (neighbor_sum >= 4 or neighbor_sum <= 1):
# 規則 1
board[i][j].state = 0
elif board[i][j].previous == 0 and neighbor_sum == 3:
# 規則 2
board[i][j].state = 1
else:
# 規則 3
board[i][j].state = board[i][j].previous
利用上面提到的方式,就可以把「生命遊戲」改成以物件導向的方式來寫。下面的例子就是以物件導向方式寫成的「生命遊戲」。
Example 7.3: Object-Oriented Game of Life

class Cell:
def __init__(self, state, x, y, cell_size):
self.screen = pygame.display.get_surface()
# 當前狀態
self.state = state
# 前一世代時的狀態
self.previous = state
self.x = x
self.y = y
self.cell_size = cell_size
def show(self):
rect = pygame.Rect(self.x, self.y, self.cell_size, self.cell_size)
if self.previous == 0 and self.state == 1:
# 活過來的細胞塗上藍色
pygame.draw.rect(screen, (0, 0, 255), rect)
elif self.previous == 1 and self.state == 0:
# 由生轉死的細胞塗上紅色
pygame.draw.rect(screen, (255, 0, 0), rect)
elif self.state == 1:
pygame.draw.rect(screen, (0, 0, 0), rect)
else:
pygame.draw.rect(screen, (0, 0, 0), rect, 1)
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 7.3: Object-Oriented Game of Life")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 10
frame_rate = pygame.time.Clock()
# 細胞方塊邊長
cell_size = 10
columns, rows = WIDTH//cell_size, HEIGHT//cell_size
board = [[Cell(0, j*cell_size, i*cell_size, cell_size) for j in range(columns)] for i in range(rows)]
# 設定初始樣式
for i in range(1, rows-1):
for j in range(1, columns-1):
board[i][j].previous = board[i][j].state = random.randint(0, 1)
screen.fill(WHITE)
# 畫出初始樣式
for i in range(rows):
for j in range(columns):
board[i][j].show()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 計算細胞新的狀態;排除位於邊緣的細胞
for i in range(1, rows-1):
for j in range(1, columns-1):
# 計算活著的鄰居數量
neighbor_sum = sum(cell.previous for cell in board[i-1][j-1:j+2]) + \
sum(cell.previous for cell in board[i][j-1:j+2:2]) + \
sum(cell.previous for cell in board[i+1][j-1:j+2])
# 依據規則決定細胞的新狀態
if board[i][j].previous == 1 and (neighbor_sum >= 4 or neighbor_sum <= 1):
# 規則 1
board[i][j].state = 0
elif board[i][j].previous == 0 and neighbor_sum == 3:
# 規則 2
board[i][j].state = 1
else:
# 規則 3
board[i][j].state = board[i][j].previous
# 畫出細胞並記錄其狀態
for i in range(rows):
for j in range(columns):
board[i][j].show()
board[i][j].previous = board[i][j].state
pygame.display.update()
frame_rate.tick(FPS)