勉強のために、プログラム言語「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