Processing でグラフを描く⑳ ライフゲーム
Processing でグラフを描く 第20回目です。
プログラミングで人工生命を作ってみます。生命を作るというと、錬金術やフランケンシュタインなどのマッドサイエンティストを思い浮かべるかもしれません。今回の生命はそんなおどろおどろしいものではありませんから、ご安心を。
今回作る生命は、あるルールに基づいて誕生、進化、淘汰(生存 または 死亡)を行うコンピューター上の疑似生命です。もっとも有名な人工生命の一つである「ライフゲーム」を Processing (Python mode) で再現します。縦横に並んだ四角(セル)の一つ一つを生命と考え、隣り合ったセルとの関係で誕生、淘汰を決めていくもっとも簡単なモデルの一つです。
では生命誕生のプログラムを作っていきましょう。
空間の中に生命を表示する
COLUMN_NUM = 300
ROW_NUM = 300
CELL_SIZE = 2
size_x = COLUMN_NUM * CELL_SIZE
size_y = ROW_NUM * CELL_SIZE
cells = []
def setup():
size(size_x, size_y)
noStroke()
frameRate(5)
# 初期状態(ランダム 生存率0.5)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if random(1) < 0.5:
cells[j].append(1)
else:
cells[j].append(0)
def draw():
global generation, cells
# セルを表示
display()
def display():
for j in range(ROW_NUM):
for i in range(COLUMN_NUM):
cell = cells[j][i]
if cell == 1:
fill(0) # 黒
else:
fill(255) # 白
rect(i * CELL_SIZE, j * CELL_SIZE, CELL_SIZE, CELL_SIZE)
ライフゲームにおいては、空間を碁盤のように正方形で区切り、その正方形をセル(細胞)と呼びます。セルは生存(黒)、死亡(白)の二つの状態を取ります。上記のコードは、生存率50% でランダムに配置したセルを表示するプログラムです。
プログラムの簡単な説明をすると、変数COLUMN_NUM, ROW_NUM が横、縦のセルの数を表しています。つまり今回は $${300 \times 300 = 90,000}$$ 個のセルが表示されます。
リストcells は空間におけるセルの生存状態を記録しています。setup関数の中で、確率50% で 1(生存)、残りを 0(死亡)を記録することで約半分のセルが生存(黒)で表示されることになります。これが初期状態です。
次に、環境によりセルを誕生、淘汰(生存、死亡)させてみましょう。
環境により生命を淘汰する
COLUMN_NUM = 300
ROW_NUM = 300
CELL_SIZE = 2
size_x = COLUMN_NUM * CELL_SIZE
size_y = ROW_NUM * CELL_SIZE
cells = []
RULE_TO_SURVIVE = [2, 3] # 生き残るための条件
RULE_TO_BE_BORN = [3] # 生まれるための条件
generation = 0 # 世代
AUTO_UPDATE = True # 自動で世代交代する
def setup():
size(size_x, size_y)
noStroke()
frameRate(5)
# 初期状態(ランダム 生存率0.5)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if random(1) < 0.5:
cells[j].append(1)
else:
cells[j].append(0)
def draw():
global generation, cells
# セルを表示
display()
if AUTO_UPDATE:
# セルを更新
cells = update()
generation += 1
fill(255)
rect(0, 0, 130, 65)
fill(0)
text('generation: ' + str(generation), 10, 15)
text('rule to survive: ' + str(RULE_TO_SURVIVE), 10, 30)
text('rule to be born: ' + str(RULE_TO_BE_BORN), 10, 45)
text('random 0.5 ', 10, 60)
def display():
for j in range(ROW_NUM):
for i in range(COLUMN_NUM):
cell = cells[j][i]
if cell == 1:
fill(0) # 黒
else:
fill(255) # 白
rect(i * CELL_SIZE, j * CELL_SIZE, CELL_SIZE, CELL_SIZE)
def update():
new_cells = []
for j in range(ROW_NUM):
new_cells.append([])
for i in range(COLUMN_NUM):
new_cells[j].append(check_neighbors(i, j))
return new_cells
def check_neighbors(i, j):
cell = cells[j][i]
neighbors = 0
# 隣接セルの生存しているセルの数を数える
for diff_row in range(-1, 2): # -1, 0, 1
for diff_column in range(-1, 2): # -1, 0, 1
if not (diff_row == 0 and diff_column == 0):
try:
if cells[j + diff_row][i + diff_column] == 1:
neighbors += 1
except IndexError:
continue
if cell == 1:
# 生存条件を満たさないときは死亡
if neighbors not in RULE_TO_SURVIVE:
cell = 0
else:
# 誕生条件を満たすときは生存
if neighbors in RULE_TO_BE_BORN:
cell = 1
return cell
def mousePressed():
global cells, generation
# セルを更新
cells = update()
generation += 1
環境によりセルを誕生、淘汰(生存、死亡)させて、120世代記録しました。まるでアメーバのような生き物がうごめいていています。よく観察すると、静止している部分と繰り返している部分、そしてランダムに動き回る部分が見て取れます。簡単なプログラムからこんな複雑な動きが現れるのは驚異です。
誕生、淘汰の条件を図にしました。ライフゲームの基本的なルールはたった2つです。
死んでいるセルに近接する生きているセルの数が3のとき、誕生する
生きているセルに近接するセルの数が2または3のとき、生存する
RULE_TO_SURVIVE = [2, 3] # 生き残るための条件
RULE_TO_BE_BORN = [3] # 生まれるための条件
def check_neighbors(i, j):
cell = cells[j][i]
neighbors = 0
# 隣接セルの生存しているセルの数を数える
for diff_row in range(-1, 2): # -1, 0, 1
for diff_column in range(-1, 2): # -1, 0, 1
if not (diff_row == 0 and diff_column == 0):
try:
if cells[j + diff_row][i + diff_column] == 1:
neighbors += 1
except IndexError:
continue
if cell == 1:
# 生存条件を満たさないときは死亡
if neighbors not in RULE_TO_SURVIVE:
cell = 0
else:
# 誕生条件を満たすときは生存
if neighbors in RULE_TO_BE_BORN:
cell = 1
return cell
淘汰のルールを Python で実装したコードです。このルールは $${23/3}$$ と呼ばれます。2重の for文を使って、隣り合ったセルを調べて、生存セルの数を数えます。そしてルール $${23/3}$$ に基づき生存、死亡を決定していきます。
def update():
new_cells = []
for j in range(ROW_NUM):
new_cells.append([])
for i in range(COLUMN_NUM):
new_cells[j].append(check_neighbors(i, j))
return new_cells
update関数で、次の世代のセルの状態を決定します。ここで注意が必要なのは、リストcells を直接編集しないようにすることです。Python の参照渡しというルールにより、編集した結果はただちにリストに反映されます。その結果、反映された結果に基づいて、次の計算が行われるため、ただしく淘汰が行われなくなるのです。かならず、新しいリストnew_cells を作って、それをリストcells に代入するようにしましょう(私はここで半日つまってしまった…)
初期状態を変更してみる
# 初期状態(ランダム 生存率0.3)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if random(1) < 0.3:
cells[j].append(1)
else:
cells[j].append(0)
ここからは初期条件を変えて、生命がどう変化するか見ていきましょう。初期条件で生存率を30% にしてみました。先ほどの50% と比べて、若干生命が少なくなったように見えます。
# 初期状態(ランダム 生存率0.1)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if random(1) < 0.1:
cells[j].append(1)
else:
cells[j].append(0)
初期条件で生存率を10% にしてみました。50% と比べて、明らかに生命が少なくなり、白の部分が多くなりました。
# 初期状態(ランダム 生存率0.01)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if random(1) < 0.01:
cells[j].append(1)
else:
cells[j].append(0)
# # 初期状態(十文字1)
初期条件で生存率を1% にしてみました。この条件では生命は生存状態を維持できずにすぐに絶滅してしまいました。つまり 1% から 10% の間に生命を維持する最低条件があるようです。興味のある方は調査してみましょう。
# 初期状態(十文字1)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if j == int(ROW_NUM / 2) or i == int(COLUMN_NUM / 2):
cells[j].append(1)
else:
cells[j].append(0)
今度は決められた形からスタートします。十文字(1列)からスタートしたライフゲームは、ジェネラティブアートを描きます。中央部分は繰り返し構造となり、安定した形を維持し続けます。
# 初期状態(十文字2)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if int(ROW_NUM / 2) <= j <= int(ROW_NUM / 2) + 1 or \
int(COLUMN_NUM / 2) <= i <= int(COLUMN_NUM / 2) + 1:
cells[j].append(1)
else:
cells[j].append(0)
先ほどと似ていますが、次は十文字で2列(2行)が生存している初期条件です。これもジェネラティブアートになりますが、先ほどと形が変わっています。繰り返し構造が美しいです。
# 初期状態(十文字3)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if int(ROW_NUM / 2) - 1 <= j <= int(ROW_NUM / 2) + 1 or \
int(COLUMN_NUM / 2) - 1 <= i <= int(COLUMN_NUM / 2) + 1:
cells[j].append(1)
else:
cells[j].append(0)
最後は、十文字で3列(3行)が生存している初期条件です。また違った模様のジェネラティブアートになりました。面白いですね。いつまでも遊んでいられます。
境界に関する考察
# 初期状態(ランダム 生存率0.5 エリア200x200)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if random(1) < 0.5 and \
50 <= i < 250 and \
50 <= j < 250:
cells[j].append(1)
else:
cells[j].append(0)
境界については今まで触れていませんでした。表示された画像の外側がどうなっていくか気になった方もおられるでしょう。画像サイズは同じにして、初期条件でランダム50% 範囲200x200 に生存セルを置いたときの変化を記録しました。
範囲200x200 の外まで伸びて、止まってしまう部分、動き続けている部分が確認できました。120世代まで記録しましたが、果たして永遠に伸び続ける部分があるのでしょうか。その研究はすでになされており、あるセルの配置からは無限にセルが伸びていくことが示されています。興味深い研究結果です。
別のルールを試してみる
RULE_TO_SURVIVE = [2, 3] # 生き残るための条件
RULE_TO_BE_BORN = [3, 6] # 生まれるための条件
def setup():
size(size_x, size_y)
noStroke()
frameRate(5)
# 初期状態(ランダム 生存率0.5)
for j in range(ROW_NUM):
cells.append([])
for i in range(COLUMN_NUM):
if random(1) < 0.5:
cells[j].append(1)
else:
cells[j].append(0)
隣接したセルの数が2 , 3 のとき生存、3 のとき誕生が「ルール 23/3」と呼ばれます。今度は別のルールを考えてみましょう。
隣接したセルの数が2 , 3 のとき生存、3, 6 のとき誕生が「ルール 23/36」です。これを再現するには、リストRULE_TO_BE_BORN = [3, 6] と代入します。実行した結果を見ると、ルール 23/3 よりも若干生存セルが多くなったように見えます。また安定して生存を維持できる(絶滅しない)ことも見て取れました。
今回はライフゲームで遊んでみました。一度プログラムを組み上げてしまえば、いろいろ条件を変えて試してみるのは簡単です。予想を立てながら、パラメーター(初期条件)を変えてみると答え合わせができて楽しいです。今回のプログラムを改造して楽しんでください。
前の記事
Processing でグラフを描く⑲ 遺伝的アルゴリズム(後編)
次の記事
Processing でグラフを描く㉑ ボイドモデル
その他のタイトルはこちら