Hey, thanks for the tips.
Drawing critters now works, even if you drag off of the window and back
on again. And I've added a pause button that, well, pauses.
— Jeremy
# The Game of Life
# This code is released into the Public Domain
# Revision 3
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 = 25
# 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 < 100
GoldenPlain.armageddon_and_resurrection
end
end
def self.pause
if @saved_speed
@speed = @saved_speed
@saved_speed = nil
GoldenPlain.background_text = nil
else
@saved_speed = @speed
@speed = 100
GoldenPlain.background_text = "Paused"
GoldenPlain.draw
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
class << self; attr_accessor :background_text; end
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
$app.para(@background_text, :stroke => $app.rgb(0.1, 0.1, 0.3), :top => 177, :left => 107, :font => '180px') if @background_text
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 x >= 0 && x < Life::WIDTH && y >=0 && y < Life::HEIGHT
if @critters[x][y]
@critters[x][y] = nil if new_click
else
@critters[x][y] = Critter.new(x, y)
end
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", :margin_left => 10) { GoldenPlain.reset }
$app.button("Seed", :margin_left => 10) { GoldenPlain.seed_randomly }
$app.button("Pause", :margin_left => 10) { Life.pause }
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)
if @sliding
@slider.move_to(x,y)
Life.set_speed(@slider.get_percentage)
end
end
def self.end_slide
@sliding = false
end
end
class Slider
LEFT_END = 335
RIGHT_END = 775
def initialize
@x, @y, @dimensions = 525, 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,
:resizable => false, :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
On Feb 5, 2008, at 3:16 PM, jawbroken wrote:
Quick bug report: If I click and drag off/near the right side of the
screen (on Leopard here) I get a Nil class exception. Looks good
otherwise. A pause toggle would be nice when you want to keep speed
consistent but still get a chance to draw stuff in without dragging
the slider to 0.