人工生命クランの開発　その１

# はじめに
Q.E.Dというミステリーマンガが好きです。
第8話「ヤコブの階段」というエピソードがあり、聖書のエピソードに準えられた少し悲しいラストが印象的です。私は、鈴木光司の小説「ループ」を思い出しました。
というわけで、その中にでてくる人工生命のシミュレーション「クラン」を再現したいと思いました。

そこでは、人工生命を以下のように設定していたようです。
1. 自分の能力を把握しなさい
2. 自分より弱いものを従え、強いものに従いなさい
3. 繁殖に適した場所を探しなさい
4. 絶滅を避けるために繁殖しなさい
5. セルの寿命は３日
6. 世代交代すると変異する

この人工生命が、どんなふうに進化し生き残り社会を作るか、その様子を見ていたところ、以下の3つのグループにわかれて安定したとのことです。
A. 狩猟民族タイプ。リーダー役のセルが配下の者を動かし目標である繁殖地を探しリーダーの命令で繁殖する
B. 放牧民族タイプ。リーダー薬のセルが配下のセルをまとめリーダー自身が繁殖地を探す。
C. 農耕民族タイプ。リーダー役のセルが配下をまとめその場でできる範囲の繁殖を行う。

# 実際に作ってみる。
まずは1つのセルが生まれて死ぬだけというところから表現してみようと思います。
google colabで作っています。
まずはセルのクラスを作成します。いずれこのCellクラスを継承してさまざまな能力を持った種族を作る予定です。
Cellクラスには座標や体力などを設定します。まずはターミナルに、各サイクルでのセルの状態を出力する仕様にします。
セルは体力をもち、１サイクル更新すると体力がだんだんなくなって死亡します。今後、移動・繁殖なども取り入れていきます。

```python
# 世界の広さ（グリッドサイズ）と時間（シミュレーションのサイクル数）。
WIDTH = 5
HEIGHT = 5
CYCLES = 10

# Cellクラスを作成する。セルが生成される時、各インスタンスに対して座標と色、体力などが設定される。
class Cell:
    def __init__(self, x, y, col='black', life=5):
        self.x = x
        self.y = y
        self.col = col
        self.life = life
        print(self) # セルの初期状態を出力する。

    # 状態を更新すると体力が減る。0以下になると死亡。
    def update(self):
        self.life -= 1
        print(self)
        return self

    # オブジェクトの文字列表現。セルの座標と体力を含む文字列を返す。print関数やリスト表示時に利用する。
    def __repr__(self):
        return f"Cell(({self.x}, {self.y}), life: {self.life})"


for i in range(CYCLES):
    if i == 0: # 初期状態
        print(i, 'CYCLE')
        # 人工生命を生成する
        cellList = [Cell(2, 2), Cell(3, 3)]
        print('---------------------')
    else:
        print(i, 'CYCLE')
        deadCells = [] # 削除対象となるセルを一時的に保管するための空リスト
        for cell in cellList:
            cell.update()
            if cell.life < 1: # 体力が尽きるとdeadCellsに追加される。
                deadCells.append(cell)
        for cell in deadCells: # deadCellsに登録されたセルをcellListから削除する
            cellList.remove(cell)
        print('---------------------')
```
これを実行すると、下記のような結果が返ってきます。
セルが2つ生成されて、時間が経つとライフがなくなって死ぬことが表されました。

```
0 CYCLE
Cell((2, 2), life: 5)
Cell((3, 3), life: 5)
---------------------
1 CYCLE
Cell((2, 2), life: 4)
Cell((3, 3), life: 4)
---------------------
2 CYCLE
Cell((2, 2), life: 3)
Cell((3, 3), life: 3)
---------------------
3 CYCLE
Cell((2, 2), life: 2)
Cell((3, 3), life: 2)
---------------------
4 CYCLE
Cell((2, 2), life: 1)
Cell((3, 3), life: 1)
---------------------
5 CYCLE
Cell((2, 2), life: 0)
Cell((3, 3), life: 0)
---------------------
6 CYCLE
---------------------
7 CYCLE
---------------------
8 CYCLE
---------------------
9 CYCLE
---------------------
```

# セルが動くようにする
move関数を作って、セルを動かしてみます。np.randomを利用します。
また、np.clipを使って、移動範囲がグリッドサイズ内に収まるように工夫します。

