GUIのライフゲームを作ってみた【Python】
ライフゲーム
ライフゲームとは,もっともシンプルな生命シミュレーションゲームである.
原語では Conway's Game of Life または略して Life と呼ばれる.
その名の通り,Conwayさんが考案したゲームで,1970年に発表された.
ルールについては後述するが,その単純なルールから派生する多くの難問の存在により,多くの数学者たちを虜にし,今もなお愛され続けているゲームである.
今回,このライフゲームをPythonで作成してみたので紹介する.
ちなみにソースはGithubにあげているので,ぜひ触ってみてほしい.
【HiroshiARAKI/lifegame: Simple Conway's Game of Life Simulator on Python!!】
全体的なルール
本題に入る前にルールから説明していく.
セル
まず上の図で表すように,セル(Cells)と呼ばれる格子状のマスが存在する.
こいつらは,「生」か「死」の二つの状態を持つ.
上の図でいうと黒が生,白が死である.
世代を進める
ライフゲームは世代を進める度に,あるルールにしたがって,次の世代での自分の状態(生or死)が決まる.
死んでいたセルから新たに誕生するかもしれないし,生きていたセルが死ぬかもしれない.
はたまた生き延びるかもしれない.
これらは次に紹介するルールで決まる.
生存ルール
ライフゲームには以下の4つルールのみが存在する.
誕生
死んでいるセルの周りに,ちょうど3つ生きているセルが存在すれば,次の世代でそのセルは誕生する.
生存
生きているセルの周りに,生きたセルが2つか3つ存在すれば,そのセルは生き残る.
過疎死
生きているセルの周りに,生きているセルが1つ以下であれば,そのセルは死ぬ.
過密死
生きているセルの周りに,生きているセルが4つ以上あれば,そのセルは死ぬ.
実装してみる
以上のルールを実装してみる.
というかしてみたので,その解説になるが全部はしきれないので,要点のみ.
最初に行ったように,コードは全てGithubにあげてあるので,手元に落として実際に動かしてみてほしい.
【HiroshiARAKI/lifegame: Simple Conway's Game of Life Simulator on Python!!】
今回実装するにあたり,4つのクラス(ファイル)から成り立っている.
それぞれのクラスを図式化すると以下のようなイメージ.
なぜこんな複雑になったのかというと,僕のコーディングの計画性の無さのせいである...
セルの管理 - cells.py
Cellsクラスでは主に,セルの管理を行う.
といっても大したことはしていない.
コンストラクタでは,セルを作成しゼロで初期化する,それだけ.
1 2 3 | def __init__(self, f_shape): self.f_shape = f_shape self.cells = np.zeros(shape=f_shape, dtype=int) |
あと重要な関数は,周囲の生きているセルを数える関数.
この関数では,numpyの np.count_nonzero() を使っているが,中心のセルを含めないようにする必要がある.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def count_alive_cells(self, x, y): """ Count alive cells surrounding a cell. :param x: :param y: :return: """ # indices of surrounding cells. ul = y - 1 if (y-1) >= 0 else 0 # upper left ur = y + 2 if (y+2) <= (self.f_shape[1]) else self.f_shape[1] # upper right bl = x - 1 if (x-1) >= 0 else 0 # bottom left br = x + 2 if (x+2) <= (self.f_shape[0]) else self.f_shape[0] # bottom right # slice cells = self.cells[bl:br, ul:ur] n_cells = np.count_nonzero(cells) return n_cells if not self.is_alive(x, y) else n_cells - 1 |
ちょっと複雑になってしまったが,スライスで3×3のセルを取り出して,生きているセルをカウントする.
その後,中心セルが生きていればカウントを一つ減らしているだけ.
ライフゲームの核 - lifegame.py
LifeGameクラスでは先のCellsクラスを継承して,セルの情報とルールに則って,世代を進めていく.
ちなみに継承せずに全て,このクラスに書いても良かったが,なんとなく継承という形をとった.
世代を進める update() から見ていく.
1 2 3 4 5 6 7 8 9 10 11 12 | def update(self) -> None: """ Update next generation :return: """ next_states = np.zeros(shape=self.cells.shape, dtype=int) for y, line in enumerate(self.cells): for x, cell in enumerate(line): next_states[x][y] = self.get_next_state(self.count_alive_cells(x, y), self.is_alive(x, y) ) self.step_next_generation(next_states) |
各セルの次世代の状態をゼロで初期化しているが,それぞれのルールによって決まる状態を以下のように割り当てている.
1 2 3 4 | BORN = 0 # 誕生 SURVIVE = 1 # 生存 DIE_SPARSE = 2 # 過疎死 DIE_DENSE = 3 # 過密死 |
そしてそれぞれのセルに対して,周囲のセルの状態を計算し, get_next_state() で次世代の状態を取得する.
この関数定義はとても単純だ.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @staticmethod def get_next_state(n_cells, my_state) -> int: """ Get next state of a cell :param n_cells: :param my_state: :return: """ if not my_state and n_cells == 3: return BORN elif my_state and (n_cells == 2 or n_cells == 3): return SURVIVE elif my_state and n_cells <= 1: return DIE_SPARSE else: return DIE_DENSE |
次の状態を取得したら,それに従って世代を進める.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def step_next_generation(self, next_states) -> None: """ Change all cells' states by the Life Rule :param next_states: :return: """ for y, line in enumerate(self.cells): for x, cell in enumerate(line): if next_states[x][y] == BORN: self.cells[x][y] = 1 elif next_states[x][y] == DIE_DENSE or next_states[x][y] == DIE_SPARSE: self.cells[x][y] = 0 else: self.cells[x][y] = 1 |
以上がライフゲームの核となる部分.
こうみるとライフゲームはとても単純なシミュレーションゲームであることがわかる.
GUIフレーム - frame.py
GUIフレームはwxPythonと呼ばれるフレームワークを使っている.
この実装については解説していると長くなってしまうので,割愛する.
気になる人はコードを見た方が早い.
GUI呼び出し - gui.py
このGUIクラスは正直必要ないが,自分の実装コンセプトとして「main(関数)内はできるだけシンプルに」があるので,作成した.
やっていることは,先のフレームを呼び出しているだけなのでこれも解説は割愛する.
動かしてみる
それでは実際に動かしてみる.
これを読んでいる皆もぜひ動かしてみて欲しい.
コードはGithubにk (以下略) → 【HiroshiARAKI/lifegame: Simple Conway's Game of Life Simulator on Python!!】
ゲームの始め方はすごく簡単で,最低限以下の3行があれば良い.
1 2 3 | from lifegame import GUILifeGame game = GUILifeGame() game.run() |
これで,50×50のセルが死んだ状態で提供される.
各セルを左クリックすると,セルの生死を反転できる.
つまり,操作している人はライフゲーム内の神様である.
上のボタン類は順に,「数秒ごとに世代を自動で進める」「1世代進める」「 Runを止める」「セルを全て死の状態にする」「ランダムにセルを誕生させる(0.2の割合)」といった具合.
ちなみに,
1 | game = GUILifeGame(f_shape=(10, 10), time_step=100) |
とすれば,10×10のライフゲームで,自動世代交代(Run)が約100ミリ秒(0.1秒)間隔になる.
「約100ミリ秒」といったのは,実際にはセルの数によって,処理が増えるので更新間隔が長くなるからである.
また,ゲームを起動するときに
1 | game.run(init_rand=True, rate=0.2) |
とすると,最初のセルの状態をランダムに初期化できる.
第2引数は生きているセルの割合.(1にすれば真っ黒になるが次の世代で壊滅する)
さいごに
今回,ふと思い立ってライフゲームを実装してみた.
最初はCLIだけでいいか,と思ったが「どうせなら...」とGUI版も作成してしまった.
GUI化の方が手こずったかもしれない...
ちなみに,今回作ったライフゲームは100×100より大きいものは例外処理を設けてGUIが立ち上がらないようにしている.
というのも,今回wxPythonで作ったが,セルが大きくなりすぎると処理がかなり重くなる上に,ウィンドウサイズを固定しているので,かなり一つ一つのセルが塩粒サイズになってしまう.
もし気力があれば,スクロールに対応したり,処理が重くならないように工夫したい...(したい)
コードはこちら↓
【HiroshiARAKI/lifegame: Simple Conway's Game of Life Simulator on Python!!】