from IPython.display import Image
Image(filename="blackjack.jpg")
What we would like to do is determine the best strategy for playing the game of blackjack.
Against a player using basic strategy, the house edge in blackjack is around 1%. That means that for every 100youbet,thecasino′staking1 on average. This is a very slim margin. Can we beat it?
We're going to try different strategies running them over a large sample of randomized trials and determine each strategy's win/loss ratio. Running a large number of randomized trials over a simulation of a real-world problem in order to solve a mathematical or statistical problem is called a Monte Carlo simulation. This method was created and named in the 1940s by Stanislaw Ulam during the Manhattan Project.
# Required libraries
import itertools
import pickle
import time
import pylab
import numpy as np
from collections import OrderedDict
from numpy import random
%matplotlib inline
Below is everything we know about cards. We know the ranks and suits, and how to determine each card's value in blackjack.
values = { 'A': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8,
'9': 9, '10': 10, 'J': 10, 'Q': 10, 'K': 10 }
ranks = list(values.keys())
suits = ['Spades', 'Clubs', 'Diamonds', 'Hearts']
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def value(self):
"""Get the blackjack value of a card. Aces count as ones: other logic
will have to determine whether to count it as a 10 or not."""
return values[self.rank]
def is_ace(self):
return self.rank == "A"
def __str__(self):
return "{rank} of {suit}".format(rank=self.rank, suit=self.suit)
A boot is the stack of cards a blackjack dealer deals from. By default, it's made up of six decks.
class Boot:
def __init__(self, decks=6):
"""Creates a boot -- the place cards are drawn from at a blackjack
table. A boot can be made up of 1 or more decks. Six is the most
common number."""
self.decks = decks
self.cards = [Card(rank, suit) for rank, suit in itertools.product(ranks * decks, suits)]
self.cut_at = int(decks * 52 * (2 / 3))
def draw(self, count=1):
"""Return a random card from the deck. Instead of shuffling the
entire deck ahead of time, we draw a random card, which has the
same result without the limitations of random.shuffle()."""
if count > 1:
return [self.draw() for i in range(count)]
else:
return self.cards.pop(random.randint(0, self.cards_left()))
def cards_left(self):
return len(self.cards)
def almost_empty(self):
"""We are going to reshuffle our boot when it is almost empty."""
return self.cards_left() < self.cut_at
The rules of the game are laid out without having a Player or Dealer class yet. We will have to define those afterward.
Rules of the game:
Hit = "hit"
Stand = "stand"
Double = "double"
class Game:
def __init__(self, boot=None, players=[]):
if boot is None:
boot = Boot()
self.dealer = Dealer("Dealer")
self.players = players
self.boot = boot
def up_card(self):
"""In blackjack, the dealer plays one card face up. We represent this by
returning their first card."""
return self.dealer.cards[0]
def play_round(self):
"""Play one round of Blackjack."""
self.check_boot()
self.reset_players()
self.deal_dealer_hand()
self.deal_player_hands()
self.run_players()
self.run_dealer()
self.record_game()
def check_boot(self):
"""Check to see if the boot needs reshuffling and do it if necessary.
Since we're not dealing with the physical world, we just create a new
boot."""
if self.boot.almost_empty():
self.boot = Boot(decks=self.boot.decks)
for player in self.players:
player.notice_reshuffle()
def reset_players(self):
"""Reset any statuses like doubling."""
for player in self.players:
player.reset()
def deal_dealer_hand(self):
"""Deal two cards to the dealer."""
self.dealer.cards = self.boot.draw(2)
def deal_player_hands(self):
for player in self.players:
self.deal_player_hand(player)
def deal_player_hand(self, player):
"""Deal two cards to the player."""
player.cards = self.boot.draw(2)
def run_players(self):
for player in self.players:
self.run_player(player)
def run_player(self, player):
"""Hit the player with more cards until they stand or bust.
A player who chooses to double can receive no more cards after
they have received the card they doubled on."""
while True:
decision = player.decide(self)
if decision == Stand:
break
elif decision == Double:
player.cards.append(self.boot.draw())
player.doubled = True
break
else:
player.cards.append(self.boot.draw())
def run_dealer(self):
"""Hit the dealer with more cards until they stand or bust."""
self.run_player(self.dealer)
def record_game(self):
"""Record the results of the game."""
cards_played = []
cards_played += self.dealer.cards
for player in self.players:
cards_played += player.cards
self.score_player(player)
for player in self.players:
player.notice_cards(cards_played)
def score_player(self, player):
if player.is_busted():
player.record_loss()
elif self.dealer.is_busted() or player.hand_value() > self.dealer.hand_value():
player.record_win()
elif self.dealer.hand_value() > player.hand_value():
player.record_loss()
# Below here, dealer and player have tied.
# Blackjack beats no blackjack, otherwise, no win or loss.
elif player.is_blackjack() and not self.dealer.is_blackjack():
player.record_win()
elif self.dealer.is_blackjack():
player.record_loss()
else:
player.record_tie()
We have a base Player class that we can inherit from for all our different strategies. We've gone ahead and created a Dealer subclass, as we'll need it for our game.
class Player:
def __init__(self, name):
self.name = name
self.cards = []
self.wins = 0
self.losses = 0
self.bet = 0
self.doubled = False
def bet_amount(self):
return 1
def reset(self):
"""In between rounds, we need to reset the players. When a player
doubles, we record that state, but we will have to reset it before
the next round."""
self.cards = []
self.doubled = False
def record_hand(self, won_loss):
"""Record our winnings or losses.
won_loss is +1 if we won, 0 if we tied, and -1 if we lost."""
bet = self.bet_amount()
if self.doubled:
bet *= 2
self.bet += bet
if won_loss > 0:
if self.is_blackjack():
self.wins += bet * 1.5
else:
self.wins += bet
elif won_loss < 0:
self.losses += bet
def record_win(self):
self.record_hand(1)
def record_loss(self):
self.record_hand(-1)
def record_tie(self):
self.record_hand(0)
def net(self):
"""The net for this player."""
return self.wins - self.losses
def house_edge(self):
"""The most important method here: the edge the house has against this player so far."""
return -self.net() / self.bet
def hand_value(self):
"""Returns the total value of the player's hand according to
the rules of blackjack."""
value = sum(list(card.value() for card in self.cards))
if value < 12 and self.has_aces():
value += 10
return value
def has_aces(self):
"""Does the player's hand contain any aces?"""
return any(card.is_ace() for card in self.cards)
def is_soft(self):
"""A soft hand is one that has an ace that is valued
at 11. This hand cannot be busted by a card draw."""
return self.has_aces() and sum(list(card.value() for card in self.cards)) < 12
def is_busted(self):
return self.hand_value() > 21
def is_blackjack(self):
return len(self.cards) == 2 and self.hand_value() == 21
def decide(self, game):
"""This should be overridden in subclasses. The player should return
Hit, Stand, or Double."""
return Stand
def notice_cards(self, cards):
"""This should be overridden if the player counts cards."""
pass
def notice_reshuffle(self):
"""This should be overridden if the player counts cards."""
pass
class Dealer(Player):
def decide(self, game):
return Hit if self.hand_value() < 17 else Stand
class NoHitPlayer(Player):
def decide(self, game):
return Stand
class AlwaysHitPlayer(Player):
def decide(self, game):
return Hit if self.hand_value() < 21 else Stand
class SimplePlayer(Player):
def decide(self, game):
return Hit if self.hand_value() < 17 else Stand
class BasicStrategyPlayer(Player):
"""https://en.wikipedia.org/wiki/Blackjack#Basic_strategy"""
def dealer_low(self, game):
return 2 <= game.up_card().value() <= 6
def decide_soft(self, game):
upval = game.up_card().value()
if self.hand_value() >= 19:
return Stand
elif self.hand_value() == 18:
if upval == 2 or upval == 7 or upval == 8:
return Stand
elif 3 <= upval <= 6:
return Double
else:
return Stand
elif self.hand_value() == 17:
if 3 <= upval <= 6:
return Double
else:
return Hit
elif self.hand_value() >= 15:
if 4 <= upval <= 6:
return Double
else:
return Hit
else:
if 5 <= upval <= 6:
return Double
else:
return Hit
def decide_hard(self, game):
upval = game.up_card().value()
if self.hand_value() >= 17:
return Stand
elif self.hand_value() >= 13:
if self.dealer_low(game):
return Stand
else:
return Hit
elif self.hand_value() == 12:
if 2 <= upval <= 3:
return Hit
elif self.dealer_low(game):
return Stand
else:
return Hit
elif self.hand_value() == 11:
if upval == 1:
return Hit
else:
return Double
elif self.hand_value() == 10:
if 2 <= upval <= 9:
return Double
else:
return Hit
elif self.hand_value() == 9:
if 3 <= upval <= 6:
return Double
else:
return Hit
def decide(self, game):
if self.is_soft():
return self.decide_soft(game)
else:
return self.decide_hard(game)
class WizardStrategyPlayer(BasicStrategyPlayer):
"""http://wizardofodds.com/games/blackjack/
A very simple and easy to remember strategy. It's not
quite as good as basic blackjack strategy, but is easier
for novice players.
"""
def decide_soft(self, game):
if 13 <= self.hand_value() <= 15:
return Hit
elif 16 <= self.hand_value() <= 18:
if self.dealer_low(game):
return Double
else:
return Hit
else:
return Stand
def decide_hard(self, game):
if self.hand_value() <= 8:
return Hit
elif self.hand_value() == 9:
if self.dealer_low(game):
return Double
else:
return Hit
elif 10 <= self.hand_value() <= 11:
return Double
elif 12 <= self.hand_value() <= 16:
if self.dealer_low(game):
return Stand
else:
return Hit
else:
return Stand
class AceFiveStrategyPlayer(BasicStrategyPlayer):
"""This player counts cards using the Ace/Five Count:
http://wizardofodds.com/games/blackjack/appendix/17/
Every time you see a 5, count +1.
Every time you see an ace, count -1."""
def __init__(self, name):
super(BasicStrategyPlayer, self).__init__(name)
self.count = 0
def bet_amount(self):
if self.count >= 2:
return 2
else:
return 1
def notice_cards(self, cards):
for card in cards:
if card.value() == 5:
self.count += 1
elif card.value() == 1:
self.count -= 1
def notice_reshuffle(self):
self.count = 0
class HiLoStrategyPlayer(BasicStrategyPlayer):
"""This player counts cards using the Hi-Lo Method:
http://wizardofodds.com/games/blackjack/card-counting/high-low/"""
def __init__(self, name):
super(BasicStrategyPlayer, self).__init__(name)
self.count = 0
self.cards_dealt = 0
def true_count(self):
decks_dealt = int(self.cards_dealt / 52)
decks_left = max(6 - decks_dealt, 1)
return int(self.count / decks_left)
def bet_amount(self):
return max(self.true_count(), 1)
def notice_cards(self, cards):
self.cards_dealt += len(cards)
for card in cards:
if 2 <= card.value() <= 6:
self.count += 1
elif card.value() == 10 or card.value == 1:
self.count -= 1
def notice_reshuffle(self):
self.count = 0
self.cards_dealt = 0
def plot_winnings(rounds=100):
game = Game(players=[SimplePlayer("Simple"),
BasicStrategyPlayer("Basic"),
WizardStrategyPlayer("Wizard"),
AceFiveStrategyPlayer("Ace/Five"),
HiLoStrategyPlayer("Hi-Lo")])
nets = {}
for player in game.players:
nets[player.name] = []
for i in range(rounds):
game.play_round()
for player in game.players:
nets[player.name].append(player.net())
for name in nets.keys():
ns = pylab.array(nets[name])
pylab.plot(range(rounds), ns, label=name)
pylab.title("Net Winnings")
pylab.xlabel("Rounds")
pylab.ylabel("Winnings")
pylab.legend()
pylab.show()
for name, player in enumerate(game.players):
print("{name:<12} | Net: {net:<8} Bet: {bet:<8} Edge: {edge:.3%}".format(
name=player.name, net=player.net(), bet=player.bet,
edge=player.house_edge()))
def percentage_below(values, threshold=0.0):
num_below = len(list(filter(lambda x: x <= threshold, values)))
return num_below / len(values)
def edges_histogram(edges, bins=20, range=None):
for idx, name in enumerate(edges.keys()):
pylab.figure()
es = np.array(edges[name])
sd = np.std(es)
avg = np.mean(es)
pylab.hist(edges[name], bins=bins, label=name, range=range)
pylab.title("{} House Edge, Mean: {:.3%}".format(name, avg))
pylab.axvline(0, color="r", alpha=0.5, linewidth=2)
pylab.axvline(avg, color="g", alpha=0.5, linewidth=2)
pylab.axvspan(avg - sd, avg + sd, facecolor="g", alpha=0.3)
ax = pylab.gca()
pylab.text(0.1, 0.9, "{:.1%} break even".format(percentage_below(es)), transform=ax.transAxes)
pylab.show()
def calc_edges(samples=1000, rounds=100):
t0 = time.time()
edges = OrderedDict((("Simple", []),
("Wizard", []),
("Basic", []),
("Ace/Five", []),
("Hi-Lo", [])))
for i in range(samples):
if (i + 1) % 25 == 0:
print(".", end="", flush=True)
if (i + 1) % 500 == 0:
print(i + 1)
game = Game(players=[SimplePlayer("Simple"),
BasicStrategyPlayer("Basic"),
WizardStrategyPlayer("Wizard"),
AceFiveStrategyPlayer("Ace/Five"),
HiLoStrategyPlayer("Hi-Lo")])
for j in range(rounds):
game.play_round()
for player in game.players:
edges[player.name].append(player.house_edge())
print()
t1 = time.time()
print("{} secs execution time".format(t1-t0))
return edges
def dump_edges(edges, filename="edges.dat"):
with open(filename, "wb") as file:
pickle.dump(edges, file)
def load_edges(filename="edges.dat"):
with open(filename, "rb") as file:
return pickle.load(file)
# data generated by:
# edges = calc_edges(samples=500000)
edges = load_edges("big_edges.dat")
edges_histogram(edges)
Teaching Python + data + systems programming starting Jan 12: http://academy.smashingboxes.com/ or http://www.theironyard.com/
This guy <- http://dreisbach.us or @cndreisbach