Let’s Play Blackjack (with Python) | by Tony Yiu | Towards Data Science

This post is in NO way an attempt to promote blackjack or the act of gambling. Any time you gamble at a casino, the odds are stacked against you — and over time you WILL lose money. Don’t risk what you cannot afford to lose!
Corrections: I realized (thanks to a friendly tip from @DonBeham) that under certain conditions, my total_up function was handling multiple aces incorrectly. I have updated both the code below and on my GitHub. My apologies!
I haven’t coded anything for the blog lately so I wanted to do a programming related post. One of the classic applications of probability and statistics is the study of games of chance (gambling). Games of chance (card games, dice games, etc.) are a favorite of statisticians because they exhibit both randomness and a certain inevitability:
Random in that you don’t know what the result will be game to game.
And inevitable in that you know what the average result of a large number of games will be.
Today, we will study blackjack by writing up a blackjack simulator in Python, simulating a bunch of games, and then studying how our player did. I will assume some basic familiarity with the game of Blackjack, but here is a quick refresher for how the game is played:
Players make their bets.Players are dealt 2 cards.Dealer is dealt 2 cards where the second card is hidden from the players.The objective of the game is to have a higher point total than the dealer (but no more than 21, anything over 21 is an automatic loss called a bust) — if you beat the dealer in this way, you win from the casino what you bet (you also win if the dealer busts). Aces can be worth either 1 or 11; every other card is worth its face amount (face cards are worth 10).An initial 2 card hand composed of an ace and a face card is called a blackjack and is the best possible hand.After the first round of dealing, each player has the option to hit (receive more cards) or stay (no more cards). If hitting results in the player busting (total going over 21), then his or her bet is lost.After all the players are done hitting/staying, the dealer flips over his hidden card. If the dealer’s total is less than 17, then he or she needs to hit (receive a new card). This process repeats until the dealer’s hand either totals to 17 or more or busts (goes above 21).After the dealer is done, the final results are decided — if the dealer busts, then any player who did not bust earlier wins his or her bet. If the dealer does not bust, then the dealer’s total is compared to each player’s. For any player whose total is greater than the dealer’s, he or she wins money (in the amount that was bet). For any player whose total is less than the dealer’s, he or she loses money. No money is exchanged in the event of a tie.
Players make their bets.
Players are dealt 2 cards.
Dealer is dealt 2 cards where the second card is hidden from the players.
The objective of the game is to have a higher point total than the dealer (but no more than 21, anything over 21 is an automatic loss called a bust) — if you beat the dealer in this way, you win from the casino what you bet (you also win if the dealer busts). Aces can be worth either 1 or 11; every other card is worth its face amount (face cards are worth 10).
An initial 2 card hand composed of an ace and a face card is called a blackjack and is the best possible hand.
After the first round of dealing, each player has the option to hit (receive more cards) or stay (no more cards). If hitting results in the player busting (total going over 21), then his or her bet is lost.
After all the players are done hitting/staying, the dealer flips over his hidden card. If the dealer’s total is less than 17, then he or she needs to hit (receive a new card). This process repeats until the dealer’s hand either totals to 17 or more or busts (goes above 21).
After the dealer is done, the final results are decided — if the dealer busts, then any player who did not bust earlier wins his or her bet. If the dealer does not bust, then the dealer’s total is compared to each player’s. For any player whose total is greater than the dealer’s, he or she wins money (in the amount that was bet). For any player whose total is less than the dealer’s, he or she loses money. No money is exchanged in the event of a tie.
If you would like to read up more on blackjack rules, check out this website. Time to start coding!
You can find the code in its entirety on my GitHub here.
It probably would have been a good idea to use object oriented programming. But at this point, I am not yet used to writing code in that way. I will probably revise my code at some point in the future to be object oriented; but that is a project for another day.
To start, let’s get our input statements out of the way:
import numpy as npimport pandas as pdimport randomimport matplotlib.pyplot as pltimport seaborn as sns
Now let’s make a few functions to help us out. First, we need a function that creates a new deck of cards for us to play with. The make_decks function does just that — it adds four of each card type (ace, 2, 3, 4, etc.) to the list new_deck, shuffles new_deck, and returns the newly created list (deck) for us to play with. Also note that we can specify via num_decks how many decks we want the function to create.
# Make a deckdef make_decks(num_decks, card_types):    new_deck = []    for i in range(num_decks):        for j in range(4):            new_deck.extend(card_types)    random.shuffle(new_deck)    return new_deck
We also need a function that can add up the value of the cards in our hand. It is slightly more complicated than simple summation because aces can be worth either 1 or 11, depending on whichever is most advantageous to its holder. So our function first tallies up the value of each non-ace card in the hand (I represent all face cards with the number 10 as they are all functionally the same in blackjack). Then it counts up the number of aces. Finally, it determines how much each ace should be worth depending on the value of the rest of your cards.
Corrections: In my previous version of the code below, there was an error. To fix it, I added the helper function ace_values which takes as input the number of aces in your hand as an integer and outputs a list of unique values that your aces could be worth. Figuring out the permutations (and their sums) for a given number of aces was more work than I thought and I needed to write the following 2 helper functions to get it done (fore more details, please refer to my comments in the following code block):
# This function lists out all permutations of ace values in the# array sum_array.# For example, if you have 2 aces, there are 4 permutations:#     [[1,1], [1,11], [11,1], [11,11]]# These permutations lead to 3 unique sums: [2, 12, 22]# Of these 3, only 2 are <=21 so they are returned: [2, 12]def get_ace_values(temp_list):    sum_array = np.zeros((2**len(temp_list), len(temp_list)))    # This loop gets the permutations    for i in range(len(temp_list)):        n = len(temp_list) - i        half_len = int(2**n * 0.5)        for rep in range(int(sum_array.shape[0]/half_len/2)):            sum_array[rep*2**n : rep*2**n+half_len, i]=1            sum_array[rep*2**n+half_len : rep*2**n+half_len*2, i]=11    # Only return values that are valid (<=21)    return list(set([int(s) for s in np.sum(sum_array, axis=1)\                     if s<=21]))# Convert num_aces, an int to a list of lists# For example if num_aces=2, the output should be [[1,11],[1,11]]# I require this format for the get_ace_values functiondef ace_values(num_aces):    temp_list = []    for i in range(num_aces):        temp_list.append([1,11])    return get_ace_values(temp_list)
The above two functions can now be used by the function total_up, which like I mentioned above, calculates the value of the cards in our hand (including the correct handling of any aces):
# Total up value of handdef total_up(hand):    aces = 0    total = 0        for card in hand:        if card != 'A':            total += card        else:            aces += 1        # Call function ace_values to produce list of possible values    # for aces in hand    ace_value_list = ace_values(aces)    final_totals = [i+total for i in ace_value_list if i+total<=21]        if final_totals == []:        return min(ace_value_list) + total    else:        return max(final_totals)
Now that our helper functions are out of the way, let’s move on to the main loop. First, I define my key variables:
stacks is the number of card stacks (where each card stack can be one or more decks) we will simulate.
players is the number of players in each simulated game.
num_decks is the number of decks in each stack.
card_types is a list of all 13 card types.
stacks = 50000players = 1num_decks = 1card_types = ['A',2,3,4,5,6,7,8,9,10,10,10,10]
Now begins the main loops of our simulator. There are two:
A for loop that iterates through the 50,000 stacks of cards that we want to simulate.A while loop, that for each stack of cards, plays blackjack until there are 20 or fewer cards in the stack. At that point it moves onto the next stack.
A for loop that iterates through the 50,000 stacks of cards that we want to simulate.
A while loop, that for each stack of cards, plays blackjack until there are 20 or fewer cards in the stack. At that point it moves onto the next stack.
The numpy array, curr_player_results, is an important variable where I store the game result for each player — 1 for a win, 0 for a tie, and -1 for a loss. Each element in this array corresponds to one player at the blackjack table.
Within the while loop, we deal a card to each player and then the dealer (where the Python comment says “Deal FIRST card”), and then we do it again so that everyone has 2 cards. In order to deal the cards, I take advantage of the pop function with an input of 0 — this returns the first element of a list while simultaneously removing it from that list (perfect for dealing cards from a stack). When the number of cards left in the dealer’s stack drops to 20 or below, a new stack of cards is used to replace the old one (moves to the next iteration of our for loop).
for stack in range(stacks):    blackjack = set(['A',10])    dealer_cards = make_decks(num_decks, card_types)    while len(dealer_cards) > 20:                curr_player_results = np.zeros((1,players))                dealer_hand = []        player_hands = [[] for player in range(players)]        # Deal FIRST card        for player, hand in enumerate(player_hands):            player_hands[player].append(dealer_cards.pop(0))        dealer_hand.append(dealer_cards.pop(0))        # Deal SECOND card        for player, hand in enumerate(player_hands):            player_hands[player].append(dealer_cards.pop(0))        dealer_hand.append(dealer_cards.pop(0))
Next the dealer checks if he or she has a blackjack (an ace and a 10). Notice that in the previous code block, I defined blackjack as a set that includes an ace and a 10.
If the dealer has a blackjack, then the players lose (and get assigned a -1 in curr_player_results) unless they too have a blackjack (in which case it is a tie).
        # Dealer checks for 21        if set(dealer_hand) == blackjack:            for player in range(players):                if set(player_hands[player]) != blackjack:                    curr_player_results[0,player] = -1                else:                    curr_player_results[0,player] = 0
