F# で“卒業試験”ことブラックジャック開発をやってみた

トランプゲーム「ブラックジャック」を F# で実装してみました。設計上の判断を述べながら実装をみていきたいと思います。

[実装するブラックジャックのルールはこちらです](https://qiita.com/hirossyi73/items/cf8648c31898216312e5#%E9%96%8B%E7%99%BA%E3%81%99%E3%82%8B%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF%E3%82%B8%E3%83%A3%E3%83%83%E3%82%AF%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%AB)。

実装全体は GitHub で1ファイルにまとめています: [2020-02-09-fsharp-blackjack/Program.fs](https://github.com/vain0x/examples/blob/master/2020-02-09-fsharp-blackjack/Program.fs)

## 文脈

「[プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし](https://qiita.com/hirossyi73/items/cf8648c31898216312e5)」によると、ブラックジャックはプログラミング入門者の卒業試験としてうってつけの題材だそうです。

さまざまな言語による実装例が報告されていますが、F# 版はまだなさそうなので、やってみました。

## カードの型

機能ごとに書いたので、記事も機能ごとに書くことにします。トランプカードの定義からはじめます。

ランク (カードの数字) はブラックジャックにおいて重要です。特に A/JQK/その他 は明確に区別したいので、判別共用体として定義するのがよさそうです。

```fsharp
type Rank =
    | Ace

    /// 2〜10
    | Rank of rank:int

    | Jack

    | Queen

    | King
```

デックを作るときのために、すべての種類のランクを列挙する関数も用意しておきます。(2〜10 のところは for 式を使ってもいいかも。)

```fsharp
let allRanks () =
    [
        Ace
        Rank 2
        Rank 3
        Rank 4
        Rank 5
        Rank 6
        Rank 7
        Rank 8
        Rank 9
        Rank 10
        Jack
        Queen
        King
    ]
```

一方、ランクと違ってスート (カードの柄) はいまのところルール上意味がありません。おそらく string で十分でしょう。F# は型検査があるので、後から変更するのも難しくありません。

```fsharp
type Suit = string

let allSuits () =
    [
        "スペード"
        "クローバー"
        "ハート"
        "ダイヤ"
    ]
```

ランクとスートを用意したので、トランプカードの型を定義できます。ジョーカーは使わないので、カードはランクとスートのペアです。タプルでもいいし、レコードでもいいし、判別共用体でもいいです。今回は私の趣味で、型推論と相性がよさげな判別共用体にしました。

```fsharp
type Card =
    | Card of suit:Suit * rank:Rank
```

全種類のカードを列挙する関数を定義すれば完了です。リスト内包表記は便利ですね。

```fsharp
let allCards () =
    [
        for suit in allSuits () do
            for rank in allRanks () do
                Card (suit, rank)
    ]
```

なお、カードの名前を文字列にする関数は自明なので省略しています。

## デック

次はデック (カードの束) です。デックはシャッフル操作やドロー操作を持つので、動的配列 `ResizeArray<T>` で表しておくと都合がよさそうです。

(.NET 勢に向けていうと、`ResizeArray<T>` は `System.Collections.Generic.List<T>` の F# における別名です。)

```fsharp
type Deck = ResizeArray<Card>
```

すべてのカードを生成してデックに入れ、シャッフルしたものを初期デックとします。シャッフルの実装は省略。(フィッシャーイェーツのアルゴリズム)

```fsharp
let generateDeck () =
    let deck = ResizeArray(allCards ())
    shuffleList deck
    deck
```

カードドローは配列末尾の要素を取り出せばいいです。デックが尽きたケースが気になりますが、現在の仕様では尽きないので、とりあえず `assert` で明記するだけにしておきます。

```fsharp
let drawFromDeck (deck: Deck) =
    // 現在の実装では、カードが尽きることはない。
    assert (deck.Count >= 1)

    let last = deck.Count - 1
    let card = deck.[last]
    deck.RemoveAt(last)

    card
```

## 手札と範囲検査

デックからプレイヤーに配られたカードの集まりを **手札** (hand) と呼ぶことにします。(場札の方がよい？)

手札の型を定義します。(いま書いてて気づきましたが、デックと同じ型になってしまっているので、判別共用体にした方がよかったかもしれません。)

```fsharp
type Hand = ResizeArray<Card>
```

手札の生成やカードの追加は `ResizeArray` のコンストラクタおよび Add メソッドを使うだけですが、別途、関数にしておきます。ゲームの実装時に、「動的配列にカードを加える」のではなく「手札にカードを加える」という表現をしたいからです。手札の型を変更したときのためでもあります。

```fsharp
let newHand (): Hand = ResizeArray()

let addToHand card (yourHand: Hand) =
    yourHand.Add(card)
```

ルールによると、ディーラーの初手は片方が裏向きであり、ディーラーの手番の開始時にそれを公開する部分があります。プレイヤーの手札とちょっと扱いが違う匂いがしますね。実装を考えてみると、初期手札が与えられたとき、裏向きのカードは2枚目なので……

```fsharp
let dealersInitialHandToFacedDownCard (dealerHand: Hand) =
    hand.[1]
```

ところで、**配列のインデックスアクセスはバグの温床** として私の中で有名です。

さきほど、デックからカードをドローする処理を書きましたが、そこでは `Count - 1` という明らかに配列の長さを超えない式を使っていて、`Count >= 1` も関数内でちゃんと確かめています。さすがに大丈夫そうです。

でも `dealersInitialHandToFacedDownCard` は関数を呼ぶ側が気をつけないと範囲外エラーが出ます。 **インデックスアクセス安全性の責任が漏れている** のです。嫌な匂いです。(まあ、こんな小さいプログラムでは実害はないかもしれませんが……)

そういうわけで、「ディーラーの初手」をただの「手札」と区別しておきます。

```fsharp
type DealerHand =
    | DealerHand of facedUp:Card * facedDown:Card
```

裏向きのカードを取る関数は型安全になります。

```fsharp
let dealerHandToFacedDownCard dealerHand =
    match dealerHand with
    | DealerHand (_, card) ->
        card
```

裏向きのカードの公開が終わったら「初手」ではなく、ただの手札になるので、Hand への変換もいります。

```fsharp
let dealerHandToHand dealerHand =
    match dealerHand with
    | DealerHand (card1, card2) ->
        let cards = [card1; card2] // リストの区切りはセミコロンなので注意！！
        ResizeArray(cards)
```

## スコア計算

エースの処理で頭を悩ませがちな点数計算機能です。

仕様上、エースを含む手札のスコアは「エース以外の和を計算した後に、エース1枚につき +11、ただしバストするなら代わりに +1」とします。スコアが複数あるとは考えません。

やや天下りですが、カードの「価値」(Value)という概念を次のように定めます。

- 2〜10 の価値はそれぞれの数字
- J/Q/K の価値は 10
- エースの価値は $α$ (整数ではない定数)

カードの価値の和は $p + qα$ と書けます。($α$ を虚数単位 $i$ みたいに扱って計算。)

実装を見ていきましょう。価値を判別共用体で定義します。

```fsharp
type Value =
    | Value of baseValue:int * aceCount:int

// カードの価値のゼロ
let noValue () =
    Value (0, 0)

// カードの価値の和
let addValue first second =
    match first, second with
    | Value (firstBase, firstAce),
        Value (secondBase, secondAce) ->
        Value (firstBase + secondBase, firstAce + secondAce)
```

カード (ランク) の価値の定義は上に書いたことを F# に翻訳すればよいです。

```fsharp
let rankToValue rank =
    match rank with
    | Ace ->
        Value (0, 1)

    | Rank n ->
        Value (n, 0)

    | Jack
    | Queen
    | King ->
        Value (10, 0)
```

手札の価値はカードの価値の総和で定義されます。

```fsharp
let handToScore (hand: Hand) =
    let mutable value = noValue ()
    for card in hand do
        value <- addValue value (cardToValue card)
    evaluate value
```

さて、手札→価値 ができたので、次は 価値→スコア です。手札の価値は、手札に含まれる「エース以外の和」と「エースの枚数」に分解できるので、最初に決めた仕様どおりの計算を書けます。

```fsharp
let scoreIsBust score =
    score > 21

/// いまのスコアが score のときのエース1枚の価値を評価する。
let evaluateAce score =
    if scoreIsBust (score + 11) then 1 else 11

let evaluate value =
    match value with
    | Value (baseValue, aceCount) ->
        let mutable sum = baseValue
        for _ in 1..aceCount do
            sum <- sum + evaluateAce sum
        sum
```

## 勝敗

いいたいことがだいたい書いたので、あとは駆け足で。

勝敗は判別共用体で表せます。

```fsharp
type GameResult =
    | YouWin
    | YouLose
```

勝敗は互いの手札で判定します。

```fsharp
let handsToGameResult yourHand dealersHand =
    let yourScore = handToScore yourHand
    let dealersScore = handToScore dealersHand

    let youWin =
        scoreIsBust dealersScore || (
            not (scoreIsBust yourScore)
            && yourScore > dealersScore
        )

    if youWin then YouWin else YouLose
```

## ディーラーの思考

ディーラーが取るアクションは「ヒットするか？」という bool でもいいですが、判別共用体にした方が明確な気がします。

```fsharp
type DealerAction =
    | DealerHitAction
    | DealerStandAction

let scoreToDealerAction (score: int) =
    assert (not (scoreIsBust score))

    if score <= 16 then
        DealerHitAction
    else
        DealerStandAction
```

## ゲーム進行

部品が揃いました。ようやくゲームを実装できます。

おそらく F# においてシンプルな方法は、ゲーム上の各場面をそれぞれ関数として定義して、相互再帰することです。例えば、ゲーム開始時に `gameStart` 関数を呼ぶことにします。内容は、デックの生成や初手のディールを行い、次にプレイヤーのアクションフェイズに入る、という流れです。

```fsharp
let gameStart () =
    // 略

    let deck = generateDeck ()

    let yourHand = newHand ()
    youHit deck yourHand
    youHit deck yourHand

    let facedUp = drawFromDeck deck
    let facedDown = drawFromDeck deck
    let dealersHand = DealerHand (facedUp, facedDown)
    // 略

    doYourActionPhase (deck, yourHand, dealersHand)
```

アクションフェイズ (ヒットするか選択するフェイズ) は、ヒットする限り繰り返されます。ここではループではなく再帰を使って「ヒットしたら、またアクションフェイズを行う」という表現にしています。「また」の部分が F# 上で表現されないため、ループになっていることが分かりにくい、というのは欠点かもしれません。

```fsharp
let doYourActionPhase (deck, yourHand, dealersHand) =
    let score = handToScore yourHand
    // 略

    if scoreIsBust score then
        // 略
        gameEnd YouLose
    else

    printfn "ヒットしますか？ (カードを引くなら Y、引かないなら N)"
    if not (confirm ()) then
        doDealerOpenPhase (deck, yourHand, dealersHand)
    else

    youHit deck yourHand
    doYourActionPhase (deck, yourHand, dealersHand)
```

なお if/else の構造が分かりにくいかもしれませんが、これは early リターンの代わりです。F# には `return` 文がないので、early リターンはできませんが、else の中身を字下げしないことで字下げが深くなる問題を回避しています。

```fsharp
    if 条件 then
       真のときの処理
    else
    偽のときの処理
```

ゲームの処理の残りは、ゴリゴリと書いていくだけなので略。`gameEnd` 関数が呼ばれたら終わりです。

```fsharp
let gameEnd result =
    match result with
    | YouWin ->
        printfn "あなたの勝ちです！ おめでとう🎉"

    | YouLose ->
        printfn "あなたの負けです。どんまい♪"

    printfn "ブラックジャック終了！ また遊んでね★"
```

## おわりに

当初の想定より、もりだくさんな内容の記事になりました。たしかに、ブラックジャックの実装は面白い題材のようです。