Elmでぷよぷよを作る（その１）

最近Elmの勉強をはじめました。
勉強をかねて、何かしらゲームを作りたいと思い、ぷよぷよを作ってみることに。。。
これがなかなか難しかったので、備忘録。

とりあえずコードは
https://github.com/m-masataka/elm-puyo
に上げます。
<font color="OrangeRed">ちょっと色々と直したので、記事も修正</font>

# Modelの定義


```
type alias Model =
    { board: Board -- 盤面
    , grippedPuyo: List (Int, Cell) -- 操作中のぷよ 
    , status: Status -- ゲームのステータス
    , nextPuyo: List Cell -- 次に待機しているぷよ
    , timecounter: Int -- ゲームの経過時間（frame的な処理に使う）
    , chain : Int -- 連鎖記録用
    , score : Int -- スコア記録用
    }

type alias Board =
    List Cell
```

## 盤面（Board）
Boardは2次元配列を用意したいところだが、Elmで２次元配列を扱うのはそこそこめんどくさい。
ということで、１次元配列で模擬する。
`Cell`にぷよの属性を持たせることで、どのマスにどのぷよがいるかがわかる。

```
type Cell
    = Red
    | Blue
    | Green
    | Yellow
    | Ojama
    | Empty
    | None
```

Noneは言わば壁を表している。ぷよの移動さきがNone（壁）だったら移動不可といった具合に使える様に用意した。
↓なイメージ

```
|　|　|　|青|　|　|壁|
|　|　|　|赤|　|　|壁|
|　|　|　|　|　|　|壁|
...
```

# View
Viewは結構簡単で、board -> row -> cellで入子に表示する様にする。
Viewを表示する場合はListとmapがめっちゃ便利。
ポイントとしては、 Tuple.pairで２次元配列っぽく変換しているところ。

```
viewBoard : Model -> Html Msg
viewBoard model =
    let
        lx = List.range 0 columns |> List.map (\a -> a * rowsLen ) 
        ly = List.range 1 columns |> List.map (\a -> a * rowsLen - 1 )
        mat = List.map2 Tuple.pair lx ly
    in
    div [ class "grid-container" ]
        [ viewEnd model
        , div [] 
            <| List.map (\(x, y) -> viewRow x y model) mat
        ]

viewRow : Int -> Int -> Model -> Html Msg
viewRow i j model =
    div [class "grid-row"] <| 
        if i /= 0 then
            let
                board = if model.status == Normal then
                        model.board |> Board.replace model.grippedPuyo
                    else
                        model.board
            in
            List.map viewCell ( board |> Array.fromList |> Array.slice (i + 1) (j + 1) |> Array.toList)
        else
            []

viewCell : Cell -> Html Msg
viewCell cell =
    case cell of
        Empty ->
            div [class "grid-cell"] 
                [ img [ class "puyo"
                  ]
                  []
                ]
        Red ->
            div [class "grid-cell"] 
                [ img
                    [ src "/img/puyo_1.png"
                    , class "puyo"
                    ]
                    []
                ]
...
```

# Subscription
ぷよぷよ の盤面変更はKey操作と自由落下が必要なので、２つ定義する。
複数定義する場合はSub.batchを使うらしい。
onKeyDownの項目はUpdateの項目で説明する。
Time.everyで一定間隔毎でイベントを発火してくれる。（これは盤面切り替えやぷよの落下に利用）

```
-- SUB
subscriptions model =
    Sub.batch
        [ onKeyDown (Decode.map KeyMsg keyDecoder )
        , Time.every 50 Tick
        ]
```
特に動きがないWebUIについてはSub.noneにしておくみたい。

# Main
（最初に書くべきだが）main文を書く

```
-- MAIN
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }
```
もはや呪文に近いが、副作用のあるコードを書く場合は``Browser.element``というBrowserモジュールを利用する。
Browserモジュールについては
https://qiita.com/jinjor/items/245959d2da710eda18fa:title
がわかりやすかった。

# Update

ここが一番しんどい
## キー入力
ElmのBrowserパッケージはとても便利で、Eventsパッケージの中で大体のイベントをキャッチできる。
ただ、スマホのピンチ・スワイプとかはサポートできてなかった気がする。
今回onKyeDownを利用。

```
import Browser.Events exposing (onKeyDown)
...
    Sub.batch
        [ onKeyDown (Decode.map KeyMsg keyDecoder )
...
-- Update
    case msg of
        KeyMsg keyname ->
            let
                ngp=
                    case model.status of
                        Normal ->
                            move_ keyname
                        _ ->
                            model.grippedPuyo
            in 
            ( { model | grippedPuyo = ngp }, Cmd.none )

...
-- Decoder
keyDecoder : Decode.Decoder KeyName
keyDecoder =
    Decode.map toKeyName (Decode.field "key" Decode.string)

toKeyName : String -> KeyName
toKeyName string =
    case string of
        "ArrowDown" ->
            Down
        "ArrowRight" ->
            Right
        "ArrowLeft" ->
            Left
        "Tap" ->
            SpinLeft
        "SwipeDown" ->
            FallDown
        _ ->
            SpinLeft
```

subscriptionの中でKeyDownのイベントを受信し、keyDecoderでStringをKeyName型（自分で定義）に変換。
Updateで定義しているKeyMsgで受け取ったKeyNameを処理している。
あとはKeyMsgの中でmodelを更新すれば動きが完成する。

## 盤面のステータス
以下の様なステータスを用意した。（StartとEndは同じでも用意かも）

* Start
 * ゲーム開始
* New
 * 盤面上のぷよの移動が完了し、新しいぷよが配置される状態
 * 新しいぷよが出現する場所が埋まってたらEndに遷移
 * それ以外はNormalに遷移