```python
import numpy as np

class Cell:
    def __init__(self, x, y, col='black', life=5, speed=1):
        self.x = x
        self.y = y
        self.col = col
        self.life = life
        self.speed = speed # move()で使う。
        print(self)

    # ライフが残っていれば移動する
    def update(self):
        self.life -= 1
        if self.life > 0:
            self.move()
        print(self)
        return self

    # セルが移動する。speedを変えることで移動速度を変えられる余地を作っている。
    def move(self):
        self.x += np.random.randint(-self.speed, self.speed + 1)
        self.y += np.random.randint(-self.speed, self.speed + 1)

        # numpy の np.clip を使って範囲内に収めた上で、整数に変換する。
        self.x = int(np.clip(self.x, 0, WIDTH))
        self.y = int(np.clip(self.y, 0, HEIGHT))

# 以降は上と同じ
```
これを実行すると、無事にセルが移動することを表現できました。
```
0 CYCLE
Cell((2, 2), life: 5)
Cell((3, 3), life: 5)
---------------------
1 CYCLE
Cell((2, 3), life: 4)
Cell((4, 3), life: 4)
---------------------
2 CYCLE
Cell((2, 3), life: 3)
Cell((5, 4), life: 3)
---------------------
3 CYCLE
Cell((2, 3), life: 2)
Cell((5, 5), life: 2)
---------------------
4 CYCLE
Cell((2, 2), life: 1)
Cell((5, 5), life: 1)
---------------------
5 CYCLE
Cell((2, 2), life: 0)
Cell((5, 5), life: 0)
---------------------
6 CYCLE
---------------------
7 CYCLE
---------------------
8 CYCLE
---------------------
9 CYCLE
---------------------
```

# アニメーションで表現する
matplotlibのアニメーションを利用します。
セルの状態を表すのに、print関数を使うのではなくて、matplotlib.pyplot.Circleで、セルの座標にライフの大きさの円を描画します。


```python
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation # アニメーション処理を実現するために利用
from matplotlib.animation import PillowWriter # アニメーションをGIFに保存するために使用


WIDTH = 5
HEIGHT = 5
CYCLES = 10

# 6×6インチの正方形のグラフ領域（Figure と Axes）を準備
fig, ax = plt.subplots(figsize=(6,6))

# plt.Circle を使って円形のパッチを作成、体力に比例した大きさで描画
class Cell:
    def __init__(self, x, y, col='black', life=5, speed=1):
        self.x = x
        self.y = y
        self.col = col
        self.life = life
        self.speed = speed
        # print(self)
        self.circle = plt.Circle((self.x, self.y), radius=0.1*self.life, facecolor=self.col)

    def update(self):
        self.life -= 1
        self.circle.set_radius(0.1 * self.life)
       # 体力が残っていればセルが移動する。移動後の座標い円の中心を更新する
        if self.life > 0:
            self.move()
            self.circle.center = (self.x, self.y)
        return self.circle

    # 変更なし
    def move(self):
        self.x += np.random.randint(-self.speed, self.speed + 1)
        self.y += np.random.randint(-self.speed, self.speed + 1)
        self.x = int(np.clip(self.x, 0, WIDTH))
        self.y = int(np.clip(self.y, 0, HEIGHT))


# Cellオブジェクトを作成する
cellList = [Cell(2, 2), Cell(3, 3)]

# animate関数は、各アニメーションフレームごとに呼ばれ、描画クリアと再設定を行なう。
def animate(i):
    # まず前のフレームの描画をクリアする
    ax.clear()
    # 表示領域のX軸・Y軸の範囲を設定。
    ax.set_xlim([-1, WIDTH+1])
    ax.set_ylim([-1, HEIGHT+1])
    # 軸の目盛りを非表示にする
    ax.set_xticks([])
    ax.set_yticks([])
    # 比率を正方形にする
    ax.set_aspect('equal')
    # 題名に何サイクル目かを表示
    ax.set_title(f"Cycle {i}")

    # 初回のフレームでは初期状態をそのまま表示する
    if i == 0:
        for cell in cellList:
            ax.add_patch(cell.circle)
    else:
        dead_cells = []
        for cell in cellList:
            # 各セルに対してupdate()が呼ばれ、体力が減り、移動して位置を更新
            # 更新されたパッチを再度ax.add_patchで追加され、最新の状態が描画される
            ax.add_patch(cell.update())
            if cell.life < 1:
                dead_cells.append(cell)
        for cell in dead_cells:
            cellList.remove(cell) # 体力が尽きたセルは今後のフレームで描画・更新されなくなる

    # 現在のフレームで表示されている全てのパッチを返す。
    return ax.patches

# FuncAnimationによって、animate関数がCYCLES分にわたって実行され、animation.gifとして保存。
ani = animation.FuncAnimation(fig, animate, frames=CYCLES, blit=False)

writer = PillowWriter(fps=2)
ani.save("animation.gif", writer=writer)
plt.close(fig)

```
![animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3999109/087f5dbe-d592-4912-affa-76456c02c0e7.gif)

