這一節的標題是
Variations on Traditional CA
因為方格子標題字數限制,所以沒完整顯現
CA的細胞長相是不是一定就是方形的呢?當然不是!它可以有各式各樣的形狀,甚至於也可以有各種不同的性質及演化方式;唯一的限制,就是你的想像力。這一節就來看看,利用先前所寫的CA程式,還可以玩出什麼不同的花樣出來。
Nonrectangular Grids
CA細胞的形狀不是非得方形不可,排列的方式也不是非得像棋盤狀不可。
Exercise 7.8
要畫正六邊形,可以先找出六個頂點,然後用pygame的draw.polygon()
方法來畫。假設中心點位於(0, 0),因為正六邊形是由六個正三角形拼排而成,頂點的位置很容易就能從圖形直接算出來。寫程式時,利用極座標轉直角坐標的方式來寫,會比較省時省力。
在細胞的排列方面,由圖可以看出來,直行間隔為1.5w,而橫列間隔則為2h。不過要注意的是,相鄰兩直行的細胞,其中心點的位置在y方向偏移了h的距離。
完整的程式及執行結果如下:

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):
center = pygame.Vector2(self.x, self.y)
vertices = []
for theta in range(0, 360, 60):
vertex = center + pygame.Vector2.from_polar((self.cell_size, theta))
vertices.append(vertex)
pygame.draw.polygon(self.screen, (0, 0, 0), vertices, 1-self.state)
# python version 3.10.9
import math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.8")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 1
frame_rate = pygame.time.Clock()
# 細胞尺寸
cell_size = 20
h = cell_size*math.cos(math.pi/6)
columns, rows = int(WIDTH/(1.5*cell_size))+1, int(HEIGHT/(2*h))+1
board = [[0]*columns for _ in range(rows)]
for j in range(columns):
x = 1.5*cell_size*j
for i in range(rows):
y = (2*i+1-j%2)*h
board[i][j] = Cell(0, x, y, cell_size)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 畫出細胞
for i in range(rows):
for j in range(columns):
board[i][j].state = random.randint(0, 1)
board[i][j].show()
pygame.display.update()
frame_rate.tick(FPS)
Probabilistic
CA的迭代規則也可以摻雜有機率的成分。
Exercise 7.9
以物件導向的方式來寫,加入新規則後之程式碼為:
# 依據規則決定細胞的新狀態
if board[i][j].previous == 1 and neighbor_sum >= 4:
# 規則 1;過度擁擠,有80%的機率死亡
board[i][j].state = 0 if random.random() < 0.8 else 1
elif board[i][j].previous == 1 and neighbor_sum <= 1:
# 規則 1;孤單,有60%的機率死亡
board[i][j].state = 0 if random.random() < 0.6 else 1
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
Continuous
細胞的狀態值並不是非要整數不可,也可以是浮點數。
Exercise 7.10
修改Exercise 7.1程式,將規則集設定成0~1間的亂數,並修改rules()
函數中關於索引值的計算方式。另外,畫細胞時,只畫出狀態值>=0.5
的細胞。

