Playground Advent Calendar を機に記事初投稿!
ライフゲームとは
以下、wikipediaより引用。下の引用を読むより、リンクに飛んだ方が分かりやすいと思う
ライフゲームでは初期状態のみでその後の状態が決定される。碁盤のような格子があり、一つの格子はセル(細胞)と呼ばれる。各セルには8つの近傍のセルがある。各セルには「生」と「死」の2つの状態があり、あるセルの次のステップ(世代)の状態は周囲の8つのセルの今の世代における状態により決定される。
セルの生死は次のルールに従う。
誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
下に中央のセルにおける次のステップでの生死の例を示す。生きているセルは■、死んでいるセルは□で表す。
使用したパッケージ
import matplotlib.pyplot as plt
import numpy as np
初期状態
n = 50
# ランダムなセルを作成
cells = np.random.randint(0, 2, (n, n), dtype=bool)
True
とFalse
のどちらか一つをランダムに選んだものが要素のn×n行列を作る。(True
なら「生」、False
なら「死」)
(n = 500
くらいまで大きくすると重くなってしまう…)
隣接する「生」のセルの個数を数える
def sum_around(i, j):
return cells[max(0, i-1):i+2, max(0, j-1):j+2].sum() - cells[i, j]
まず、行列のi
行j
列成分を中心とする3×3行列を抽出して、その行列の要素の和を求める。その結果から中心を引くと、i
行j
列成分に隣接する「生」のセルの個数が求められる。(True = 1, False = 0
として計算される)
max
関数を使った理由
0行目(または0列目)にあるセルの左(または上)のセルを指定したいときi - 1 = -1
となってしまってもmax(0, -1) = 0
となるから、壁の内側の範囲のみを参照できる。Pythonはインデックスが行列のサイズをオーバーしても大丈夫らしく、n行目(またはn列目)の時は同様にする必要はなかった。
次の世代に更新する
def update(old):
@np.vectorize
def sum_around(i, j):
return cells[max(0, i-1):i+2, max(0, j-1):j+2].sum() - cells[i, j]
around = np.fromfunction(sum_around, old.shape)
new = np.where(old , ((2 <= around) & (around <= 3)), (around == 3))
return new
np.fromfunction
関数の第一引数にsum_around
関数を入れることで、 各要素の値 = 隣接する「生」のセルの個数 となる新しい行列around
を作る。
各要素がTrue
のとき、つまり「生」のセルだったときは生存・過疎・過密の判定結果を返し、False
のとき、つまり「死」のセルだったときは誕生の判定結果を返す。返り値はbool
型で、これがそのまま次の世代のセルになる。
np.vectorize
関数を使った理由
np.fromfunction
の第一引数はユニバーサル関数(ufunc)である必要がある。
Parameters :
function : callable
The function is called with N parameters, where N is the rank of shape. Each parameter represents the coordinates of the array varying along a specific axis. For example, if shape were (2, 2), then the parameters would be array([[0, 0], [1, 1]]) and array([[0, 1], [0, 1]])
numpy.fromfunction
sum_around
関数の引数はint
型を想定しているので、ndarray
型を受け取れるようにnp.vectorize
でufunc化した。
出力
while True:
# for _ in range(200): # 回数指定したい場合
cells = update(cells)
plt.imshow(cells)
plt.pause(.01) # 0.01秒ごとに更新
plt.cla() # これがないとどんどん重くなる
クリスマスカラー
from matplotlib.colors import LinearSegmentedColormap
colors = ['green', 'red']
xmas = LinearSegmentedColormap.from_list('xmas', colors)
norm = plt.Normalize(0, 1)
plt.tick_params(labelbottom=False, labelleft=False,
bottom=False, left=False)
while文の中をplt.imshow(cells, cmap=xmas, norm=norm)
に変更すると指定した色に変更できる。
実行結果
コード全体
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
n = 15
cells = np.zeros([n, n], dtype=bool)
# 銀河
cells[3:9, 3:5] = True
cells[3:5, 6:12] = True
cells[-5:-3, 3:9] = True
cells[6:12, -5:-3] = True
# ランダム
# cells = np.random.randint(0, 2, (n, n), dtype=bool)
def update(old):
@np.vectorize
def sum_around(i, j):
return cells[max(0, i-1):i+2, max(0, j-1):j+2].sum() - cells[i, j]
around = np.fromfunction(sum_around, old.shape, dtype=int)
new = np.where(old , ((2 <= around) & (around <= 3)), (around == 3))
return new
colors = ['green', 'red']
xmas = LinearSegmentedColormap.from_list('xmas', colors)
norm = plt.Normalize(0, 1)
plt.tick_params(labelbottom=False, labelleft=False,
bottom=False, left=False)
# for _ in range(200):
while True:
cells = update(cells)
plt.imshow(cells, cmap=xmas, norm=norm)
plt.pause(.01)
plt.cla()
感想
ゼロから一人で作ったものが動いたのってもしかしてプログラミング始めてから初めてかもしれない。やったあ。