5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rubyでライフゲーム

Posted at

ライフゲーム書き初め。やっぱりCLIでも動くものは楽しいですネ。

他の実装をなるべく見ないで作ったので、ポカがあるかもしれません。

動き

life.gif

動作環境

  • ruby 2.0.0p648 (2015-12-16) [x86_64-linux]

複雑なことはしないだろうとdockerやrbenvよりyum installで低バージョンを選択した結果、見事に機能不足に引っかかる。

コード

class Map
  ALIVE = 1
  DEAD  = 0

  def clear
    puts"\e[H\e[2j"
  end

  def initialize(xNum, yNum, isRandom = false)
     @map = []
     @xNum = xNum
     @yNum = yNum
    (0...xNum).each {|i|
      @map[i] = []
      (0...yNum).each {|j|
        @map[i][j] = isRandom ? rand(ALIVE + 1) : DEAD # 未満なのでrand(2)
      }
    }
  end

  def getState(i, j)
    # マイナスは配列を後ろから読むのでnilチェックとも異なる。先に除外
    if i < 0 || j < 0
      return DEAD
    end

    if @map[i].nil?
       DEAD
    elsif @map[i][j].nil?
       DEAD
    else
       @map[i][j]
    end
  end

  def getSum8Neighborhood(i, j)
    sum = 0
    sum = sum + getState(i - 1, j)
    sum = sum + getState(i + 1, j)
    sum = sum + getState(i,     j - 1)
    sum = sum + getState(i,     j + 1)
    sum = sum + getState(i - 1, j - 1)
    sum = sum + getState(i + 1, j - 1)
    sum = sum + getState(i - 1, j + 1)
    sum = sum + getState(i + 1, j + 1)
  end

  def update
    newMap = []
    (0...@xNum).each {|i|
      newMap[i] = []
      (0...@yNum).each {|j|
        newMap[i][j] = calcNewState(i, j)
      }
    }
    @map = newMap
  end

  def calcNewState(i, j)
    state = @map[i][j]
    sum = getSum8Neighborhood(i, j)
    if    state == DEAD && sum == 3
       ALIVE
    elsif state == ALIVE && (sum == 2 || sum == 3)
       ALIVE
    elsif state == ALIVE && (sum <= 1)
       DEAD
    elsif state == ALIVE && sum >= 4
       DEAD
    else
       state
    end
  end

  def draw
    clear
    (0...@xNum).each{|i|
      (0...@yNum).each{|j|
        if @map[i][j] == DEAD
          print "_"
        else
          print "@"
        end
      }
      puts ''
    }
  end

  def setState(i, j, state)
     @map[i][j] = state
  end

  def isExtinction
     #!@map.flatten.sum sumは2.4からでした。ぬかった
     @map.flatten.inject(:+) == 0
  end
end

map = Map.new(25,25, true)
while !map.isExtinction do
  sleep(1)
  map.update
  map.draw
end

ダ、ダサい…
eachやsum、周囲のセルの状況から状態を決定するコードをもっとかっこよく(Rubyっぽく)書きたい。

引っかかった点

  • 最後の評価が返り値 → 早期リターンのifにreturnを書かなかったせいで後続のifまで処理してしまう
  • 添字計算 0 - 1 の結果、[-1]で末尾の要素を参照してしまう
  • array[i][j].nil? とチェックするが、そもそもarray[i]がnilの可能性見落とし
  • array[i] = nilのときにarray[i][j] = [i]が自動で配列になることを期待
  • !!0falseと期待

PHPやJavaScriptに浸かりすぎを痛感。
最初のものはRubyに甘えすぎたからか。

ちょこっと書き直してみる

2重ループの書き換え失敗

ruby 2重ループで検索するとこんな記事がありました。

Array#product で多重ループを回避しよう - Qiita

なんとなくzipでできるかな?と思っていたことができる感じですね。

早速これを使いつつ、よくわかりませんが内部動作を変えられるようにlambda?を使えるように書いてみました。(lambda == コールバックの認識でいいのかしら)

# ~~~
  def mapProduct(callable)
    Array(0...@xNum).product(Array(0...@yNum)) {|x, y|
        callable.call(x,y)
    }
  end
# ~~~

  def draw
    clear

    return mapProduct(lambda {|x, y|
      print @map[x][y] == DEAD ? "_" : "@"
    })
    # ↑↓書き換えてみた
    (0...@xNum).each{|i|
      (0...@yNum).each{|j|
        if @map[i][j] == DEAD
          print "_"
        else
          print "@"
        end
      }
      puts ''
    }
  end

はい、ちゃんと動きませんね。
lambdaの方にputs ''での改行が無いです。
よく読み直すと、全体的に2重めのループ前後(1ループ目)に処理があるパターンがあるので、callable引数は3つ用意しなくてはなりませんでしたし、productで一つにまとめてもいけませんでした。

万能にするならこんなかんじ?

  def mapLoop(callable1 = lambda{|x|}, callable2 = lambda{|x, y|}, callable3 = lambda{|x|})
    (0...@xNum).each{|x|
      callable1.call(x)
      (0...@yNum).each{|y|
        callable2.call(x, y)
      }
      callable3.call(x)
    }
  end

うーん???
便利なのか??????
2重ループを何度も書きたくないという目的だけど、呼び出す側としてはどうなのだ?良いのか?
美しくないと思うけれど、もともとの処理が美しくないので、うーん…

  def update
    newMap = []
    (0...@xNum).each {|i|
      newMap[i] = []
      (0...@yNum).each {|j|
        newMap[i][j] = calcNewState(i, j)
      }
    }
    @map = newMap
  end

あ、それぞれの処理(変数)が連動するから別々のlambdaじゃうまくいかないか…

周囲8方向の生存セルを取得する

  def getSum8Neighborhood(i, j)
    return Array((i - 1)..(i + 1)).product(Array((j - 1)..(j + 1))).reject {|ri, rj| (ri == i && rj == j) }.inject(0) {|sum, item, | sum + getState(item[0], item[1])}
    # ↑↓書き換えてみた
    sum = 0
    sum = sum + getState(i - 1, j)
    sum = sum + getState(i + 1, j)
    sum = sum + getState(i,     j - 1)
    sum = sum + getState(i,     j + 1)
    sum = sum + getState(i - 1, j - 1)
    sum = sum + getState(i + 1, j - 1)
    sum = sum + getState(i - 1, j + 1)
    sum = sum + getState(i + 1, j + 1)
  end

うーーーんんん?????

  1. Range(いやArray)を使って書きなおせそう
  2. 2重ループになるならproduct
  3. 自分自身のcellは弾くようにrejectして
  4. 合計をinjectで出す

という流れでワンライナーになりました。
が、普段ワンライナーを使わない目指さないなので、良くなったのかわからない…

正直ややこしい…ややこしくない?
おそらく加減算(につかうスペース)のせいでごっつく見える部分もあると思いますが、意図を含めた可読性は気になります。
リファクタリングすると大抵可読性が下がる方へなぜか行ってしまうので…

参考

5
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?