Pythonでライフゲーム！(Conway's Game of Life)

はじめに
------------------------------
セル・オートマトンとして有名な[ライフゲーム][link01]が面白そうだったので、Pythonとnumpyでライフゲームのマップ配列を生成するコードを書いてみました。`0`(dead)と`1`(live)で書かれた初期世代のマップ配列を与えることで、iterativeに次世代のマップ配列を生成するgeneratorとして実装しています。マップは周期的境界条件(端と端がつながった状態)にも対応させてみました！

lifegame.py
------------------------------
以下の`lifegame.py`をモジュールとしてimportして使用します。実際の使用例は下記のサンプルコードをご覧ください。進化のルールはWikipediaで紹介されている`23/3`に準拠してますが、コードの中をいじって容易に変えることができます。

### usage
```py
>>> lg_map = lifegame.MapGenerator(map_init, periodic=False)
>>> lg_map.evolve()
array([...]) # 第0世代のマップ配列 (= map_init)
>>> lg_map.evolve()
array([...]) # 第1世代のマップ配列
>>> ...
```
+ `map_init` : 初期世代のマップ配列 (list or numpy.ndarray)
+ `periodic` : マップに周期的境界条件を使用するかどうか (bool)



### lifegame.py
```py:lifegame.py
import numpy as np
from itertools import product

class MapGenerator(object):
    def __init__(self, map_init, periodic=False):
        self.map_init  = np.array(map_init, np.int64)
        self.periodic  = periodic
        self.generator = self.map_evolver()

    def map_evolver(self):
        map_now   = self.map_init
        map_shape = self.map_init.shape

        while True:
            map_next = np.zeros_like(map_now, np.int64)
            for (i,j) in product(range(map_shape[0]), range(map_shape[1])):
                islive = bool(map_now[i,j])
                nlive  = self.num_live_neighbours(map_now, i, j)
                # write the rules of lifegame here!
                # ........................................
                if nlive == 2:
                    if islive: map_next[i,j] = 1
                elif nlive == 3:
                    map_next[i,j] = 1
                # ........................................

            yield map_now
            map_now = map_next

    def num_live_neighbours(self, map_now, i, j):
        if self.periodic:
            neighbours = np.roll(np.roll(map_now, -i+1, 0), -j+1, 1)[:3,:3]
        else:
            sl_i = slice(0,i+2) if i == 0 else slice(i-1,i+2)
            sl_j = slice(0,j+2) if j == 0 else slice(j-1,j+2)
            neighbours = map_now[sl_i,sl_j]

        return neighbours.sum() - map_now[i,j]

    def evolve(self):
        return self.generator.next()
```

matplotlibを使ったサンプルコード
------------------------------
以下では[パルサー][link02]と呼ばれる振動子の一種が、15x15のマップ上でエンドレスに進化するサンプルコードです。
あくまでサンプルなので、配列の計算速度よりもmatplotlibの描画効率でリミットされていると思われます…

### サンプルコード
```py:pulsar.py
import time
import numpy as np
import matplotlib.pyplot as plt
import lifegame

pulsar = np.array([
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])

lg_map = lifegame.MapGenerator(map_init=pulsar, periodic=False)
plt.ion()
while True:
    plt.pcolormesh(lg_map.evolve())
    plt.xlim([0, pulsar.shape[1]])
    plt.ylim([0, pulsar.shape[0]])
    plt.draw()
    plt.clf()
    time.sleep(0.05)
```

### 描画の様子
![lifegame-pulsar.png][link03]

今後の予定？
------------------------------
現時点では初期世代のマップ配列をテキストで予め用意する必要があるので、`matplotlib.widgets`を使って画面上からマップ配列をポチポチと入力できるようにもしたいなと思います。遊びなのでいつ作るかは未定ですが(笑)

[link01]: http://ja.wikipedia.org/wiki/ライフゲーム
[link02]: http://ja.wikipedia.org/wiki/パルサー_(ライフゲーム)
[link03]: https://qiita-image-store.s3.amazonaws.com/0/44000/18e4ac52-9120-dae6-b683-8ca29cd1d507.png