勉強のために、プログラム言語「Ruby」で「LifeGame」を作って見ました。と言っても一からではなく「Ruby本」にTkでの例題がありますので、それを参考にしています。GUIは、「VisualuRuby計画(仮称)」で作成しました。なので、動作環境はMS-Windows上です。
また、「Exerb」によるexeファイルも作成してみました。手軽に(Rubyのインストールなしで)動かしてみることができます。
LifeGameについては「ライフゲーム保存会」をご覧ください。色々な初期配置パターンも置かれています。
2004年10月29日追記 「VisualuRuby計画(仮称)」の作者のnyasu@3webさんより描画についてのbugfixを頂戴しました。ありがとうございます。
ところで「Ruby本」初版のプログラムは1.6以降ではうまく動かないようです。killする時にnilの代入ではなく、配列のdeleteをすることが必要でした。
具体的には、
@lives[geom] = nil
となっている所を、
@lives.delete(geom)
に置き換えました。その他、数箇所修正しています。
また、273頁のリストは入れ違っているようでテストルーチンにはなっていません。528頁からのリストを参照しました。
lifegame.rbを実行すると、20×10マスの中央にグライダーのパターンが表示され、リターンキーで次世代が表示されていきます。
GUIは、VisualuRubyで作成しました。「ライフゲーム保存会」で初期配置集を見つけて、データの読み込み機能をつけました。ついでに書き出し機能も。
「窓辺でRuby」で「Exerb」を知り、exeファイルにしてみました。なお、exerbの実行がうまくいかなかったので(なんかインストールでミスったか?)「窓辺でRuby」とその他を参考にして、exerb.batを次の内容でパスの通っているところに作りました。
@echo off if "%OS%" == "Windows_NT" goto WinNT ruby -Sx "exerb" %1 %2 %3 %4 %5 %6 %7 %8 %9 goto endofruby :WinNT ruby -Sx "exerb" %* :endofruby
また、レシピファイルの拡張子が.exrに変更になっています。従って、レシピファイルの自動生成は、次のようになりました。
c:\temp\rubysrc>ruby -r exerb/mkexr vrlifegame.rb
次のようにしてexeファイルを生成しました。
c:\temp\rubysrc>exerb.bat vrlifegame.exr
# 座標クラス class Geometry # 座標[y,x]の生成 def Geometry.[](y,x) new(y, x) end # 初期化 def initialize(y, x) @y = y @x = x end # x, yのアクセサ attr :y, true attr :x, true # 加法 def +(other) case other when Geometry # otherがGeometryか? Geometry[@y + other.y, @x + other.x] when Array # otherがArrayか? Geometry[@y + other[0], @x + other[1]] else raise TypeError, "wrong argument type #{other.type} (expected Geometry or Array)" end end # 減法 def -(other) case other when Geometry # otherがGeometryか? Geometry[@y - other.y, @x - other.x] when Array # otherがArrayか? Geometry[@y - other[0], @x - other[1]] else raise TypeError, "wrong argument type #{other.type} (expected Geometry or Array)" end end # 比較 def ==(other) # 1.8ではObject#typeは警告される # type == other.type and @x == other.x and @y == other.y self.class == other.class and @x == other.x and @y == other.y end # ハッシュ関数 def hash @x.hash ^ @y.hash end # ハッシュ比較関数 alias eql? == # 文字列化 def to_s format("%d@%d", @y, @x) end # インスペクト def inspect format("#<%d@%d>", @y, @x) end end
require "geometry" #-- # # x @neighbors = [ # +----> [.....] # | [.....] # y| [.....] # V [.....]] # # ライフゲーム本体 class LifeGame DefaultCompetitionArea = [ Geometry[-1, -1], Geometry[-1, 0], Geometry[-1, 1], Geometry[0, -1], Geometry[0, 1], Geometry[1, -1], Geometry[1, 0], Geometry[1, 1] ] InitialPositionOffset = [ # 初期配置をグライダーに # [-1, 0], [-1, 1], # [0, -1], [0, 0], # [1, 0] [-1, 1], [0, -1], [0, 0], [1, 0], [1, 1] ] # 初期化 def initialize(width=80, height=23) @width = width @height = height @lives = {} #(A) @neighborsの初期化 @neighbors = Array.new(height) for y in 0..height - 1 @neighbors[y] = a = Array.new(width) if y == 0 competition_area = DefaultCompetitionArea.find_all{|geom| geom.y >= 0} elsif y == height - 1 competition_area = DefaultCompetitionArea.find_all{|geom| geom.y <= 0} else competition_area = DefaultCompetitionArea end a[0] = competition_area.find_all{|geom| geom.x >= 0} for x in 1.. width - 2 a[x] = competition_area end a[width - 1] = competition_area.find_all{|geom| geom.x <= 0} end #(B) 最初の生物の設定 center = Geometry[height / 2, width / 2] for po in InitialPositionOffset born(center + po) end end # 生きているか? def live?(geom) @lives[geom] end # 生まれる def born(geom) @lives[geom] = true end # 殺す def kill(geom) @lives.delete(geom) # 1.6対応 # @lives[geom] = nil end # 生きている生物全体のイテレータ def each_life @lives.each_key {|geom| yield geom } end # 次世代の生成 def nextgen n = Hash.new(0) # n[geom+pos]に値が設定していなければ0を代入 # (C) その座標の周りに生存する生物の数 each_life {|geom| @neighbors[geom.y][geom.x].each {|pos| n[geom+pos] += 1 } } #p n # 計算結果の確認 # (D) その座標における生存条件のチェック n.each {|geom, count| if count == 3 && !live?(geom) born(geom) end } each_life {|geom| if n[geom] != 2 && n[geom] != 3 kill(geom) end } # n.each {|geom, count| # ロジックを少し整理 # if count == 3 || @lives[geom] && count == 2 # @lives[geom] = true # else # @lives.delete(geom) # 1.6対応 # @lives[geom] = nil # end # } end # 文字列化 def to_s s = '.' * (@width * @height) each_life {|geom| s[geom.y * @width + geom.x, 1]='*'} s end end # キャラクタ版ライフゲーム if __FILE__ == $0 width = 20 height = 10 g = LifeGame.new(width, height) loop { for i in 0...height print g.to_s[i*width...(i+1)*width], "\n" end break unless gets g.nextgen } end
=begin = VisualuRuby版LifeGame VisualuRubyによるLifeGameのGUIの実装 マウスクリックでセルのセット&リセット [next] 次世代を表示 [go] 連続実行 [stop] 実行停止 [reset] 世代カウンタのリセット [open] 初期設定パターンファイルの読み込み [save] 保存 [quit] 終了 nyasu@3webさんより描画処理のbugfix 2004/10/29 =end require 'vr/vruby' require 'vr/vrcontrol' require 'vr/vrhandler' require 'vr/vrtimer' require "lifegame" $width = 80 $height = 80 $rectsize = 6 # 描画領域 class MyCanvas < VRCanvasPanel include VRMouseFeasible def construct @white = RGB(0xff, 0xff, 0xff) @black = RGB(0, 0, 0) @lifegame = LifeGame.new($width, $height) @prevgrid = {} end # nyasu@3webさんよりbugfix 2004/10/29 def easyrefresh dopaint do self_paint end end def self_lbuttondown(shift, x, y) geom = Geometry[y / $rectsize, x / $rectsize] if @lifegame.live?(geom) @lifegame.kill(geom) resetrect(geom) @prevgrid.delete(geom) else @lifegame.born(geom) setrect(geom) @prevgrid[geom] = true end self.easyrefresh #self.refresh # bugfix 2004/10/29 end # 点の表示 def setrect(geom) @canvas.setPen(@black) @canvas.setBrush(@black) @canvas.fillRect( geom.x * $rectsize + 1, geom.y * $rectsize + 1, geom.x * $rectsize + $rectsize - 1, geom.y * $rectsize + $rectsize - 1) end # 点の消去 def resetrect(geom) @canvas.setPen(@white) @canvas.setBrush(@white) @canvas.fillRect( geom.x * $rectsize + 1, geom.y * $rectsize + 1, geom.x * $rectsize + $rectsize - 1, geom.y * $rectsize + $rectsize - 1) end # 表示 def display nextgrid = {} @lifegame.each_life {|geom| if @prevgrid[geom] @prevgrid.delete(geom) else setrect(geom) end nextgrid[geom] = true } @prevgrid.each_key {|geom| resetrect(geom) } @prevgrid = nextgrid self.easyrefresh #self.refresh # bugfix 2004/10/29 end #次世代の計算 def nextgen @lifegame.nextgen display end # データ読み込み def opendata(filename) y = 0 @lifegame.each_life {|geom| @lifegame.kill(geom) resetrect(geom) } @prevgrid = {} nextgrid = {} file = open(filename, "r") while data = file.gets if data[0,1] == '#' or data =~ /^\s*$/ then next end for x in 0...($width < data.length ? $width : data.length) if data[x,1] == '*' geom = Geometry[y, x] nextgrid[geom] = true end end y += 1 if y >= $height then break end end dx = ( $width - x ) / 2 dy = ( $height - y ) / 2 nextgrid.each_key {|geom| @lifegame.born(Geometry[geom.y + dy, geom.x + dx]) } display end # データ保存 def savedata(filename) file = open(filename, "w") file.print '#O ', Time.now, "\n" file.print '#P', "\n" data = @lifegame.to_s for i in 0...$height file.print data[i*$width...(i+1)*$width], "\n" end end end # MyCanvas ###################### class MyForm < VRForm include VRTimerFeasible # メインのWindow生成 def construct @goflag = false cvwid = $width * $rectsize cvhigh = $height * $rectsize btnwid = 45 btnhigh = 25 self.caption = "LifeGame" move 50, 50, $width * $rectsize + 18, $height * $rectsize + 67 addControl VRButton,"next","next",5, 5,btnwid,btnhigh addControl VRButton,"go","go", btnwid + 7, 5,btnwid,btnhigh addControl VRStatic,"cnt","cnt", btnwid * 3, 10,btnwid,btnhigh - 5 addControl VRButton,"rst","reset",btnwid * 4, 5,btnwid,btnhigh addControl VRButton,"open","open",cvwid - btnwid * 3 - 4,5,btnwid,btnhigh addControl VRButton,"save","save",cvwid - btnwid * 2 - 2,5,btnwid,btnhigh addControl VRButton,"quit","quit",cvwid - btnwid + 5, 5,btnwid,btnhigh @cnt.caption = @count = 1 addControl MyCanvas,"cv","canvas",5,btnhigh + 10,cvwid,cvhigh @cv.createCanvas cvwid,cvhigh @cv.display end # [next]ボタン処理 def next_clicked nextgen end # [go/stop]ボタン処理 def go_clicked @goflag = !@goflag if @goflag @go.caption = "stop" @rst.enabled = false @next.enabled = false @save.enabled = false @open.enabled = false addTimer 200, "timer1" else @go.caption = "go" @rst.enabled = true @next.enabled = true @save.enabled = true @open.enabled = true deleteTimer "timer1" end end # timer処理 def timer1_timer nextgen end # 次世代処理 def nextgen @cv.nextgen @cnt.caption = @count += 1 #@cnt.caption = sprintf("%3s", ("000" << @count += 1)) end # カウンターリセット def rst_clicked @cnt.caption = @count = 1 end # データ読み込み def open_clicked f = openFilenameDialog [ ["life(*.life)","*.life"] ] if f !=nil @cv.opendata f display @cnt.caption = @count = 1 end end # データ保存 def save_clicked f = saveFilenameDialog [ ["life(*.life)","*.life"] ] if f != nil if f =~ /[^(\.life)]\z/ f << ".life" end @cv.savedata f end end # [quit]ボタン処理 def quit_clicked self.close end end # MyForm # メインループ VRLocalScreen.start(MyForm)
ダウンロード用ソースvrlifegamesrc.lzh
ダウンロード用実行ファイルvrlifegameexe.lzh