モンテカルロ法でブラックジャックの最適戦略を作らせてみた

# はじめに
今回は、「ChatGPTにハンズオンを作らせてみた」の第9弾で、強化学習（モンテカルロ法）でブラックジャックの最適戦略を作成しました。

第8弾はこちら↓

https://qiita.com/UKI_datascience/items/477d1527df0ba10f676a

# ブラックジャックのルール
個人的にブラックジャックに詳しくないので、改めてルールの確認です。

```
# 目的
プレイヤーはディーラーと対戦し、手札の合計値を21に近づけることを目指します。
ただし、21を超えると負け（バースト）となります。

# カードの点数
- 2～10：表示されている数字のまま
- 絵札（J,Q,K）：10点
- エース（A）：1点または11点（有利な方が適用される）

# ゲームの流れ
1. ベット（賭け金を置く）
    - ゲーム開始前にプレイヤーはチップを賭けます。
2. カード配布
    - プレイヤーとディーラーにそれぞれ2枚ずつ配布されます。
    - プレイヤーのカードは両方表向き、ディーラーのカードは1枚だけ表向きになります。
3. プレイヤーのアクション
    - プレイヤーは、ディーラーより良い手を作るために以下の選択を行います。:
        1. ヒット（Hit）：もう1枚カードを引く
        2. スタンド（Stand）：これ以上カードを引かず、手札を確定させる
        3. ダブルダウン（Double Down）：最初の賭け金を2倍にして、1枚だけカードを追加する
        4. スプリット（Split）：同じ値のカード2枚を別の手札に分けてプレイする（追加の賭け金が必要）
        5. サレンダー（Surrender）：その時点で降参し、賭け金の半分を返してもらう（一部のルールのみ） 

4. ディーラーのターン
    - プレイヤーのターンが終わると、ディーラーの裏向きのカードを公開します。
    - ディーラーは合計値が17以上になるまでヒットしなければならない（ただし、ソフト17をヒットするかどうかはカジノのルールによる）。
    - 21を超えたらバーストし、プレイヤーが勝利します。
    ※ソフト17: 合計値17のうち、エース（A）を含んでいること

5. 勝敗の決定
    - プレイヤーがバースト：プレイヤーの負け
    - ディーラーがバースト：プレイヤーの勝ち
    - プレイヤーの合計値 ＞ ディーラー：プレイヤーの勝ち
    - ディーラーの合計値 ＞ プレイヤー：プレイヤーの負け
    - 同点：引き分け（賭け金は戻る）

6. ブラックジャックの特別ルール
    - 最初の2枚のカードがAと10点のカード（ブラックジャック）なら、即勝利（通常の勝利よりも高い配当がもらえる）
    - ただし、ディーラーもブラックジャックなら引き分け

# 勝利の配当
- 通常勝利：賭け金と同額の配当
- ブラックジャック勝利：1.5倍の配当
- 引き分け（プッシュ）：賭け金はそのまま戻る
```


# 使用コード・結果
```python
import gym
import numpy as np
import matplotlib.pyplot as plt
from collections import defaultdict
```

```python
env = gym.make('Blackjack-v1', natural=True, sab=True)

# 環境の初期化
state = env.reset()
print("初期状態:", state)

# サンプルエピソード（ランダムな行動）
done = False
while not done:
    action = env.action_space.sample()  # ランダムにヒット or スタンド
    next_state, reward, done, _, _ = env.step(action)
    print(f"行動: {action}, 次の状態: {next_state}, 報酬: {reward}")
```

```python
def random_agent(n_episodes=10000):
    env = gym.make('Blackjack-v1', natural=True, sab=True)
    wins, draws, losses = 0, 0, 0

    for _ in range(n_episodes):
        state, _ = env.reset()
        done = False

        while not done:
            action = env.action_space.sample()  # ランダムに行動
            state, reward, done, _, _ = env.step(action)

        if reward == 1:
            wins += 1
        elif reward == 0:
            draws += 1
        else:
            losses += 1

    win_rate = wins / n_episodes
    print(f"勝率: {win_rate:.3f}, 引き分け: {draws/n_episodes:.3f}, 負け: {losses/n_episodes:.3f}")

random_agent()
```

