コンウェイのライフゲームを Ruby で実装してみた

有名なコンウェイのライフゲーム」を素朴に実装してみました。一種の生態系シミュレーションですね。ルールは簡単です。「セル」が集まって長方形領域を形成しているとき、それぞれの「セル」はまわりの 8つの「セル」の状態によって「生きる」か「死ぬ」かします。

  • 「セル」がないとき
    • まわりにちょうど 3つの「セル」があれば「誕生」します。
  • 「セル」があるとき
    • まわりに 0 または 1個の「セル」があれば過疎で「死に」ます。
    • まわりに 2 または 3個の「セル」があればそのまま「生き」ます。
    • まわりに 4個以上の「セル」があれば、過密で「死に」ます。

このパターンを繰り返します。



パターンはランダムに与えられ、毎回自動的に lifemap.txt に(上書き)記録されます。-r オプションをつけて実行すると、開始パターンを lifemap.txt からロードして実行します。描画には GTK+ を使っています。

ソースは以下です。Gist

require 'gtk2'

Width  = 30
Height = 30

class Component
  def initialize
    f = Field.new(Width, Height)
    f.generate(150)     #ランダムに150個セットする
    if ARGV[0] == "-r"
      File.open("lifemap.txt") {|io| f = Marshal.load(io)}
    else
      File.open("lifemap.txt", "w") {|io| Marshal.dump(f, io)}
    end
    @window = Gui.new(f)
    @disp = FieldDisplay.new(f)
  end
  
  def main
    @disp.show
  end
end

class Field
  def initialize(width, height)
     @width  = width
     @height = height
     @field = Array.new(@width + 2) {Array.new(@height + 2, 0)}
  end
  attr_reader :width, :height
  
  def set(x, y)
    @field[x][y] = 1
  end
  
  def reset(x, y)
    @field[x][y] = 0
  end
  
  def get(x, y)
    @field[x][y]
  end
  
  def generate(n)
    n.times {set(rand(@width) + 1, rand(@height) + 1)}
  end
  
  #次の世代を得る
  def next
    nxf = Array.new(@width + 2) {Array.new(@height + 2, 0)}
    @width.times do |x|
      @height.times do |y|
        x1, y1 = x + 1, y + 1
        n = alive_cells(x1, y1)
        if get(x1, y1).zero?
          nxf[x1][y1] = 1 if n == 3
        else
          nxf[x1][y1] = 1 if n == 2 or n == 3
        end
      end
    end
    @field = nxf
  end
  
  def alive_cells(x, y)
    get(x - 1, y - 1) + get(x, y - 1) + get(x + 1, y - 1) +
      get(x - 1, y) + get(x + 1, y) +
      get(x - 1, y + 1) + get(x, y + 1) + get(x + 1, y + 1)
  end
end

class Gui
  CellWidth = 10
  Space = 3
  Margin = 8
  
  def initialize(f)
    @f = f
    
    @w = Gtk::Window.new
    @width  = CellWidth * @f.width  + Space * (@f.width  - 1) + 2 * Margin
    @height = CellWidth * @f.height + Space * (@f.height - 1) + 2 * Margin
    @w.set_size_request(@width, @height)
    @w.set_app_paintable(true)
    @w.realize
    @drawable = @w.window
    @gc = Gdk::GC.new(@drawable)
    colormap = Gdk::Colormap.system
    
    @white = Gdk::Color.new(65535, 65535, 65535)
    @black = Gdk::Color.new(0, 0, 0)
    colormap.alloc_color(@white, false, true)
    colormap.alloc_color(@black, false, true)
  end
end

class FieldDisplay < Gui
  def initialize(f)
    super(f)
  end
  
  def show
    @w.signal_connect("expose_event") do
      @gc.set_foreground(@black)
      @drawable.draw_rectangle(@gc, true, 0, 0, @width, @height)
    end
    
    Gtk.timeout_add(160) do
      @f.width.times do |x|
        @f.height.times do |y|
          if @f.get(x + 1, y + 1).zero?
            reset(x + 1, y + 1)
          else
            set(x + 1, y + 1)
          end
        end
      end
      @f.next
    end
    @w.signal_connect("destroy") {Gtk.main_quit}
    @w.show
    Gtk.main
  end
  
  def set(x, y)
    @gc.set_foreground(@white)
    draw_cell(x, y)
  end
  
  def reset(x, y)
    @gc.set_foreground(@black)
    draw_cell(x, y)
  end
  
  def draw_cell(x, y)
    x1 = Margin + (x - 1) * CellWidth + (x - 1) * Space
    y1 = Margin + (y - 1) * CellWidth + (y - 1) * Space
    @drawable.draw_rectangle(@gc, true, x1, y1, CellWidth, CellWidth)
  end
end

Component.new.main


※追記(2018/1/18)
コンソール版も作ってみました。

 

※追記(2018/4/12)
フィールド・エディタ付きのを作ってみました。