Original: tklifegame.rb in "O-O Script Language Ruby," by Matz, ASCII 1999
Modifications to run on ruby 1.8:
[1] hash clean up (ruby 1.8), "hash[key] = nil" --> "hash.delete key"
[2] reduced screen size, 480x480 (tile size 6) --> 400x400 (10)
[3] attribute update (ruby 1.8), Object#type --> Object#class
[4] hash function (Matz' challenge), "x ^ y" --> "x + y<<8"
require "tk"
class Geometry
def Geometry.[](y,x)
new(y, x)
end
def initialize(y, x)
@y = y
@x = x
end
attr :y, true
attr :x, true
def +(other)
case other
when Geometry
Geometry[@y + other.y, @x + other.x]
when Array
Geometry[@y + other[0], @x + other[1]]
else
raise TypeError,
"wrong argument type #{other.type} (expected Geometry or Array)"
end
end
def ==(other)
self.class == other.class and @x == other.x and @y == other.y
end
def hash
(@x.hash + 40) + (@y.hash + 40) << 8
end
alias eql? ==
end
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]
]
def initialize(width=80, height=23)
@width = width
@height = height
@lives = {}
@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
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
end
def each_life
@lives.each_key {|geom|
yield geom
}
end
def nextgen
n = {}
@lives.each_key {|geom|
n[geom] ||= 0
@neighbors[geom.y][geom.x].each {|pos|
n[geom+pos] ||= 0
n[geom+pos] += 1
}
}
n.each {|geom, count|
if count == 3 || @lives[geom] && count == 2
@lives[geom] = true
else
@lives.delete geom
end
}
end
end
class TkLifeGame
include Tk
def initialize(width=40, height=40, rectsize=10)
@lifegame = LifeGame.new(width, height)
@rectsize = rectsize
@goflag = false
@canvas = TkCanvas.new(nil,
'width'=>(width - 1) * rectsize,
'height'=>(height - 1) * rectsize,
'borderwidth'=>1,
'relief'=>'sunken')
@nextbutton = TkButton.new(nil,
'text'=>'next',
'command'=>proc {@lifegame.nextgen; display})
@gobutton = TkButton.new(nil,
'text'=>'go',
'command'=>proc {
@goflag = !@goflag
if @goflag
@gobutton.text 'stop'
go
else
@gobutton.text 'go'
end
})
@quitbutton = TkButton.new(nil,
'text'=>'quit',
'command'=>proc {exit})
@canvas.pack
@nextbutton.pack('side'=>'left')
@gobutton.pack('side'=>'left')
@quitbutton.pack('side'=>'right')
@prevgrid = {}
@rectangles = {}
@canvas.bind '1', proc {|x, y|
geom = Geometry[y/@rectsize, x/@rectsize]
if @lifegame.live?(geom)
@lifegame.kill(geom)
else
@lifegame.born(geom)
end
display
update
}, '%x %y'
@after = TkAfter.new
@after.set_start_proc(0, proc {go})
end
def go
@lifegame.nextgen
display
update
if @goflag
@after.restart
end
end
def run
display
mainloop
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
end
def setrect(geom)
@rectangles[geom] = TkcRectangle.new(@canvas,
geom.x * @rectsize,
geom.y * @rectsize,
geom.x * @rectsize + @rectsize - 2,
geom.y * @rectsize + @rectsize - 2,
'fill'=>'black')
end
def resetrect(geom)
@rectangles[geom].destroy
@rectangles.delete geom
end
end
g = TkLifeGame.new
g.run