pythonでブラックジャック【カードカウンティング】

カードカウンティングを加味したブラックジャックのシミュレーションを行いました。

はじめに

ブラックジャックというカードゲームがあります。カジノなんかでは定番のゲームで昔から根強い人気がありますね。

こういったギャンブルは、確率的にみると胴元が絶対に勝つようにできています。しかし、ブラックジャックにおいてはカードカウンティングを使うことで、勝ち越すことが可能となるらしいです。

今回はpythonで実際に勝てるのかどうか、シミュレータを実装してみました。その紹介です。

ブラックジャックについて

知っている方は読み飛ばしてください。元々ブラックジャックというゲームは還元率がとても高いです。短な賭け事の還元率は大体以下のような数値と言われています。還元率とは、賭けた金額のうち、どの程度配当として返ってくるかを示した数値です。

賭け事還元率
ブラックジャック99%
パチンコ80~85%
競馬75~80%
競輪75%
宝くじ46%

上記の表を見てもわかるようにブラックジャックはプレイヤーが最適な行動をした場合、99%の還元率が見込めるゲームです。

ブラックジャックでは確率的に最適な行動パターンが決まっていて、ベーシックストラテジーと呼ばれています。ベーシックストラテジーの通りに行動すれば、99%の還元率が見込めます。

引用元 : https://slotsia.com/ja/game-blackjack/basic-strategy

つまり、後1.1%どうにか足してあげることで恒常的に勝つことができるはずです。そのための手法が今回の検証でもあるカードカウンティングです。これはデッキ内のカードの分布を把握して、プレイヤーに有利なタイミングで掛け金を増やすという手法です。勝ちやすい時に多めに賭けるという考えのもと生まれた手法です。

ただし実際のカジノでは、このカードカウンティングは禁止されていて、バレると出禁になったり、勝ち分を没収されたりするみたいです。

まりカジノ側が嫌がるということは、裏返せば勝てる手法であるとも言えます。

シミュレーション

以下のような条件のもと今回は実装しました。

実施条件

  • 賭け金が 0 または 2倍 になったら終了
  • 所持金は$1,000、賭け金は$10
  • デッキは4デッキ
  • 1/4以下になった時に3デッキ補充する
  • 上記の条件で100回検証する

コードはこちらのサイトのコードを参考にし、フォークしました。ありがとうございます。

コード

