Rubyでライフゲーム

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

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

# 動き

![life.gif](https://qiita-image-store.s3.amazonaws.com/0/62354/79d4a752-d502-6fab-7e22-1a926d301123.gif)


# 動作環境

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

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

# コード

```rb
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]`が自動で配列になることを期待
* `!!0`を`false`と期待

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

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

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

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

[Array#product で多重ループを回避しよう - Qiita](https://qiita.com/QUANON/items/e23d35cf193b5d07bf58)

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

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

```rb
# ~~~
  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で一つにまとめてもいけませんでした。

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

```rb
  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重ループを何度も書きたくないという目的だけど、呼び出す側としてはどうなのだ？良いのか？
美しくないと思うけれど、もともとの処理が美しくないので、うーん…


```rb
  def update
    newMap = []
    (0...@xNum).each {|i|
      newMap[i] = []
      (0...@yNum).each {|j|
        newMap[i][j] = calcNewState(i, j)
      }
    }
    @map = newMap
  end
```
あ、それぞれの処理(変数)が連動するから別々のlambdaじゃうまくいかないか…


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

```rb
  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で出す

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

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


# 参考

* [ライフゲーム - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)
* [配列やハッシュで合計値を求める時の書き方まとめ - Qiita](https://qiita.com/rllllho/items/94e8fde8266f14275961)
* [flatten, flatten! (Array) - Rubyリファレンス](https://ref.xaio.jp/ruby/classes/array/flatten)
* [Rubyでコンソール上の文字をクリアする方法 - 脳汁portal](http://portaltan.hatenablog.com/entry/2015/09/24/182836)
* [一定時間処理を停止（sleep）する - Ruby｜ONE-RUN](https://st40.xyz/one-run/article/229/)


* [【Ruby】乱用厳禁！？後置ifで書くとかえって読みづらくなるケース - Qiita](https://qiita.com/jnchito/items/4e47559a4c821474233a) 
普段使えない後置ifを使いたくなるが、ぐっとこらえてみた。