Blackjack Basics

Also known as 21, Blackjack is a card game popularly played in casinos worldwide. The game’s premise is to beat the dealer by having a hand value as close to 21 as possible, without going over 21.

Card Values

The card values are as follows:

  • Cards 2 through 10 are face value.
  • Jack, Queen, and King are all worth 10.
  • Ace counts as 1 or 11, depending on which value benefits the player’s hand more.

Game Play

Dealing a Hand

At the beginning of the game each player, including the dealer, is dealt two cards. The players’ cards are dealt face up, while the dealer has one card face up, known as the up-card, and one card face down, known as the hole card.

Player’s Turn

Once cards are dealt the player takes their turn deciding how to play their hand. A player can choose to “Hit” where they will be given another card or “Stand” where they will keep their current total. A player can Hit as many times as they like, but if their total exceeds 21, they “bust” and lose the hand.

Dealer’s Turn

After the player has completed their turn, the dealer reveals their hole card. The dealer cannot decide whether to Hit or Stand; they must follow specific rules:

  • If the dealer’s total is 16 or less, they must Hit until their total is 17 or more.
  • If the dealer’s total is 17 or higher, they must Stand.

Our Code

The Deck of Cards

In order to begin coding Blackjack, we need to create a deck of cards. Below, is one way to do just that.

initial_deck <- function(noOfDecks) {
    suits <- c("♠", "♥", "♦", "♣") 
    cards <- c(2:10, "J", "Q", "K", "A") 
    Deck <- paste(cards, rep(suits, each = 13), sep = "")
    d <- rep(Deck, noOfDecks)  
    shuffledDeck <- sample(d, length(d))  
    return(shuffledDeck)
  }

deck <- initial_deck(1)

The code above shows a function named “initial_deck”. This function creates the deck of cards we will use in our game of Blackjack. The input for this function, “noOfDecks”, allows the user to input any number, and the function will create that specified number of decks.

Following, suits are designated with a corresponding symbol; Spades “♠”, Hearts “♥”, Diamonds “♦”, and Clubs “♣”. Cards are then listed from two to ten, followed by Jack “J”, Queen “Q”, King “K”, and Ace “A”. These are assigned using character vectors, which allow users to enter a list of data to be stored under a specified name.

The “Deck” command pastes the cards repeating each of the four suits thirteen times. The last part in this line, “sep”, indicates that a space should not separate the cards and the suits. Additionally, the “d” command repeats the created “Deck” to the specified number of decks input at the beginning of the function. Last, the “shuffledDeck” command shuffles the cards so that the deck(s) are randomized. Finally, the final line in the function returns shuffled deck(s) of cards created.

Below, the “initial_deck" function is called to return a singular deck of cards, which we have named”deck“. This deck of cards has been formatted into a data table to be more easily viewed.

datatable(data.frame(Deck_of_Cards = deck), options = list(scrollX = TRUE))

Card Value

As explained above the suits and cards are assigned using character vectors. The issue with this is that when these are combined to create a card a new variable is created. For example, “Q♥” will be read as a word rather than a numerical value. Thus, there is a need for a function that addresses this issue.

rank_values <- c("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_value <- function(card_v) {
  rank <- substr(card_v, 1, nchar(card_v) - 1)
  return(rank_values[rank])
  }

First, we define the numerical values, one through eleven, for each card by using “rank_values”.
The function “card_value” begins by using “card_v” as the input which stands for a playing card. Then the “rank” command uses “substr” to extract a card from the created deck(s), such as “Q♥”. Then it removes the suit from the card so that the only thing remaining is “Q”. Using this information the function looks back to “rank_values”, to find the corresponding value for the card, so “Q” is mapped to 10.

Below, the function has been called to show the deck of cards from above and their respective values in a data table.

datatable(data.frame(Deck_of_Cards = deck, Values_of_Deck = card_value(deck)), options = list(scrollX = TRUE))

Dealing

Now that we have created a deck with respective values for each card we can begin to deal cards.

draw_card <- function(deck) {
  if (length(deck) == 0) {
    stop("The deck is empty")
  }
  index <- sample(length(deck), 1)
  card <- deck[index]
  deck <<- deck[-index]
  return(card)
}

The “draw_card” function takes the input of “deck”, and then begins by checking “if” the “length” of the “deck” is equal to zero. If it is, there are no more cards left to draw in the deck. The code will then “stop” and print, “The deck is empty”. If the deck is not empty the code will continue.

In continuation, we define “index” as a random sample from the length of the deck. This will give a random position number, 1 - 52, to indicate a card within the deck. Then we define “card” to be the card from the given position number within the deck. For example, if the “index” command outputs the number thirty-two, the “card” command will look for the card in the thirty-second position in the deck. After, we need to redefine the “deck” to be updated to reflect that the selected card is no longer in the deck.

