Pythonでライフゲームの実装

# 初めに
久しぶりにPygameを使ってプログラムを実装しました。今回はゲームから少し離れてライフゲームを実装しました。（名前にゲームが入っていますがゲームじゃないのでセーフ）実装の難易度はそこまで高くないです。

# ライフゲームとは
ライフゲームは簡単に言うと生命のシミュレーションです。時間の経過とともに生物が誕生したり、死滅していく様子が観察できます。ライフゲームにはほんの少しのルールがあり、それに従ってセルと呼ばれるブロックの個数が更新されていきます。

## ライフゲームのルール
ライフゲームの更新規則は誕生、生存、死に分けられます。
誕生はセルが死んでいるとき、隣接するセルに生存しているが3つのときに新たにそのセルに生存しているセルが誕生します。
一方で、生存しているセルは隣接する生存しているセルの個数が3または4のときに生存し、それ以外のときに死滅します。ここでの死滅は現実世界の過疎や過密に対応しています。
ここで下の図では、中心のセルはそれぞれ左から誕生、生存、死(過疎)、死(過密)の規則が適用されます。
![Screenshot from 2019-11-09 00-49-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/55e1199c-7c31-443c-7deb-270962e9d0f5.png)
規則が適用されるのに必要なセル数は自由に決められます。今回はよく用いられる上記で説明した数を使いました。

# 実装
基本的には説明したとおりに実装を行いました。注意しなくてはいけないのは、セルの更新を行うときに逐次的に更新してしまうと、一気にセルが死滅してしまうことがあります。なので、セルの更新を行う際は、一度別の場所に更新後のセル状態を保持しておきすべての更新後の状態が求まってから一気に更新をする必要があります。(私は初めてライフゲームを実装したときにこれで詰まりました)
追加で今回はいくつかの機能も加えています。
## 色表示
大体のライフゲームはセルを一色で表示することが多いですが、今回は多色のセルを使いました。具体的には、初期状態では白、緑、赤、青の4種類から始まり、誕生の処理が行われるときに隣接するセルの色の平均の色のセルが誕生するようにしました。
## キー入力
矢印キーで時間経過の早送りと遅送り、数字キーでいつでも初期化できるようにしました。押された初期数字キーをnとすると、セルの比率がn0%になるように初期化されます。

## プログラム本体

```Python3
import pygame
from pygame.locals import *
import sys

display = [1010, 1010]
world_size = [100, 100]
num_key = [K_1, K_2, K_3, K_4, K_5, K_6, K_7, K_8, K_9]

class World:
    def __init__(self):
        self.speed = 1 # 描画速度
        self.world = np.zeros(tuple(world_size + [3]))
        self.color = np.asarray([[255.0, 255.0, 255.0], [255.0, 0.0, 0.0], [0.0, 255.0, 0.0], [0.0, 0.0, 255.0], [0.0, 0.0, 0.0]])
        
    # ランダムに初期化　flag=Trueの場合色もランダム
    def random_init(self, p, color_flag=False):
        for i in range(world_size[0]):
            for j in range(world_size[1]):
                if random.random() > p:
                    continue
                
                if color_flag:
                    color = self.color[random.randint(0, 3)]
                else:
                    color = self.color[0]
                self.world[i, j] = color
        
    def draw(self, screen):
        for i in range(world_size[0]):
            for j in range(world_size[1]):
                pygame.draw.rect(screen, tuple(self.world[i, j]), Rect(10*j + 10, 10*i + 10, 10, 10))
        
    def update(self):
        next_world = np.zeros(tuple(world_size + [3]))
        flags = self.world.sum(axis=2) > 0
        
        for i in range(world_size[0]):
            for j in range(world_size[1]):
                min_x = max(0, j-1)
                max_x = min(world_size[1], j+2)
                min_y = max(0, i-1)
                max_y = min(world_size[0], i+2)
                count = np.sum(flags[min_y:max_y, min_x:max_x])
                if flags[i, j] == 0: # 死んだセル
                    if count == 3: # 誕生
                        area = self.world[min_y:max_y, min_x:max_x]
                        next_world[i, j] = area.reshape(-1, 3).sum(axis=0) / count
                else:
                    if 3 < count < 6: # not 過疎 or 過密
                        next_world[i, j] = self.world[i, j]
                        
        self.world = next_world

def main():
    pygame.init()
    screen = pygame.display.set_mode(display)
    pygame.display.set_caption("Lifegame")
    
    world = World()
    world.random_init(0.3, True)
    counter = 0
    
    while(1):
        screen.fill((0, 0, 0))
        
        world.draw(screen)
        pygame.display.update()
        pygame.time.wait(5)
        
        counter += 1
        if counter > world.speed:
            world.update()
            counter = 0

        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN:
                if event.key == K_ESCAPE:
                    pygame.quit()
                    sys.exit()      
                if event.key == K_DOWN:
                    world.speed = world.speed+1
                if event.key == K_UP:
                    world.speed = max(0, world.speed-1)
                if event.key in num_key:
                    world.random_init((num_key.index(event.key)+1.0)*0.1, True)
                
if __name__ == "__main__":
    main()
```
前回のテトリスに比べてだいぶ短いですね。

# 出力画面
見た目が砂嵐っぽいですね。セルの比率は30％になっています。
![Screenshot from 2019-11-09 01-05-36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/8a595f91-ef70-b080-20d7-12c024d341da.png)
ある程度時間が経過したものです。いい感じに混色(交配)が進んでいる感じがしますね。
![Screenshot from 2019-11-09 01-05-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/f6bec301-a0dd-10ab-a831-f0443eca1623.png)
予想通りというか、終末期に生存しているのは汚い色が多いですね。それだけ混色が進んだということでしょう。最初はあんなに多かったのに。
![Screenshot from 2019-11-09 01-06-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407395/37f4fa7b-73ad-3448-0a1a-7908c321ff7b.png)

# おわりに
以上でライフゲームの実装は終わりになります。実を言うともう少し機能を追加したかったんですけど、Pythonの処理速度が怪しかったので、諦めました。もしかしたらC++の方を使って実装し直すかもしれません。
それでは、また