VisualuRubyによるLifeGameの作成、またはRubyのお勉強

勉強のために、プログラム言語「Ruby」で「LifeGame」を作って見ました。と言っても一からではなく「Ruby本」にTkでの例題がありますので、それを参考にしています。GUIは、「VisualuRuby計画(仮称)」で作成しました。なので、動作環境はMS-Windows上です。

また、「Exerb」によるexeファイルも作成してみました。手軽に(Rubyのインストールなしで)動かしてみることができます。

LifeGameについては「ライフゲーム保存会」をご覧ください。色々な初期配置パターンも置かれています。

2004年10月29日追記 「VisualuRuby計画(仮称)」の作者のnyasu@3webさんより描画についてのbugfixを頂戴しました。ありがとうございます。

キャラクタ版LifeGame

ところで「Ruby本」初版のプログラムは1.6以降ではうまく動かないようです。killする時にnilの代入ではなく、配列のdeleteをすることが必要でした。
具体的には、

@lives[geom] = nil

となっている所を、

@lives.delete(geom)

に置き換えました。その他、数箇所修正しています。

また、273頁のリストは入れ違っているようでテストルーチンにはなっていません。528頁からのリストを参照しました。

lifegame.rbを実行すると、20×10マスの中央にグライダーのパターンが表示され、リターンキーで次世代が表示されていきます。

VisualuRubyによるGUIの実装

GUIは、VisualuRubyで作成しました。「ライフゲーム保存会」で初期配置集を見つけて、データの読み込み機能をつけました。ついでに書き出し機能も。

Exerbによるexeファイルの作成

「窓辺でRuby」「Exerb」を知り、exeファイルにしてみました。なお、exerbの実行がうまくいかなかったので(なんかインストールでミスったか?)「窓辺でRuby」とその他を参考にして、exerb.batを次の内容でパスの通っているところに作りました。

@echo off
if "%OS%" == "Windows_NT" goto WinNT
ruby -Sx "exerb" %1 %2 %3 %4 %5 %6 %7 %8 %9
goto endofruby
:WinNT
ruby -Sx "exerb" %*
:endofruby

また、レシピファイルの拡張子が.exrに変更になっています。従って、レシピファイルの自動生成は、次のようになりました。

c:\temp\rubysrc>ruby -r exerb/mkexr vrlifegame.rb

次のようにしてexeファイルを生成しました。

c:\temp\rubysrc>exerb.bat vrlifegame.exr

トップページへ戻る

リスト

# 座標クラス
class Geometry

  # 座標[y,x]の生成
  def Geometry.[](y,x)
    new(y, x)
  end

  # 初期化
  def initialize(y, x)
    @y = y
    @x = x
  end

  # x, yのアクセサ
  attr :y, true
  attr :x, true

  # 加法
  def +(other)
    case other
    when Geometry                            # otherがGeometryか?
      Geometry[@y + other.y, @x + other.x]
    when Array                               # otherがArrayか?
      Geometry[@y + other[0], @x + other[1]]
    else
      raise TypeError,
        "wrong argument type #{other.type} (expected Geometry or Array)"
    end
  end

  # 減法
  def -(other)
    case other
    when Geometry                            # otherがGeometryか?
      Geometry[@y - other.y, @x - other.x]
    when Array                               # otherがArrayか?
      Geometry[@y - other[0], @x - other[1]]
    else
      raise TypeError,
        "wrong argument type #{other.type} (expected Geometry or Array)"
    end
  end

  # 比較
  def ==(other)
#    1.8ではObject#typeは警告される
#    type == other.type and @x == other.x and @y == other.y
    self.class == other.class and @x == other.x and @y == other.y
  end

  # ハッシュ関数
  def hash
    @x.hash ^ @y.hash
  end

  # ハッシュ比較関数
  alias eql? ==

  # 文字列化
  def to_s
    format("%d@%d", @y, @x)
  end

  # インスペクト
  def inspect
    format("#<%d@%d>", @y, @x)
  end
end
require "geometry"