import random
# Constants
SUITS = ('♡', '◇', '♠', '♣')
RANKS = ('A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K')
CARD_VAL = {'2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, '10':10, 'J':10, 'Q':10, 'K':10, 'A':11}
CARD_COUNT = True

class Card(object):
    def __init__(self, suit, rank):
        self.suit = suit
        self.rank = rank
        
    def __str__(self):
        return  self.suit + ' ' + self.rank
    
class Deck(object):
    def __init__(self):
        self.deck = [] 
        self.add_cards(n=4)
        self.count = 0

    def add_cards(self, n):
        for i in range(n):
          for suit in SUITS:
              for rank in RANKS:
                  self.deck.append(Card(suit,rank))
                
    def shuffle(self):
        random.shuffle(self.deck)

    def card_counting(self, card):
        card_value = CARD_VAL[card.rank]
        if card_value <= 6:
            self.count += 1
        
        elif card_value >= 10:
          self.count -= 1
        
    def deal(self):
        if len(self.deck) < 54:
            self.add_cards(n=3)
            self.shuffle()

        dealt_card = self.deck.pop()
        self.card_counting(dealt_card)
        return dealt_card
    
class Hand(object):
    def __init__(self):
        self.cards = []
        self.value = 0
        self.aces = 0
    
    def add_card(self, card):
        self.cards.append(card)
        self.value += CARD_VAL[card.rank]
        if card.rank == 'A':
            self.aces += 1
    
    def adjust_for_ace(self):
        while self.value > 21 and self.aces:
            self.value -= 10
            self.aces -= 1
            
class Money(object):
    def __init__(self, total):
        self.player_money = total
        self.bet = 0
        self.history = []
        self.set_history()

    def set_history(self):
        self.history.append(self.player_money)
        
    def player_win(self):
        self.player_money += self.bet
        self.set_history()
    
    def player_lose(self):
        self.player_money -= self.bet
        self.set_history()

    def player_bj(self):
        self.player_money += self.bet * 1.5
        self.set_history()

    def dealer_bj(self):
        self.player_money -= self.bet
        self.set_history()

        
def take_bet(money, deck, bet=10):
    if CARD_COUNT:
        if deck.count > 8:
            bet = bet*5

        elif deck.count > 5:
            bet = bet*3

    money.bet = bet
    if money.bet > money.player_money:
        money.bet = money.player_money
    
    
def hit(deck, hand):
    hand.add_card(deck.deal())
    hand.adjust_for_ace()


def double(deck, hand, money):
    print('ダブルしました。')
    hand.add_card(deck.deal())
    hand.adjust_for_ace()
    money.bet *= 2
    

def show_cards_first(player, dealer):
    print("\nディーラー:")
    print('', dealer.cards[0])  
    print(" ??")
    print("\nプレーヤー:", *player.cards, sep='\n ')
    

def show_cards_last(player, dealer):
    print("\nディーラー:", *dealer.cards, sep='\n ')
    print("ディーラーの合計:", dealer.value)
    print("\nプレーヤー:", *player.cards, sep='\n ')
    print("プレーヤーの合計", player.value)
    

def player_busts(player, dealer, money):
    print("プレーヤーがバーストしました。")
    money.player_lose()
def player_wins(player, dealer, money):
    print("プレーヤーが勝ちました。")
    money.player_win()
def dealer_busts(player, dealer, money):
    print("ディーラーがバーストしました。")
    money.player_win()
def dealer_wins(player, dealer, money):
    print("ディーラーが勝ちました。")
    money.player_lose()
def player_bj(player, dealer, money):
    print("プレーヤーがBJ。")
    money.player_bj()
def dealer_bj(player, dealer, money):
    print("ディーラーがBJ。")
    money.dealer_bj()
def push(player, dealer, money):
    print("プッシュです。")
    money.set_history()

def basic_strategy(deck, player, dealer, money):
    player_turn = True
    dealer_value = CARD_VAL[dealer.cards[0].rank]

    while player_turn: 
        if player.value <= 8:
            hit(deck, player)

        elif player.value <= 9 and len(player.cards)==2 and dealer_value>=3 and dealer_value<=6:
            double(deck, player, money)
            player_turn = False
        
        elif player.value <= 9:
            hit(deck, player)
        
        elif player.value <= 10 and len(player.cards)==2 and dealer_value<=9:
            double(deck, player, money)
            player_turn = False
        
        elif player.value <= 10:
            hit(deck, player)

        elif player.value <= 11 and len(player.cards)==2 and dealer_value<=10:
            double(deck, player, money)
            player_turn = False
        
        elif player.value <= 11:
            hit(deck, player)

        elif player.value <= 12 and dealer_value>=4 and dealer_value<=6:
            player_turn = False
          
        elif player.value <= 12:
            hit(deck, player)

        elif player.value <= 16 and dealer_value<=6:
            player_turn = False
          
        elif player.value <= 16:
            hit(deck, player)

        else:
            player_turn = False

def main():
    player_turn = True
    fundage = 1000
    count_game = 0
    count_win = 0
    count_tie = 0
    money = Money(fundage)  
    deck = Deck()
    deck.shuffle()

    while True:
        count_game += 1
        print('\nカードをシャッフルします。')
        
        # 賭ける
        print('カードカウント: ', deck.count)
        take_bet(money, deck, 10)
        
        print('\nカードを配ります。')
        player_hand = Hand()
        player_hand.add_card(deck.deal())
        player_hand.add_card(deck.deal())
        
        dealer_hand = Hand()
        dealer_hand.add_card(deck.deal())
        dealer_hand.add_card(deck.deal())

        # BJの処理
        if player_hand.value == 21 and dealer_hand.value != 21:
            player_bj(player_hand, dealer_hand, money)
            count_win += 1

        elif player_hand.value != 21 and dealer_hand.value == 21:
            dealer_bj(player_hand, dealer_hand, money)
          
        elif player_hand.value == 21 and dealer_hand.value == 21:
            push(player_hand, dealer_hand, money)
            count_tie += 1
        
        else:
            print('\nプレーヤーのターンです。')
            # ベーシックストラテジー
            basic_strategy(deck, player_hand, dealer_hand, money)
            show_cards_first(player_hand, dealer_hand)

            if player_hand.value > 21:
                player_busts(player_hand, dealer_hand, money)
            
            # プレーヤーがバーストしなければディーラーのターン。        
            if player_hand.value <= 21:
                print('\nディーラーのターンです。')
                
                # ディーラーは17以上にならなければカードを引き続ける。
                while dealer_hand.value < 17:
                    hit(deck, dealer_hand)
                    
                # カードを見せる。
                show_cards_last(player_hand,dealer_hand) 
                # ディーラがバースト
                if dealer_hand.value > 21:
                    dealer_busts(player_hand, dealer_hand, money)
                    count_win += 1
                # ディーラーの勝ち
                elif dealer_hand.value > player_hand.value:
                    dealer_wins(player_hand, dealer_hand, money)
                # プレーヤーの勝ち
                elif dealer_hand.value < player_hand.value:
                    player_wins(player_hand, dealer_hand, money)
                    count_win += 1
                # 引き分け
                else:
                    push(player_hand, dealer_hand, money)
                    count_tie += 1
        
        # プレーヤー破産でゲーム終了
        if money.player_money <= 0:
            print('プレーヤーは破産しました。')
            break
        # プレイヤーの勝ちでゲーム終了
        if money.player_money >= fundage*2:
            print('プレーヤーの資産が目標値に到達しました。')
            break
        # 残金の表示    
        print(f"\nプレーヤーの残金は、{money.player_money}です。")
    
    # 勝率の表示
    print('\nあなたの対戦数は{}でした。'.format(count_game))
    print('\nあなたの勝率は{:.0%}でした。'.format((count_win + 0.5*count_tie) / count_game))
    make_plot_figure(money)

def make_plot_figure(money):
    import numpy as np
    import matplotlib.pyplot as plt
    import japanize_matplotlib
 
    # 折れ線グラフを出力
    left = np.array(list(range(len(money.history))))
    height = np.array(money.history)
    plt.plot(left, height,)
    plt.title("BJ シミュレータ")
    plt.xlabel("試行回数")
    plt.ylabel("所持金 ($)")
    plt.grid(True)
    plt.show()

if __name__ == '__main__':
    main()

結果

100回試した結果、2倍になったのは27回で勝率は27%でした。

今回は賭け金を手持ちの1%、カウント5以上の時に賭け金を2倍、カウント8以上の時に賭け金を3倍にしました。ここらへんの最適な値も求められそうですね。

比較のためにカウンティングを行わなかった場合、ベーシックストラテジーのみで立ち向かった場合もカウントしてみたところ、所持金が2倍になったのは一回だけで勝率は1%でした。

今回の条件でカードカウンティングを行う3割程度の勝率が見込めました。また、後から知ったのですが A が絡むバージョンのベーシックストラテジーもあるみたいで、ここを考慮するともう少し勝率がのびるのかな?といった感じですね。

おわりに

以前から気になっていた、カードカウンティングをするとどの程度勝てるの?ということをシミュレーションしていきました。

今回の結果では、ベーシックストラテジーだけよりは勝てるようになりますが絶対に勝てるようになるわけではない。といった結果でした。

賭け方や、ベーシックストラテジーを見直せばもう少し勝率は上がりそうですが、それでも勝率は5割に届くかどうかぎりぎりといった感じでしょうか。

やっぱりカジノは雰囲気を楽しむ場ということですね。

おすすめの記事