The Nature of Code閱讀心得與Python實作:7.6 Variations on...

更新 發佈閱讀 42 分鐘
這一節的標題是
Variations on Traditional CA
因為方格子標題字數限制,所以沒完整顯現

CA的細胞長相是不是一定就是方形的呢?當然不是!它可以有各式各樣的形狀,甚至於也可以有各種不同的性質及演化方式;唯一的限制,就是你的想像力。這一節就來看看,利用先前所寫的CA程式,還可以玩出什麼不同的花樣出來。

Nonrectangular Grids

CA細胞的形狀不是非得方形不可,排列的方式也不是非得像棋盤狀不可。

Exercise 7.8

vocus|新世代的創作平台

要畫正六邊形,可以先找出六個頂點,然後用pygame的draw.polygon()方法來畫。假設中心點位於(0, 0),因為正六邊形是由六個正三角形拼排而成,頂點的位置很容易就能從圖形直接算出來。寫程式時,利用極座標轉直角坐標的方式來寫,會比較省時省力。

在細胞的排列方面,由圖可以看出來,直行間隔為1.5w,而橫列間隔則為2h。不過要注意的是,相鄰兩直行的細胞,其中心點的位置在y方向偏移了h的距離。

完整的程式及執行結果如下:

vocus|新世代的創作平台
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的細胞。

vocus|新世代的創作平台
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。其中rgb都是介於0~255的整數。

vocus|新世代的創作平台
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%的機會改變其狀態。

vocus|新世代的創作平台
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細胞的狀態值會隨機產生,而不是依照規則集產生。

vocus|新世代的創作平台
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)



留言
avatar-img
ysf的沙龍
27會員
168內容數
寫點東西自娛娛人
ysf的沙龍的其他內容
2025/05/12
這一節要用物件導向的方式來寫「生命遊戲」。
Thumbnail
2025/05/12
這一節要用物件導向的方式來寫「生命遊戲」。
Thumbnail
2025/05/05
這一節介紹的是赫赫有名的2D CA:生命遊戲(the Game of Life)。
Thumbnail
2025/05/05
這一節介紹的是赫赫有名的2D CA:生命遊戲(the Game of Life)。
Thumbnail
2025/04/04
基礎CA的規則集總共有256個,經由這些規則集所生成的圖案,大部分看起來都平平無奇;不過,還是有些圖案真的是會讓人驚嘆不已,因為實在是跟自然界中可以看到的圖案樣式很相像。根據這些圖案的樣式和特性,Wolfram把它們分成四大類,接下來就來看看,這四大類的圖案各有怎樣的樣式和特性。
Thumbnail
2025/04/04
基礎CA的規則集總共有256個,經由這些規則集所生成的圖案,大部分看起來都平平無奇;不過,還是有些圖案真的是會讓人驚嘆不已,因為實在是跟自然界中可以看到的圖案樣式很相像。根據這些圖案的樣式和特性,Wolfram把它們分成四大類,接下來就來看看,這四大類的圖案各有怎樣的樣式和特性。
Thumbnail
看更多
你可能也想看
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
在模擬自然界中的事物時導入隨機性,可以讓結果看起來比較自然,但如果導入的隨機性都是uniform distribution,那未免也太呆板了。這時候,我們需要nonuniform distribution亂數,來讓模擬出來的結果,更像真的一樣。
Thumbnail
在模擬自然界中的事物時導入隨機性,可以讓結果看起來比較自然,但如果導入的隨機性都是uniform distribution,那未免也太呆板了。這時候,我們需要nonuniform distribution亂數,來讓模擬出來的結果,更像真的一樣。
Thumbnail
高中數學主題練習—根式化簡
Thumbnail
高中數學主題練習—根式化簡
Thumbnail
高中數學主題練習—對數方程式
Thumbnail
高中數學主題練習—對數方程式
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
高中數學主題練習—根式化簡
Thumbnail
高中數學主題練習—根式化簡
Thumbnail
解答來囉 (AI設計的縮圖代表算數學很耗能,沒錯!)
Thumbnail
解答來囉 (AI設計的縮圖代表算數學很耗能,沒錯!)
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News