* Normal
 * 操作中のぷよが自由落下する状態
 * キー入力を受付可能
 * ぷよが下に進めなくなったらFallに遷移
* Fall
 * ぷよが落ちる状態
 * キー入力不可
 * 落下するぷよが無ければRemoveに遷移
* Remove
 * ぷよが連結して、消える状態
 * キー入力不可
 * ぷよを削除した場合はFallに遷移
 * 削除するぷよが無ければNewに遷移
* End
 * ゲーム終了


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/be24244d-1159-4dcf-39f1-5a2f7f7cd1da.png)
あとは定義通りに盤面とぷよを動かす。


### 操作ぷよ動作
操作対象ぷよ（grippedPuyo）をキー入力に応じて動かす。
簡単には、KeyNameによって下のマスに動く（i + rowsLen）か、横に動く（i+1）か等を決めている。
動いた先がEmptyでは無い場合は、移動不可として元のぷよの位置を返す。

回転の動作は若干面倒なので、spin関数を別途作った。

```
move_ : KeyName -> List (Int, Cell)
move_ keyname =
    let
        spin_ : List (Int, Cell) -> Board -> List (Int, Cell)
        spin_ l b =
            let
                (i1, c1) = List.getAt 0 l |> Maybe.withDefault (0, None)
                (i2, c2) = List.getAt 1 l |> Maybe.withDefault (0, None)
                np_ = 
                    if i1 - i2 > 0  then
                        if (i1 - i2 |> abs) == rowsLen then
                            [ (i1, c1), (i1 - 1, c2) ]
                        else
                            [ (i1, c1), (i1 + rowsLen, c2) ]
                    else
                        if (i1 - i2 |> abs) == rowsLen then
                            [ (i1, c1), (i1 + 1, c2) ]
                        else
                            [ (i1, c1), (i1 - rowsLen, c2) ]
            in
            if check_ np_ then
                np_
            else
                [(i1, c2), (i2, c1)]

        check_ : List (Int, Cell) -> Bool
        check_ l =
            List.map (\(i, c) -> Board.get i model.board) l |> List.all (\c -> c == Empty)

        check__ = (\l -> if check_ l then l else model.grippedPuyo)

    in
    case keyname of
        Down ->
            check__ <| List.map (\(i, c) -> (i + rowsLen, c) ) model.grippedPuyo
        Left ->
            check__ <| List.map (\(i, c) -> (i - 1, c) ) model.grippedPuyo
        Right ->
            check__ <| List.map (\(i, c) -> (i + 1, c) ) model.grippedPuyo
        _ ->
            spin_ model.grippedPuyo model.board
```


### ぷよの落下
かなりゴチャゴチャした関数になってしまったが、やっていることは、
fall__は再帰関数。というより、Boarの操作はほとんど再帰処理。
盤面の右下から捜査していって、自分の下のマスがEmptyだったら下に落下。
これを繰り返すことで全部のぷよが下に詰まる。


```
fall_ : Board -> ( Board, Bool )
fall_ board_ =
    let
        fall__ : List (Int, Cell) -> Board -> Board
        fall__ l b =
            case List.head l of
                Just (i, c) ->
                    let
                        fall___ : Int -> Int
                        fall___ j =
                            if Board.get (i + rowsLen * j) b == Empty then fall___ (j + 1) else j - 1
                    in
                    if List.member (Board.get i b) Board.normal &&
                        Board.get (i + rowsLen) b == Empty then
                        Board.replace [(i, Empty), (i + rowsLen * (fall___ 1), c)] b
                        |> fall__ (List.tail l |> Maybe.withDefault [])
                    else
                        fall__ (List.tail l |> Maybe.withDefault []) b
                Nothing ->
                    b
        
        nb = fall__ (List.indexedMap Tuple.pair board_ |> List.reverse) board_
    in
    ( nb , nb /= board_)
```

### ぷよの削除
ぷよの連結判定は↓のサイトがわかりやすい。
https://ponk.jp/cpp/el/puyopuyo
１つのぷよを起点に周りのぷよを捜査していく。同じ色だったら更に捜査。別の色だったらreturnって感じで再起処理を進めていく感じ。
Elmだとこういったコードは書きやすいね。

```
remove_ : Board -> (List (List Int), Bool)
remove_ board_ =
    let
        chain_ : Int -> Cell -> List Int -> List Int
        chain_ i c l =
            let
                next = [i - rowsLen, i - 1, i + 1, i + rowsLen]
                c_ = Board.get i board_
            in
            if c == c_ then
                List.filterNot (\n -> List.member n l) next
                |> List.map (\n -> chain_ n c ( i :: l ))
                |> List.concat |> (\li -> i :: li)
            else
                []

        picked_ = List.indexedMap Tuple.pair board_
            |> List.filterNot (\(i, c) -> c == Empty || c == None )
            |> List.map (\(i, c) -> chain_ i c [] )
            |> List.filter (\l -> List.length l > 3)
    in
    ( picked_, List.length picked_ > 0 )
```

### Score
スコア計算は適当に↓を参考に計算
https://bozitoma.com/games/point-calculator/
計算に必要な要素は
連鎖数・連結数・消した色数なので、remove fallでの処理の際に全部算出可能。


# ここまでの完成版

![ぷよぷよ .gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/82dae574-4423-c0b9-1f77-2017e803c55c.gif)


だいぶ前だが、elm-gamesに入れてもらった
https://github.com/rofrol/elm-games

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/2ea75532-e2f0-f3ea-b681-6e07e52dcaad.png)

今の状態だとカクカク動いてるので、次はアニメーションにチャレンジかな