For your consideration, a game. A Game of Life. Some even call it
*The* game of life.
To play with it, click and drag across the screen to make critters.
Click individual critters to make them disappear. Use the slider at the
bottom to speed up and slow down the generations. If you slow down
enough, the critters will pause, so that you can edit them to your
liking.
"Clear" and "Seed with Critters" do what you'd expect.
Three avenues of inquiry:
I'm having a bit of trouble with consistency between Shoes Curious
on Mac and Windows. It works great on Leopard. But on Windows XP
the main flow doesn't seem to draw anything. Does Shoes on Windows
not clear individual stacks and flows yet?
Does this work on Linux at all?
Maybe we could get a slider control in Shoes itself, patterned after the
one towards the bottom of the file. It returns a float between 0 and 1.
Okay,
— Jeremy
# The Game of Life
# This code is released into the Public Domain
class Life
CRITTERS_TO_START_WITH = 100 # The amount of critters to make if you seed.
# WIDTH and HEIGHT are counted in critters
WIDTH = 35
HEIGHT = 25
UNIT = 24 # The dimensions of a single critter
PIXEL_WIDTH = UNIT * WIDTH
PIXEL_HEIGHT = UNIT * HEIGHT
CONTROLS_HEIGHT = 50
@speed = 8
# Iterator for each critter
def self.each
WIDTH.times do |x|
HEIGHT.times do |y|
yield(x,y)
end
end
end
def self.set_speed(speed_factor)
@speed = (speed_factor * 50 + 1).to_i
end
# Let's us change speed without messing with the animate loop.
def self.maybe_update(counter)
if counter % @speed == 0 && @speed < 50
GoldenPlain.armageddon_and_resurrection
end
end
# Delegate clicks to the GoldenPlain or to the Slider, as necessary
def self.click(button, x, y)
if button == 1
if y < Life::PIXEL_HEIGHT
@clicking = true
GoldenPlain.click(x, y, true)
else
ControlPanel.start_slide(x, y)
end
end
end
def self.motion(x,y)
GoldenPlain.click(x, y, false) if @clicking
ControlPanel.maybe_motion(x, y)
end
def self.release(button,x,y)
if button == 1
@clicking = false
ControlPanel.end_slide
end
end
end
class GoldenPlain
def self.reset
@critters = self.fresh_array
@plain.clear if @plain
@plain = $app.flow(:margin => 0, :top => 0, :left => 0, :width => Life::PIXEL_WIDTH, :height => Life::PIXEL_HEIGHT + 10)
self.draw
end
def self.fresh_array
return Array.new(Life::WIDTH) { Array.new(Life::HEIGHT) { nil } }
end
def self.seed_randomly
self.reset
Life::CRITTERS_TO_START_WITH.times do
x, y = rand(Life::WIDTH), rand(Life::HEIGHT)
@critters[x][y] = Critter.new(x,y)
end
self.draw
end
def self.draw
@plain.clear do
Life.each {|x,y| @critters[x][y].draw if @critters[x][y] }
end
end
# Makes it easier to draw critters than it is to erase them.
def self.click(x, y, new_click)
x, y = (x/Life::UNIT).to_i, (y/Life::UNIT).to_i
if @critters[x][y]
@critters[x][y] = nil if new_click
else
@critters[x][y] = Critter.new(x, y)
end
self.draw
end
# Check all surrounding squares, and add up the head count.
def self.has_neighbor?(x, y)
sum = 0
(-1..1).each do |x_around|
(-1..1).each do |y_around|
test_x, test_y = x + x_around, y + y_around
if test_x >= 0 && test_x < Life::WIDTH && test_y >= 0 && test_y < Life::HEIGHT
unless x_around == 0 && y_around == 0
sum += 1 if @critters[test_x][test_y]
end
end
end
end
sum
end
# Rules of life and death.
def self.alive?(x, y, sum)
if (sum == 3) || (sum == 2 && @critters[x][y])
return true
else
return false
end
end
# Make a new generation of critters
def self.armageddon_and_resurrection
afterlife = self.fresh_array
Life.each do |x, y|
sum = self.has_neighbor?(x, y)
afterlife[x][y] = Critter.new(x,y) if self.alive?(x, y, sum)
end
@critters = afterlife
self.draw
end
end
class Critter
def initialize(x,y)
@x, @y = x, y
@stroke_color = $app.gray(1.0, 0.6)
@fill_color = $app.rgb(0.2, 0.2, 0.4 + (rand*0.6), 0.45)
end
def draw
$app.stroke(@stroke_color)
$app.fill(@fill_color)
$app.oval(@x*Life::UNIT, @y*Life::UNIT, Life::UNIT-1, Life::UNIT-1)
end
end
class ControlPanel
# Setup the controls - they will not be redrawn.
def self.setup
$app.flow :margin_top => 4, :margin_left => 15, :top => Life::PIXEL_HEIGHT + 10, :left => 0 do
$app.nostroke
$app.fill($app.gray(0.1))
$app.rect(0, Life::PIXEL_HEIGHT, Life::PIXEL_WIDTH, Life::CONTROLS_HEIGHT)
$app.button("Clear") { GoldenPlain.reset }
$app.button("Seed with Critters", :margin_left => 15) { GoldenPlain.seed_randomly }
end
@slider = Slider.new
end
def self.start_slide(x, y)
if @slider.contains?(x, y)
@sliding = true
end
end
# Make sure to only change the speed if you actually clicked on the slider.
def self.maybe_motion(x, y)
@slider.move_to(x,y) if @sliding
Life.set_speed(@slider.get_percentage)
end
def self.end_slide
@sliding = false
end
end
class Slider
LEFT_END = 335
RIGHT_END = 775
def initialize
@x, @y, @dimensions = 505, Life::PIXEL_HEIGHT + 19, 15
$app.fill($app.gray(0.8, 0.7))
$app.stroke($app.gray(1.0, 0.8))
$app.strokewidth(3)
@slider = $app.oval(@x, @y, @dimensions, @dimensions)
$app.nostroke
$app.fill($app.gray(0.8, 0.12))
$app.rect(LEFT_END, @y + 5, RIGHT_END - LEFT_END, 5)
$app.para("Speed:", :stroke => $app.gray(0.95), :font => '14px', :left => LEFT_END - 65, :top => @y - 5)
end
# Check for clicks on the slider knob.
def contains?(x,y)
return true if (@x..(@x + @dimensions)).include?(x) && (@y..(@y + @dimensions)).include?(@y)
return false
end
def move_to(x, y)
if (LEFT_END..RIGHT_END).include?(x)
@x = x - (@dimensions/2)
@slider.move(@x, @y)
end
end
def get_percentage
return 1 - (@x - LEFT_END).to_f / (RIGHT_END - LEFT_END).to_f
end
end
Shoes.app :width => Life::PIXEL_WIDTH, :height => Life::PIXEL_HEIGHT + Life::CONTROLS_HEIGHT, :title => "The Game of Life" do
$app = self
background(rgb(0.05, 0.05, 0.2))
counter = 0
GoldenPlain.reset
ControlPanel.setup
animate(30) do
counter += 1
Life.maybe_update(counter)
end
click do |button, x, y|
Life.click(button, x, y)
end
motion do |x, y|
Life.motion(x, y)
end
release do |button, x, y|
Life.release(button, x,y)
end
end