def rules(left, middle, right, ruleset):
idx = int(4*left + 2*middle + right) % 8
return ruleset[7-idx]
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.10")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 1
frame_rate = pygame.time.Clock()
# 細胞方塊邊長
cell_size = 10
# 細胞數量
n_cells = WIDTH // cell_size
# 世代數量
n_generation = HEIGHT // cell_size
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 規則
ruleset = [random.random() for _ in range(8)]
# 初代CA
cells = [0]*n_cells
cells[n_cells//2] = 0.5
next_generation = cells.copy()
for generation in range(n_generation):
for i in range(n_cells):
# 只畫出狀態值>=0.5的細胞
if cells[i] >= 0.5:
rect = pygame.Rect(i*cell_size, generation*cell_size, cell_size, cell_size)
pygame.draw.rect(screen, (0, 0, 0), rect)
for i in range(1, n_cells-1):
left = cells[i-1]
middle = cells[i]
right = cells[i+1]
next_generation[i] = rules(left, middle, right, ruleset)
cells = next_generation.copy()
pygame.display.update()
frame_rate.tick(FPS)
Image Processing
有許多影像處理(image processing)演算法的做法,其實和CA的做法很類似;例如,要讓影像變模糊時,就是把像素的顏色,設定成周遭像素顏色的平均值。
Exercise 7.11
設定細胞的大小為1個像素,而其狀態值是(r, g, b)
,也就是一個包含三個元素的tuple
。其中r
、g
、b
都是介於0~255的整數。

def rules(left, middle, right, ruleset):
r = (left[0] + middle[0] + right[0]) % 256
g = (left[1] + middle[1] + right[1]) % 256
b = (left[2] + middle[2] + right[2]) % 256
return (ruleset[r], ruleset[g], ruleset[b])
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.11")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)
FPS = 1
frame_rate = pygame.time.Clock()
# 細胞數量
n_cells = WIDTH
# 世代數量
n_generation = HEIGHT
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 隨機選取的規則集
ruleset = [random.randint(0, 255) for _ in range(256)]
# 初代CA
cells = [(0, 0, 0)]*n_cells
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
cells[random.randint(0, n_cells)] = color
next_generation = cells.copy()
for generation in range(n_generation):
# 畫出細胞
for i in range(n_cells):
screen.set_at((i, generation), cells[i])
for i in range(1, n_cells-1):
left, middle, right = cells[i-1], cells[i], cells[i+1]
next_generation[i] = rules(left, middle, right, ruleset)
cells = next_generation.copy()
pygame.display.update()
frame_rate.tick(FPS)
Historical
在用物件導向的寫法寫「生命遊戲」時,是用兩個變數來記錄細胞在當前及上一世代時的狀態值。那如果我們用一個list
來記錄、追蹤細胞在很長一段期間內的狀態值,不就能讓細胞可以根據過往的生命歷程,隨著時間的流逝,不斷地調整它的活動規則嗎?這個就是後續會再詳細介紹的複雜適應性系統(complex adaptive system)的概念。
Exercise 7.12
利用一個list
來記錄細胞在最近100個世代中的狀態,並依據記錄中細胞曾經活著的世代總數來設定細胞的顏色。另外,如果細胞在有記錄的所有世代中,都持續維持相同的狀態,則會有50%的機會改變其狀態。

class Cell:
def __init__(self, state, x, y, cell_size):
self.screen = pygame.display.get_surface()
# 當前狀態
self.state = state
# 狀態歷程
self.history = [state]
# 細胞位置
self.x, self.y = x, y
self.cell_size = cell_size
def remember_state(self):
self.history.append(self.state)
# 只記錄100世代內的狀態
if len(self.history) > 100:
del self.history[0]
def show(self):
# 依據記錄中曾經活著的世代總數來設定細胞的顏色
n = sum(self.history)
match n:
case n if n < 25:
color = (0, 0, 0)
case n if 25 <= n < 50:
color = (255, 0, 0)
case n if 50 <= n < 75:
color = (0, 255, 0)
case _:
color = (0, 0, 255)
rect = pygame.Rect(self.x, self.y, self.cell_size, self.cell_size)
if self.state == 0:
pygame.draw.rect(screen, color, rect, 1)
else:
pygame.draw.rect(screen, color, rect)
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.12")
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].state = random.randint(0, 1)
board[i][j].remember_state()
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.history[-1] for cell in board[i-1][j-1:j+2]) + \
sum(cell.history[-1] for cell in board[i][j-1:j+2:2]) + \
sum(cell.history[-1] for cell in board[i+1][j-1:j+2])
# 依據規則決定細胞的新狀態
if board[i][j].history[-1] == 1 and (neighbor_sum >= 4 or neighbor_sum <= 1):
# 規則 1
board[i][j].state = 0
elif board[i][j].history[-1] == 0 and neighbor_sum == 3:
# 規則 2
board[i][j].state = 1
else:
# 規則 3
board[i][j].state = board[i][j].history[-1]
# 如果細胞在歷史記錄中從未改變狀態,則有50%的機會改變狀態
n = sum(board[i][j].history)
if (n in [0, len(board[i][j].history)]) and (random.random() < 0.5):
board[i][j].state = 1 if n==0 else 0
# 畫出細胞並記錄其狀態
for i in range(rows):
for j in range(columns):
board[i][j].show()
board[i][j].remember_state()
pygame.display.update()
frame_rate.tick(FPS)
Moving Cells
細胞不一定非得待在同樣的位置上不可,它也可以在畫面上到處亂跑。
Exercise 7.13
修改Example 5.12。在Boid
類別的__init__()
方法中加入
self.state = random.random()
來使boid
具有state
屬性,並設定起始值為0~1間的亂數。
修改flock()
方法,讓狀態值隨周遭同伴的數量是否適中而升、降,並讓狀態值成為影響boid
行動能力的因素。
def flock(self, boids):
# 周遭同伴數量適中,則狀態值升高,行動能力提升,
# 否則狀態值降低,行動能力變弱
n = len(boids)
if 50 <= n <= 100:
self.state += 0.001
else:
self.state -= 0.001
# 狀態值0,移動速度會變慢
if self.state < 0:
self.state = 0
self.velocity *= 0.9
separation = self.separate(boids)
alignment = self.align(boids)
coherence = self.cohere(boids)
# 狀態值會強化或減弱轉向力
steer = self.state*(1.5*separation + alignment + coherence)
self.apply_force(steer)
Nesting
複雜系統的一個特徵是,它可以層層套疊而形成一個更大的複雜系統。例如,城市是由人所組成的複雜系統,人是由器官所組成的複雜系統,而器官則是由細胞所組成的複雜系統。事實上,這種層層套疊的方式,也是我們在研究、處理許多事物時,會採用的方式。例如在第四章中,我們讓許多粒子組成一個粒子系統,然後再讓許多粒子系統組成一個更大的粒子系統;如果有需要,可以就這麼繼續下去。
上述這種層層套疊的方式,也可以應用到CA上嗎?當然可以!我們可以把好幾個CA所組成的系統,看作是一個更大CA系統的細胞;這種方式可以一直持續下去,直到你滿意為止。
Exercise 7.14
讓「生命遊戲」的細胞由基礎CA所構成。當基礎CA的細胞有超過半數以上活著時,「生命遊戲」的細胞會變成紅色。另外,當「生命遊戲」的細胞活著時,基礎CA細胞的狀態值會隨機產生,而不是依照規則集產生。

