ブラックジャックの戦略を強化学習で作ってみる（①ブラックジャックの実装）

# はじめに
Pythonと強化学習の勉強を兼ねて，ブラックジャックの戦略作りをやってみました．
ベーシックストラテジーという確率に基づいた戦略がありますが，それに追いつけるか試してみます．


こんな感じで進めていきます
1. ブラックジャック実装　← 今回はここ
2. [OpenAI gymの環境に登録](https://qiita.com/namachoco/items/3e246ec5df627e6a3c6b)
3. [強化学習でブラックジャックの戦略を学習](https://qiita.com/namachoco/items/c2217ae5f2e649c034a0)

## なぜブラックジャック？
- プログラミングの勉強によさそう（[プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし](https://qiita.com/hirossyi73/items/cf8648c31898216312e5)）
- 学習後の戦略をベーシックストラテジーと比較できる（ベンチマークがある）
- ブラックジャックうまくなりたい


## 開発環境
- Windows 10
- Python 3.6.9
- Anaconda 4.3.0 (64-bit)

## ブラックジャックのルール
ブラックジャックはカジノの中でも人気のテーブルゲームです．
簡単にルールを紹介します．
### 基本ルール
- ディーラー対プレーヤーでカードを配布
- 手札の合計が21点に近い方が勝ち
- 手札の合計点が22点を超えると負け（バースト）
- ポイントの数え方
    - 2～9・・・そのまま2～9点
    - 10と絵札・・・10点
    - A（エース）・・・1点または11点


### 実装するブラックジャックのルール
こんな感じのブラックジャックを作ります．

- トランプ6組を使用
- BET機能あり（ただし額は固定で$100．リターンは強化学習の報酬にします．）
- 初期の所持金は$1000
- Playerの選択肢
    - スタンド（Stand）・・・カードを引かず勝負する
    - ヒット（Hit）・・・もう一枚カードを引く
    - ダブルダウン（Double Down）・・・BETを2倍にしてもう1枚だけカードを引く
    - サレンダー（Surrender）・・・BETの半分を放棄してプレイを降りる
- 実装しないこと
    - スプリット（Split）・・・配られた2枚のカードが同じ数字の場合，はじめのBETと同じ額を追加し2つに分けてプレイする
    - インシュランス（Insurance）・・・ディーラーの表向きカードがAの時，BETの半分を追加して保険をかける
    - ブラックジャック（BlackJack）・・・A+絵札or10の2枚で21点になること


## 実装
コード全体は末尾に記載します．

### Cardクラス
トランプのカードを生成します．
ブラックジャックの特殊な絵札の点数の数え方はここで定義します．
Aも特殊なのですが，ここでは1点として生成します．
Aの点数はHandクラス内で他の手札の点数を考慮して決定します．

```python

class Card:
    '''
    カードを生成
    数字：A，２～１０，J，Q，K
    スート：スペード，ハート，ダイヤ，クラブ
    '''
    SUITS = '♠♥♦♣'
    RANKS = range(1, 14)  # 通常のRank
    SYMBOLS = "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"
    POINTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]  # BlackJack用のポイント

    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank
        self.index = suit + self.SYMBOLS[rank - self.RANKS[0]]
        self.point = self.POINTS[rank - self.RANKS[0]]

    def __repr__(self):
        return self.index
```

### Deckクラス
`NUM_DECK = 6`と定義して，6組のトランプを使ったデッキを生成します．
デッキ生成と同時にシャッフルまでやってしまいます．

```python
class Deck:
    '''
    カードがシャッフルされたデッキ（山札を生成）
    '''
    def __init__(self):
        self.cards = [Card(suit, rank) \
            for suit in Card.SUITS \
            for rank in Card.RANKS]

        if NUM_DECK > 1:
            temp_cards = copy.deepcopy(self.cards)
            for i in range(NUM_DECK - 1):
                self.cards.extend(temp_cards)
        random.shuffle(self.cards)
・・・略
```

### Handクラス
手札へのカード追加やポイントの計算をします．
ソフトハンド（Aを含む手札）の判定も入れました．


```python
class Hand:
    """
    手札クラス
    """
    def __init__(self):
        self.hand = []
        self.is_soft_hand = False

    def add_card(self, card):
        # 手札にカードを加える処理．
        ・・・略

    def check_soft_hand(self):
        # ソフトハンド（Aを含む手札）かチェックする
        ・・・略

    def sum_point(self):
        # 手札のポイントを計算．
        # ソフトハンドなら，Aが1の場合と11の場合，両方計算する
        ・・・略

    def calc_final_point(self):
        # Dealerと勝負するときのポイントを計算する
        # BUSTしていない最終的なポイントを返す
        ・・・略

    def is_bust(self):
        # 手札がBUST（21を超えている）かどうか判定する
        ・・・略

    def deal(self, card):
        # Deal時の処理
        ・・・略

    def hit(self, card):
        # Hit時の処理
        ・・・略

```


### Playerクラス
StandやHitといった選択はここで定義します．
未実装のSplitやInsuranceも追記するならここへ．
おまけで自動プレイモードをつけました．後述の適当AIでプレイできます．

```python
class Player:
    def __init__(self):
        self.hand = Hand()
        self.chip = Chip()
        self.done = False  # Playerのターン終了を示すフラグ
        self.hit_flag = False  # Player が Hit を選択済みかどうか示すフラグ
        self.is_human = False  # True:人がプレイ，False:自動プレイ

    def init_player(self):
        # 手札や各フラグを初期化する
        ・・・略

    def deal(self, card):
        # Stand時の処理
        ・・・略

    def hit(self, card):
        # Hit時の処理
        ・・・略

    def stand(self):
        # Stand時の処理
        ・・・略

    def double_down(self, card):
        # Double down時の処理
        ・・・略

    def surrender(self):
        # Surrender時の処理
        ・・・略
```

### Dealerクラス
Playerクラスと一緒でよかったかもしれません．
ディーラーならではのメソッドが必要かと思って作りましたが特にありませんでした．

### Chipクラス
勝負の後のチップのやり取り．
勝ったら賭けた額の2倍がもらえ，負けたら全額没収．引き分けなら賭けた分がそのままPlayerに戻ります．

```python
class Chip:
    def __init__(self):
        self.balance = INITIAL_CHIP
        self.bet = 0

    def bet_chip(self, bet):
        self.balance -= bet
        self.bet = bet

    def pay_chip_win(self):
        self.balance += self.bet * 2

    def pay_chip_lose(self):
        self.balance = self.balance

    def pay_chip_push(self):
        self.balance += self.bet
```
### AIクラス
自動プレイ用の適当なAIが入っています．おまけ程度．
強化学習をする前のテスト用です．

```python
class AI:
・・・略
    def select_random3(self, hand, n):
        if hand < 11:
            selection = 'h'  # hit
        elif hand == 11 and n == 2:
            selection = 'd'  # double down
        elif hand == 16 and n == 2:
            selection = 'r'  # surrender
        elif hand > 17:
            selection = 's'  # stand
        else:
            r = random.randint(0, 1)
            if r > 0.5:
                selection = 'h'  # hit
            else:
                selection = 's'  # stand

        return selection

```

### Gameクラス
ブラックジャックのメイン機能です．
表示関係のメソッドが混在していて見にくいかも．改善の余地ありです．
player_step関数は，gymのstep関数でも使います．


```python
class Game:
    def __init__(self):
        self.game_mode = 0  # 0:開始待ち，1:ゲーム中, 2:ゲーム終了
        self.deck = Deck()
        self.player = Player()
        self.dealer = Dealer()
        self.judgment = 0  # 1:勝ち，0:引き分け, -1:負け
        self.game_count = 0
        self.start()

        self.message_on = True #self.player.is_human  # True:コンソールにメッセージ表示する，False:コンソールにメッセージ表示しない

    def start(self):
        # デッキをシャッフル，Player, Dealerを初期化
        ・・・略

    def reset_game(self):
        # Player, Dealerの手札をリセット
        ・・・略

    def bet(self, bet):
        # PlayerがBETする
        ・・・略

    def deal(self, n=2):
        # Player, Dealerにカードを配る
        ・・・略

    def player_turn(self):
        # Playerのターン．行動をコンソールから入力する or 適当AIで自動決定する
        ・・・略

    def player_step(self, action):
        # Stand, Hit, Double down, Surrenderに応じた処理
        ・・・略

    def show_card(self):
        # カードを表示．Dealerの伏せられているカードは「？」表示
        ・・・略

    def dealer_turn(self):
        # Dealerのターン．ポイントが17以上になるまでカードを引く
        ・・・略

    def open_dealer(self):
        # Dealerの伏せられていたカードをオープンする
        ・・・略

    def judge(self):
        # 勝敗の判定
        ・・・略

    def pay_chip(self):
        # Chipの精算
        ・・・略

    def check_chip(self):
        # Playerの所持額がMinimum Bet（最低限賭けなければいけない額）を下回っていないかチェック
        # 下回っていたらゲームを終了する
        ・・・略

    def show_judgement(self):
        # 勝敗の結果を表示
        ・・・略

    def ask_next_game(self):
        # ゲームを続けるか尋ねる
        ・・・略

    def check_deck(self):
        # カードの残り枚数をチェックし，少なければシャッフルする
        ・・・略

```

### main関数
main関数にゲームの流れに沿った処理を書きます．
これを実行するとブラックジャックで遊べます．
ちなみに，
・ NUM_DECK = 6  # デッキ数
・ INITIAL_CHIP = 1000  # 初期チップ
・ MINIMUM_BET = 100  # 最低限賭けなければいけない額
としています．

```python
def main():
    game = Game()
    game.start()
    while game.game_mode == 1:
        game.reset_game()       # いろいろをリセットする
        game.bet(bet=100)       # 賭ける
        game.deal()             # カードを配る
        game.player_turn()      # プレイヤーのターン
        game.dealer_turn()      # ディーラーのターン
        game.judge()            # 勝敗の判定
        game.pay_chip()         # チップの精算
        game.check_chip()       # プレイヤーの残額を確認
        game.ask_next_game()    # ゲームを続けるか尋ねる
        game.check_deck()       # 残りカード枚数の確認

    print("BlackJackを終了します")
    print(str(game.game_count) + "回ゲームをしました")

    return game.player.chip, game.game_count

```

## 実行結果

`Hit(h) or Stand(s) or Double down(d) or Surrender(r): `の後に，
キーボードから文字を入力してPlayerの選択を決めます．

```console

$100 賭けました
残りは $900
Playerのターン
Player : [♠A, ♦9] = [10, 20], soft card : True
Dealer : ♣6, ? = 6
Hit(h) or Stand(s) or Double down(d) or Surrender(r): s
Dealerのターン
Player : [♠A, ♦9] = 20
Dealer : [♣6, ♥K] = 16
Dealerのターン
Player : [♠A, ♦9] = 20
Dealer : [♣6, ♥K, ♥3] = 19

Playerの勝ち

Playerの所持チップは $1100
続けますか？ y/n: y
残りカード枚数は 307

$100 賭けました
残りは $1000
Playerのターン
Player : [♠2, ♥K] = [12], soft card : False
Dealer : ♥3, ? = 3
Hit(h) or Stand(s) or Double down(d) or Surrender(r): h
Playerのターン
Player : [♠2, ♥K, ♥2] = [14], soft card : False
Dealer : ♥3, ? = 3
Hit(h) or Stand(s) or Double down(d) or Surrender(r): h
Playerのターン
Player : [♠2, ♥K, ♥2, ♠Q] = [24], soft card : False
Dealer : ♥3, ? = 3
Player BUST

Playerの負け

Playerの所持チップは $1000
続けますか？ y/n: y
残りカード枚数は 301

$100 賭けました
残りは $900
Playerのターン
Player : [♥7, ♥5] = [12], soft card : False
Dealer : ♠8, ? = 8
Hit(h) or Stand(s) or Double down(d) or Surrender(r): d
Playerのターン
Player : [♥7, ♥5, ♠8] = [20], soft card : False
Dealer : ♠8, ? = 8
Double down が選択されました．掛け金を倍にしました
残りは $800
Dealerのターン
Player : [♥7, ♥5, ♠8] = 20
Dealer : [♠8, ♥2] = 10
Dealerのターン
Player : [♥7, ♥5, ♠8] = 20
Dealer : [♠8, ♥2, ♣7] = 17

Playerの勝ち

Playerの所持チップは $1200
続けますか？ y/n: n
残りカード枚数は 295

BlackJackを終了します
3回ゲームをしました
```

## コード
記録用に置いておきます．
<details><summary>以下コード(クリックで展開)</summary><div>

```blackjack.py
import random
import copy


# 定数
NUM_DECK = 6  # デッキ数
NUM_PLAYER = 1  # プレイヤー数

INITIAL_CHIP = 1000  # 初期チップ
MINIMUM_BET = 100


class Card:
    '''
    カードを生成
    数字：A，２～１０，J，Q，K
    スート：スペード，ハート，ダイヤ，クラブ
    '''
    SUITS = '♠♥♦♣'
    RANKS = range(1, 14)  # 通常のRank
    SYMBOLS = "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"
    POINTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]  # BlackJack用のポイント

    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank
        self.index = suit + self.SYMBOLS[rank - self.RANKS[0]]
        self.point = self.POINTS[rank - self.RANKS[0]]

    def __repr__(self):
        return self.index


class Deck:
    '''
    カードがシャッフルされたデッキ（山札を生成）
    '''
    def __init__(self):
        self.cards = [Card(suit, rank) \
            for suit in Card.SUITS \
            for rank in Card.RANKS]

        if NUM_DECK > 1:
            temp_cards = copy.deepcopy(self.cards)
            for i in range(NUM_DECK - 1):
                self.cards.extend(temp_cards)
        random.shuffle(self.cards)

    def draw(self, n=1):
        '''
        デッキから指定した枚数分だけ引く関数
        '''
        cards = self.cards[:n]
        del self.cards[:n]  # 引いたカードを山札から削除
        return cards

    def shuffle(self):
        '''
        デッキをシャッフルする
        '''
        random.shuffle(self.cards)
        return

    def count_cards(self):
        """
        デッキの残り枚数を返す
        """
        count = len(self.cards)
        return count


class Hand:
    """
    手札クラス
    """
    def __init__(self):
        self.hand = []
        self.is_soft_hand = False

    def add_card(self, card):
        self.hand.append(card)

    def check_soft_hand(self):
        """
        ソフトハンド（Aを含むハンド）かチェックする
        """
        hand_list = []
        for i in range(len(self.hand)):
            hand_list.append(self.hand[i].point)
        hand_list.sort()  # 手札を昇順にソート

        if hand_list[0] == 1 and sum(hand_list[1:]) < 11:  # ソフトハンドなら
            self.is_soft_hand = True
        else:
            self.is_soft_hand = False

    def sum_point(self):
        """
        ポイントの合計値を返す
        """
        self.check_soft_hand()
        hand_list = []

        for i in range(len(self.hand)):
            hand_list.append(self.hand[i].point)
        hand_list.sort()  # 手札を昇順にソート
        s1 = 0  # Aを1とカウントする場合の初期値
        for i in range(len(self.hand)):
            s1 += self.hand[i].point

        if self.is_soft_hand == True:  # ソフトハンドなら
            s2 = 11  # 1枚目のAを11としてカウント
            for i in range(len(hand_list)-1):
                s2 += hand_list[i+1]
            s = [s1, s2]
        else:
            s = [s1]

        return s

    def calc_final_point(self):
        """
        BUSTしていない最終的なポイントを返す
        """
        temp_point = self.sum_point()
        if max(temp_point) > 22:
            p = temp_point[0]  # ポイントの大きい方がBUSTなら小さい方
        else:
            p = max(temp_point)
        return p

    def is_bust(self):
        """
        BUSTかどうか判定する
        """
        if min(self.sum_point()) > 21:  # ポイントの小さい方が21を超えていたら
            return True
        else:
            return False

    def deal(self, card):
        """
        Dealされたカードを手札に加える
        """
        for i in range(len(card)):
            self.add_card(card[i])

    def hit(self, card):
        # 1枚ずつHitする
        if len(card) == 1:
            self.add_card(card[0])
        else:
            print("カードの枚数が正しくありません")


class Player:
    def __init__(self):
        self.hand = Hand()
        self.chip = Chip()
        self.done = False  # Playerのターン終了を示すフラグ
        self.hit_flag = False  # Player が Hit を選択済みかどうか示すフラグ
        self.is_human = True  # True:人がプレイ，False:自動プレイ

    def init_player(self):
        self.hand = Hand()
        self.done = False
        self.hit_flag = False

    def deal(self, card):
        self.hand.deal(card)

    def hit(self, card):
        # カードを1枚手札に加える
        self.hand.hit(card)
        self.hit_flag = True

    def stand(self):
        # ターン終了
        self.done = True

    def double_down(self, card):
        # betを2倍にして一度だけHitしてターン終了
        self.chip.balance -= self.chip.bet
        self.chip.bet = self.chip.bet * 2
        self.hand.hit(card)
        self.done = True  # double down後は一度しかhitできないルールとする

    def surrender(self):
        # Betを半分にして（betの半分を手持ちに戻して）ターン終了
        # self.chip.balance += int(self.chip.bet / 2)
        self.chip.bet = int(self.chip.bet / 2)
        self.chip.balance += self.chip.bet
        self.done = True

    def insurance(self):
        # 未実装
        pass

    def split(self):
        # 未実装
        pass


class Dealer:
    def __init__(self):
        self.hand = Hand()

    def init_dealer(self):
        self.hand = Hand()

    def deal(self, card):
        self.hand.deal(card)

    def hit(self, card):
        self.hand.hit(card)


class Chip:
    def __init__(self):
        self.balance = INITIAL_CHIP
        self.bet = 0

    def bet_chip(self, bet):
        # Chipを掛けたら手持ちから減らす
        self.balance -= bet
        self.bet = bet

    def pay_chip_win(self):
        # 勝った時，BETの2倍を得る
        self.balance += self.bet * 2

    def pay_chip_lose(self):
        # 負けた時，BETを全額失う
        self.balance = self.balance

    def pay_chip_push(self):
        # 引き分けのとき，BETした分だけ戻る
        self.balance += self.bet


class AI:
    def select_random1(self):
        r = random.randint(0, 1)
        if r > 0.5:
            selection = 'h'  # hit
        else:
            selection = 's'  # stand

        return selection

    def select_random2(self, hand):
        if hand <= 11:
            selection = 'h'
        else:
            r = random.randint(0, 1)
            if r > 0.5:
                selection = 'h'  # hit
            else:
                selection = 's'  # stand

        return selection

    def select_random3(self, hand, n):
        if hand < 11:
            selection = 'h'  # hit
        elif hand == 11 and n == 2:
            selection = 'd'  # double down
        elif hand == 16 and n == 2:
            selection = 'r'  # surrender
        elif hand > 17:
            selection = 's'  # stand
        else:
            r = random.randint(0, 1)
            if r > 0.5:
                selection = 'h'  # hit
            else:
                selection = 's'  # stand

        return selection


class Game:
    def __init__(self):
        self.game_mode = 0  # 0:開始待ち，1:ゲーム中, 2:ゲーム終了
        self.deck = Deck()
        self.player = Player()
        self.dealer = Dealer()
        self.judgment = 0
        self.game_count = 0
        self.start()

        self.message_on = True #self.player.is_human  # True:コンソールにメッセージ表示する，False:コンソールにメッセージ表示しない

    def start(self):
        self.deck.shuffle()
        self.game_mode = 1
        self.player = Player()
        self.dealer = Dealer()
        self.game_count = 0

    def reset_game(self):
        self.player.init_player()
        self.dealer.init_dealer()
        self.game_count += 1

    def bet(self, bet):
        self.player.chip.bet_chip(bet=bet)
        if self.message_on:
            print("$" + str(self.player.chip.bet) + " 賭けました")
            print("残りは $" + str(self.player.chip.balance))

    # カードを配る
    def deal(self, n=2):
        '''
        カードを配る
        '''
        card = self.deck.draw(n)
        self.player.deal(card)
        # print(self.player.hand.hand)

        card = self.deck.draw(n)
        self.dealer.deal(card)
        # print(self.dealer.hand.hand)

        self.judgment = 0   # 勝敗の判定
        self.player.done = False
        self.show_card()

    # Playerのターン
    def player_turn(self):
        '''
        プレーヤーのターン
        '''
        if self.player.hand.calc_final_point() == 21:  # 合計が21だったらすぐにDealerのターンへ
            self.player.done = True

        while not self.player.done and not self.player.hand.is_bust():
            if self.player.is_human is True:
                action = input("Hit(h) or Stand(s) or Double down(d) or Surrender(r): ")
            elif self.player.is_human is True and self.player.hit_flag:
                action = input("Hit(h) or Stand(s): ")  # Hitの後はhit/standのみ
            else:
                action = AI().select_random3(hand=self.player.hand.calc_final_point(), n=len(self.player.hand.hand))

            self.player_step(action=action)

    def player_step(self, action):
        if action == 'h':  # Hit
            card = self.deck.draw(1)
            self.player.hit(card)
            self.show_card()
            if self.player.hand.calc_final_point() == 21:  # 合計点が21になったらこれ以上Hitはできない
                self.player.done = True
            if self.player.hand.is_bust():
                self.player.done = True
                self.judgment = -1  # PlayerがBUSTしたら即負け
                if self.message_on:
                    print("Player BUST")

        elif action == 's':  # Stand
            self.player.stand()

        elif action == 'd' and self.player.hit_flag is False:  # Double down. Hit選択していない場合に可
            card = self.deck.draw(1)
            if self.player.chip.balance >= self.player.chip.bet:  # 残額が賭けた額以上にあればDouble Down可
                self.player.double_down(card)
                self.show_card()
                if self.message_on:
                    print("Double down が選択されました．掛け金を倍にしました")
                    print("残りは $" + str(self.player.chip.balance))
                if self.player.hand.is_bust():
                    self.player.done = True
                    self.judgment = -1  # PlayerがBUSTしたら即負け
                    if self.message_on:
                        print("Player BUST")
            else:  # 残額が賭けた額未満ならばHitとする
                print("チップが足りないためHitします")
                self.player.hit(card)
                self.show_card()
                if self.player.hand.calc_final_point() == 21:  # 合計点が21になったらこれ以上Hitはできない
                    self.player.done = True
                if self.player.hand.is_bust():
                    self.player.done = True
                    self.judgment = -1  # PlayerがBUSTしたら即負け
                    if self.message_on:
                        print("Player BUST")

        elif action == 'r' and self.player.hit_flag is False:  # Surrender. Hit選択していない場合に可
            self.player.surrender()
            self.judgment = -1  # Surrenderを選択したので負け
            if self.message_on:
                print("Surrender が選択されました")

        else:
            if self.message_on:
                print("正しい選択肢を選んでください")

    def show_card(self):
        '''
        プレーヤーのカードを表示
        '''
        if self.message_on:
            print("Playerのターン")
            print("Player : " + str(self.player.hand.hand) + " = " +
                  str(self.player.hand.sum_point()) + ", soft card : " + str(self.player.hand.is_soft_hand))
            print("Dealer : " + str(self.dealer.hand.hand[0].index) +
                  ", ? = " + str(self.dealer.hand.hand[0].point))
        else:
            pass

    def dealer_turn(self):
        '''
        ディーラーのターン
        '''
        if self.judgment == -1:
            return
        self.open_dealer()
        while self.dealer.hand.calc_final_point() < 17 and self.judgment == 0:
            card = self.deck.draw(1)
            self.dealer.hit(card)
            self.open_dealer()
        if self.dealer.hand.calc_final_point() > 21:
            self.judgment = 1
            if self.message_on:
                print("Dealer BUST")

    def open_dealer(self):
        '''
        hole cardをオープンする
        '''
        if self.message_on:
            print("Dealerのターン")
            print("Player : " + str(self.player.hand.hand) + " = " +
                  str(self.player.hand.calc_final_point()))
            print("Dealer : " + str(self.dealer.hand.hand) + " = " +
                  str(self.dealer.hand.calc_final_point()))
        else:
            pass

    def judge(self):
        '''
        勝敗の判定
        '''
        if self.judgment == 0 and self.player.hand.calc_final_point() > \
                self.dealer.hand.calc_final_point():
            self.judgment = 1
        elif self.judgment == 0 and self.player.hand.calc_final_point() < \
                self.dealer.hand.calc_final_point():
            self.judgment = -1
        elif self.judgment == 0 and self.player.hand.calc_final_point() == \
                self.dealer.hand.calc_final_point():
            self.judgment = 0

        if self.message_on:
            self.show_judgement()

    def pay_chip(self):
        previous_chip = self.player.chip.balance
        if self.judgment == 1:  # Player win
            self.player.chip.pay_chip_win()
        elif self.judgment == -1:  # Player lose
            self.player.chip.pay_chip_lose()
        elif self.judgment == 0:  # Push
            self.player.chip.pay_chip_push()
        if self.message_on:
            print("Playerの所持チップは $" + str(self.player.chip.balance))

        reward = self.player.chip.balance - previous_chip  # このゲームで得た報酬
        return reward

    def check_chip(self):
        if self.player.chip.balance < MINIMUM_BET:
            self.game_mode = 2
            if self.message_on:
                print("チップがMinimum Betを下回ったのでゲームを終了します")

    def show_judgement(self):
        '''
        勝敗の表示
        '''
        if self.message_on:
            print("")
            if self.judgment == 1:
                print("Playerの勝ち")
            elif self.judgment == -1:
                print("Playerの負け")
            elif self.judgment == 0:
                print("引き分け")
            print("")
        else:
            pass

    def ask_next_game(self):
        '''
        ゲームを続けるか尋ねる
        '''
        if self.player.is_human == True:
            while self.game_mode == 1:
                player_input = input("続けますか？ y/n: ")
                if player_input == 'y':
                    break
                elif player_input == 'n':
                    self.game_mode = 2
                    break
                else:
                    print('y/nを入力してください')
        else:
            pass  # 自動プレイなら継続する
        print('残りカード枚数は ' + str(self.deck.count_cards()))
        print("")

    def check_deck(self):
        '''
        カードの残り枚数をチェックし，少なければシャッフルする
        '''
        if self.deck.count_cards() < NUM_PLAYER * 10 + 5:
            self.deck = Deck()
            if self.message_on:
                print("デッキを初期化しました")
                print('残りカード枚数は ' + str(self.deck.count_cards()))
                print("")


def main():
    game = Game()
    game.start()
    while game.game_mode == 1:
        game.reset_game()       # いろいろをリセットする
        game.bet(bet=100)       # 賭ける
        game.deal()             # カードを配る
        game.player_turn()      # プレイヤーのターン
        game.dealer_turn()      # ディーラーのターン
        game.judge()            # 勝敗の判定
        game.pay_chip()         # チップの精算
        game.check_chip()       # プレイヤーの残額を確認
        game.ask_next_game()    # ゲームを続けるか尋ねる
        game.check_deck()       # 残りカード枚数の確認

    print("BlackJackを終了します")
    print(str(game.game_count) + "回ゲームをしました")

    return game.player.chip, game.game_count


if __name__ == '__main__':
    main()


```
</div>

## 終わりに
Pythonでブラックジャックを作ってみました．
ベーシックストラテジーになるべく近づけるため，Double downとSurrenderの実装にも挑戦してみました．
Splitもいつか入れたいです．
学習することを想定してBET機能を入れましたが，所持金がゼロなのにDouble downしてしまうなど，
バグが多々発見され，修正が大変でした．（まだバグがあったらご指摘ください）

次は今回作ったブラックジャックを自作の環境として, OpenAI のgymに登録します．
[ブラックジャックの戦略を強化学習で作ってみる（②gymに環境を登録）](https://qiita.com/namachoco/items/3e246ec5df627e6a3c6b)


## 参考にさせていただいたサイト
- [Pythonでブラックジャック作ったよ](https://qiita.com/fantm21/items/de109f2ebf147679c12d)
- [プログラミング入門者からの卒業試験は『ブラックジャック』を開発すべし](https://qiita.com/hirossyi73/items/cf8648c31898216312e5)
- [ブラックジャック基本用語集](http://blackjack.ooban-koban.com/03/0005.html#deal)