```
勝率: 0.281, 引き分け: 0.043, 負け: 0.676
```

```python
def monte_carlo_blackjack(n_episodes=500000, gamma=1.0, epsilon=0.1):
    env = gym.make('Blackjack-v1', natural=True, sab=True)
    Q = defaultdict(lambda: np.zeros(env.action_space.n))  # Qテーブル
    returns = defaultdict(list)  # 状態-行動の報酬リスト

    for episode in range(n_episodes):
        state, _ = env.reset()
        episode_data = []  # エピソードの記録

        # 1エピソードの収集
        done = False
        while not done:
            action = np.random.choice([0, 1]) if np.random.rand() < epsilon else np.argmax(Q[state])
            next_state, reward, done, _, _ = env.step(action)
            episode_data.append((state, action, reward))
            state = next_state

        # モンテカルロ法でQ値を更新
        G = 0
        visited = set()
        for state, action, reward in reversed(episode_data):
            G = gamma * G + reward
            if (state, action) not in visited:
                visited.add((state, action))
                returns[(state, action)].append(G)
                Q[state][action] = np.mean(returns[(state, action)])

    return Q

Q_table = monte_carlo_blackjack()
```

```python
def evaluate_policy(Q, n_episodes=10000):
    env = gym.make('Blackjack-v1', natural=True, sab=True)
    wins, draws, losses = 0, 0, 0

    for _ in range(n_episodes):
        state, _ = env.reset()
        done = False

        while not done:
            action = np.argmax(Q[state]) if state in Q else env.action_space.sample()
            state, reward, done, _, _ = env.step(action)

        if reward == 1:
            wins += 1
        elif reward == 0:
            draws += 1
        else:
            losses += 1

    win_rate = wins / n_episodes
    print(f"勝率: {win_rate:.3f}, 引き分け: {draws/n_episodes:.3f}, 負け: {losses/n_episodes:.3f}")

evaluate_policy(Q_table)
```

```
勝率: 0.431, 引き分け: 0.086, 負け: 0.483
```

```python
import pandas as pd
import numpy as np  # np.argmax を使用するために必要

policy_df = pd.DataFrame({(player, dealer): 'スタンド' if np.argmax(Q_table[(player, dealer, False)]) == 0 else 'ヒット'
                             for player in range(12, 22) for dealer in range(1, 11)},
                            index=["推奨行動"]).T

policy_df
```