#--
#
#      x  @neighbors = [
#  +---->   [.....]
#  |        [.....]
# y|        [.....]
#  V        [.....]]
#
# ライフゲーム本体
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]
                      [-1, 1],
     [0, -1], [0, 0],
              [1, 0], [1, 1]
  ]

  # 初期化
  def initialize(width=80, height=23)
    @width = width
    @height = height
    @lives = {}

    #(A) @neighborsの初期化
    @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
    #(B) 最初の生物の設定
    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) # 1.6対応
#    @lives[geom] = nil
  end

  # 生きている生物全体のイテレータ
  def each_life
    @lives.each_key {|geom|
      yield geom
    }
  end

  # 次世代の生成
  def nextgen
    n = Hash.new(0)    # n[geom+pos]に値が設定していなければ0を代入
    # (C) その座標の周りに生存する生物の数
    each_life {|geom|
      @neighbors[geom.y][geom.x].each {|pos|
          n[geom+pos] += 1
      }
    }
#p n  # 計算結果の確認

    # (D) その座標における生存条件のチェック
    n.each {|geom, count|
      if count == 3 && !live?(geom)
        born(geom)
      end
    }
    each_life {|geom|
      if n[geom] != 2 && n[geom] != 3
        kill(geom)
      end
    }
#    n.each {|geom, count| # ロジックを少し整理
#      if count == 3 || @lives[geom] && count == 2
#        @lives[geom] = true
#      else
#        @lives.delete(geom) # 1.6対応
#        @lives[geom] = nil
#      end
#    }
  end

  # 文字列化
  def to_s
    s = '.' * (@width * @height)
    each_life {|geom| s[geom.y * @width + geom.x, 1]='*'}
    s
  end

end

# キャラクタ版ライフゲーム
if __FILE__  == $0
  width = 20
  height = 10
  g = LifeGame.new(width, height)
  loop {
    for i in 0...height
      print g.to_s[i*width...(i+1)*width], "\n"
    end
    break unless gets
    g.nextgen
  }
end
=begin
= VisualuRuby版LifeGame
VisualuRubyによるLifeGameのGUIの実装

マウスクリックでセルのセット&リセット
[next]  次世代を表示
[go]    連続実行
[stop]  実行停止
[reset] 世代カウンタのリセット
[open]  初期設定パターンファイルの読み込み
[save]                            保存
[quit]  終了

nyasu@3webさんより描画処理のbugfix 2004/10/29
=end

require 'vr/vruby'
require 'vr/vrcontrol'
require 'vr/vrhandler'
require 'vr/vrtimer'

require "lifegame"

$width = 80
$height = 80
$rectsize = 6

# 描画領域
class MyCanvas < VRCanvasPanel
  include VRMouseFeasible
  def construct
    @white = RGB(0xff, 0xff, 0xff)
    @black = RGB(0, 0, 0)
    @lifegame = LifeGame.new($width, $height)
    @prevgrid = {}
  end

  # nyasu@3webさんよりbugfix 2004/10/29
  def easyrefresh
    dopaint do
      self_paint
    end
  end

  def self_lbuttondown(shift, x, y)
    geom = Geometry[y / $rectsize, x / $rectsize]
    if @lifegame.live?(geom)
      @lifegame.kill(geom)
      resetrect(geom)
      @prevgrid.delete(geom)
    else
      @lifegame.born(geom)
      setrect(geom)
      @prevgrid[geom] = true
    end
    self.easyrefresh
    #self.refresh # bugfix 2004/10/29
  end

  # 点の表示
  def setrect(geom)
    @canvas.setPen(@black)
    @canvas.setBrush(@black)
    @canvas.fillRect(
      geom.x * $rectsize + 1,
      geom.y * $rectsize + 1,
      geom.x * $rectsize + $rectsize - 1,
      geom.y * $rectsize + $rectsize - 1)
  end

  # 点の消去
  def resetrect(geom)
    @canvas.setPen(@white)
    @canvas.setBrush(@white)
    @canvas.fillRect(
      geom.x * $rectsize + 1,
      geom.y * $rectsize + 1,
      geom.x * $rectsize + $rectsize - 1,
      geom.y * $rectsize + $rectsize - 1)
  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
    self.easyrefresh
    #self.refresh # bugfix 2004/10/29
  end

  #次世代の計算
  def nextgen
    @lifegame.nextgen
    display
  end

  # データ読み込み
  def opendata(filename)
    y = 0
    @lifegame.each_life {|geom|
      @lifegame.kill(geom)
      resetrect(geom)
    }
    @prevgrid = {}
    nextgrid = {}
    file = open(filename, "r")
    while data = file.gets
      if data[0,1] == '#' or data =~ /^\s*$/ then next end
      for x in 0...($width < data.length ? $width : data.length)
        if data[x,1] == '*'
          geom = Geometry[y, x]
          nextgrid[geom] = true
        end
      end
      y += 1
      if y >= $height then break end
    end
    dx = ( $width - x ) / 2
    dy = ( $height - y ) / 2
    nextgrid.each_key {|geom|
      @lifegame.born(Geometry[geom.y + dy, geom.x + dx])
    }
    display
  end

  # データ保存
  def savedata(filename)
    file = open(filename, "w")
    file.print '#O ', Time.now, "\n"
    file.print '#P', "\n"
    data = @lifegame.to_s
    for i in 0...$height
      file.print data[i*$width...(i+1)*$width], "\n"
    end
  end

