はじめに
この記事は作ってみた系の記事です
こういう風に作った、こういう風に拡張したい、なんか重い、+1つTipsみたいな構成です
作ったもの
これは何?
ライフゲームです
ゲームといってもマス目がどう変化していくかぼけっと眺めて楽しむだけです
昔流行ったとか何とか
2次元セルオートマトンとか呼ばれたりします
簡単に説明すると(元から簡単なのですが)
- 時間は離散化されている(tickという単位で進む)
- 各セルは生(明るい緑)と死(暗い緑)の状態を持つ
- セルは8近傍(周りの8個体)の状態を見て次のtickでの状態が決定する
- 周囲に3マス生きているセルがいると誕生(生状態)
- 生きているセルは周囲に2か3マス生きていると生き残る
- それ以外は死ぬ
ほどよい密度があればよいが、過密や過疎状態では死んでしまう感じです
何が面白いか
眺めてると、固定的なものや周期的に繰り返すパターン(2回で繰り返すものから長周期のものまで)、同じものを生み出し続けるパターン(グライダーガン)ができたりします。自己複製的なふるまいは生物っぽいです
チューリング完全らしいのでなんでもできます
module構成
Cells.elmとLifeGame.elmです。CellsのほうにState更新ロジックとセルのviewをいれていて、LifeGameのほうにはゲームのコントロールロジックとかを入れています
データ構造
elm-community/graph
グラフあるじゃんって思って使ってみました。ただ欲しいAPIなかったのと、できあがって動かしたら結構重いのでどうしようかなってところです
type alias Size =
Int
type State
= Living
| Dead
type alias Cells =
{ cells : Graph State ()
, size : Size
}
データ構造はこんな感じです。
主なデータはcellsですね。Graphそのままです。Graph State ()
はグラフのノードに乗っている値としてState、グラフのエッジには何も値が乗ってないことを表しています
ちなみにインデントはスペース2派です。Elmのcoreのコードでも、4と2が同じファイルでも混ざったりしていてよくわからんですね
今回作ったライフゲームでは8近傍のセルの状態を参照しますが、場合によっては4近傍や6近傍、自分も含めて9近傍、1次元にしたら2近傍や3近傍も考えられます。
ということでcellsの作成時に影響を受ける範囲にエッジを張って、エッジが張られたセルの生存数を調べて自身の状態を更新するように実装しています
livingCount =
alongIncomingEdges cell
|> List.filterMap (\id -> Graph.get id cells)
|> List.map (.node >> .label)
|> List.filter ((==) Living)
|> List.length
コードとしてはここら辺です。alongIncomingEdges
で自分に入ってくるエッジの先のnodeIdを取ってきます
filterMap
は型合わせをしていて、List (Maybe a)
のMaybe
をつぶしています
elm-community/graphはelm-community/intdictを使って実装されているのでGraph.get
のオーダーは*O(32)*です。(たぶん)
遅いとしたらここか全体の状態の更新だとは思うんですが。
Size
現状Sizeは正方形の一辺の長さですが、長方形とか1次元に対応してもいいかなって思ってモデルに含ませてます
State
State
はライフゲームの説明の通りにそのまま書いています
もし状態を増やす拡張をするなら、下に1行足しましょう
type State
= Living
| Dead
| Dying
死にかけ(Dying)を足してみました。これでコンパイル通してエラーした部分を直していけば拡張完了だと思います。やってませんが
わたしはvscodeをつかっているので保存したら、errorを赤線でwarnを緑線で教えてくれるので便利です
rule : Int -> State -> State
rule count state =
case (count, state) of
(2, Living) ->
Living
(3, _) ->
Living
(_, _) ->
Dead
周囲の生きているセルの数と現在のStateを受け取って新しいStateを返す関数です。
上で説明したほぼそのままです。
case文をunion typeにそのまま使うのはよくあるんですが、一緒にパターンマッチしたいものとタプルにするとネストが1段で済むので楽ですし、見やすいです
rule : Int -> State -> State
rule count state =
case state of
Living ->
if count == 2 || count == 3 then
Living
else
Dead
Dead ->
if count == 3 then
Living
else
Dead
nestするとこんな感じでしょうか.
よりフローチャート的ですね。上のはクロステーブル?みたいなイメージです
Maybe Int
とかもJust 1
でマッチできるので場合によってはネストが減ります。
ていうかよくみたらrule
関数、_
(なんにでもマッチする)でマッチしているのでDying
足してもエラー出しませんね。全遷移を明記したほうが安心安全ですね
LifeGame
type alias Tick =
Int
type alias LifeGame =
{ cells : Cells
, tick : Tick
, stopped : Bool
}
ゲームの制御周りです。
進行を止めたり進めたりしたかったので変数を持たしています。あとは更新スピードを変える値が足りないです
type Msg
= NoOp
| NewGame Cells
| Start
| Stop
| Reset
| ToggleState Int
| NextTick
Msg
みれば大体、制御できることがわかる気がします
subscriptions : LifeGame -> Sub Msg
subscriptions game =
if game.stopped then
Sub.none
else
Time.every (Time.millisecond * 40) (always NextTick)
subscriptionsです。Time.every (Time.millisecond * 40) (always NextTick)
が大事なところですね。一定の間隔でNextTickメッセージを送っています。
この実装だと、一定間隔で毎に、描画ではなく次のStateの計算が始まるんですよね。先に計算するように実装しておけばいいんでしょうね。やっていませんが
おわりに
現在の状態から新しい状態を計算する。The Elm Architectureそのままなので素直に実装できます。
elm-community/graphはちょっと思ってたのと違いました。グラフDBのNeo4Jみたいに特定のグラフの条件にマッチする部分だけ更新する、みたいなことできたらよかったんですけどね。
グラフは好きなのでまた使ってみようと思います
今日はクリスマスイブでこの記事はElm Advent Calendarの24日分でした。
Life Gameで生命を生み出して記事を生み出しました。