STACKERゲームを作ろう その2

nekoroll.hatenablog.com
の続きというか修正版というか…

今回やること

  • 指定したマスの色を変えられるようにする

…の前に何があったか

( ゚д゚)

(AB先生ありがとうございます)

AB先生が想定している問題かどうかはわからないんですが
問題に直面したので発覚した問題と解決策をメモ

Model渡せねえ問題

🐊「BoxRowを作る関数を作って…それを受け取った数でリスト化する関数も作る!」
🐊「これで数も可変なマス目を作れるし完璧でしょ!クリックして色変えれるようにしよ!」

(TheSacredLiptonさんAB先生ありがとうございます)

問題点

  • Modelをバケツリレーする必要が産まれて物凄い使いづらい関数になっていた
    驚くほど不便。単に描画できるだけで何も操作できないマス目になっていた
  • 関数を細かく作成したのに、結局それをリスト化する関数から呼び出しているだけなので、使い勝手が最悪
    makeBoxListの関数でうまく操作してマス目を作るべきだった気がする
    makeBoxByCountが一見使いやすかったんだけど拡張性が最悪
  • 引数がプリミティブな型で扱いづらい
    qiita.com
    実際実装中もInt -> Intが凄い分かりづらくて苦労してました
    記事にもある通り今回作った関数は拡張されていく可能性が大いにあるにも関わらず、プリミティブな型なので拡張性が最悪

🐊「ああ……これはアンチパターンですね……なんだこれは……たまげたなぁ」

ということで色々考えて、拡張性や使いやすさを自分なりに意識して直してみた
ellie-app.com

Boxを作る所から考えていく

  • ModelにはBoxリストを持つようにして、これを軸に処理していく
    表示も操作もこれ。前はBoxSizeだけ持っていて操作が出来なかった
  • Boxリストを作る関数を作る
  • Boxリストを表示する関数を作る

こんな感じでまずはゼロから作り直した

Model

type alias Box =
    { x : Int
    , y : Int
    , isLightOn : Bool
    }


type alias Model =
    { boxList : List Box }


initialModel : Model
initialModel =
    { boxList = [ Box 0 0 False, Box 1 0 False ]
    }

Boxは操作に使うため、座標と点灯中フラグを持つようにした
initialModel関数でBoxをn個(検証なので2個で固定)生成しboxListに詰めるようにした
これをベースに表示や操作を行っていく

Boxリストの表示

.box {
  border: 1px solid;
  width: 50px;
  height: 50px;
}
view : Model -> Html Msg
view model =
    div []
        (List.map showBox model.boxList)


showBox : Box -> Html Msg
showBox box =
    div
        [ class "box"
        , setLampMode box.isLightOn
        , onClick (InvertLight box)
        ]
        []

showBox関数でBoxHtml Msgに変換して表示できるようにした
表示するだけの関数なので、これをList.mapboxListに適用して表示できるようにした
setLampModeonClickについては後述

クリックで色を変更する

type Msg
    = InvertLight Box


update : Msg -> Model -> Model
update msg model =
    case msg of
        InvertLight box ->
            let
                invertLight b =
                    if b.x == box.x && b.y == box.y then
                        { b | isLightOn = not b.isLightOn }
                    else
                        b
            in
                { model | boxList = List.map invertLight model.boxList }

以前作ったTODOリストの更新処理の応用
クリックされたBoxを受け取り、x,yが一致するBoxの点灯中フラグを反転させる関数を作成
List.mapboxListに適用してboxList再度詰め直すようにした
これでクリックしたBoxの点灯フラグを更新できるようになった

点灯フラグで背景色を変更する

setLampMode : Bool -> Attribute Msg
setLampMode isLampTurnOn =
    if isLampTurnOn then
        style "background-color" "red"

    else
        style "background-color" "white"

Box.isLightOnを受け取り、Trueなら赤Falseなら白の背景色に変更するようにした

これで操作可能なBoxをn個表示するという部分まで実現できた
しかしまだ2個Boxを表示できているだけなので、任意の個数のBoxに修正する

