PythonでLifegame。

<h2>前書き</h2>
あけましておめでとうございます。
大した数を投稿してないですが、今年もよろしくお願いします。

個人的にLifegame的なプログラムをPythonで書きたいなー、とか思っていたので、年末年始で挑戦しました。
「松の明けぬうちにせめて一本記事を」という思いもありますので、ざっくりした説明とコードを投稿します。
従来同様、あちこちのサイトを調べては書き、「我ながらイケてない」と独り言をつぶやいては手直しし、の繰り返しなので、お見苦しい点もありますが、ご容赦くださいませ。

<h2>環境</h2>
- Windows 10 Pro Build 19042.685
- Python 3.9.1
- matplotlib 3.3.3

<h2>概要</h2>
- Lifegameの詳細については[こちら](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)。(Wikipedia)
- 今回はmatplotlib.pyplotの[scatter](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.scatter.html)(散布図)クラスを使用して描画しています。本来はこういった使い方をするものではないと思いますが。
- また、各セルの再描画のため、matplotlib.pyplotは[インタラクティブモード](https://matplotlib.org/tutorials/introductory/usage.html#what-is-interactive-mode)で使用しています。
- プログラムを実行すると描画され、各セルの状態が更新され続けられます。描画領域をクリックするとプログラム終了です。ウィンドウ右上の「×」でも閉じられますが、Tracebackが表示されます。ウィンドウのリサイズは(Windows 10の場合)ウィンドウ端の「ちょっと外側」でマウスカーソルが矢印に変わる辺りでドラッグすればOKです。
- 各セルの状態を計算する処理はループでゴリ押しです。よってセルの数が多いほど進行が遅くなります。参考までにIntel Core i7-8700K CPU @ 3.70GHzで描画サイズ1000、間隔5(セルの数は200x200=40000個)だと約0.5秒間隔で更新される感じ。(もうちょっと勉強して高速化したいところ)
- 描画領域の左右と上下は繋がっている状態です。よって「移動する集団が右端から出て左端から入ってくる」のようなこともあります。
- 各セルには寿命(1～9)があり、9を超えると消えるようになっていますが、9のまま永続化することもできます。
- 各種条件などパラメータ関連はコードの最初の方に寄せたので、いろいろ変えるとちょっと楽しいかなと思います。大体は全滅かほぼ覆いつくすかどちらかと思いますが、初期値を多め／少な目にして全滅or繁栄を楽しむのもありかな、と。
- コードに一通りのコメントは入れてあるので、プログラム解析(というほどのものではないですが)のヒントにはなると思います。計算自体も複雑なものは使っていないので多少言語知識があれば一通り読めるのではないかと思っています。

<h2>コード</h2>
<details><summary>ちょっと長いので折りたたんでます。</summary>
<div>

```python:lifegame.py
import time
import matplotlib.pyplot as plt
import random

# インタラクティブモード オン
plt.ion()

# 各変数の設定
# グラフの描画サイズ
sizeX = sizeY = 1000
# 描画時のセルの間隔
# 各軸のセルの数は描画サイズ÷セルの間隔で求められます。
space = 5
# 過疎条件
depop = 1
# 過密条件
overcrow = 4
# 発生条件(下限)
occurLower = 3
# 発生条件(上限)
occurUpper = 3
# セルの永続化フラグ
eternal = False
# 初期セルの発生率
occurInit = 20 # %
# セル描画時の大きさ係数
sizeFacter = 5

# 描画領域の初期化
fig, ax = plt.subplots()
ax.set_xlim(0, sizeX + space)
ax.set_ylim(0, sizeY + space)
ax.set_aspect('equal')
ax.grid(False)

# セルと描画用データの初期化
cells =[]
x = []
y = []
color = []
scale = []
for i in range(1, int(sizeX / space) + 1, 1):
    tempCells = []
    for j in range(1, int(sizeY / space) + 1, 1):
        x.append(j * space)
        y.append(i * space)
        color.append('b')
        if random.randint(1, 100) <= occurInit:
            temp = random.randint(0,9)
        else:
            temp = 0
        tempCells.append(temp)
        scale.append(temp * sizeFacter)
    cells.append(tempCells)

# scatter(散布図)オブジェクトの作成
sc = ax.scatter(x, y, c=color, s=scale, alpha=0.3, edgecolors='none')

# マウスクリックで終了するためのイベントの設定
loop = True
def onclick(event):
    print('Program is over.')
    global loop
    loop = False

cid = fig.canvas.mpl_connect('button_press_event', onclick)

# セルの状態チェック
def checkCell(cells, x, y):
    # セルの周囲8セルの座標計算
    cellsAxixes = []
    for i in range(-1,2,1):
        for j in range(-1,2,1):
            cellsAxixes.append({'x': x+i, 'y':y+j})
    # 中央のセルは自分自身なので計算に含まない
    del cellsAxixes[4]

    # 上下左右端の補正
    for i in cellsAxixes:
        if i['x'] < 0:
            i['x'] = int(sizeX/space) - 1
        elif i['x'] > int(sizeX/space) - 1:
            i['x'] = 0

        if i['y'] < 0:
            i['y'] = int(sizeY/space) - 1
        elif i['y'] > int(sizeY/space) - 1:
            i['y'] = 0

    # 周囲の生きているセルの数をカウント
    check = 0
    for i in cellsAxixes:
        if cells[i['y']][i['x']] != 0:
            check += 1

    return check

# (自身の)セルの計算
def calcCells(cells):
    afterCells = []
    posY = 0
    for i in cells:
        posX = 0
        tempCells = []
        for j in i:
            status = checkCell(cells, posX, posY)
            # セルの生存確認
            if j > 0 and j < 9:
                # 過密、過疎判定
                if status >= overcrow or status <= depop:
                    j = 0
                else:
                    j += 1
            elif j == 9:
                # 永続(eternal)フラグがFalseなら。。。
                if not eternal:
                    # 寿命でお亡くなりになる
                    j = 0
            else:
                # 発生条件判定
                if status <= occurUpper and status >= occurLower:
                    j = 1

            tempCells.append(j)
            posX += 1
        afterCells.append(tempCells)
        posY += 1
    return afterCells

# メインループ
while loop:
    # 処理が早すぎて困る場合のスリープ(秒)
    #time.sleep(0.01)
    newScale = []
    newColor = []
    cells = calcCells(cells)
    # セルの状態を元に描画設定を作成
    for j in cells:
        for i in j:
            # 大きさ
            newScale.append(i * sizeFacter)

            # 色    
            if i > 0 and i < 4:
                newColor.append('b') # 青
            elif i < 8:
                newColor.append('g') # 緑
            else:
                newColor.append('r') # 赤
    # 描画の設定と再描画
    sc.set_sizes(newScale)
    sc.set_color(newColor)
    sc.figure.canvas.draw_idle()
    sc.figure.canvas.flush_events()
```

</div>
</details>

<h2>後書き</h2>
Lifegame自体は古典的なものと思いますが、繰り返しや分岐などプログラミングの基本要素を一通り入れ込まないといけないので、習作として取り組んでみるのも悪くないな～、とか思いました。
コードに手を入れるとすれば、寿命の範囲を変えるとか、セルに重み付けをして過疎・過密・発生の条件を細かくするとか考えられますが、気力が続かないので今回はこれまで、という事で。