Hunter Liu's Website

11. Week 5 Thursday: Blackjack

≪ 10. Week 5 Tuesday: Functions, Part 1 | Table of Contents

Last time, we wrote four function signatures that we would need to implement to code up the game of blackjack: we need to be able to shuffle a deck of cards; deal a card from the deck to a hand; score a hand; and print out the state of the game. Currently, our “program” might look like this:

 1#include <iostream> 
 2#include <string> 
 3
 4using namespace std; 
 5
 6void shuffle(string& deck) { 
 7    // TODO 
 8} 
 9
10void deal(string& deck, string& hand) {
11    // TODO 
12} 
13
14int score(string hand) {
15    // TODO 
16    return 0; 
17} 
18
19void print_game(string player, string dealer, bool hide_dealer) {
20    // TODO 
21} 
22
23int main() {
24    // TODO 
25    return 0; 
26} 

The only comments are those indicating what we actually have to fill in…very helpful. Besides two return 0;’s, there are no significant lines of code that actually “do something”. At the very least, this code compiles, though, and this is a good first step: we have declared to C++ that there is a verb called shuffle, etc., and C++ is willing to accept this without yet knowing what they do.

Let’s now fill in the main function. I want to convert the pseudocode that we wrote into C++, one step at a time. This will ensure that the way I’ve designed the function signatures and the choices I’ve made in representing my data actually work together. Often times, one will mistakenly write a function that’s somewhat difficult to make work. By writing out how exactly these functions are being used, we have an opportunity to fix that before it becomes an issue.

First, we need to declare the deck (a string) with the 52 standard cards, then shuffle it. One can just write out the 52 cards by hand, or one can use a pair of nested for loops just as well:

 1// 1. create a deck of 52 cards. 
 2string deck = ""; 
 3
 4// for each index i in the string values...
 5//      for each index j in the string suits...
 6//          add values[i] and suits[j] to the deck. 
 7const string values = "A23456789TJQK", suits = "SCDH"; 
 8for(int i = 0; i < values.length(); i++) {
 9    for(int j = 0; j < suits.length(); j++) {
10        deck += values[i]; 
11        deck += suits[j]; 
12    } 
13} 
14
15// shuffle the deck! 
16shuffle(deck); 

Notice how we have taken the very complicated action of shuffling a deck and pretended that someone (i.e. future us) will make it work by using this shuffle function.

Next, we create two strings for the player’s hand and dealer’s hand, repsectively, then deal one card to each hand.

1// 2. create a hand for the player and dealer. deal one card to each. 
2string player, dealer;
3deal(deck, player); 
4deal(deck, dealer); 
5deal(deck, player); 
6deal(deck, dealer); 

Again, we are pretending that the deal function performs the rather complicated action of removing a card from a deck and places it into someone’s hand, even though we haven’t filled in these functions yet! C++ will accept this code as is because we’ve declared the “grammar” of these functions already.

1// 3. if the player's score is 21, they win. end the game. 
2if(score(player) == 21) {
3    cout << "Wow, you got a blackjack!" << endl; 
4    cout << "You won the game, and the dealer is crying." << endl; 
5    return 0; 
6} 

With step 3, there are two things to note. First, the expression score(player) is an integer to C++ whose value is computed based on the value of the variable player. Second, we are using the command return 0 to end the program early. The return command will always immediately end the function that it’s being used in, including the main program. This allows us to avoid placing the remainder of the game of blackjack in a very large (and somewhat unmanageable) else block.

The next step is the player’s actual turn, which involves a while loop and asking for some input. We’ll use the print_game function to display each player’s hand — after all, we expect the user to make a decision based on what they see — noting that one of the dealer’s cards should be obscured.