Below you can see the function, “draw_card”, selects a random card from the deck. Under this, you can see the updated deck does not contain the selected card anymore.

draw_card(deck)
## [1] "9♠"
deck
##  [1] "2♥"  "5♠"  "8♦"  "Q♣"  "Q♠"  "J♠"  "3♠"  "10♥" "Q♦"  "A♦"  "7♦"  "6♠" 
## [13] "3♥"  "A♥"  "9♦"  "2♠"  "7♥"  "10♠" "J♦"  "4♦"  "K♥"  "A♣"  "5♦"  "8♣" 
## [25] "5♣"  "4♣"  "K♠"  "2♦"  "8♠"  "K♣"  "9♣"  "3♦"  "3♣"  "Q♥"  "10♣" "A♠" 
## [37] "J♥"  "7♣"  "6♦"  "4♠"  "9♥"  "J♣"  "4♥"  "7♠"  "8♥"  "5♥"  "6♥"  "2♣" 
## [49] "10♦" "K♦"  "6♣"

Using this we can deal out the player’s and dealer’s hand.

player_hand <- replicate(2, unlist(draw_card(deck)))
player_hand
## [1] "K♠" "Q♠"
dealer_hand <- replicate(2, unlist(draw_card(deck)))
dealer_hand
## [1] "8♠" "2♣"

Hand Value

Now that both player and dealer are dealt we need to determine the total value of the hand.

get_hand_value <- function(hand) {
  values <- sapply(hand, card_value)
  total <- sum(values)
    
  num_aces <- sum(values == 11)
  while (total > 21 && num_aces > 0) {
    total <- total - 10
    num_aces <- num_aces - 1
  }
  return(total)
}

The “get_hand_value” function uses the input “hand” to stand for a hand of cards. Then, the “values” values command uses “sapply” so the “card_value” function is applied to each card within the hand. Following, the “total” command uses “sum” to add the values from the hand.

The “num_aces” command checks the hand for aces by looking for the card(s) with a value of eleven. Then, “while” the total hand value is greater than twenty-one and the number of aces is greater than zero the loop changes the “total” to be reduced by ten. This means that the ace value is being played as a one instead of eleven. After this, the “num_aces” is updated to note that an ace has been converted from eleven to one. The function ends with returning the calculated total.

Below we can see how the “get_hand_value” function works with “player_hand” and “dealer_hand”.

get_hand_value(player_hand)
## [1] 20
get_hand_value(dealer_hand)
## [1] 10

Hand Display

Now that we can deal a hand of cards with the respective total to the player and dealer we need a way to display this information. The player’s hand should be displayed so both cards are displayed with the total and the dealer’s hand should be displayed with only one of the two cards face up.

  Total <- get_hand_value(player_hand)
  
  cat("Player's Hand:", player_hand, "Total:", Total,
      "Dealer's Hand:", dealer_hand[1], "X\n\n")
## Player's Hand: K♠ Q♠ Total: 20 Dealer's Hand: 8♠ X

We define the “Total” to be the value of the player’s hand using the “get_hand_value” function with “player_hand” as the input. Then we use “cat” to display the information “Player’s Hand:” is printed to display the cards from “player_hand” and the “Total”. Then, “Dealer’s Hand:” is printed to display one card from the dealer, “dealer_hand[1]” and then “X” as the hole card, “\n\n” is used to create spacing between the displays.

Player’s Turn

Now that we can display a dealt hand of Blackjack, we can begin the player’s turn.

stand <- FALSE

if (stand == FALSE){
  if (Total < 21) {
    action <- readline("Do you want to hit or stand? ")
    if (tolower(action) == "hit") {
      Card_3 <- unlist(draw_card(deck))
      value_of_card3 <- card_value(Card_3)
      new_total <- Total + value_of_card3 
      
      if (new_total > 21) {
        if (get_hand_value(player_hand[1]) == 11) {
          player_hand[1] ==  1
          new_total <- new_total - 10
        }
        if (get_hand_value(player_hand[2]) == 11) {
          player_hand[2] ==  1
          new_total <- new_total - 10
        }
        if (value_of_card3 == 11) {
          value_of_card3 <- 1
          new_total <- new_total - 10
        }
      }
      
      Total <- new_total
      cat("Player:", player_hand, Card_3, "Total:", Total, "\n")
      if (Total > 21) {
        cat("Player busts!\n")
      }
      
    } else if (tolower(action) == "stand") {
      stand <- TRUE
      
    } else {
      cat("Invalid input. Enter 'hit' or 'stand'.\n")
    }
  }
}
## Do you want to hit or stand? 
## Invalid input. Enter 'hit' or 'stand'.