class Cell:
def __init__(self, state, x, y, cell_size):
self.screen = pygame.display.get_surface()
# 當前與前一世代時的狀態
self.state = state
self.previous = state
# 細胞位置與尺寸
self.x, self.y = x, 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)
class ElementaryCA():
def __init__(self, state, x, y, size, rule_number, length):
self.screen = pygame.display.get_surface()
# 當前與前一世代時的狀態
self.state = state
self.previous = self.state
# 位置與尺寸
self.x, self.y = x, y
self.size = size
# 基礎CA所含細胞數量
self.length = length
# 細胞迭代規則集
self.ruleset = [int(c) for c in f'{rule_number:08b}']
# 細胞初始狀態
cells_state = [int(c) for c in f'{self.state:0{self.length}b}']
self.cells = [Cell(cells_state[i], i, 1, 1) for i in range(self.length)]
def calculate_state(self):
# 計算細胞狀態。當基礎CA的狀態值為1時,其細胞的狀態值為隨機產生。
for i in range(self.length):
left = self.cells[(i-1)%self.length].previous
middle = self.cells[i].previous
right = self.cells[(i+1)%self.length].previous
idx = 4*left + 2*middle + right
self.cells[i].state = self.ruleset[7-idx] if self.state==0 else random.randint(0, 1)
# 記住細胞當前的狀態
for i in range(self.length):
self.cells[i].previous = self.cells[i].state
def show(self):
# 如果有超過半數以上的細胞活著,則基礎CA為紅色;否則為黑色
color = (255, 0, 0) if sum(cell.state for cell in self.cells)>self.length/2 else (0, 0, 0)
rect = pygame.Rect(self.x, self.y, self.size, self.size)
pygame.draw.rect(screen, color, rect, 1-self.state)
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 7.14")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 750, 360
screen = pygame.display.set_mode(screen_size)
FPS = 10
frame_rate = pygame.time.Clock()
# 基礎CA方塊邊長
ECA_size = 20
# 基礎CA規則集
rule_number = 90
# 基礎CA細胞數量
length = 3
# 側邊欄寬度
sidebar = 150
board_width = WIDTH - sidebar
columns, rows = board_width//ECA_size, HEIGHT//ECA_size
board = [[ElementaryCA(0, j*ECA_size, i*ECA_size, ECA_size, rule_number, length)
for j in range(columns)] for i in range(rows)]
screen.fill(WHITE)
# 狀態欄及按鈕文字
font = pygame.font.SysFont(None, 32)
text_status = font.render('', True, (0, 0, 0))
text_start = font.render('Start', True, (255, 255, 255), (0, 0, 0))
text_pause = font.render('Pause', True, (255, 255, 255), (0, 0, 0))
text_reset = font.render('Reset', True, (255, 255, 255), (0, 0, 0))
# 狀態欄及按鈕位置
status_block = text_status.get_rect()
status_block.topleft = (board_width+15, 50)
button_start = text_start.get_rect()
button_start.topleft = (board_width+35, 100)
button_pause = text_pause.get_rect()
button_pause.topleft = (board_width+35, 130)
button_reset = text_reset.get_rect()
button_reset.topleft = (board_width+35, 160)
evolving = False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
x, y = event.pos
if x <= board_width:
row, column = y//ECA_size, x//ECA_size
board[row][column].state = 1 - board[row][column].state
elif button_start.collidepoint(x, y):
evolving = True
text_status = font.render('Evolving...', True, (0, 0, 0))
elif button_pause.collidepoint(x, y):
evolving = False
text_status = font.render('Pausing...', True, (0, 0, 0))
elif button_reset.collidepoint(x, y):
evolving = False
text_status = font.render('', True, (0, 0, 0))
board = [[ElementaryCA(0, j*ECA_size, i*ECA_size, ECA_size, rule_number, length)
for j in range(columns)] for i in range(rows)]
if evolving:
# 計算細胞新的狀態
for i in range(rows):
for j in range(columns):
board[i][j].calculate_state()
for i in range(1, rows-1):
for j in range(1, columns-1):
# 計算活著的鄰居數量
neighbor_sum = -board[i][j].previous
for m in range(-1, 2):
row = (i+m) % rows
for n in range(-1, 2):
col = (j+n) % columns
neighbor_sum += board[row][col].previous
# 依據規則決定細胞的新狀態
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
# 畫出ECA並記錄其狀態
for i in range(rows):
for j in range(columns):
board[i][j].show()
board[i][j].previous = board[i][j].state
# 放置狀態欄及按鈕
screen.blit(text_status, status_block.topleft)
screen.blit(text_start, button_start.topleft)
screen.blit(text_pause, button_pause.topleft)
screen.blit(text_reset, button_reset.topleft)
pygame.display.update()
frame_rate.tick(FPS)