STACKERゲームを作ろう その4

nekoroll.hatenablog.com
の続き

今回やること

  • n個ずつ点灯させる
  • n秒毎に1マス隣に移動する
  • 端に辿り着いたら反転させる
    f:id:nekorollykk:20200922185935g:plain
    反転している様子

完成品はこちら
ellie-app.com
(色々気になる部分があったのでリファクタ予定)

実装解説

いらないものを消した

update

type Msg
    = InvertLight Box


        InvertLight { point, lightMode } ->
            let
                invert box =
                    if point == box.point then
                        { box | lightMode = invertLightMode box.lightMode }

                    else
                        box
            in
            ( { model | boxList = List.map invert model.boxList }
            , Cmd.none
            )
invertLightMode : LightMode -> LightMode
invertLightMode lightMode =
    case lightMode of
        LightOn ->
            LightOff

        LightOff ->
            LightOn
onClick (InvertLight box)

クリックイベントによる操作は不要になったので消した
点滅は全てsubscription駆動になる

n個ずつ点灯させる

Model

type alias Model =
    { boxList : List Box
    , fieldSize : FieldSize
    , lightPoints : List Point
    }

光らせる範囲の処理のためにサイズが必要になったのでfieldSizeを追加 光っている座標を保持してyを加算/減算して移動させたかったので
lightPointsで光っている座標リストを持つようにした
PointではなくList Pointなのはn個光らせるため

Init

initialModel : () -> ( Model, Cmd Msg )
initialModel _ =
    let
        fieldSize =
            { rowSize = 10, columnSize = 7 }

        pointList =
            makePointMatrix fieldSize
    in
    ( { boxList = List.map makeBox pointList
      , fieldSize = fieldSize
      , lightPoints = setStartPoints fieldSize 3
      , moveMode = Left
      }
    , Cmd.none
    )

initでfieldSizeを保持するようにして、散らばっていたサイズ定義をまとめた
スタートの個数をsetStartPoints関数に渡し、最初の光るボックスを生成(後述)

setStartPoints

setStartPoints : FieldSize -> Int -> List Point
setStartPoints fieldSize lightCount =
    let
        makeStartPoint y =
            { x = fieldSize.rowSize - 1, y = y }

        headPoint =
            fieldSize.columnSize - lightCount

        tailPoint =
            fieldSize.columnSize - 1
    in
    List.range headPoint tailPoint
        |> List.map makeStartPoint

fieldSizeと初期で光らせる個数を引数に受ける
光る範囲の先頭と末尾をfieldSize.columnSizelightCountから計算
List.rangeyの範囲を生成しmakeStartPointx一番下の段に固定しつつ
初期の光る範囲を生成する

Update

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Blinking _ ->
            let
                blinking box =
                    if List.member box.point model.lightPoints then
                        { box | lightMode = LightOn }

                    else
                        { box | lightMode = LightOff }
            in
            ( { model | boxList = List.map blinking model.boxList
              }
            , Cmd.none
            )

予め定義しておいた初期で光る範囲lightPointsに含まれているか
で光らせるかどうかを判定しlightModeを切り替える

n秒毎に1マス隣に移動し、端に辿り着いたら向きが反転する

Model

type MoveMode
    = Left
    | Right


type alias Model =
    { boxList : List Box
    , fieldSize : FieldSize
    , lightPoints : List Point
    , moveMode : MoveMode
    }

y-1していって端にたどり着きyがマイナスになったらyの最大値に戻る
と最初考えていたけどSTACKERゲームは端にたどり着くと反転するので
進行方向をカスタム型として定義

Init

, moveMode = Left

開始進行方向は左

進行方向の判定

getMoveMode : MoveMode -> List Point -> MoveMode
getMoveMode moveMode lightPoints =
    let
        headPoint =
            case lightPoints of
                [] ->
                    { x = 0, y = 0 }

                head :: rest ->
                    head

        length =
            List.length lightPoints

        tailPoint =
            case List.drop (length - 1) lightPoints of
                [] ->
                    { x = 0, y = 0 }

                tail :: rest ->
                    tail
    in
    case moveMode of
        Left ->
            if tailPoint.y == 1 then
                invertMoveMode moveMode

            else
                moveMode

        Right ->
            if headPoint.y == 5 then
                invertMoveMode moveMode

            else
                moveMode

現在の進行方向と光っている範囲を引数に受ける
光っている範囲の先頭と末尾を取得
進行方向が左の場合、光っている範囲の末尾が端っこなら進行方向を反転
進行方向が右の場合、光っている範囲の先頭が端っこなら進行方向を反転
という実装にした(左が先頭で右が末尾になる)

head,tail取得時の[] ->のパターンマッチが不要だなーと実装中思っていた
この辺りでリファクタを考え始めた

向きの反転

invertMoveMode : MoveMode -> MoveMode
invertMoveMode moveMode =
    case moveMode of
        Left ->
            Right

        Right ->
            Left

シンプルイズベスト
AB先生が教えてくれたinvertLightのパクリ

光っている範囲の移動

moveLightPoints : List Point -> MoveMode -> List Point
moveLightPoints lightPoints moveMode =
    let
        movePoint { x, y } =
            { x = x, y = getOperator moveMode y 1 }
    in
    lightPoints
        |> List.map movePoint


getOperator : MoveMode -> number -> number -> number
getOperator moveMove =
    case moveMove of
        Left ->
            (-)

        Right ->
            (+)

光っている範囲と進行方向を引数に受ける
進行方向が左ならyを減算、進行方向が右ならyを加算する必要があるので
左なら(-)、右なら(+)という形で+-1する処理を共通化した
中置演算子+ -が関数として扱える性質を上手く利用できた気がする

update

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Blinking _ ->
            let
                blinking box =
                    if List.member box.point model.lightPoints then
                        { box | lightMode = LightOn }

                    else
                        { box | lightMode = LightOff }

                moveMode =
                    getMoveMode model.moveMode model.lightPoints
            in
            ( { model
                | boxList = List.map blinking model.boxList
                , lightPoints = moveLightPoints model.lightPoints model.moveMode
                , moveMode = moveMode
              }
            , Cmd.none
            )

光っている位置に応じて進行方向を更新するためにgetMoveModeを呼び出し更新
光っている箇所は先程定義したmoveLightPointsで更新

これで左右に光っているマスを移動できるようになった

まとめ

  • ::の意味がわかって配列操作がちょっと出来るようになってきた

    www.amazon.co.jp
    コチラの本でバッチリ理解しました、皆さん買いましょう
  • -+が中置演算子である性質を上手く利用できた気がする
    個人的にこれがトップクラスにありがたかった。他の言語でもほしいと本気で思った
  • 実装中に違和感(よくない部分)が何となく感じ取れるようになってきた気がする
    • fieldSizeがべた書きで多数出現
    • 行のhead,tailが毎回使いづらい
    • 実装上ありえないはずの[] ->のパターンマッチ
      この辺りはリファクタで解消予定
  • subscription便利だし使いやすい
    他の処理に移動速度が紛れ込まないのが特にいい
  • 実装が楽しい(n回目)
    当然ロジックの誤りでバグったりするんだけどjsや言語として苦しむことが一切ない
    全て自分のミスで、かつコンパイラママが全て教えてくれる
    Elmを業務で使えたらコードフォーマットとかルールではなく、もっと本質的なロジックとか仕様について着目して指摘しやすいんだろうなぁ…と妄想した