The code starts with taking “stand” set to “FALSE” which indicates whether the player chooses to stand. Following, we start with our first if statement, “if (stand == FALSE)”. This checks that the player has decided not to stand. Then our second if statement, “if (Total < 21)”, checks the that player’s total is less than 21. If these conditions are satisfied the code continues.

The next line defines “action” to display the prompt “Do you want to hit or stand?”. The third if statement checks if the player input “hit”. If the player has decided to hit the code continues with “Card_3” which draws a third card and removes the drawn card from the deck. Then, “value_of_card3” assigns “Card_3” its respective value using the “card_value” function. Finally, the “new_total” is defined to be the past “Total” plus the “value_of_card3”.

The code then checks if the “new_total” is greater than 21. If the “new_total” is greater than 21 the code then checks each card in hand for aces. If the code finds an ace within the hand, it will convert the ace from a value of eleven to one so the total will be less than or equal to 21.

The “Total” is then updated to be the “new_total”. The player’s new hand is shown with the updated total and the new card. If the total is greater than 21 the code will display “Player busts!”. However, if the player has chosen to stand, “stand” will be updated to “TRUE” and the player’s turn will end. The last line displays “Invalid input. Enter ‘hit’ or ‘stand’.” this ensures the user inputs hit or stand for their action and rejects other inputs.

This code is then repeated three more times allowing the player to hit a total of four times after their initial hand.

Dealer’s Turn

Once the player has either busted or decided to stand the dealer’s turn begins.

while (get_hand_value(dealer_hand) < 17 || (get_hand_value(dealer_hand) == 17 && sum(dealer_hand == 11) > 0)) {
  dealer_hand <- c(dealer_hand, unlist(draw_card(deck)))
}
dealer_value <- get_hand_value(dealer_hand)

cat("Dealer's hand:", dealer_hand, "Total:", dealer_value, "\n")
## Dealer's hand: 8♠ 2♣ 5♠ 3♠ Total: 18

The code starts by checking the value of the dealer’s hand. While the dealer’s hand is less than 17 or if the hand value is exactly 17 but contains at least one Ace valued at 11 the dealer hits. When the dealer hits “dealer_hand” draws a card from the deck and removes the drawn card from the deck. Then “dealer_value” is updated to be the value of “dealer_hand” using the “get_hand_value” function. Finally, the dealer’s hand is displayed with all cards and the total.

Outcome of the Game

Now that both the player and dealer have taken their turns we need to determine the outcome of the game.

if (Total > 21) {
  cat("Player busts. Dealer wins!\n")
  } else if (dealer_value > 21 || Total > dealer_value) {
    cat("Player wins!\n")
  } else if (Total == dealer_value) {
    cat("It's a tie!\n")
  } else {
    cat("Dealer wins!\n")
  }
## Player wins!

The code starts by determining whether the player’s “Total” is greater than 21, if it is the code will display “Player busts. Dealer wins!”. Then the code checks if the dealer’s hand is less than 21 and if the player’s hand is greater than the dealer’s, if it is the code displays “Player wins!”. The code then checks if the player’s and the dealer’s hands are the same, if they are the code displays “It’s a tie!”. Finally, if none of the previous conditions were met the code will display “Dealer wins!”.

Simulating the Game

When creating our code we wanted to determine the best possible decision in each situation. In order to do this, we developed a simulation based off of our original Blackjack code. This new code simulates hits until the player busts and records each total until the last hit that busted. Using these totals, we were able to determine the probability of busting depending on the player’s current total.

Creating the Simulation Code

Let’s take a look at how we changed this one-player game of blackjack into a simulation.

Initializing the Simulation

To start the simulation we need a way to keep a tally of the totals throughout each game.

p_total <- numeric(0)

We developed a variable vector named “p_total” which uses “numeric(0)” to create an empty vector that will store the running totals through each simulated game.

Looping the Simulation

In order to simulate the game, we need a way to repeat the game multiple times.

num_simulations <- 1

for (i in 1:num_simulations) {   
    player_hand <- character(0)     
    player_total <- 0
   " ..."
}

Using a “for” loop we can loop the game of blackjack however many times designated by the “num_simulations” placeholder. We also see “player_hand” and “player_total” which are variables that are specific to each game, thus they need to be reset to zero before the start of a new game.

Simulating Hits

When creating a simulation of blackjack, we need to write a code that will automatically hit eliminating human input.

new_card <- draw_card(deck)

We accomplished this by creating a “new_card” variable that draws a new card into the player’s hand using the “draw_card” function with “deck” as the input. This allows us to simulate a “Hit” in a game of blackjack without the manual input.

However, to simulate a full game, we need to hit until we bust because we are looking for the probability of busting depending on the total of the player.

 while (player_total <= 21) { 
   p_total <- c(p_total, player_total)
  " ..."
 }