For the input, I’ll print a short prompt, then store the user’s input as a string (it should be “hit” or “stand”). For now, I’ll just assume the user isn’t a moron and actually enters one or the other.

 1// While the player’s score is less than 21…
 2while(score(player) < 21) {
 3    // Print both hands and scores.
 4    print_game(player, dealer, true); 
 5
 6    // Ask the player if they want to hit or stand.
 7    cout << "Hit or stand? "; 
 8    string input; 
 9    cin >> input; 
10
11    // If the player wants to hit, deal another card to their hand.
12    if(input == "hit") {
13        deal(deck, player); 
14    } 

So far, so good. But a wrinkle arises if the player says they want to stand! One must somehow terminate the while loop. This is exactly what the break statement is made for: it will immediately jump out of whatever loop it’s used in.

1    // If the player wants to stand, end their turn.
2    else if(input == "stand") {
3        break; 
4    } 
5} 

I want to highlight the readability of the code we are writing so far. Commands like deal(deck, player); abstract away some of the hairy details of how things work, allowing us to focus on the high-level logic involving shallow layers of loops and if statements.

The remaining steps of the pseudocode are similar to the first four we’ve just written:

 1// 5. check if the player has busted. if they did, 
 2// they've lost and the game is over. 
 3if(score(player) > 21) {
 4    cout << "You busted! You owe me money now." << endl; 
 5    return 0; 
 6} 
 7
 8// 6. while the dealer's score is less than 17... 
 9while(score(dealer) < 17) {
10    // display the game board 
11    // (this time, dealer's cards are shown)
12    print_game(player, dealer, false); 
13
14    // the dealer hits unconditionally. 
15    deal(deck, dealer); 
16} 
17
18// print the final state of the game for good measure. 
19print_game(player, dealer, false); 
20
21// 7. determine the winner.
22if(score(dealer) > 21) {
23    // dealer busted! 
24    cout << "You didn't win, the dealer lost." << endl; 
25    cout << "Congrats anyways, I guess..." << endl; 
26} else if(score(dealer) > score(player)) {
27    // dealer wins! 
28    cout << "The dealer won! Better luck next time." << endl; 
29} else if(score(dealer) < score(player)) {
30    // player wins! 
31    cout << "Wow, you bested the dealer!" << endl; 
32    cout << "Congrats on your win!" << endl; 
33} else {
34    // it's a tie... 
35    cout << "It's a tie. Boring..." << endl; 
36} 

That’s the whole entire main program! For good measure, you should make sure that everything compiles at this stage (although the game will be pretty weird…). This is just to make sure that our usage of the functions within the main program is consistent with our definitions at the beginning.

The first thing we should implement is the printout function. I want to print something that looks like the following when it’s the player’s turn:

Dealer (Score: ?) 
??  AS 

Player (Score: 15) 
8D  7D

Here, the dealer has an ace of spades and an unknown card (thus an unknown score); the player has an 8 of diamonds and a 7 of diamonds, totalling 15. We’ll reuse the score function to compute the scores — again, that’ll be done later — but everything else we can implement now. Let us start by writing the pseudocode:

  1. Print the header for the dealer alongside their score. Print a ? for the score of the dealer’s hand is hidden.
  2. For each even index i = 0, 2, ... in the dealer’s hand:
    • If i = 0 and the dealer’s hand is hidden, print ?? followed by two spaces.
    • Otherwise, print the characters at index i and i + 1, followed by two spaces.
  3. Print the header for the player alongside their score.
  4. For each even index i = 0, 2, ... in the player’s hand:
    • Print the characters at index i and i + 1, followed by two spaces.

Note that our “cards” are two characters long, so we have to be a bit finnicky with how we’re setting up our for loops. While we’re starting at 0 as usual, we should type i += 2 rather than i++:

 1void print_game(string player, string dealer, bool hide_dealer) {
 2    // print the dealer's "header text", hiding the score 
 3    // if necessary. 
 4    cout << "Dealer (Score: "; 
 5    if(hide_dealer) {
 6        cout << "?)" << endl; 
 7    } else {
 8        cout << score(dealer) << ")" << endl; 
 9    } 
10
11    // for each even index i in the string dealer...
12    for(int i = 0; i < dealer.length(); i += 2) {
13        // hide the first card, if necessary. 
14        if(i == 0 && hide_dealer) {
15            cout << "??  "; 
16        } else {
17            cout << dealer.at(i) << 
18                    dealer.at(i + 1) << "  "; 
19        } 
20    } 
21
22    // print an empty line for pretty spacing. 
23    cout << endl << endl; 
24
25    // repeat for the player! 
26    cout << "Player (Score: " << score(player) << ")" << endl; 
27    for(int i = 0; i < player.length(); i += 2) {
28        cout << player.at(i) << player.at(i + 1) << "  "; 
29    } 
30    cout << endl << endl; 
31} 

