自分のキャリアをあれこれ考えながら、Pythonで様々なデータを分析していくブログです

Pythonでライフゲーム(Conway’s Game of Life)を作って遊んでみた

Python
Python

今日は少し趣向を変えてPythonでライフゲーム(Conway's Game of Life)を作って遊んでみたいと思います。(元旦ですけど 笑)

スポンサーリンク

ライフゲームとは

ライフゲームとはwikipediaの説明の通り、生物が生まれたり、死滅したり、駆逐されたりと言ったプロセスを再現したシュミレーションになります。

ライフゲーム (Conway's Game of Life[1]) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。
引用: https://ja.wikipedia.org/wiki/ライフゲーム

作ると言っても検索すれば数多のサイトがサンプルを載せていますので、参考にしつつ改良していこうと思います。

私はアニメが好きなのですが、簡略的でも「ソードアートオンライン アリシゼーション」みたいな仮想世界を構築して、シュミレーションが出来たら最高ですね 笑

今回参考にしたサイトは「Conway’s Game Of Life (Python Implementation)」になります。

理解しやすくするために必要最低限なコードに簡略化し、一部改良した部分があります。

ステップバイステップでコードの動きを見つつ、最後に全体を動かしていこうと思います。

基本的な流れは、下記になるようです。

  1. N x Nのグリッドと生物(数字の255だと仮定)をnumpyで作成 (世界と生物をランダムで構築)
  2. 各セルの周辺環境を確認し、ロジックに従い新しい生物を誕生させたり、死滅させたりする
  3. 2番を繰り返して描画

それでは見てみたいと思います。

スポンサーリンク

ステップバイステップで基本的な動きを確認

初期空間の構築

9x9のグリッドの作成と初期値のランダム配置
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# 変数宣言
N=9      # 9 x 9のグリッドを構築
ON = 255 # 生物
OFF = 0  # 何もなし
vals = [ON, OFF]

# N x Nの配列を作成。ON:OFFの割合を2:8で初期化
grid = np.random.choice(vals, N*N, p=[0.2, 0.8]).reshape(N, N)
grid
Out[0]
array([[  0,   0,   0,   0,   0,   0,   0,   0,   0],
       [  0, 255,   0,   0,   0,   0,   0,   0,   0],
       [  0, 255,   0,   0, 255,   0,   0,   0, 255],
       [  0,   0, 255,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0, 255,   0,   0, 255],
       [  0,   0,   0,   0,   0,   0,   0, 255,   0],
       [  0,   0,   0,   0,   0,   0, 255,   0,   0],
       [  0,   0,   0, 255,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0,   0]])

9 x 9のグリッドに生物を配置しました。numpyの機能を使っています。

matplotlibで描画
fig, ax = plt.subplots()
img = ax.imshow(grid)
Out[0]

黄色が生物を表しています。

ルールに則り生物を増やしたり減らしたりする

参考サイトは0と255の2種類が前提だったので、今後別の種類も作成できるように改良しました。

各セルの周囲を全て確認しルールに則りグリッドを更新する
# https://stackoverflow.com/questions/1692388/python-list-of-dict-if-exists-increment-a-dict-value-if-not-append-a-new-dic
from collections import defaultdict
newGrid = grid.copy()

# 各セルの情報を取得し、gridを更新
for i in range(N):
    for j in range(N):
        cell_info = defaultdict(int)
        # 今後の拡張性を考えて、各要素の出現回数を保存
        cell_info[grid[i, (j-1)%N]] += 1 # (0,8) 左のセル
        cell_info[grid[i, (j+1)%N]] += 1 # (0,1) 右のセル
        cell_info[grid[(i-1)%N, j]] += 1 # (8,0) 上のセル
        cell_info[grid[(i+1)%N, j]] += 1 # (1,0) 下のセル
        cell_info[grid[(i-1)%N, (j-1)%N]] += 1 # (8,8) 左上のセル
        cell_info[grid[(i-1)%N, (j+1)%N]] += 1 # (8,1) 右上のセル
        cell_info[grid[(i+1)%N, (j-1)%N]] += 1 # (1,8) 左下のセル
        cell_info[grid[(i+1)%N, (j+1)%N]] += 1 # (1,1) 右下のセル

        print("(%d,%d)のセルの値は%d: 周辺の%sの数は%d" % (i, j,grid[i,j],ON,cell_info[ON]))

        # refer to https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
        # 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
        # 2. Any live cell with two or three live neighbours lives on to the next generation.
        # 3. Any live cell with more than three live neighbours dies, as if by overpopulation.
        # 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
        if grid[i, j] == ON:
            # 1. underpopulation and 3. overpopulation
            if (cell_info[ON] < 2) or (cell_info[ON] > 3):
                newGrid[i, j] = OFF
            else:
                # two or three live neighbours
                # do nothing and remains on (to the 2. next generation)
                continue
        else:
            # 4. reproduction
            if cell_info[ON] == 3:
                newGrid[i, j] = ON
Out[0]
(0,0)のセルの値は0: 周辺の255の数は1
(0,1)のセルの値は0: 周辺の255の数は1
(0,2)のセルの値は0: 周辺の255の数は1
・・・省略・・・
(8,6)のセルの値は0: 周辺の255の数は0
(8,7)のセルの値は0: 周辺の255の数は0
(8,8)のセルの値は0: 周辺の255の数は0
更新したグリッドの確認
newGrid
Out[0]
array([[  0,   0,   0,   0,   0,   0,   0,   0,   0],
       [255,   0,   0,   0,   0,   0,   0,   0,   0],
       [255, 255, 255,   0,   0,   0,   0,   0,   0],
       [255,   0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0,   0,   0, 255, 255,   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]])
新グリッドを描画する
fig, ax = plt.subplots()
img = ax.imshow(newGrid)
Out[0]

生存セルの位置が変更になって、更新されていることが分かります。

あとはmatplotlibのアニメーションメソッド(FuncAnimation)を使って描画を繰り返していけば良いようです。

FuncAnimationはもう少しドキュメントを読んで理解する必要がありますが、上記のグリッド更新コードをファンクション化して引数として渡してあげる必要があるようです。

その際に下記コマンドでイメージとnumpy arrayの更新も必要になるようです。

img.set_data(newGrid)
grid[:] = newGrid[:] #shallow copy

セル更新の補足情報

私は理解するのに時間がかかったので、一番最初のポジション(i,j = 0,0)の周囲8セルの確認方法を図にしてみました。

9x9のグリッドになっているので、一番左上のセル(赤)の場合を考えてみます。

セル(緑)が状態を確認するセル(赤)の周囲のセルになります。

世界が環状体という仮定でセルの状態を確認するので、上辺と左辺は反対側のセルを参照するということになるようです。

セル(ピンク)は環状体を考慮した仮想的なセル(赤)の位置を示しています。

スポンサーリンク

ライフゲームの実行

それでは今までのコードを一まとめにして、ライフゲームを実行してみたいと思います。

jupyter notebookで実行する場合はbackendを%matplotlib notebookにしないと画面が更新されませんでしたのでご留意ください。

ライフゲームの実行
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# 変数宣言
N=9      # 9 x 9のグリッドを構築
ON = 255 # 生物
OFF = 0  # 何もなし
vals = [ON, OFF]

# N x Nの配列を作成。ON:OFFの割合を2:8で初期化
grid = np.random.choice(vals, N*N, p=[0.2, 0.8]).reshape(N, N)

def update(frameNum, img, grid, N):
    # https://stackoverflow.com/questions/1692388/python-list-of-dict-if-exists-increment-a-dict-value-if-not-append-a-new-dic
    from collections import defaultdict
    newGrid = grid.copy()

    # 各セルの情報を取得し、gridを更新
    for i in range(N):
        for j in range(N):
            cell_info = defaultdict(int)
            # 今後の拡張性を考えて、各要素の出現回数を保存
            cell_info[grid[i, (j-1)%N]] += 1 # (0,8) 左のセル
            cell_info[grid[i, (j+1)%N]] += 1 # (0,1) 右のセル
            cell_info[grid[(i-1)%N, j]] += 1 # (8,0) 上のセル
            cell_info[grid[(i+1)%N, j]] += 1 # (1,0) 下のセル
            cell_info[grid[(i-1)%N, (j-1)%N]] += 1 # (8,8) 左上のセル
            cell_info[grid[(i-1)%N, (j+1)%N]] += 1 # (8,1) 右上のセル
            cell_info[grid[(i+1)%N, (j-1)%N]] += 1 # (1,8) 左下のセル
            cell_info[grid[(i+1)%N, (j+1)%N]] += 1 # (1,1) 右下のセル

            #print("(%d,%d)のセルの値は%d: 周辺の%sの数は%d" % (i, j,grid[i,j],ON,cell_info[ON]))

            # refer to https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
            # 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
            # 2. Any live cell with two or three live neighbours lives on to the next generation.
            # 3. Any live cell with more than three live neighbours dies, as if by overpopulation.
            # 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
            if grid[i, j] == ON:
                # 1. underpopulation and 3. overpopulation
                if (cell_info[ON] < 2) or (cell_info[ON] > 3):
                    newGrid[i, j] = OFF
                else:
                    # two or three live neighbours
                    # do nothing and remains on (to the 2. next generation)
                    continue
            else:
                # 4. reproduction
                if cell_info[ON] == 3:
                    newGrid[i, j] = ON
    # update data
    img.set_data(newGrid)

    # Shallow copies refer to below link
    # https://stackoverflow.com/questions/6167238/what-does-mean
    grid[:] = newGrid[:]
    return img

%matplotlib notebook
# set up animation
fig, ax = plt.subplots()
img = ax.imshow(grid)
ani = animation.FuncAnimation(fig, update, fargs=(img, grid, N),frames = 10,interval=500,save_count=50)

plt.show()
Out[0]

スクリーンショットなので画面が更新されないですが、実際に動かしてみてチカチカしていたら成功です。

スポンサーリンク

まとめ

ライフゲームは今後の拡張が楽しみです。

考えているのは、男女のセルを作成し両方が存在する場合に新しい生命が生まれるなどいう変更を加えてみるのも面白そうかなと思っています。

あとは寿命や食事という概念を追加したり、グリッドもただのグリッドではなく、水や森といったフィールドをランダムで発生させるのも楽しそうかなと思います。

また息抜きにやってみたいと思います。

タイトルとURLをコピーしました