Matz' TkLife Resurrected


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