If the dealer does not have a blackjack, then play continues. The players do their own blackjack checks — if they have one, they win (in some casinos blackjack pays 1.5 to 1, in other words if your bet was $100 then you win $150). I record a win by setting the element corresponding to that player in the array curr_player_results to 1.
For the players without blackjack, they now have the option to hit, stay, etc. For this simulation, my objective was to capture all kinds of player decisions — smart ones, lucky ones, and stupid ones. So I based the decision for the player on a coin flip (if random.random() generates a value higher than 0.5 the player hits, otherwise the player stays).
Deciding via coin flip might sound silly, but by making the hit/stay decision uncorrelated to what is actually happening in the game, we can observe all types of situations and ultimately generate a rich dataset for analysis.
I am not trying to figure out the optimal strategy right this instance. Rather, I want to use this simulator to generate training data, with which I can eventually train a neural network to play blackjack optimally (in a future post).
For Python, the \ character denotes line continuation and can be used to format extra long lines of code for better readability — you will see me use it in the following code block.
        else:            for player in range(players):                # Players check for 21                if set(player_hands[player]) == blackjack:                    curr_player_results[0,player] = 1                else:                    # Hit randomly, check for busts                    while (random.random() >= 0.5) and \                    (total_up(player_hands[player]) <= 11):                        player_hands[player].append]                        (dealer_cards.pop(0))                        if total_up(player_hands[player]) > 21:                            curr_player_results[0,player] = -1                            break
