カードカウンティングを加味したブラックジャックのシミュレーションを行いました。
はじめに
ブラックジャックというカードゲームがあります。カジノなんかでは定番のゲームで昔から根強い人気がありますね。
こういったギャンブルは、確率的にみると胴元が絶対に勝つようにできています。しかし、ブラックジャックにおいてはカードカウンティングを使うことで、勝ち越すことが可能となるらしいです。
今回はpythonで実際に勝てるのかどうか、シミュレータを実装してみました。その紹介です。
ブラックジャックについて
知っている方は読み飛ばしてください。元々ブラックジャックというゲームは還元率がとても高いです。短な賭け事の還元率は大体以下のような数値と言われています。還元率とは、賭けた金額のうち、どの程度配当として返ってくるかを示した数値です。
賭け事 | 還元率 |
ブラックジャック | 99% |
パチンコ | 80~85% |
競馬 | 75~80% |
競輪 | 75% |
宝くじ | 46% |
上記の表を見てもわかるようにブラックジャックはプレイヤーが最適な行動をした場合、99%の還元率が見込めるゲームです。
ブラックジャックでは確率的に最適な行動パターンが決まっていて、ベーシックストラテジーと呼ばれています。ベーシックストラテジーの通りに行動すれば、99%の還元率が見込めます。
つまり、後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割に届くかどうかぎりぎりといった感じでしょうか。
やっぱりカジノは雰囲気を楽しむ場ということですね。