黒丸で表されたセルが、小さくなりつつ移動して消滅しています。

# 場から養分を得ることでセルが大きくなって増殖する
Fieldクラスを作ります。セルが移動してくると、フィールドから養分を得て体力を回復し、フィールドは養分を失います。時間が経つと養分を回復し、またセルに養分を供給できるという仕組みです。
フィールドは正方形で描画します。養分の有無で色を変えます。

```python
# フィールドはグリッド上の1セル
# 座標、サイズ、エネルギー、ライフがあり、表示色は col。
class Field:
    def __init__(self, x, y, size=1, energy=2, col='green'):
        self.x = x
        self.y = y
        self.size = size
        self.life = 1
        self.energy = energy
        self.facecolor = col
        self.rectangle = plt.Rectangle((self.x-self.size/2, self.y-self.size/2), self.size, self.size, facecolor=self.facecolor)
        self.rectangle.set_zorder(1)
        # 矩形パッチとして生成し、軸に追加
        ax.add_patch(self.rectangle)

    def update(self):
        # フィールドの life が 1 より下なら、色を 'brown' に変更し、life を補正
        if self.life < 1:
            self.rectangle.set_facecolor('brown')
            self.life += 1
        # それ以外の場合は、元の色（facecolor）に戻す
        else:
            self.rectangle.set_facecolor(self.facecolor)

        return self.rectangle

# セルとフィールドのリスト
fieldList = []
for i in range(HEIGHT+1):
    for j in range(WIDTH+1):
        fieldList.append(Field(j, i))
```

Cellクラスの中にeat()、divide()を追加する。背景が緑色になるのでセルの色も白色に変えます。thresholdプロパティを追加して、養分をとって体力がこれを超えると分裂するという設定にします。
```python
# def __init__():のところでzorderを指定してセルが背景のフィールドによって隠れてしまうのを防ぐ。
class Cell:
    def __init__(self, x, y, col='white', life=5, speed=1, threshold=10):
        self.x = x
        self.y = y
        self.col = col
        self.life = life
        self.speed = speed
        self.threshold = threshold
        # print(self)
        self.circle = plt.Circle((self.x, self.y), radius=0.1*self.life, facecolor=self.col)
        self.circle.set_zorder(10)

    def update(self): # updateにもeatとdivideを入れる。
        self.life -= 1
        self.circle.set_radius(0.1 * self.life)
       # 体力が残っていればセルが移動する。移動後の座標い円の中心を更新する
        if self.life > 0:
            self.move()
            self.eat()
            self.divide()
            self.circle.center = (self.x, self.y)
        return self.circle

    def move(self): # ここは変わりなし。

    def eat(self):
        # 現在のセルの位置に対応するフィールドを fieldList からインデックスを用いて取得
        index = self.y * (WIDTH + 1) + self.x
        field_obj = fieldList[index]
        # 対応するフィールドの life が 1 であれば、フィールドの life を減らし、そのフィールドの energy 値をセルの life に加える
        if field_obj.life == 1:
            field_obj.life -= 10
            self.life += field_obj.energy

    def divide(self):
        if self.life >= self.threshold:
            # 分裂前に体力を半分に減らし、初期設定のセルをセルリストに追加します。
            self.life //= 2
            cellList.append(Cell(x=self.x, y=self.y))
```
def animate(i)内では、cellList内の各セルをupdateして更新されたパッチを再度ax.add_patchで追加したのと同様に、fieldList内の全てのfieldをupdateして更新されたパッチを追加します。


![animation (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3999109/a7672f0a-69b0-43c4-8c3e-51e4b007763b.gif)

セルが移動しつつ養分をとって大きくなり、分裂して、体力が減って死んでいます。養分を取られた場所が一定時間後、元の状態に回復します。

# 改善点
ただし、このアニメーションは毎回ax.clear()を呼んでしまうので、軸設定も毎フレーム再設定する必要があり、またすべてのパッチを1から追加するためパフォーマンスに影響します。
そこで、次回は、各フレームでそのプロパティだけを更新する方法（＝blittingを使う方法）に変更します。