レトロゲームエンジン Pyxel でライフゲーム

[レトロゲームエンジン Pyxel でプログラミングを始めよう！](http://tkitao.hatenablog.com/entry/2018/11/24/185346) を見て何か作りたいなという気分になっていたので、ライフゲームをシミュレーションできるやつを作ってみました。

# ライフゲームとは

名前の通り生き物をシミュレーションしたようなもので、[Wikipediaの記事](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)を見てもらうのが早いです。
実は今年のアドベントカレンダーでも、既に（今これを書いている12/7時点で）

- [Google Spread Sheetでライフゲームを作った](http://takanakahiko.hatenablog.com/entry/2018/12/05/232355)
- [リスト内包表記でライフゲーム(おまけもあるよ)](https://trap.jp/post/604/)

と2つもライフゲームに関する記事を観測していて、早くもネタ被りが怖い。

# できたもの

こんな感じ。
![pyxel-181207-225400.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/f2c9e1aa-4c92-9313-9cd7-75afbc467a2f.gif)

![pyxel-181209-131104.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/e7fbdd95-21e6-c338-6c58-012c4833ac84.gif)


# コード

こんな感じで書いてます。gist: https://gist.github.com/HelloRusk/09e255522b6827fe3f4abb3df377d4f4

```py
import pyxel


class App:
    def __init__(self):
        pyxel.init(100, 110, caption="Pyxel Life Game")
        self.is_alive = [[False] * 50 for i in range(50)]
        self.is_alive_next = [[False] * 50 for i in range(50)]

        self.is_moving = False

        pyxel.mouse(True)
        pyxel.run(self.update, self.draw)

    def update(self):
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        if pyxel.btnp(pyxel.MOUSE_LEFT_BUTTON):
            x_n, y_n = pyxel.mouse_x, pyxel.mouse_y

            if 101 <= y_n <= 110:
                if 59 <= x_n <= 78:
                    self.is_moving = False
                elif 79 <= x_n <= 100:
                    self.is_moving = True

            if 0 <= x_n <= 99 and 0 <= y_n <= 99:
                self.is_alive[x_n // 2][y_n // 2] = not self.is_alive[x_n // 2][y_n // 2]
                self.is_alive_next[x_n // 2][y_n // 2] = not self.is_alive_next[x_n // 2][y_n // 2]

        if self.is_moving:

            x = [1, 1, 0, -1, -1, -1, 0, 1]
            y = [0, 1, 1, 1, 0, -1, -1, -1]

            for i in range(1, 49):
                for j in range(1, 49):
                    alive_num = 0

                    for k in range(8):
                        if self.is_alive[i + x[k]][j + y[k]]:
                            alive_num += 1

                    if self.is_alive[i][j]:
                        if alive_num <= 1:  # 過疎による死滅
                            self.is_alive_next[i][j] = False
                        elif alive_num >= 4:  # 過密による死滅
                            self.is_alive_next[i][j] = False
                    else:
                        if alive_num == 3:  # 誕生
                            self.is_alive_next[i][j] = True

            for i in range(1, 49):
                for j in range(1, 49):
                    self.is_alive[i][j] = self.is_alive_next[i][j]

    def draw(self):
        pyxel.cls(0)

        for i in range(50):
            for j in range(50):
                if self.is_alive[i][j]:
                    pyxel.pset(2*i, 2*j, 8)
                    pyxel.pset(2*i, 2*j+1, 8)
                    pyxel.pset(2*i+1, 2*j, 8)
                    pyxel.pset(2*i+1, 2*j+1, 8)

        pyxel.text(1, 102, "Time: {:0>6}".format(pyxel.frame_count), 10)
        pyxel.text(60, 102, "STOP", 6 if self.is_moving else 3)
        pyxel.text(80, 102, "START", 3 if self.is_moving else 6)


App()
```

今回は、分かりやすさのために、100ピクセル * 100ピクセルに対して2ピクセル * 2ピクセルを1単位としてプログラムを書いています。なので、動けるのは50マス * 50マスという狭い範囲になってしまっています。
Pyxelでは256ピクセル * 256ピクセルが最大でサポートされているので、より広範囲で繁殖する物体を観察する場合は`pyxel.init()`のプロパティや`self.is_alive`の配列の大きさを適宜変えてみましょう。

# さまざまなライフゲームの物体

ここからはもうプログラミングの記事ではないです。Wikipediaの記事を見ながら面白そうなものをいくつか試してみました。

## パルサー

周期3の振動子

![pyxel-181209-131846.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/d4ea3921-eac4-fe0a-1619-b91764a3520f.gif)

## ペンタデカスロン

周期15の振動子

![pyxel-181209-132133.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/8e6898ab-bf06-aec2-3149-5bd150b3172d.gif)

## スイッチ機関車

長寿型の物体

![pyxel-181209-133422.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/d28321f3-d6d5-b6fd-4e63-671427d2aed9.gif)

## シャトル

特にこれは女王蜂とも言われる

![pyxel-181209-134047.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/21b54210-e47f-f017-b46e-ae9d13fc084e.gif)


## 宇宙船の衝突

軽量級宇宙船の衝突により2つのグライダーが発生

![pyxel-181209-132743.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/df1aff6f-9d26-494c-7d7a-d7d5afba0bb4.gif)

中量級宇宙船の衝突によりパルサーが発生

![pyxel-181209-133103.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/c8a93f8b-f94e-bf80-4c72-992802f04928.gif)

## グライダー銃

グライダーを永遠に打ち出し続ける

![pyxel-181209-134644.gif](https://qiita-image-store.s3.amazonaws.com/0/233069/66bc6413-228a-28cf-3c10-6ce07cdd8719.gif)