Hmm. You certainly should be able to draw critters. 
What platform are you running Shoes on? (Also, are your Shoes Curious?)

You're absolutely right about the speed bar. Thanks for catching that.
 Here's a fixed version (#4).

— Jeremy

# The Game of Life
# This code is released into the Public Domain
# Revision 4

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)
    if @paused
      @paused = (speed_factor * 50 + 1).to_i
    else
      @speed = (speed_factor * 50 + 1).to_i
    end
  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 @paused
      @speed = @paused
      @paused = nil
      GoldenPlain.background_text = nil
    else
      @paused = @speed
      @speed = 100
      GoldenPlain.background_text = "Paused"
    end
    GoldenPlain.draw
  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 4:21 PM, Ryan Mulligan wrote:

Am I supposed to be able to draw individual critters? The left mouse click on the Golden Plain doesn't seem to do much... Also, when paused, if you move the speed bar it goes unpaused and gets confused.

rm

On 2/5/08, Jeremy Ashkenas <[EMAIL PROTECTED]> wrote:
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






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.




Reply via email to