Elmでぷよぷよ

前提として、Javascript関数型言語もほとんどやったことないので、慣れてない部分が多くコードも綺麗じゃないのはご勘弁いただきたい。

また、途中まで qiita.com の丸パクリなので、最初は↑読んだ方が良いかも。

とりあえずコードは

GitHub - m-masataka/elm-puyo

に上げる。

盤面を定義

まずは、Elmでどうやって盤面を定義・表示するかを考えてみる。

ElmではModel(アプリの状態)、Veiw(画面表示)、Update(状態の更新)の3つの要素で構成されるらしい。 The Elm Architecture · An Introduction to Elm まずはわかりやすいようにUpdateは考えずに、「Modelを定義して、初期値入れて、Viewで表示する」ってところまでやってみたい。

Modelの定義します。後々の処理をわかりやすくするために、盤面は2次元配列的な感じで定義する。 下のような感じ。

type alias Model =
    { board: Board
    }

type alias Board =
    Array Row


type alias Row =
    Array Cell


type Cell
    = Tile Int
    | Empty

Boardは2次元配列になっていて、各配列にはCell情報が入っている。

CellはTileとEmptyの2つの状態を持っていて、Emptyはその名の通り何もないCell、Tileは何らかのぷよがある状態。

Tile Intとすることで、Intの部分でぷよの種類を識別するようにしている。

次にModelに初期値をセットする。

init : () -> ( Model, Cmd Msg )
init _ =
    let
        board =
            Array.repeat 10 <| Array.repeat 8<| Empty
    in
    ( Model board
      , Cmd.none
    )

let 〜 inの部分で初期値となる2次元配列(board)を生成する。Array.repeatはelm coreで用意されている関数で、指定した長さの配列を自動で生成してくれる。

Elmでは処理内容に応じて多くのmoduleが用意されているのでどんどん使っていく。

続いてView

先ほどModelで定義した2次元配列をひたすらdiv囲って表示することで何となく盤面っぽいのを作る。

-- VIEW
view : Model -> Html Msg
view model =
   div []
       [ div [] [ viewBoard model.board ]
       ]


viewBoard : Board -> Html Msg
viewBoard board =
    div
        Styles.boardStyle
    <|
        Array.toList (Array.map viewRow board)


viewRow : Row -> Html Msg
viewRow row =
    div [] <|
        Array.toList (Array.map viewCell row)

viewCell : Cell -> Html Msg
viewCell cell =
    case cell of
        Tile number ->
            div
                Styles.cellStyle
                [ text t
                ]
        Empty ->
            div
                Styles.cellStyle
                [ text ""
                ]

少しわかりにくいが、viewBoardでboardをRowに切り分けてviewRowへ。viewRowではRowをCellに分けてviewCellへ。 viewCellではboardに定義されたcellの状態をcaseで分類して、何を表示するか決めている。

Style.cellStyleは外出ししたスタイルシートを読み込んでいるだけなので、今は無視でOK。

最後に(最初に書くべきだが)main文を書く

-- MAIN
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

もはや呪文に近いが、副作用のあるコードを書く場合はBrowser.elementというBrowserモジュールを利用する。

Browserモジュールについては

Elm 0.19 の初期化方法 6 種類 - Qiita

がわかりやすかった。

今の状態では副作用の無いコードだが、後々副作用を入れてくのでこれにする。

subscriptionsはまだ使わないので側だけ定義

-- SUB
subscriptions model =
    Sub.none

ここまでのコードをまとめ

import Browser
import Array exposing (Array)
import Html exposing (..)
import Html.Attributes exposing (..)
import Styles

-- Model

type alias Model =
    { board: Board
    }


type alias Board =
    Array Row

type alias Row =
    Array Cell

type Cell
    = Tile Int
    | Empty

init : () -> ( Model, Cmd Msg )
init _ =
    let
        board =
            Array.repeat 10 <| Array.repeat 8<| Empty
    in
    ( Model board
      , Cmd.none
    )

-- Update
type Msg
    = Demo

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Demo ->
            ( model
            , Cmd.none
            )


-- VIEW
view : Model -> Html Msg
view model =
   div []
       [ div [] [ viewBoard model.board ]
       ]


viewBoard : Board -> Html Msg
viewBoard board =
    div
        Styles.boardStyle
    <|
        Array.toList (Array.map viewRow board)


viewRow : Row -> Html Msg
viewRow row =
    div [] <|
        Array.toList (Array.map viewCell row)

viewCell : Cell -> Html Msg
viewCell cell =
    case cell of
        Tile number ->
            div
                Styles.cellStyle
                [ text "foo"
                ]
        Empty ->
            div
                Styles.cellStyle
                [ text ""
                ]
-- SUB
subscriptions model =
    Sub.none

-- MAIN
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

とりあえず盤面はこれで完成。

f:id:m-masataka:20190801154523p:plain

盤面の変更

次に盤面の変更を行う。

今は全部Emptyなので、盤面を変更する。

やることは、2次元配列に値をセットするだけ。ただし、Elmで配列を参照する際は、必ず結果がMaybeで帰ってくるので、そこだけ注意して書く必要がある。

setBoard : (Int, Int) -> Cell -> Board -> Board
setBoard ( x, y ) cell board =
    Array.get y board
        |> Maybe.map (\oldRow -> Array.set x cell oldRow)
        |> Maybe.map (\newRow -> Array.set y newRow board)
        |> Maybe.withDefault board

最初のArray.get yで縦軸のRowをgetし、Maybe.mapでMaybe Rowを受け取る。

(\oldRow -> Array.set x cell oldRow)はコールバック関数で、バックスラッシュは無名関数の意味を持つ。ということでここではcellを新しい値に更新し、新しいRowを作成。

2こ目のMaybe.mapで更新されたRowをBoardにセットし、更新されたBoardが完成。

Maybe.withDefaultではMaybeの値がNothingになった場合の値をセットしている。ここではNotingになったら元のboardを返すという処理になる。

Maybeについては

Elmの練習5 | ぬわーーーーーーー!!!

がわかりやすかった。

setBoard関数をinitで使うと

board =
            setBoard (0,0) (Tile 1)
            <| Array.repeat 10 <| Array.repeat 8<| Empty

こんな感じで盤面の特定のCellを更新できる。

f:id:m-masataka:20190801154650p:plain

次はいよいよぷよぷよ的な盤面の動きを実装していく。