LoginSignup
4
2

More than 1 year has passed since last update.

Numpyでライフゲームを作ってみた

Last updated at Posted at 2019-12-01

 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)

 TrueFalseのどちらか一つをランダムに選んだものが要素の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]

 まず、行列のij列成分を中心とする3×3行列を抽出して、その行列の要素の和を求める。その結果から中心を引くと、ij列成分に隣接する「生」のセルの個数が求められる。(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()        # これがないとどんどん重くなる

:santa:クリスマスカラー:christmas_tree:

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)に変更すると指定した色に変更できる。

実行結果

ezgif-3-866aafd6a847.gif

コード全体

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()

:8ball:感想:8ball:

 ゼロから一人で作ったものが動いたのってもしかしてプログラミング始めてから初めてかもしれない。やったあ。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2