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

　Playground Advent Calendar を機に記事初投稿！

# ライフゲームとは
以下、[wikipedia](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)より引用。~~下の引用を読むより、リンクに飛んだ方が分かりやすいと思う~~

　ライフゲームでは初期状態のみでその後の状態が決定される。碁盤のような格子があり、一つの格子はセル（細胞）と呼ばれる。各セルには8つの近傍のセルがある。各セルには「生」と「死」の2つの状態があり、あるセルの次のステップ（世代）の状態は周囲の8つのセルの今の世代における状態により決定される。

　セルの生死は次のルールに従う。
>
**誕生**
　死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
**生存**
　生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
**過疎**
　生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
**過密**
　生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
下に中央のセルにおける次のステップでの生死の例を示す。生きているセルは■、死んでいるセルは□で表す。


# 使用したパッケージ
```python
import matplotlib.pyplot as plt
import numpy as np
```

# 初期状態
```python
n = 50

# ランダムなセルを作成
cells = np.random.randint(0, 2, (n, n), dtype=bool)
```
　`True`と`False`のどちらか一つをランダムに選んだものが要素のn×n行列を作る。(`True`なら「生」、`False`なら「死」)
　(`n = 500`くらいまで大きくすると重くなってしまう…)


# 隣接する「生」のセルの個数を数える
```python
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列目)の時は同様にする必要はなかった。


# 次の世代に更新する
```python
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](https://docs.scipy.org/doc/numpy/reference/generated/numpy.fromfunction.html)

`sum_around`関数の引数は`int`型を想定しているので、`ndarray`型を受け取れるように`np.vectorize`でufunc化した。

# 出力
```python
while True:
# for _ in range(200):　 # 回数指定したい場合

    cells = update(cells)
    
    plt.imshow(cells)
    plt.pause(.01)   # 0.01秒ごとに更新
    plt.cla()        # これがないとどんどん重くなる
```


# :santa:クリスマスカラー:christmas_tree:
```python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488916/b02fb5aa-8665-826d-3ade-492bb3ad9bff.gif)


# コード全体
```python
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:
　ゼロから一人で作ったものが動いたのってもしかしてプログラミング始めてから初めてかもしれない。やったあ。