At this stage, our game of blackjack at least has the appearance of being a game: it prompts us for input and displays some cards and such. However, the scores stay at zero and no cards are ever dealt.

Let’s now implement the deal function so that the state of the game is actually changing. Recall that after the command deal(deck, player); is run, for instance, one card (e.g. the first two characters) should be removed from the deck and added to the end of player; likewise for when dealing to the dealer’s hand. Our pseudocode might look like:

  1. Append the index 0 and index 1 characters from deck to the end of the variable hand.
  2. Erase the first two characters from the variable deck.

Oh okay, that’s not so bad…

1void deal(string& deck, string& hand) {
2    // add the first card (2 chars.) from deck to the end of hand.
3    hand += deck.at(0); 
4    hand += deck.at(1); 
5
6    // erase the first card from the deck. 
7    deck.erase(0, 2); 
8} 

Now that the game is adding and removing cards, we should introduce some randomness via shuffling! We are currently getting the same sequence of cards every single run of the program: four aces, four twos, four threes, etc.

To get some randomness going, we’ll include the cstdlib library and borrow the function rand();, which spits out a random positive integer! By doing something like rand() % 52, for instance, we’ll obtain a random integer between 0 and 51, inclusive.

Let’s think through how we should shuffle a deck of cards, accepting on faith that rand() will generate as many random numbers as we need. Trying to simulate the action of riffle shuffling or something like that would be extremely difficult…instead, we’ll create a separate pile of cards, maybe called shuffled_deck, and one by one remove a random card from deck while adding it to shuffled_deck. By moving cards randomly one at a time, we’ll have placed all 52 cards in a random order once the whole deck is moved to this auxiliary variable.

To select a random card from deck, we can generate a random index i between 0 and deck.length() - 1. This may be even — in which case i and i+1 are the indices of a card — or it may be odd — in which case i-1 and i are the indices of a card. More specifically:

  1. Create an empty string called shuffled_deck.
  2. While deck is not empty:
    • Set an integer i to a random number between 0 and deck.length() - 1.
    • If i is even, add the index i and index i+1 characters to shuffled_deck, then remove them from deck.
    • Otherwise, add the index i-1 and i characters to shuffled_deck, then remove them from deck.
  3. Replace deck with shuffled_deck.

In code, this would look something like this:

 1void shuffle(string& deck) {
 2    string shuffled_deck; 
 3
 4    // while the deck is not empty...
 5    while(deck.length() > 0) {
 6        // generates a random number from 0 to deck.length() - 1. 
 7        int i = rand() % deck.length(); 
 8
 9        // if i is even...
10        if(i % 2 == 0) {
11            // add i and i+1 to the shuffled deck
12            shuffled_deck += deck.at(i); 
13            shuffled_deck += deck.at(i + 1); 
14
15            // then remove them from deck. 
16            deck.erase(i, 2); 
17        } 
18
19        // likewise for i being odd... 
20        else {
21            shuffled_deck += deck.at(i - 1); 
22            shuffled_deck += deck.at(i);
23
24            deck.erase(i - 1, 2); 
25        } 
26    } 
27
28    // replace `deck` with the shuffled deck. 
29    deck = shuffled_deck; 
30} 

Remark 1. Seeding the Random Number Generator

You may notice at this point that although the deck is getting shuffled, you’re getting the same shuffled deck on every run of the game. To solve this, we need to include the ctime library, then insert the command srand(time(0)) at the very top of the shuffle function (or at the very beginning of the main function). C++ generates random numbers based on a very obscure but deterministic rule, and the stream of numbers depends on a parameter called the “seed”. srand sets the “seed” of the random number generator to the current time, hence produces a new stream of numbers whenever the time changes!

The very, very last thing to do is to implement a proper scoring mechanism. I’ll leave that as an exercise for you; if you do this properly, you’ll now have a functional game of blackjack!