LoginSignup
16
7

More than 5 years have passed since last update.

[Elm]Life Gameで生命を生み出す

Posted at

はじめに

この記事は作ってみた系の記事です

こういう風に作った、こういう風に拡張したい、なんか重い、+1つTipsみたいな構成です

作ったもの

こちらに置いてあります
アプリgithub

これは何?

ライフゲームです
ゲームといってもマス目がどう変化していくかぼけっと眺めて楽しむだけです
昔流行ったとか何とか
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で生命を生み出して記事を生み出しました。

16
7
0

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
16
7