前回より、TkinterというGUIライブラリを利用して、ゲーム制作に挑戦している。今回作るのは、知る人ぞ知る『ライフゲーム』というシミュレーションゲームだ。比較的簡単でありながら、見た目が面白いので挑戦してみよう。

ライフゲームとは?

筆者が、新しいプログラミング言語やフレームワークを覚える時には、必ず作っているプログラムがある。それが「ライフゲーム(LIfe Game)」だ。これは、生物集団の栄枯盛衰をシミュレーションする環境ゲームで、イギリスの数学者ジョン・ホートン・コンウェイによって考案されたものだ。

実際の画面は、次のようなものだ。

ライフゲームの画面 - 生物の誕生と淘汰がルールに基づいて決定される

ライフゲームでは、この画面の通り、二次元のグリッドで生物(セル)の生死を表現する。赤い円が生きているセルだ。そして、生物集団は、過疎でも過密でも生きてはいけないという基本的なルールがある。

生物の周囲(8方向)を調べて、何匹の生きたセルがあるかによって、次の世代の生死が決定される。つまり、生物の誕生と淘汰が以下の簡単なルールに基づいて決定される。

ライフゲームのルールは、次の通り:

- 生物の誕生 --- 死んでいるセルの周囲に、生きているセルが3つあれば、次の世代に生物が誕生する
- 生物が継続して生存 --- 生きているセルの周囲に、生きているセルが2つ以上3つ以下あれば、次の世代に生物は継続して生存する
- 過疎状態 --- 周囲に生きているセルが1つ以下しかなければ、次の世代には死滅する
- 過密状態 --- 生きているセルの周囲に、生きているセルが4つ以上あれば、過密により死滅する

これだけのルールだが、配置するセルの形によって、動きに規則があり、見た目が面白いので、楽しめる。

プログラムを作ってみよう

それでは、さっそく、プログラムを作ってみよう。以下は、最初に、ステージをランダムに初期化し、その後、ライフゲームのルールに沿って、300ミリ秒ごとにシミュレーションを行うプログラムだ。

 from random import randint
 from tkinter import *

 # 変数・定数の定義 --- (*1)
 COLS, ROWS = [30, 20] # ステージのサイズを定義
 CW = 20 # セルの描画サイズ
 data = [] # ステージデータ
 for y in range(0, ROWS): # ステージをランダムに初期化
     data.append([(randint(0, 9) == 0) for x in range(0, COLS)])

 # ライフゲームのルールを実装したもの --- (*2)
 def check(x, y):
     # 周囲の生存セルを数える
     cnt = 0
     tbl = [(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)]
     for t in tbl:
         xx, yy = [x + t[0], y + t[1]]
         if 0 <= xx < COLS and 0 <= yy < ROWS:
             if data[yy][xx]: cnt += 1
     # ルールに沿って次世代の生死を決める
     if cnt == 3: return True # 誕生
     if data[y][x]:
         if 2 <= cnt <= 3: return True # 生存
         return False # 過疎 or 過密
     return data[y][x]

 # データを次の世代に進める --- (*3)
 def next_turn():
     global data
     data2 = []
     for y in range(0, ROWS):
         data2.append([check(x, y) for x in range(0, COLS)])
     data = data2 # データの内容を次の世代へ差し替え

 # 画面を構築 --- (*4)
 win = Tk() # ウィンドウを作成
 cv = Canvas(win, width = 600, height = 400) # キャンバスを作成
 cv.pack()

 # ステージを描画 --- (*5)
 def draw_stage():
     cv.delete('all') # 既存の描画内容を破棄
     for y in range(0, ROWS):
         for x in range(0, COLS):
             if not data[y][x]: continue
             x1, y1 = [x * CW, y * CW]
             cv.create_oval(x1, y1, x1 + CW, y1 + CW,
                 fill="red", width=0) # 生きているセルを描画

 # 300ミリ秒ごとに世代を進める --- (*6)
 def game_loop():
     next_turn() # 世代を進める
     draw_stage() # ステージを描画
     win.after(300, game_loop) # 指定時間後に再度描画

 game_loop() # ゲームループを実行
 win.mainloop() # イベントループ

上記のプログラムを「lifegame-simple.py」(ソースコード:lifegame-simple.lzh)という名前で保存しよう。そして、WindowsではコマンドプロンプトやPowerShellを、macOSではターミナルを起動し、それぞれ以下のコマンドを実行してみよう。

 # Windows
 python lifegame-simple.py
 ---
 # macOS
 python3 lifegame-simple.py

プログラムが実行されると、以下のような画面が出て、生物の栄枯盛衰の様子を楽しめる。

プログラムを実行したところ

プログラムを見てみよう。プログラムの冒頭(*1)では、変数や定数の定義を行っている。こうした宣言は、プログラムの先頭にまとめておくと、あとあと変更が容易だ。ステージ上のセルの生死の状態を二次元配列で管理するのが変数dataだ。ランダムにセルの初期状態を決定している。Trueが生、Falseが死を表すことにした。

プログラムの(*2)と(*3)の部分では、ルールに沿って次世代のセルの状態を決定する。(*2)のcheck()関数では、(x, y)の位置について、次世代の生死の状態を返す。生死の状態は、その位置の周囲(8方向)に生きているセルがいくつあるかによって決定される。ちょっと分かりづらいのだが、ここで、セルの8方向を調べるために、変数tblに現在の位置からの相対座標を代入している。そして、for .. in .. 構文を使って、実際のセルを計算して、生死の状態を確認する。(*3)の部分では、ステージの全てのセルについてcheck()関数で次世代の生死を確認する。

プログラムの(*4)の部分では、ウィンドウを作成し、図形の描画が可能なキャンバスを載せる。この点に関しては、前回詳しく紹介した。

(*5)では、既存の描画内容を全て削除した後で、改めて、セルを描画する。このとき、create_oval()メソッドで、赤い円を描画する。

そして、(*6)の部分で、300ミリごとに、世代を進め、ステージの描画を行う。after()メソッドを使うことで、定期的な描画を行う。このメソッドは、以下の書式で指定する。

 [書式] msecミリ秒後に、関数funcを実行する

 win.after(msec, func)

まとめ

以上、今回は、ライフゲームの作り方を紹介した。これまで本連載で紹介したプログラムと比べると、少し複雑かもしれない。しかし、60行以下のプログラムなので、各部分の役割を意識しならが分析してみると良いだろう。Pythonの基本文法の理解度を確かめることもできるだろう。

なお、ステージ上のセルを自由にマウスで配置できるようにしたライフゲームも作ってみた。プログラムが長くなったので、全プログラムを掲載することはしないが、以下よりダウンロードできるので、興味のある方は、そちらも確認してみて欲しい。

プログラムのダウンロード: lifegame-full.lzh

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。