|   プレイヤーの合計 |   ディーラーのアップカード | 推奨行動   |   プレイヤーの合計 |   ディーラーのアップカード | 推奨行動   |
|-------------------:|---------------------------:|:-----------|-------------------:|---------------------------:|:-----------|
|                 12 |                          1 | ヒット     |                 17 |                          1 | スタンド   |
|                 12 |                          2 | ヒット     |                 17 |                          2 | スタンド   |
|                 12 |                          3 | スタンド   |                 17 |                          3 | スタンド   |
|                 12 |                          4 | ヒット     |                 17 |                          4 | スタンド   |
|                 12 |                          5 | スタンド   |                 17 |                          5 | スタンド   |
|                 12 |                          6 | スタンド   |                 17 |                          6 | スタンド   |
|                 12 |                          7 | ヒット     |                 17 |                          7 | スタンド   |
|                 12 |                          8 | ヒット     |                 17 |                          8 | スタンド   |
|                 12 |                          9 | ヒット     |                 17 |                          9 | スタンド   |
|                 12 |                         10 | ヒット     |                 17 |                         10 | スタンド   |
|                 13 |                          1 | ヒット     |                 18 |                          1 | スタンド   |
|                 13 |                          2 | スタンド   |                 18 |                          2 | スタンド   |
|                 13 |                          3 | スタンド   |                 18 |                          3 | スタンド   |
|                 13 |                          4 | スタンド   |                 18 |                          4 | スタンド   |
|                 13 |                          5 | スタンド   |                 18 |                          5 | スタンド   |
|                 13 |                          6 | スタンド   |                 18 |                          6 | スタンド   |
|                 13 |                          7 | ヒット     |                 18 |                          7 | スタンド   |
|                 13 |                          8 | ヒット     |                 18 |                          8 | スタンド   |
|                 13 |                          9 | ヒット     |                 18 |                          9 | スタンド   |
|                 13 |                         10 | ヒット     |                 18 |                         10 | スタンド   |
|                 14 |                          1 | ヒット     |                 19 |                          1 | スタンド   |
|                 14 |                          2 | スタンド   |                 19 |                          2 | スタンド   |
|                 14 |                          3 | スタンド   |                 19 |                          3 | スタンド   |
|                 14 |                          4 | スタンド   |                 19 |                          4 | スタンド   |
|                 14 |                          5 | スタンド   |                 19 |                          5 | スタンド   |
|                 14 |                          6 | スタンド   |                 19 |                          6 | スタンド   |
|                 14 |                          7 | ヒット     |                 19 |                          7 | スタンド   |
|                 14 |                          8 | ヒット     |                 19 |                          8 | スタンド   |
|                 14 |                          9 | ヒット     |                 19 |                          9 | スタンド   |
|                 14 |                         10 | ヒット     |                 19 |                         10 | スタンド   |
|                 15 |                          1 | ヒット     |                 20 |                          1 | スタンド   |
|                 15 |                          2 | スタンド   |                 20 |                          2 | スタンド   |
|                 15 |                          3 | スタンド   |                 20 |                          3 | スタンド   |
|                 15 |                          4 | スタンド   |                 20 |                          4 | スタンド   |
|                 15 |                          5 | スタンド   |                 20 |                          5 | スタンド   |
|                 15 |                          6 | スタンド   |                 20 |                          6 | スタンド   |
|                 15 |                          7 | ヒット     |                 20 |                          7 | スタンド   |
|                 15 |                          8 | ヒット     |                 20 |                          8 | スタンド   |
|                 15 |                          9 | スタンド   |                 20 |                          9 | スタンド   |
|                 15 |                         10 | スタンド   |                 20 |                         10 | スタンド   |
|                 16 |                          1 | ヒット     |                 21 |                          1 | スタンド   |
|                 16 |                          2 | スタンド   |                 21 |                          2 | スタンド   |
|                 16 |                          3 | スタンド   |                 21 |                          3 | スタンド   |
|                 16 |                          4 | スタンド   |                 21 |                          4 | スタンド   |
|                 16 |                          5 | スタンド   |                 21 |                          5 | スタンド   |
|                 16 |                          6 | スタンド   |                 21 |                          6 | スタンド   |
|                 16 |                          7 | ヒット     |                 21 |                          7 | スタンド   |
|                 16 |                          8 | ヒット     |                 21 |                          8 | スタンド   |
|                 16 |                          9 | ヒット     |                 21 |                          9 | スタンド   |
|                 16 |                         10 | スタンド   |                 21 |                         10 | スタンド   |

# おわりに
ランダムにプレイした時と比べて、モンテカルロ法で学習させてからプレイした時の方が勝率は上がりました。
ブラックジャックになじみがあるわけではないのですが、ベースストラテジーというもの（今回作成した最適戦略の決定版みたいなやつ）があるらしく、それを使うと、勝率49.5%ぐらいになるらしいです。ベースストラテジーを使わない場合は43%ほどになるらしいので、今回のモンテルカルロ法による戦略はそこまで最適戦略とは言えないと思います。

https://ozbet.co/bj_bs_winrate/