ライフゲームをnumpyを使って実装してみた

# はじめに
会社の新卒勉強会でライフゲームについて学んだ。以前から存在自体は認識しており実装してみたいと思っていたものの一つであった。いい機会なのでPythonで実装してライフゲームが作り出す世界観に浸ってみる。

# Lifeゲームとは
Wikipediaによると、ライフゲームは、「イギリスの数学者ジョン・ホートン・コンウェイが考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである」とある。要は、ある初期条件を定義し、それを変化させるルールを適用し、時間の経過とともに状況がどう変わっていくのかをシミュレーションするものである。

まずは簡単のため、以下の図のような3×3の黒いセルと白いセルが敷き詰められた正方形で考える。黒いセルは生命が存在し、白いセルは何もない状態を表している。
一般に、生命が誕生するには、近くにある一定数の生命が存在する必要がある。逆に生命が周辺に全く存在しない場合、新たな生命が生まれることはなく、絶滅してしまう。
これを、この白と黒のマスの変化で表すのが、ライフゲームである。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71289/5d83c08b-096b-681a-ef91-b5803863f49e.png)

ここで考慮するべきルールは3つある。
1. 中央のセルが白いときにその周辺に3つの黒いセルが存在するとき次の世代で中央のセルは黒いセルになる(生命の誕生)。
2. 中央のセルが黒いとき、そのセルの周囲に2つか3つの黒いセルがある場合、次の世代でも黒いセルになる(維持)。
3. 1,2以外のとき、次の世代で中央のセルが白くなる(淘汰)。

以上の3つのルールに従って、次の世代のセル配置が決まる。

# Python(numpy)での実装
まず、ルールによって次の世代のセルの色を決定するには、$3\times3$の正方形のブロックの、現在の中央の色とその周りの黒いセルの数を知る必要がある。ここでは、現在の$3\times3$ブロックを受け取り、次の世代のブロックを返す関数を定義しておく。

```python

# calc score
def calc(a):
    a[1,1] = 0
    return a.sum()

# 状態を変化させる
def change(a):
    x = a[1,1]
    score = calc(a)
    if (x==1):
       if(score==2 or score==3):
         a[1,1] = 1
       else:
         a[1,1] = 0
    if (x==0):
       if (score==3):
         a[1,1] = 1
       else:
         a[1,1] = 0
    return a
```
定義した関数を用いて、次の世代の状態をすべて求める関数を以下のように定義した。ここでは、端の状態を判断することができるように、$N\times N$の行列を$(N+2)\times(N+2)$の行列に拡張して処理を行っている。

```python

# matrix すべての状態を変化させる
def change_state(matrix_input):
    width = matrix_input.shape[0]+2
    matrix_pad = np.zeros(width**2).reshape(width,width)
    new_matrix = np.zeros(width**2).reshape(width,width)
    matrix_pad[1:width-1,1:width-1] =matrix_pad[1:width-1,1:width-1] + matrix_input
    for i in range(width-2):
        for j in range(width-2):
            mat = np.copy(matrix_pad[i:i+3,j:j+3])
            new_matrix[i+1,j+1] = change(mat)[1,1]
    return np.array(new_matrix[1:width-1,1:width-1])
```

以下では、[ここ](http://aidiary.hatenablog.com/entry/20120113/1326464820)を参考に、Imageを返す関数を定義している。後述するが世代ごとのImageを保存しておき、最終的にアニメーション化することで、ライフゲームの状態の推移を確認する。

```python

# http://aidiary.hatenablog.com/entry/20120113/1326464820
def render(results, filename="ca.png"):
    """セルオートマトンを描画"""
    width = len(results[0])
    height = len(results)
    img = Image.new("RGB", (width, height), (255,255,255))
    draw = ImageDraw.Draw(img)
    for y in range(height):
        for x in range(width):
            if results[y][x] == 1:
               draw.point((x, y), (0, 0, 0))
    return img
```
この関数は、matsというlistに格納されたImageを画像として表示するimshowを呼ぶだけの関数である。これは後に使用するFuncAnimationに時系列ごとにライフゲームのセルの状態を表現したImageを渡すために定義してある。

```python
  
def __updated_plot(i):
    return plt.imshow(mats[i])
```

ここまで定義してきた関数を用いて、ライフゲームをやってみる。ここでは、$8\times 8$のセルで、ランダムに0か1を配置した状態を初期値として実行する。取得したImageオブジェクトに対し、__updated_plot関数を実行し、FuncAnimationに時系列ごとにmatplotlibのaxiesImageを渡す。得られたものをHTML5 Video形式に変換して動画にしている。

```python

mats = []
new_mat = np.random.choice([0.,1.],64).reshape(8,8)
im = render(new_mat)
mats.append(im)
for i in range(100):
    new_mat = change_state(new_mat)
    im = render(new_mat)
    mats.append(im)
fig = plt.figure()
anim = FuncAnimation(fig, __updated_plot, frames=len(mats), interval=1000)
HTML(anim.to_html5_video())
```

以下に示したのは、以前に、適当な初期状態(グライダーと呼ばれるものを2つ少しずらして配置)から世代を経過するごとにどう変化するのかを見たものである。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">できた。寝る。 <a href="https://t.co/R4SlMbKYeR">pic.twitter.com/R4SlMbKYeR</a></p>&mdash; Blackcat🌗 (横山雅季) (@myblackcat7112) <a href="https://twitter.com/myblackcat7112/status/1156969718030299136?ref_src=twsrc%5Etfw">August 1, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

最後に、colabratoryでの実装を[ここ](https://colab.research.google.com/drive/1QFwXtVIzpcWtyl6r2NVCjp-vXdEEbnLj)に示しておく。