前回より、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貢献者章受賞。技術書も多く執筆している。