In the final section of our loop (almost there!), it’s the dealer’s turn. The dealer must hit until either he or she busts or has a hand that sums to at least 17. So the while loop deals cards to the dealer until 17 is reached, then we check if our dealer busted. If a dealer bust occurs, then every player that has not already lost (via busting) wins and we record a 1 for them in curr_player_results.
        # Dealer hits based on the rules        while total_up(dealer_hand) < 17:            dealer_hand.append(dealer_cards.pop(0))        # Compare dealer hand to players hand         # but first check if dealer busted        if total_up(dealer_hand) > 21:            for player in range(players):                if curr_player_results[0,player] != -1:                    curr_player_results[0,player] = 1
If the dealer does not bust, then each player compares his or her hand to the dealer’s — the higher hand wins.
        else:            for player in range(players):                if total_up(player_hands[player]) > \                total_up(dealer_hand):                    if total_up(player_hands[player]) <= 21:                        curr_player_results[0,player] = 1                elif total_up(player_hands[player]) == \                total_up(dealer_hand):                    curr_player_results[0,player] = 0                else:                    curr_player_results[0,player] = -1
Finally, at the conclusion of each blackjack game, we append the game results along with other variables that we care about to the lists that we will use to track our overall simulation results:
        # Track features        dealer_card_feature.append(dealer_hand[0])        player_card_feature.append(player_hands)        player_results.append(list(curr_player_results[0]))
