Elmでブラックジャックを実装してみた

# はじめに
[前回](https://qiita.com/Keck/items/57d500e5c9913d149829#%E4%B8%89%E7%95%AA%E7%85%8E%E3%81%98)に引き続き、今度はElmでブラックジャックを実装してみます。
元はと言えば[この記事](https://qiita.com/hirossyi73/items/cf8648c31898216312e5)に影響されてやろうと思ったものでした。
今度こそQiita内には先駆者はいないはずです・・・

私のElm歴は半年で、しかもあまり書いていないのでとてもレベルが低いと思います。
Elmerな方々にはぜひアドバイスなどいただければと思います。

一部解説している順番と実際にコードに書かれている順番は異なります。ご了承ください。

完成品は一応以下にあります。
https://github.com/IamKeck/elm-blackjack

# 動作イメージ
![elm-blackjack.gif](https://qiita-image-store.s3.amazonaws.com/0/151779/d338e8ed-494a-0eb8-2a27-7c62fd05c13b.gif)

もうちょっと見た目こだわれよって感じですね。

# プログラムの中身
## モジュール構成
Model.elm、Update.elm、View.elm、Main.elmというわけ方をしました。
Model.elmにはModelとMsgを、Update.elmにはupdateを、View.elmにはviewを、
Main.elmにはmainを定義しています。そのまんまです。

## Model
まずカード周りから定義します。

```elm
-- Card


type Suit
    = Spade
    | Heart
    | Club
    | Diamond


type alias Card =
    { suit : Suit, number : Int }

```
例によって例のごとく(前回もそうだった)、数字が14だったり0だったりするカードを定義できてしまいますがまあ・・・

デッキを定義します。Haskellならリストモナドを使って簡単にスートリストと数字リストを組み合わせることができました。 
Elmの` |> List.concatMap`がHaskellの`>>=`に対応している気がするので、Elmではこれを使うのでしょう。
ただ私は、Appricativeのap `<*>`を使うとスマートにできるのではないかと思ったので、この際演算子を定義してみました。

```elm
(<*>) : List (a -> b) -> List a -> List b
(<*>) fs xs =
    List.concatMap (\f -> List.map f xs) fs
infixl 4 <*>
```

ついでに<$>も定義して

```elm
(<$>) : (a -> b) -> List a -> List b
(<$>) =
    List.map
infixl 4 <$>
```

デッキは以下のように定義することができました。 Cool!

```elm
deck : List Card
deck =
    Card <$> [ Spade, Heart, Club, Diamond ] <*> (List.range 1 13)
```
・・・
まあ今回は実務コードでもなんでもないので許してください・・・

得点計算用に、`Card -> Int`な関数を定義します。
A=1だったりA=11だったりするので2つ定義します。 この辺は前回同様です。

```elm
cardToPointAceAsOne : Card -> Int
cardToPointAceAsOne c =
    if c.number > 10 then
        10
    else
        c.number


cardToPointAceAsEleven : Card -> Int
cardToPointAceAsEleven c =
    if c.number == 1 then
        11
    else
        cardToPointAceAsOne c
```

さっきの関数とList Cardを受け取って総得点を計算する関数を定義します。
まだバースト(21点を超えてしまい失格になること)の判定はしません。

```elm
calcPoint : (Card -> Int) -> List Card -> Int
calcPoint f cs =
    List.map f cs |> List.sum
```

List Cardを受け取って、バースト判定込みで総得点を計算する関数を定義します。
バーストをNothingで表現したいため、Maybeを使います

```elm
calcValidPoint : List Card -> Maybe Int
calcValidPoint cs =
    let
        high =
            calcPoint cardToPointAceAsEleven cs

        low =
            calcPoint cardToPointAceAsOne cs
    in
        if low > 21 then
            Nothing
        else if high > 21 then
            Just low
        else
            Just high
```

デッキをシャッフルする処理を書いていきます。
今回も[この記事](https://qiita.com/ogata-k/items/a3c30c7edf549ac5299f)を参考にします。

簡単にいうと、`["Alice", "Bob", "Charlie"]`というリストがあるとき、同じ長さの乱数値リスト`[2,3,1]`を作成し、zipします。
`[("Alice", 2), ("Bob", 3), ("Charlie", 1)]`というリストができるので、リストの値のsndでソート(sortOnとかsortBy)し、map fstして余計な乱数値数列を取り除きます。


ただし、Elmで全ての数がユニークな乱数値数列を作るのは面倒そうでした。 
ユニークにするのは諦める代わりに、数値の範囲を1 ~ 5200にしてなるべく値が被らないようにしました。
この辺説明不足ですね。

```elm
-- デッキと乱数値数列を受け取り、シャッフル済みのデッキを返す
shuffleCards : List Card -> List Int -> List Card
shuffleCards cs xs =
    List.map2 (,) cs xs |> List.sortBy Tuple.second |> List.map Tuple.first

-- ソート用乱数値数列を作るGenerator
sortListGenerator : List Card -> Random.Generator (List Int)
sortListGenerator cs =
    let
        cs_l =
            List.length cs
    in
        cs_l * 100 |> Random.int 0 |> Random.list cs_l
```

カードから一旦離れます。

ゲーム結果を定義します。 バーストと純粋な得点での勝敗は一応分けました。 念のためErrorという値も設定しています。

```elm
type Result
    = YouWin
    | DealerWins
    | YouBust
    | DealerBust
    | Draw
    | Error
```

ゲーム状態を定義します

```elm
type GameStatus
    = Title --　起動直後
    | UsersTurn -- ユーザーが行動を選択するフェーズ
    | Over --結果表示
```

モデル本体はこんな感じでしょうか。
deckは山札を、playerやdealerは各々の手札を表します。

```elm
type alias Model =
    { deck : List Card
    , player : List Card
    , dealer : List Card
    , status : GameStatus
    , result : Maybe Result
    , playersPoint : Maybe Int
    , dealersPoint : Maybe Int
    }
```

初期モデルもついでに定義します


```elm
initialModel =
    { deck = []
    , player = []
    , dealer = []
    , status = Title
    , result = Nothing
    , playersPoint = Nothing
    , dealersPoint = Nothing 
    }
```

Msgも定義します
Hitはカードを引く行動、Standは現在の手札で勝負する行動を示します。
GotShuffleListは先ほどのソート用乱数を受け取った時のMsgです。

```elm
type Msg
    = GameStart
    | Hit
    | Stand
    | GotShuffleList (List Int)
```

最後に、勝敗判定を行う関数を定義します。

```elm
judge : Model -> Result
judge m =
    case ( m.playersPoint, m.dealersPoint ) of
        ( Nothing, dp ) ->
            DealerWins

        ( yp, Nothing ) ->
            YouWin

        ( Just yp, Just dp ) ->
            if yp == dp then
                Draw
            else if yp > dp then
                YouWin
            else
                DealerWins

```

## Update

さてupdate関数を・・・といきたいところですが、まずディーラーの行動フェイズを定義します。
ディーラーはユーザーがStandした後、点数が17点以上になるまでカードを引き続けます。
Modelを丸ごと作り直すので、Update側に書いた方がいいかなあと思いました。

```elm
dealersTurn : Model.Model -> Model.Model
dealersTurn m =
    case m.dealersPoint of
        Nothing ->
            m

        Just p ->
            if p < 17 then
                case m.deck of
                    nc :: nd ->
                        dealersTurn
                            { m
                                | dealer = nc :: m.dealer
                                , deck = nd
                                , dealersPoint =
                                    Model.calcValidPoint (nc :: m.dealer)
                            }

                    [] ->
                        m
            else
                m

```
繰り返し部分を再帰にしていますが、コンパイル後のjsファイルを見るとうまく末尾再帰最適化されているようでした。

ではでかいupdate関数の中身に行きます。
冒頭３行は以下の通りです。

```elm
update : Model.Msg -> Model.Model -> ( Model.Model, Cmd Model.Msg )
update msg model =
    case msg of
```
以下caseの各branchについて書いていきます。

まずはGameStartです。「開始」ボタンやゲーム終了後の「再挑戦」を押した際にメッセージが発火されます。

```elm
        Model.GameStart ->
            let
                cmd =
                    Model.sortListGenerator Model.deck |> Random.generate Model.GotShuffleList

                nm =
                    { model | player = [], dealer = [], result = Nothing }
            in
                ( nm, cmd )
```
デッキソート用乱数をCmdで要求し、プレイヤーやディーラーの手札を初期化、結果も初期化しています。

次に、ソート用乱数の受け取りMsgです

```elm

            let
                newDeck =
                    Model.shuffleCards Model.deck xs
            in
                case newDeck of
                    a :: b :: c :: d :: xs ->
                        let
                            dp =
                                Model.calcValidPoint [ a, c ]

                            pp =
                                Model.calcValidPoint [ b, d ]

                            result =
                                if (dp == Just 21) && (pp == Just 21) then
                                    Just Model.Draw
                                else if pp == Just 21 then
                                    Just Model.YouWin
                                else if dp == Just 21 then
                                    Just Model.DealerWins
                                else
                                    Nothing

                            status =
                                case result of
                                    Just _ ->
                                        Model.Over

                                    Nothing ->
                                        Model.UsersTurn

                            player =
                                d :: b :: model.player

                            dealer =
                                a :: c :: model.dealer
                        in
                            ( { model
                                | deck = xs
                                , status = status
                                , result = result
                                , player = player
                                , dealer = dealer
                                , playersPoint = Model.calcValidPoint player
                                , dealersPoint = Model.calcValidPoint dealer
                              }
                            , Cmd.none
                            )

                    _ ->
                        ( { model | status = Model.Over, result = Just Model.Error }, Cmd.none )
```
一見長いのですが、elmのこのコーディングスタイル(elm-formatのもの)でそう見えるだけで、
そこまで複雑ではないんですよね。
初期山札をシャッフルした後、ディーラーとプレイヤーに各々２枚ずつカードを配っています。
この初回手札だけで21点になってしまうことがあります。(ナチュラルブラックジャック)
この時はゲームを即終了させるようにします。
それ以外の場合、ゲームはプレイヤーの行動フェイズに移行します。

山札が４枚なかった時の処理を一応書いています。 即終了、結果はエラーとしておきました。
起こり得ないことですが・・・

次はプレイヤーの行動フェイズです。

```elm
        Model.Hit ->
            case model.deck of
                newCard :: newDeck ->
                    let
                        playersNewHand =
                            newCard :: model.player

                        newPoint =
                            Model.calcValidPoint playersNewHand

                        ( newStatus, result ) =
                            case newPoint of
                                Nothing ->
                                    ( Model.Over, Just Model.YouBust )

                                Just _ ->
                                    ( model.status, Nothing )
                    in
                        { model
                            | player = playersNewHand
                            , playersPoint = newPoint
                            , deck = newDeck
                            , status = newStatus
                            , result = result
                        }
                            ! []

                [] ->
                    ( { model | status = Model.Over, result = Just Model.Error }, Cmd.none )
```
カードを引いて、得点計算をしています。 バーストしていたら即ゲーム終了です。
そうでなければ引き続きプレイヤーの行動フェイズです。
また山札がなくなっていた場合の処理を書いています。 
本来は山札がなくなった時点でStandするようにすべきなんでしょうが、その処理を追加するのを忘れていました・・・
まあディーラーとプレイヤーの一対一の勝負では起こり得ないので・・・

最後にStandした際の処理です。

```elm
        Model.Stand ->
            let
                newModel =
                    dealersTurn model

                result =
                    Model.judge newModel
            in
                ( { newModel
                    | status = Model.Over
                    , result = Just result
                  }
                , Cmd.none
                )
```
まあ前述のディーラー行動フェイズでModelを更新した後、得点を計算して結果を取得、
ゲーム状態を終了済みにしているだけですね。

Updateは以上です

## View
view関数本体に入る前にいくつかヘルパー関数を定義します。

表示のため、カードを文字列表現にします。

```elm
showCard : Model.Card -> String
showCard c =
    let
        suit =
            case c.suit of
                Model.Club ->
                    "♣"

                Model.Heart ->
                    "♥"

                Model.Spade ->
                    "♠"

                Model.Diamond ->
                    "♦"

        number =
            case c.number of
                1 ->
                    "A"

                11 ->
                    "J"

                12 ->
                    "Q"

                13 ->
                    "K"

                n ->
                    toString n
    in
        suit ++ " " ++ number
```

結果によって表示するメッセージを決定します。
英語はガバガバです

```elm
showResult : Maybe Model.Result -> String
showResult result =
    case result of
        Just Model.YouWin ->
            "You Win!"

        Just Model.DealerWins ->
            "You Lose!"

        Just Model.YouBust ->
            "You've Busted!"

        Just Model.DealerBust ->
            "The Dealer Has Busted! You Win!"

        Just Model.Draw ->
            "Draw"

        Just Model.Error ->
            "Error!"

        Nothing ->
            "Error!"
```

ではview関数です。
最初の3行はこの通りです。

```elm
view : Model.Model -> Html Model.Msg
view m =
    let
```

letの各変数(変数でいいんでしょうか)について説明していきます。

上の方に出すプレイヤーへのメッセージです。

```elm
        statusText =
            case m.status of
                Model.Title ->
                    "Welcome To Black Jack"

                Model.UsersTurn ->
                    "Black Jack: Your Turn. Hit or Stand"

                Model.Over ->
                    "Black Jack: Game Over" ++ (showResult m.result)
````

ディーラーの手札として表示する文字列です。
ユーザー行動フェーズは最初の一枚だけ表示させます。
ゲーム終了後は全ての手札を見せます。
それ以外の場合で手札を見せることはないと思うので、""としておきます。

```elm
        dealersCards =
            case m.status of
                Model.UsersTurn ->
                    List.reverse m.dealer
                        |> List.head
                        |> Maybe.andThen (showCard >> Just)
                        |> Maybe.withDefault ""
                        |> flip (++) " ?"

                Model.Over ->
                    List.reverse m.dealer |> List.map showCard |> String.join " "

                _ ->
                    ""
```

プレイヤーの手札です。これは特に分岐などせず普通に表示させます。

```elm
        playersCards =
            List.reverse m.player |> List.map showCard |> String.join " "
```

プレイヤーの得点です。 ModelのplayersPointがNothingの時はバーストしているのですが、"Busted!"とでも表示させておきます。
初期状態でもplayersPointはNothingなので、表示文字列が"Busted"となってしまいます。
まあこれはHtml構築時に表示させないようにします。

```elm
        playersPoint =
            Maybe.map toString m.playersPoint |> Maybe.withDefault "Busted!"
```

ディーラーの点数です。 これも同様です。

```elm
        dealersPoint =
            Maybe.map toString m.dealersPoint |> Maybe.withDefault "Busted!"
```

操作ボタン群です。
これだけHTMLになってしまいました。

```elm
        buttons =
            case m.status of
                Model.Title ->
                    [ button [ onClick Model.GameStart ]
                        [ text "Start" ]
                    ]

                Model.UsersTurn ->
                    [ button [ onClick Model.Hit ]
                        [ text "Hit" ]
                    , button [ onClick Model.Stand ]
                        [ text "Stand" ]
                    ]

                Model.Over ->
                    [ button [ onClick Model.GameStart ]
                        [ text "Retry" ]
                    ]
```

では後半(in部分)です。

```elm
    in
        div []
            [ p []
                [ text statusText ]
            , div [ hidden <| m.status == Model.Title ]
                [ p []
                    [ text ("Dealer's Card: " ++ dealersCards) ]
                , p []
                    [ text ("Player's Cards: " ++ playersCards) ]
                , p []
                    [ text ("Player's Point: " ++ playersPoint) ]
                , p [ hidden <| m.status /= Model.Over ]
                    [ text ("Dealer's Point: " ++ dealersPoint) ]
                ]
            , div []
                buttons
            ]
```
まあまあコンパクトにまとまりました。 
Modelの状態によっては表示させたくない部分はhiddenを使っています。正しい使い方なのかはよくわかりませんが。

## Main
あとはくっつけるだけです。

```elm
init : ( Model.Model, Cmd Model.Msg )
init =
    (Model.initialModel ! [])


subscriptions : Model.Model -> Sub Model.Msg
subscriptions _ =
    Sub.none


main =
    program { init = init, view = View.view, update = Update.update, subscriptions = subscriptions }
```

## おまけ

ビルドは以下のようにnpm scriptで行うようにしました。 
test書いてないですねすみません・・・
[watchパッケージ](https://www.npmjs.com/package/watch)のおかげでwatchもできます。

```json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "cd src && elm-make Main.elm --yes --output ../main.js",
    "watch": "watch 'npm run build' ./src -p=/elm-stuff/"
  },
```


# 感想など
Viewの書き方これで合ってるんでしょうか・・・ あまり人のコードを読んでいないせいであまり自信がありません。 メインのHTML生成部には余計な式をあまり入れず、スッキリさせたほうがいいんだろうなという気はするのですが・・・
431行も使ってしまいましたが、これはelm-formatのコーディングスタイルの影響も大きいはずです。

あと見た目のやる気がなさすぎるので、もうちょっとグラフィカルにしようと思いました。
せっかくブラウザゲーとして実装してるんだし、せっかくElm使ってるんだし。

ともかく、今度こそ二番煎じ、三番煎じではないはずです！
ご意見、ご指摘等ぜひよろしくお願いします！
ここまでありがとうございました！