第18回 ライフゲームを作ろう
2004/7/29作成
2004/8/5更新
Wikipediaを見ていたら、秀逸な記事に「ライフゲーム」というのが載っていました。
読んでみると、ロジックが明確で私のようなプログラムの初心者でも書けそうなものだったので書いてみることにしました。
そうしてできあがったのがこちらになります。
まだまだ改良の余地はありますが、お楽しみいただけると幸いです。
1.ライフゲームのルール
わかりやすく3マスのフィールドを用意してみました。
ここで、3×3のそれぞれ9つの要素をセルと言い、セルの状態が「□」の時はセルが死んでいることとし、「■」のときは生きているものとします。
セルは現在の自分のセルの状態と自分に接している8つのセルの状態から次の状態が決定します。
- 自分のセルが「生」のとき
- まわりの8つのセルのうち生きているセルが2または3個ならば次の世代でも「生」
- それ以外は次の世代で「死」
たとえば、フィールドが次のような状態だったとき、(X,Y)=(1,1)のセル(まんなかのセル)は次の世代では死にます。
0 1 2 0 □ □ □ 1 □ ■ □ 2 □ □ □
それでは次のような場合はどうでしょう。
(1,1)のまわりには(1,0)、(0,2)、(2,2)と3つのセルが「生」なので次の世代でも生きます。
0 1 2 0 □ ■ □ 1 □ ■ □ 2 ■ □ ■
次はどうでしょうか。
今度は(1,1)のまわりには4つの生きているセルがあります。
なので、(1,1)のセルは次の世代では死んでしまいます。
0 1 2 0 ■ ■ □ 1 □ ■ ■ 2 ■ □ □
- 自分のセルが「死」のとき
- まわりの8つのセルのうち生きているセルが3個の時は次の世代で「生」
- それ以外は次の世代は「死」
なので、次の場合、(1,1)のセルのまわりに(1,0)(2,0)(0,2)と3つの生きているセルがあるので次の世代で生きます。フィールドをはみ出した場合はどうでしょうか。それ以外の時は「死」のままになります。
0 1 2 0 □ ■ ■ 1 □ □ □ 2 ■ □ □
0 1 2 0 □ □ ■ 1 □ □ □ 2 □ □ ■
真のライフゲームは無限のフィールドで動かすことを期待しているのですが、さすがにそれは無理なのではみ出したときはとなりのセルは「死」にしてしまいます。なので、(0,0)のセルは(1,0)(0,1)(1,1)の3つのセルの生死によって状態が決まります。
2.仕様をきめる
プログラムに入る前に、ざっくりと仕様を決めることにしましょう。
- フィールドの大きさは自由に選べるようにする
- フィールドに初期値を入れる
- 初期値を確認する
- 世代を進める
ということで、上記の4画面構成です。
それぞれの画面の名前は、今後は次のように呼びます。
- フィールド設定
- 初期値入力
- 初期値展開
- 世代更新
画面と画面の間のデータ交換はフォームを使って渡します。
1のフィールド設定から初期値入力へは当然ながらフィールドの大きさが渡されます。
初期値入力画面では受け取ったフィールドの大きさを展開して初期値を入力するフォームを生成します。
フォームでは見づらいので、設定した初期値がどんな感じなのかを確認する画面を表示します。
初期値展開のページではフォームで渡されたデータを展開するのですが、全体の大きさがわからないと初期値の表示ができないので「たて」と「よこ」の大きさもつけました。
初期値展開の画面からはセルの状態を渡します。
これだけでも世代の更新はできるのですが、今が何世代目なのかがわかるとおもしろそうなので世代も渡しています。
ここではセルの状態の渡し方を工夫してあるので、たてとよこの大きさは渡していません。
世代更新ではセルの状態からライフゲームのルールを適用して次の世代に更新します。
更新されたセルの状態と世代を1つ更新してまた自分に戻します。
これが一連の流れになります。
3.フィールドを設定
フィールドの設定をするだけのページなので、とくに動的ページにする必要はありません。
フォームのパーツはなにを使ってもいいと思いますが、入力値を小手先でフィルタできるのでプルダウンを使いました。
うちで作ったのは動的ページになっていますが、OPTIONタグを書くのが面倒なのでCGIに書かせているだけです。
この程度のことならサーバサイドでやるよりはJavaScriptなどでやった方が効率的だと思います。私はJavaが不自由なのでやってません。
ちなみに最初に作ったのは正方形のフィールドのみの対応だったのですが、ソースをながめているうちに長方形のフィールドでもそれほど変更点がなさそうだったのでたてとよこの2項目入れるように仕様変更した経緯があります。
長方形フィールド対応に変えたらソースの可読性が上がったという副産物も得られました。
4.初期値入力
たてとよこの長さをもらったので、このサイズでフォームを作ります。
フォームは直感的なチェックボックスにしました。
チェックボックスの'name'値は次のページが展開しやすいようにうまい名付けをしてあげます。
うちの場合は'cel00_00'みたいな感じでたてとよこの座標が入っています。
たてとよこの最大値となるセル(右下のセル)が確実に渡されるのであれば次のページでそのセルを探してたてとよこの大きさを知ることができるのですが、チェックボックスの場合、onのものしか次のページに渡らないためデータが「死」の時はそのままではフィールドの大きさを知ることができません。
したがって、hiddenでたてとよこの大きさも次のページに渡してあげます。
name値の展開が少々気を使いましたが他にはあまりロジックが入っていないので、比較的作成が楽でした。
これもほとんどが画面表示の簡略化でしかCGIを使っていないので、ある程度はJavaScriptで書いたほうが都合がいいと思います。
5.初期値展開
フォームで送られてきたものを展開します。
フォームだとどんな値を入力したのかわかりにくいので、いったん「□」と「■」に展開します。
セルは位置情報とセルの状態(生死)という3つの情報を持っています。
こういうときは、情報を格納するのは配列が便利です。
でも位置情報はたてとよこの2つなので、二次元配列を作るのが便利でしょう。
しかしながら、Rubyでは標準で二次元配列のクラスが用意されていません。
地道に配列の中に配列を入れていきました。
たて5×よこ8の二次元配列(cel)はこんな感じで作ります。
tate = 5
yoko = 8
cel = []
(0 ... tate).each do |i|
cel[i] = Array.new( yoko , 0)
end
これでまっさらな二次元配列ができました。値は数字の0(ゼロ)で初期化しています。
やり方はいろいろあると思いますが、私の場合は0で「死」、1で「生」にしました。
真偽値(true,false)にしようとも考えたのですが、次の世代更新の画面に渡すとき変換しなければいけないので結局これに落ち着きました。
真偽値にすれば、次章の世代更新のロジックを論理演算にできてかっこよかったのかもしれませんが。
値を取り出すときはcel[tate][yoko]で取り出せます。
上書きをするときも同様です。
ということで、フォームで受け取った座標でチェックが入っていたものを1に置き換えましょう。
正規表現の出番です。
cgi = CGI.new
cgi.params.each do |key,value|
if /cel(\d\d)_(\d\d)/ =~ key.to_s
yy = $1.to_i
xx = $2.to_i
cel[yy][xx] = 1 if /on/i =~ value.to_s
end
end
Ruby得意のイテレータを使いながらチェックボックスの値をハッシュで取り出しています。
ハッシュのキー値はcel(\d\d)_(\d\d)で最初の括弧はたての位置が、後の括弧はよこの位置が入っているのでそのまま二次元配列につっこんでしまいます。
念のため、valueがonになっていることもチェックしています。
このロジックはあまりきれいじゃないのでもう少し修正の余地があるでしょう。入力チェックも甘いですし。
ここまで来ると二次元配列の中に現在のセルの状態が格納されたことになります。
あとはこの二次元配列を逐一展開して「□」や「■」に置換して書き出せばOKです。
最後の仕事は世代更新画面へのデータの引き渡しになります。
やっぱりフォームを使います。
ここでもhiddenタグが大活躍です。
初期値入力画面のときは生きているセルの情報だけが送られてきたため、フィールドの大きさが把握できませんでした。
そこの不便さを解消するため、世代更新画面にはすべてのセルデータを送ることにします。
といっても、すべての座標について送っていたのではかっこ悪いので、たての座標ごとに一括で送ることにしました。
(0 ... tate).each do |y|
hid_value = cel[y].join
yy = y.to_s.rjust(2).gsub(/ /,"0")
print "<INPUT type='hidden' name='cel#{yy}' value='#{hid_value}'><BR>\n"
end
少々強引ですが、cel[y]を逐一取り出して配列をStringに変換してあります。
yyはたて座標が1桁のものを強制的ゼロをパディングして2桁にしています。1.8系ではもっとスマートな関数があるのですが、下位互換です。
そしておまけで世代=0もhiddenで送ります。
6.世代更新
フォームで送られてきたものをもう一度二次元配列に戻します。
二次元配列を初期化するために、フィールドの大きさが必要になります。
ちょっとした算数で、たてとよこの長さを求めましょう。
cgi = CGI.new
if cgi['sedai'] == ""
tate = cgi.params.size
val_size = cgi.params.values.join.size
sedai = "0"
else
tate = cgi.params.size - 1
val_size = cgi.params.values.join.size - cgi.params['sedai'].size
sedai = cgi['sedai']
end
yoko = val_size / tate
たての長さはフォームで送られてきたキーの数と同じですが、世代のデータが入っているので1をひいておきます。
ちょっとだけ丁寧に書いて、世代の値が入ってなかったときも想定して書いてあります。
よこの長さはフォームで渡ってきた値のサイズから世代の値をひいて、たての長さで割れば出てきます。
ついでに世代も入れてあります。
たてよこの準備ができたら、今度は二次元配列にします。
初期値展開の要領で二次元配列に格納していきますが、今回はデータのフォーマットが違うのでロジックを変えます。
フォームで渡されたデータはcel00=00001といったフォーマットになっているので、これをcel[0]=['0','0','0','0','1']となるように変換しましょう。
やり方はいろいろありますが、こんな感じでどうでしょう。
cel = []
cgi.params.keys.sort.each do |key|
if /cel\d\d/ =~ key
yy = key[/\d\d/]
cel[yy.to_i] = cgi[key].scan(/./)
end
end
リファレンスを読んでわかったのですが、key[/\d\d/]のところでkeyがStringクラスのとき、[ ]内の正規表現に一致する値を返すらしいのです。
正規表現で取り出す値は"01"や"02"といったゼロが頭についた文字列なのですが、to_iメソッドを使うと、頭のゼロを勝手に取ってくれます。
2つ目の代入式はscanが返すのが配列(ハッシュの場合もあり)という特性を活かして横着しています。
きっともっと楽しいやり方があると思います。
この辺のロジック考えるのが特に正規表現に強いRubyプログラミングの醍醐味かもしれません。
そして、いよいよセルを次の世代へ更新するロジックです。
とりあえずやっつけで作ったのがあるのですが、あまりにもRubyらしくないソースで、しかも可読性が悪いのでお見せするのが恥ずかしい限りです。
あとで直すつもりだし、恥の上塗りなので解説は入れません。
new_cel = []
(0 ... tate).each do |y|
new_cel[y] = []
(0 ... yoko).each do |x|
count = 0
x = x.to_i
y = y.to_i
for k in [y-1 ,y ,y+1]
if (0 ... tate).include?(k)
for l in [x-1 ,x ,x+1]
if (0 ... yoko).include?(l)
count += cel[k][l].to_i
end
end
end
end
count -= cel[y][x].to_i
if cel[y][x].to_i == 1
if (2 .. 3).include?(count)
new_cel[y][x] = 1
else
new_cel[y][x] = 0
end
elsif count == 3
new_cel[y][x] = 1
else
new_cel[y][x] = 0
end
end
end
こんな感じでnew_celには世代が更新されたセルの状態が二次元配列で入りました。
あとは初期値展開のロジックを流用して、画面表示とhiddenフォームの生成をすればこのページは完了です。
このページの出力がそのまま入力になって、繰り返し実行されることで世代がどんどん更新されることになります。
以上、最後の方は駆け足でしたが、こんな感じでライフゲームができました。
ちなみに今後の拡張はこんなのを考えています。
参考文献
Wikipedia ライフゲーム
ライフゲーム保存会 日本関西支部
Rubyリファレンスマニュアル