課題
ライフゲームとは何なのかについては,以下の「ライフゲームの概要」を参照してください. 今回の課題では, 授業で提供するライブラリ(プログラムで利用できる部品集)を用いることで, ライフゲームを実行してデータを生成するようにします. 課題のプログラムでは,ライブラリを利用して生成されるデータに基づいて, ゲームの実行過程を順にアニメーション表示する仕組みを実現してください.
キー入力あるいはマウスクリックでアニメーションを開始するようにして下さい. また一旦停止,再開もできるようにして下さい.
ライフゲームの概要
ライフゲームとは状態が時間変化するセルで構成されるシステムです. 格子状に敷き詰められている均質なセルがあって,各セルはそれぞれ0か1のどちらかの状態をとります. 各セルの状態は周囲のセルの影響のもとで単位時間ごとに変化していくように定められます. 次の画面は実際のゲームの進行例を一部示しています. 画面中で小さな白い四角の一つ一つが状態が1のセルを示しています.
…→ | …→ | …→ |
各セルの次の時刻の状態は,次の図のように,そのセルと周囲8個の3×3個のセルの現時点での状態によって決められます.
状態変化のルールは次の表の通りです. あるセルの状態が次にどうなるかは, 周囲の8個のセルに「状態1」のセルが何個あるかで決められます.
周囲の「1」の個数 | セルの次状態 |
---|---|
1個以下 | 0 |
2個 | 変化しない |
3個 | 1 |
4個以上 | 0 |
対象のセル(Cとします)の周囲に「状態1」のセルが1個以下または4個以上あるときは次状態は0, 3個のときは次状態は1になります(現在のセルCの状態には依存しません). また周囲に「状態1」のセルが2個のときはセルCの状態は変化しません. つまり現在の状態が0なら次状態も0,現在の状態が1なら次状態も1のままとなります.
ここで状態1のセルには生命体が存在して,状態0のセルは空である(何もいない)と考えてみます. このときライフゲームのルールを次のように解釈できます.
- 生命体は周囲が過疎(周囲の生命体が1個以下)でも,過密(周囲の生命体が4個以上)でも生き残れない(次に状態が0になる).
- 生命体は周囲の密度がほどほど(周囲の生命体が2個か3個)であれば,生き残る(1の状態を保つ).
- 空のセルの周囲の密度がちょうどよければ(周囲の生命体が3個),そこに新たな生命体が誕生する.
次にごく小さなライフゲーム(セルが4×4=16個)の進行の例をデータとして示します. 上で示したルールにしたがって,ステップごとにセルの状態が変わっていることを確かめてみてください.
なおこの例も含めて,今回は左右の端のセル,上下の端のセルは互いに隣接しているものとして扱います. つまり各横列の左端のセルは同じ横列の右端のセルと隣接していて, 各縦列の上端のセルは同じ縦列の下端のセルと隣接しているものとして扱います(このように扱うのは境界のセルを特別扱いすることを避けるためです).
step 0 | step 1 | step 2 | step 3 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
→ |
|
→ |
|
→ |
|
ライフゲームのルールはごく単純ですが,しばしば思いも寄らない変化のパターンが見られます.
課題の進め方
セルの初期データを適当に決めて, ライフゲームのルールにしたがって,各セルの状態をステップごとに順次更新していく仕組みをライブラリとして用意してあります.それを使うことで,各時刻での各セルの状態を順に求めていくことができます. そこで課題のプログラムにおいては,ライフゲームで各セルのデータがどうなるのか計算する処理を自分で実現する必要はありません.
この課題では,各時刻での各セルの状態はライブラリによって計算されていくものとして,それらのセルの状態に基づいて画面を描いて,それを自動的に更新していくプログラムを作成してください. キー入力あるいはマウスクリックでアニメーションを開始するようにして, 一旦停止,再開もできるようにして下さい.
課題のために次のテンプレートを用意しています.
テンプレートには,ライフゲームを開始できるように準備する処理は記述してあります. このテンプレートに次の2つの処理を適宜追加して,プログラムを完成させてください.
- ゲームを1ステップ進行させる処理(セルの状態更新)
- 現在の各セルの状態に従って画面を描画する処理(セルの表示)
なお次のサンプルプログラムも参考にして下さい.
以下でゲームを進行させる方法とセルを描画する方法について,詳しく説明します.
ライフゲームのデータ管理
今回のプログラムでは状態変数__gameを用いて,ライフゲームに関するさまざまな処理を実行するようになっています. ゲームの進行とセルの描画において,ともに__gameを利用します.
ゲームの進行(セルの状態更新)
ライフゲームを1ステップ進行して,セルの状態を更新するには次を実行します.
__game.step | ライフゲームを1ステップ進行させる |
---|
__game.stepをプログラムで実行すると,セルのデータがライフゲームのルールにしたがって一斉に更新されます. ただしこれを実行した時点で画面が更新されるわけではありません. この__game.stepでは,セルごとの「生命体が存在する・しない」のデータ(存在しない=0,存在する=1)を更新するだけです. 画面を更新するには,描画コールバック(display)にセルを描画する処理を記述しておいた上で,データに従って画面を再描画する必要があります.
参考のためにテンプレートにはkeyboardコールバックの中で[s]を押したときにライフゲームを1ステップ進行させて,描画コールバック(display)を呼び出す処理(GLUT.PostRedisplay())が組み込まれています(keyboardコールバックを参照のこと). 描画コールバック(display)が適切に記述されていれば,[s]を押すたびに各セルの状態が1ステップずつ更新されて,それに合わせて画面も更新されます.
なおこのステップ動作([s]を押したときの処理)は,参考のために入れているだけです.削除しても構いませんし,そのまま残しておいても構いません. またステップ動作とアニメーションとの干渉(アニメーション実行中に[s]を押したらどうなるか)も気にする必要はありません(この問題の解消のために何か工夫をすることも歓迎です).
セルの描画
生命体が存在するセルを四角で描画することにします. つまり状態が1のセルをそのセルの位置に応じて描画します. テンプレートではセルはN×N個配置されるようになっています. 次の図に示すように,セルの位置を横列と縦列の番号(i,j)で表すとi,jの範囲は0 ≦ i,j < Nとなっています. 左上角が(0,0)で右下角が(N-1,N-1)です.
セルの状態を知るために以下の枠組みが用意されています.
__game.each do |i,j| : : end |
生命体が存在するセル(i,j)のそれぞれについて,do〜endの内部に記述されている処理を実行 |
---|
このeachでは,生命体の存在するセルのみを対象として,それぞれについてdo ... endの内部の処理を実行します. セル(i,j)に生命体が存在するとしたときに,その位置(i,j)のセルについて実行する処理を「do ... end」に記述するわけです.
プログラム実行時には,各セルが順にチェックされて, 生命体がみつかるたびにそのセルの位置が(i,j)に代入され, その(i,j)の値のもとで「do ... end」の間の処理が順に実行されるようになっています.
テンプレートでは,例として次のような処理を行っています(ここではテンプレートに書かれている説明文は変更しています). この場合,生命体の存在するセルの位置が順にすべて端末画面に表示されます.
__game.each do |i,j|
# 一般に「p 式」で式の値を端末画面に表示できる
p ["life",i,j]
end
たとえば次のようにセルが配置されているとします.
0 | 0 | 1 | 1 |
0 | 0 | 0 | 0 |
0 | 1 | 0 | 0 |
0 | 0 | 0 | 1 |
このとき上の例のようなプログラムを実行すると, 端末の画面に次のように表示されるはずです.
["life",2,0] ["life",3,0] ["life",1,2] ["life",3,3]
このeachの処理を具体的に見てみます. 上のマップでは生命体が(2,0),(3,0),(1,2),(3,3)のセルに存在していることから, これら各生命体の位置が順に(i,j)に代入されて, それぞれについてdo...endの処理が実行されています(合計で4回実行されます). ここでは,それぞれについてp ["life",i,j]が実行されることから,次のような結果が得られるわけです.
["life",2,0] # i=2, j=0のもとで,p ["life",i,j] を実行した結果 ["life",3,0] # i=3, j=0のもとで,p ["life",i,j] を実行した結果 ["life",1,2] # i=1, j=2のもとで,p ["life",i,j] を実行した結果 ["life",3,3] # i=3, j=3のもとで,p ["life",i,j] を実行した結果
このようにeachでは各生命体の位置が(i,j)に代入されて, その対応のもとでdo...endの処理がそれぞれ実行されます.
作成するプログラムでは,eachで得られる生命体のセルの位置(i,j)を利用して,その位置に合わせて四角を描画します. そのためにはセルの位置とOpenGLの画面の世界座標との対応を適切に定める必要があります. セルの位置,画面の座標系は次に示す通りです.
セルの位置 | 画面の座標系 |
iの範囲: i=0,1,...,N-1 | xの範囲: -1≦x≦1 |
jの範囲: j=0,1,...,N-1 | yの範囲: -1≦y≦1 |
(左上が原点,iは右向き,jは下向き) | (中心が原点,x軸は右向き,y軸は上向き) |
このときNの値を変えたとしてもそのままプログラムが動作するように,記述を一般化しておくとよいでしょう.
四角を描画するにはGL.Rectを使うとよいでしょう.
# 左上角(x0,y0),右下角(x1,y1)の矩形を描く
GL.Rect(x0,y0,x1,y1)
プログラムの挙動を修正する手がかりを得る方法
たとえプログラムが動作しても,結果が想定通りにならないことはよくあります. そのような場合,プログラムに何らかの誤りがあるはずですが,プログラムの記述を調べたり,CGの画面を見るだけでは誤りが容易には見つけられないことも少なからずあります. そのようなとき,プログラム実行中に「誤りに関連がありそうな変数の値」を調べてみると,手がかりになりえます. 次を参考にしてみてください.
座標値の計算に関する注意
整数と整数で除算を行うと商が得られることに注意して下さい.
N = 64
i = 32
i/N # ==> 0 (0.5にはならない!)
整数を小数点数に変換すればこの問題を避けられます.
N = 64 i = 32 # 『整数.to_f』で整数を小数点数に変換できる i.to_f/N # ==> 0.5 # 除算の前に変換しないとやはり0になってしまう (i/N).to_f # ==> 0.0 k = 5 # 整数定数を使場う合には「.0」をつければよい(to_fを使う必要はない) 1.0/k # ==> 0.2 1/k # ==> 0
(参考)グリッドの描画
一つ一つのセルをはっきり区別するために枠線を描いてもよいかもしれません(課題では実現する必要はありません). これを実現するには画面を縦横に区切る線(グリッド)を描けばよいでしょう. 次のような繰り返し処理を利用すれば,これを簡単に実現できます. なお線を描くにはGL::LINESのプリミティブを使います.
N.times do |k|
# k = 0,1,...,N-1について(計N回),
# do...endの処理をそれぞれ実行する
end
ライフゲームでの実験
プログラムができたら,ライフゲームのパラメータを変えてみたり, 特別な初期配置からゲームを開始させたりしてみるとよいでしょう. パラメータとしては次の二つが調整可能です.
- RATE(初期配置に出現させる生命体の割合[0以上1以下])
- N(1辺のセルの個数)
初期配置データファイル
今回のプログラムでは,生命体セルを特定のパターンに配置してからライフゲームを始めるようにできるようにもなっています. 興味があれば,以下のファイルをダウンロードして,次のように実行してみてください(プログラムファイル名をlg.rbとしています).
なおこれらのデータはライフゲームの情報サイト(Lifewiki)のページを参照して作成しました. 初期配置に関するLifewikiの解説ページへのリンクも示しておきます.
$ ruby lg.rb 初期配置ファイル名
参考文献・情報源
次にライフゲームの参考文献,情報源を示します.
- Lifewiki
- 『ライフゲイムの宇宙』[新装版]
ウィリアム・パウンドストーン 著,有澤誠 訳
日本評論社(2003)