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




Reply via email to