ライフゲーム書き初め。やっぱりCLIでも動くものは楽しいですネ。
他の実装をなるべく見ないで作ったので、ポカがあるかもしれません。
動き
動作環境
- 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]
が自動で配列になることを期待 -
!!0
をfalse
と期待
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
うーーーんんん?????
- Range(いやArray)を使って書きなおせそう
- 2重ループになるならproduct
- 自分自身のcellは弾くようにrejectして
- 合計をinjectで出す
という流れでワンライナーになりました。
が、普段ワンライナーを使わない目指さないなので、良くなったのかわからない…
正直ややこしい…ややこしくない?
おそらく加減算(につかうスペース)のせいでごっつく見える部分もあると思いますが、意図を含めた可読性は気になります。
リファクタリングすると大抵可読性が下がる方へなぜか行ってしまうので…