boxListを任意の個数のBoxで詰めるようにする

  • row * column個のリストを作る
  • row, columnを受けて生成する関数はやめる
    同じことは繰り返さない
  • それぞれにx,yを振る必要があるので2重のforeachみたいな処理を想定
  • データとしてはList Box表示側ではList (List Box)としたい
    7*10=70であれば、7個ずつ区切って10行にして表示するイメージ

こんな感じで考えた
ellie-app.com

n * n個のBoxを生成する関数とModel

type alias Model =
    { boxList : List (List Box) }


initialModel : Model
initialModel =
    { boxList = makeRow 9
    }


makeRow : Int -> List (List Box)
makeRow rowCount =
    List.map makeBox (List.range 0 rowCount)

makeBox : Int -> List Box
makeBox y =
    List.map (\x -> Box x y False) (List.range 0 6)

makeBoxでn個のBoxリストを作成(x軸)
makeRowでn行分Boxリストを作成(y軸)
これでn * n, row * column個のBoxが作れた

表示する

view : Model -> Html Msg
view model =
    div []
        (List.map (\boxList -> 
            div [ style "display" "flex" ]
                (List.map showBox boxList)) model.boxList)

List (List Box)となっているのでList Boxdivで囲って表示してあげた
これで任意の個数表示できるようになった

…と思ったが問題が発生

InvertLightが処理できない

List.mapで1Boxずつチェックしているので、ネストしたListは想定していない

  • 操作側はList Boxが欲しい
  • 表示側はList (List Box)が欲しい

操作を軸に考える

  • List (List Box)ではなくList Boxで持つ
  • 表示側はn個で分割して表示する

とするように修正した
ellie-app.com

List.boxでデータを持つ

type alias Model =
    { boxList : List Box }


initialModel : Model
initialModel =
    { boxList = List.concat (makeRow 9)
    }

丁度いい関数List.concatがあったので結合して
List (List Box)からList Boxに変更した
これで操作側は問題なく扱えるようになった

表示側は分割して表示する

view : Model -> Html Msg
view model =
    div []
        (List.map (\boxList -> 
            div [ style "display" "flex" ]
                (List.map showBox boxList)) (split 7 model.boxList))


split : Int -> List Box -> List (List Box)
split n boxList =
    case List.take n boxList of
        [] ->
            []
        takedList ->
            takedList :: split n (List.drop n boxList)

(List.map showBox boxList)) model.boxList)
だった部分が
(List.map showBox boxList)) (split 7 model.boxList))
になっただけ。分割して表示するようにした

そして今回の肝なのがsplitBox関数
package.elm-lang.org
Listのドキュメントを探したが期待する関数が無かったので、無い知恵を絞って作った
再帰関数苦手だけど、わかりやすくて割と簡単に実装できた(時間がかからなかったとは言ってない)

これでようやくその1の完成段階に戻ってきて、かつ指定したマスを光らせることが出来るようになった
今はonClickイベントで光らせているけど、処理を変えるだけなので大丈夫(だと思う)

次回はsubscriptionを使ってn秒毎にペカペカさせる所を進めていく

おまけ

  • boxList初期化のmakeRowmakeBoxがなんか怪しい
    固定値持ってるし、なんか違和感がある
    ただ今はとりあえず動くことを重視しているという点と
    今後自由にサイズを変えるかどうかわからないので、まずはこのまま進めてヨシとしている
  • 苦労したsplitBox関数がList.Extraに存在していた
    package.elm-lang.org
    f:id:nekorollykk:20200913020910p:plain

groupsOfいい加減にしろって感じだよ(笑う
完全に車輪の再発明です/(^o^)\
でも再帰関数の勉強になったしdroptakeの使い方分かった…業務じゃなくて勉強だし…
車輪の再発明しないとわからないこともあるし…ええんや…と自分を慰めています
🐊「トホホw」
f:id:nekorollykk:20200909020013j:plain