end # MyCanvas

######################
class MyForm < VRForm
  include VRTimerFeasible
  # メインのWindow生成
  def construct
    @goflag = false

    cvwid = $width * $rectsize
    cvhigh = $height * $rectsize
    btnwid = 45
    btnhigh = 25

    self.caption = "LifeGame"
    move 50, 50, $width * $rectsize + 18, $height * $rectsize + 67

    addControl VRButton,"next","next",5,                     5,btnwid,btnhigh
    addControl VRButton,"go","go",    btnwid + 7,            5,btnwid,btnhigh
    addControl VRStatic,"cnt","cnt",  btnwid * 3,           10,btnwid,btnhigh - 5
    addControl VRButton,"rst","reset",btnwid * 4,            5,btnwid,btnhigh
    addControl VRButton,"open","open",cvwid - btnwid * 3 - 4,5,btnwid,btnhigh
    addControl VRButton,"save","save",cvwid - btnwid * 2 - 2,5,btnwid,btnhigh
    addControl VRButton,"quit","quit",cvwid - btnwid + 5,    5,btnwid,btnhigh
    @cnt.caption = @count = 1

    addControl MyCanvas,"cv","canvas",5,btnhigh + 10,cvwid,cvhigh

    @cv.createCanvas cvwid,cvhigh
    @cv.display
  end

  # [next]ボタン処理
  def next_clicked
    nextgen
  end

  # [go/stop]ボタン処理
  def go_clicked
    @goflag = !@goflag
    if @goflag
      @go.caption = "stop"
      @rst.enabled = false
      @next.enabled = false
      @save.enabled = false
      @open.enabled = false
      addTimer 200, "timer1"
    else
      @go.caption = "go"
      @rst.enabled = true
      @next.enabled = true
      @save.enabled = true
      @open.enabled = true
      deleteTimer "timer1"
    end
  end

  # timer処理
  def timer1_timer
    nextgen
  end

  # 次世代処理
  def nextgen
    @cv.nextgen
    @cnt.caption = @count += 1
    #@cnt.caption = sprintf("%3s", ("000" << @count += 1))
  end

  # カウンターリセット
  def rst_clicked
    @cnt.caption = @count = 1
  end

  # データ読み込み
  def open_clicked
    f = openFilenameDialog [ ["life(*.life)","*.life"] ]
    if f !=nil
      @cv.opendata f
      display
      @cnt.caption = @count = 1
    end
  end

  # データ保存
  def save_clicked
    f = saveFilenameDialog [ ["life(*.life)","*.life"] ]
    if f != nil
      if f =~ /[^(\.life)]\z/
        f << ".life"
      end
      @cv.savedata f
    end
  end

  # [quit]ボタン処理
  def quit_clicked
    self.close
  end
end # MyForm

# メインループ
VRLocalScreen.start(MyForm)

ダウンロード用ソースvrlifegamesrc.lzh

ダウンロード用実行ファイルvrlifegameexe.lzh

トップページへ戻る