We accomplished this by creating a “while” loop that runs by drawing new cards and updating totals. This loop checks the total, and if it is under 21, it goes back to hit again. If the total is over 21, it breaks the loop and ends that round.

At the start of the while loop, we see that the code also updates “p_total”. This is important because once it hits if the total is less than 21, the code will loop back and update the total. If the total is over 21, it will NOT update the total.

Handling Aces

Aces were a challenge within the simulation. We could not just change the value of all aces to 1 since there is a chance of a player being dealt an ace and a face card. We need a way of checking the total with the value of aces and updating their value respectively.

if (card_value == 11 && player_total + card_value > 21) {
        card_value <- 1
      }
      if (rank_values[substr(player_hand[1], 1, nchar(player_hand[1]) - 1)] == 11 && player_total + card_value > 21) {
        player_hand[1] <- sub("A", "1", player_hand[1])
      }
      if (rank_values[substr(player_hand[2], 1, nchar(player_hand[2]) - 1)] == 11 && player_total + card_value > 21) {
        player_hand[2] <- sub("A", "1", player_hand[2])
      }
      player_total <- player_total + card_value

Here, we first check if each card is an ace, and if the total would be over 21. If both are true, we update the card that is an ace from eleven to one and update the total.

Marking the End

Since we are simulating multiple games at a time, we need a way of identifying where one game ends and another begins in our running “p_total”.

if (player_total > 21) {
      p_total <- c(p_total, " ")
}

We did this by adding an empty space, " ", in the vector once it busts. This way, we can tell that a game has finished and a new one has started

Simulation Analysis

Now that we have looked at the changes we made to our original code, we can start to look at what we did with these simulations.

We previously mentioned a variable called “num_simulation”. In order to really get the most out of our data, we need a lot of simulations.

num_simulations <- 1000                           
game <- simulate_blackjack(num_simulations)

Above, we simulated 1000 games of blackjack, and stored the results in a variable called “game”. This “game” variable now holds every running total from all 1000 games we simulated, except for the last total which was over 21.

Below we can see all the totals for every game played. The empty row between each string of numbers shows where one game ends and another begins.

datatable(data.frame(Totals = game))

Now that we have every total, let’s pull out the last total of every game, to find the totals before busting on each game.

last_vector <- vector("list",1000)            
  j <- 1
  for(i in 1:length(game)){
    if (game[i]== " "){
      last_vector[j] <- game[i-1]
      j <- j+1
    }
  }
datatable(data.frame(Last_Total = unlist(last_vector)), options = list(scrollX = TRUE))

Results

Now that we have the number of hits that busted at each number, as well as all the totals throughout every game, we are able to find the chance of busting at each number.

21

(sum(last_vector==21)/sum(game==21))*100 
## [1] 100

When at 21, we busted every time (duh!).

20

(sum(last_vector==20)/sum(game==20))*100
## [1] 93.36283

19

(sum(last_vector==19)/sum(game==19))*100
## [1] 84.07643

18

(sum(last_vector==18)/sum(game==18))*100 
## [1] 81.37931

17

(sum(last_vector==17)/sum(game==17))*100 
## [1] 72.13115

16

(sum(last_vector==16)/sum(game==16))*100 
## [1] 68.11594

15

(sum(last_vector==15)/sum(game==15))*100 
## [1] 56.09756

14

(sum(last_vector==14)/sum(game==14))*100  
## [1] 48.57143

13

(sum(last_vector==13)/sum(game==13))*100  
## [1] 42.20183

12

(sum(last_vector==12)/sum(game==12))*100  
## [1] 30.47619

11

(sum(last_vector==11)/sum(game==11))*100  
## [1] 0

All Numbers Below 12:

If your total is below 12, you will never bust. The highest valued card you can get from a single hit is 11. Thus, the highest you can get if your total is below 12 is 21.

Graphs

Here we can see all the totals for every game played, as well as all the final totals of each game before busting.

game <- as.numeric(game)

ggplot(data = data.frame(Results = game), aes(x = game)) +
  geom_histogram(binwidth = 1, fill = "blue", color = "black") +
  labs(title = "Total Scores", x = "Player Total") +
  theme_minimal()  

last_vector <- as.numeric(last_vector)

ggplot(data = data.frame(Results = last_vector), aes(x = last_vector)) +
  geom_histogram(binwidth = 1, fill = "blue", color = "black") +
  labs(title = "Final Scores", x = "Player Total") +
  theme_minimal()  

Conclusion

Blackjack is a game of luck. Each player takes the chances you want to take.

  • is 16 a safe bet
  • Bust percentages above ~15 break 50%
  • How much risk are you willing to take?

Other Research Topics

  • Further development of our code to incorporate betting and splitting pairs.
  • Analyzing the game with more than one deck of cards in play
  • If data from the dealer was gathered, we could calculate possible win percentages from the most recent successful hit.