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.
The card values are as follows:
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.
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.
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:
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))
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))
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♣"
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
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.
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.
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.
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!”.
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.
Let’s take a look at how we changed this one-player game of blackjack into a 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.
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.
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.
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.
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
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))
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.
(sum(last_vector==21)/sum(game==21))*100
## [1] 100
When at 21, we busted every time (duh!).
(sum(last_vector==20)/sum(game==20))*100
## [1] 93.36283
(sum(last_vector==19)/sum(game==19))*100
## [1] 84.07643
(sum(last_vector==18)/sum(game==18))*100
## [1] 81.37931
(sum(last_vector==17)/sum(game==17))*100
## [1] 72.13115
(sum(last_vector==16)/sum(game==16))*100
## [1] 68.11594
(sum(last_vector==15)/sum(game==15))*100
## [1] 56.09756
(sum(last_vector==14)/sum(game==14))*100
## [1] 48.57143
(sum(last_vector==13)/sum(game==13))*100
## [1] 42.20183
(sum(last_vector==12)/sum(game==12))*100
## [1] 30.47619
(sum(last_vector==11)/sum(game==11))*100
## [1] 0
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.
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()
Blackjack is a game of luck. Each player takes the chances you want to take.