Nice, we can examine some results now. I ran the simulator for 50,000 decks. With this many decks being used, there ended up being:
312,459 blackjack games played.
The player lost in 199,403 of the games (64% of the time).
The player won in 99,324 of the games (32% of the time).
The player tied in 13,732 of the games (4% of the time).
We can take a look at how the win/tie probability (the probability of not losing money to the casino) changes due to key observable factors. For example, here is the win/tie probability for all the possible dealer cards (recall that players can only see one of the dealer’s cards when they are deciding what to do):
From 2 to 6, the probability of win/tie increases. But then after 6, the probability declines dramatically. That’s interesting. Let’s think about why this occurs:
If the dealer is showing a low card, then all things equal it is more likely that he has a lower total hand value — which is easier for the players to beat. This partially explains why the probabilities from 2 to 6 are on average higher than those from 7 to ace.
Also, remember the rule for the dealer — if his total is less than 17, he must hit. And if he must hit, there is a decent chance that he will bust. This explains why the probability increases from 2 to 6. Think of it this way — what’s the most common card value? It’s 10 because there are 16 of them per each 52 card deck (four 10 cards, Jacks, Queens, and Kings). So if the dealer is showing a 6, (assuming we are not card counting) the most likely preliminary total for our dealer is 16. Since 16 is less than 17, he must hit. And there are many cards that could cause him to bust — anything worth 6 or more. The same logic holds true if the dealer is showing a 5, just that there is one fewer card that would cause him to bust (has to be 7 or more now).
Now let’s think what about what happens when the dealer is showing a 7. In this case, odds are that the other hidden card is worth 10. Then the players who have 16 or less will feel compelled to hit. If they don’t, the probability of losing is material. But if they do hit, then they face a high probability of busting (due to all the 10s).
That, in a nutshell, is the advantage that casinos hold over blackjack players — by hiding one of the dealer’s cards while the players act (and forcing the players to act before the dealer), casinos force blackjack players to assume the worst and expose themselves to the risk of busting (which are significant).
So if you are at a casino and the hand you are dealt totals somewhere between 12 and 16, good luck to you — because you have become the casino’s mark and the odds are now stacked heavily against you. Let’s take a look at how the player’s initial hand value (the value of his or her initial two cards) affects his or her win/tie probability:
As expected, the win/tie probability is lowest for initial player hand values between 12 and 16. These are the players that are frequently forced into the lose-lose situation of “if I hit I bust, if I stay I lose”. It also makes sense why initial player hand values of 4 and 5 have the next lowest probabilities. Let’s say you have a 5, then you need to hit (there is no reason not to) — but if you do hit, then the most likely result is that your hand now totals 15. And now you’ve fallen into the same dilemma as the players whose initial hands totaled between 12 and 16 (where if you hit, your next card is likely to cause you to bust).
Now that we’ve explored the risks of blackjack, what’s next? In an upcoming post, I will use the training data that I generated above to train a neural network to play blackjack. That way I can examine what the machine chooses as the optimal strategy.
But for today, let’s see if we can use a simple heuristic to improve our odds. Recall two things:
A major disadvantage that players face is that they are forced to act first (and face the risk of busting before the dealer). So the casino’s strategy is to force the players to act under uncertainty in hopes that they will hit and bust.
The player in our simulator chooses to hit or stay based on a coin flip regardless of the value of his or her hand (unless he or she is at 21). So even if he or she is at 20, there is still a 50% chance of hitting.
So let’s see whether we can improve our odds merely by choosing to hit only when we know there is zero chance of busting. So instead of a coin flip, our new decision rule is to keep hitting only if our total hand value is 11 or less.
It’s not the optimal strategy I know, but it is simple. And because it prevents us from ever busting, we have effectively shifted the risk of busting from ourselves to the dealer/casino.
The following chart compares our new “smart” strategy (in blue) to our original strategy (the coin flip, in red):
Wow, the simple decision to never risk busting improves our odds of winning across the board. While the old trends are still there, no matter what card the dealer shows, our probability of not losing money has increased.
Let’s take a look at how the win/tie probabilities of our new strategy look when we bucket by our initial hand value:
It’s more clear what is going on from looking at this plot. We have improved our probability of winning for all initial hand values besides 12 through 16. Those hand values are relatively unaffected because by choosing to stay (in order to eliminate the risk of busting), we make it easier for the dealer to beat our hand (because the dealer can only stop hitting when he or she reaches a hand value of 17 or more).
But for all other hand values, it looks like our strategy of avoiding busts is pretty helpful.
I hope you enjoyed reading, and stay tuned for the next post where we see if a neural network can beat our naive strategy. Cheers and remember — NEVER bet what you cannot afford to lose!
A selection of my recent posts that I hope you will check out:
Is Your Company Truly Data Driven?
Are Data Scientists at Risk of Automation
How Much Do Data Scientists Make?
How Much Do Data Scientists Make Part 2
How Much Do Software Engineers Make?
A Better Way to Skill Up