14. Week 8: Blackjack!
≪ 13. Week 7 Thursday: const References and Overloading | Table of Contents | 15. Week 9 Tuesday: Constructors, Initialisation Lists, and RAII ≫Our goal for the week is to create a functional game of blackjack.
We’ll start by discussing briefly the principles
of .h and .cpp file organisation,
then setting up pseudocode
and a code skeleton for blackjack using these principles.
We’ll spend Thursday filling in the “actual” code.
A Quick Note on header files and implementation files
Last week,
you should have learned about the idea
of separating code into .h files (header files)
and .cpp files (implementation files).
Thinking back to week 1, we learned that code is turned into a computer program in two distinct steps: compilation and linking. Compilation is a highly nontrivial task, particularly when projects get very large and start spanning millions of lines of code. Logically, if we changed just one line of code, the compiler shouldn’t need to re-compile the entire project… that’s a lot of redundant work. Instead, it would make sense to reuse whatever it had already compiled and only compile what little got changed.
Thus people split their C++ code into distinct .cpp files.
Each .cpp file is complied separately;
changes to one file do not require other files to be recompiled.
For instance, think about how Google Chrome was coded.
There may be a chrome.cpp that contains the main function
and gets the ball rolling when you open your browser;
there may be a network.cpp
that handles all the internet capabilities;
there may be a video.cpp that handles video playback;
probably a telemetry.cpp
that sends all your personal information to the CIA.
But now there’s a problem:
if chrome.cpp wants to use a function or variable
defined in the file video.cpp,
how is the C++ compiler supposed to know that?
The solution is the header file. These are short “summary” files that describe the contents of their corresponding header files so the compiler knows that whatever code is missing will be compiled in another file later on. It’s then the linker’s job to stitch together all the missing pieces.
Remark 1.
Whenever you type
1#include <iostream>
you are actually including a file called
iostream.h somewhere on your computer.
The actual code in iostream was written once upon a time
and compiled once;
your linker is secretly connecting that compiled code to your program.
We’ll see how to write these files momentarily…
Blackjack: Pseudocode and Skeleton
Let’s now get cracking on writing up a blackjack program, starting with the pseudocode and code skeleton.
In case you’re unfamiliar, the game of blackjack (with one player and one dealer) is played as follows:
- A standard deck of 52 cards is shuffled.
- The player and the dealer are each dealt two cards; one of the dealer’s cards remains face-down, and the rest of the cards are put face up.
- If the player’s score is 21, they win immediately (this is called a blackjack).
- Otherwise, while the player’s score is less than 21…
- The player may choose to either hit or stand.
- If the player chooses to hit, they receive another card.
- If the player chooses to stand, their turn ends.
- If the player has a score of over 21, they bust or lose the game.
- The dealer plays. The dealer always hits if their score is less than 17, and they stand otherwise.
- If the dealer goes over a score of 21, they bust. Otherwise, the player with a higher score wins.
A player’s cards are scored as follows: number cards receive their face value (e.g. a 3 of clubs is worth 3, a 7 of spades is worth 7, etc.). Face cards are all worth 10 points. Aces can either be worth 11 points, or 1 point if this causes the player to bust.
First and foremost: let’s decide on how we are handling the nouns in our program. How will C++ remember the player’s and dealer’s cards? Which cards are in the deck? etc.?
I’ll choose to represent a card by a pair of characters in a string,
one character for the value and one for the suit.
For instance, AC will be Ace of Clubs;
7S will be 7 of Spades;
TD will be Ten of Diamonds; etc.
The deck and each player’s hands will thus be vectors of strings!
You may object that the player and dealer are complex nouns. For now, I don’t think we need to worry about them — the only thing they do is decide whether to hit or stand, and they don’t yet have any persistent data. In the future, you may want to change this if you want to add a balance or a record or something like that.
Anyways, now knowing how we’re keeping track of cards, let’s highlight all the complex verbs in our pseudocode and think about how to convert them into functions.
- Make a
vector<string>variable calleddeckwith the 52 standard cards, then shuffle thedeck. - Create
stringvariablesplayeranddealer. Deal two cards to each. - Compute the player’s score. If it’s 21, they’ve already won — print a message congratulating them and end the game.
- While the player’s score is less than 21…
- Print both hands and scores.
- Ask the player if they want to hit or stand.
- If the player wants to hit, deal another card to their hand.
- If the player wants to stand, end their turn.
- Compute the player’s score. If it’s over 21, they’ve lost already — print a message denigrating them and end the game.
- While the dealer’s score is less than 17…
- Print both hands and scores.
- The dealer hits, so deal a card to the dealer’s hand.
- Determine the winner:
- Compute the dealer’s score and the player’s score.
- If the dealer’s score is over 21, the player wins.
- Otherwise, whoever has the highest score wins. If the scores are equal, it’s a tie.
So here’s the plan:
we’ll first declare all these functions
in a header file called blackjack.h,
then write a bunch of placeholder definitions
in an implementation file called blackjack.cpp.
We’ll fill out the main program
as if all our functions work,
then slowly fill in the individual functions.
This falls into our paradigm of incremental programming
that we highlighted when we made Wordle,
with the bonus that our code is better organised.
For the header file, the first thing we have to do is write a header guard:
This is usually automatically generated by your IDE when you create a new header file.
So let’s write these function declarations, one at a time.
First, we need a way to shuffle a deck of cards.
I think this should be a void function,
and it should accept a deck of cards
(i.e. a vector<string>) by reference.
1void shuffle(vector<string>& deck);
Next, we need a way to deal a card.
This verb modifies both a deck of cards
and a player’s hand,
so it should take two vector<string>&’s
and still return void.
1void deal(vector<string>& deck, vector<string>& hand);
Third, we need to compute the score for a hand.
This does need to return an int —
we’re asking for a score after all —
and we need a hand to compute the score of.
This should be a const vector<string>&:
1int score(const vector<string>& hand);
This one function takes care of both computing the dealer’s score and the player’s score.
Finally,
we need a way to display someone’s hand on the screen.
Similar to printing a vector,
this returns void and takes a constant reference:
1void print_hand(const vector<string>& hand);
These all belong in the header file.
Since we’re referring to vector and string,
it’s good practise to include them just to be safe.
blackjack.h
1#ifndef BLACKJACK_H
2#define BLACKJACK_H
3
4#include <vector>
5#include <string>
6
7using namespace std;
8
9void shuffle(vector<string>& deck);
10void deal(vector<string>& deck, vector<string>& hand);
11int score(const vector<string>& hand);
12void print_hand(const vector<string>& hand);
13
14#endif
If you want to be extra careful, you should comment in what exactly each of these functions does…
Let’s now write the implementation file.
This amounts to just providing empty definitions
of all of the functions declared in the header file.
We’ll mark them all as TODO,
just as we did with Wordle.
blackjack.cpp
1#include <vector>
2#include <string>
3
4using namespace std;
5
6void shuffle(vector<string>& deck) {
7 // TODO
8}
9
10void deal(vector<string>& deck, vector<string>& hand) {
11 // TODO
12}
13
14int score(const vector<string>& hand) {
15 // TODO
16 return 0;
17}
18
19void print_hand(const vector<string>& hand) {
20 // TODO
21}
This is so that we can actually write the main program now, thinking about anything we’ll need to do or change about our outline as we go before we actually commit to the designs we’ve made thus far.
So let’s do that:
let’s use the function declarations we made
to convert our pseudocode into C++ code.
Let me remark now that there are some things
that are nontrivial to implement,
and I’ve marked these in as TODOs.
1#include <iostream>
2#include <string>
3#include <vector>
4
5#include "blackjack.h"
6
7int main() {
8 // 1. shuffle a deck of 52 cards.
9 vector<string> deck;
10
11 // TODO: fill in the standard 52 playing cards.
12 shuffle(deck);
13
14 // 2. deal 2 cards to the player and dealer.
15 vector<string> player;
16 vector<string> dealer;
17 deal(deck, player); deal(deck, dealer);
18 deal(deck, player); deal(deck, dealer);
19
20 // 3. if the player's score is 21, they win.
21 if(score(player) == 21) {
22 cout << "Holy smokes! A Blackjack!!" << endl;
23 return 0; // ends game instantly.
24 }
25
26 // 4. while player's score is <= 21....
27 while(score(player) <= 21) {
28 cout << "Your hand: ";
29 print_hand(player);
30
31 // ask the player if they should hit or stand.
32 cout << "Hit or stand? [enter h or s] ";
33 char response; cin >> response;
34
35 if(response == 'h') {
36 // hit: deal another card.
37 deal(deck, player);
38 } else if(response == 's') {
39 // stand: player doesn't want to keep going.
40 break;
41 }
42 }
43
44 // 5. if player's score is > 21, it's a bust.
45 if(score(player) > 21) {
46 cout << "haha you busted xd" << endl;
47 return 0; // ends game instantly.
48 }
49
50 // 6. dealer hits until score >= 17.
51 while(score(dealer) < 17) {
52 print_hand(dealer);
53 deal(deck, dealer);
54 }
55
56 // 7. if dealerbusted, player wins
57 if(score(dealer) > 21) {
58 cout << "Wow, you beat the house!" << endl;
59 return 0;
60 }
61
62 // check who has a better score.
63 if(score(dealer) > score(player)) {
64 cout << "The house took ur money." << endl;
65 } else if(score(dealer) == score(player)) {
66 cout << "It's a tie..." << endl;
67 } else {
68 cout << "Good job, you won some money!" << endl;
69 }
70
71 return 0;
72}
Assuming that the functions we have declared do what they intend to do, this main function should build and run to produce an almost functional game of blackjack. There’s just one step missing: we need to set up the deck of cards. I think we can implement that directly in the main program:
1// 1. shuffle a deck of 52 cards.
2vector<string> deck;
3
4// sets up the 52 standard playing cards.
5string values = "A23456789TJQK";
6string suits = "SCDH";
7for(int i = 0; i < values.size(); i++) {
8 for(int j = 0; j < suits.size(); j++) {
9 string card;
10 card += values.at(i);
11 card += suits.at(j);
12 deck.push_back(card);
13 }
14}
15shuffle(deck);
This says, “for every card value and every suit, add the value/suit pair to the deck.”
This should now build and run without problems.
This means that C++ understands the grammar
that we are trying to implement!
Although it doesn’t know what deal and shuffle and print_hand
actually mean just yet,
it’s at least able to compile the code.
We can now focus on the blackjack.cpp file.
All we need to do is to fill in the four missing functions.
Let’s start with the print_hand function,
so we can see what we’re doing in the game.
This is pretty straightforward:
1void print_hand(const vector<string>& hand) {
2 // for each card in the hand,
3 // print that card, followed by a space.
4 for(int i = 0; i < hand.size(); i++) {
5 cout << hand.at(i) << " ";
6 }
7
8 cout << endl;
9}
Upon building and running the program now,
we can at least see each player’s empty hand.
Let’s now implement the deal function,
which will allow us to see changes as the game progresses:
1void deal(vector<string>& deck, vector<string>& hand) {
2 // take the *last* card off the deck,
3 // then add it to the hand.
4 hand.push_back(deck.back());
5 deck.pop_back();
6}
Remark 2.
You can absolutely implement a deal function as follows:
1string deal(vector<string>& deck) {
2 string card = deck.back();
3 deck.pop_back();
4 return card;
5}
In the main program, one can write
1player.push_back(deal(deck));
instead of the above. This is a purely stylistic choice! You have the freedom to implement one or both of these options.
Building and running the program again lets us see actual changes on the screen, but the game is not so interesting. It’s the same game every single time, and there’s no scorekeeping, so you can exhaust the whole deck if you want to.
Problem 3.
Implement the score function.
Solution
Let’s begin by writing some pseudocode.
We will at a high level
scan through all the cards in the given hand,
adding the corresponding scores (11 for aces).
We will simultaneously keep track of the number of aces we’ve seen.
At the end, we’ll check if the user has busted.
If they have, and if there are some aces in their hand,
we’ll subtract 10 from their score,
then repeat:
- Set a variable
score = 0andaces = 0. - For each card in the user’s hand:
- Add the corresponding value to
score, using11for aces. - If the card is an ace, increment
aces.
- Add the corresponding value to
- While the user’s score is above
21and the number ofacesis bigger than1:- Subtract
10from the user’s score and decrementaces.
- Subtract
In code:
1int score(const vector<string>& hand) {
2 // 1. Set a variable `score = 0` and `aces = 0`.
3 int score = 0;
4 int aces = 0;
5
6 // 2. For each card in the user's hand:
7 for(int i = 0; i < hand.size(); i++) {
8 string card = hand.at(i);
9
10 // * Add the corresponding value to `score`, using `11` for aces.
11 // * If the card is an ace, increment `aces`.
12 if(card.at(0) == 'A') {
13 aces++;
14 score += 11;
15 } else if(card.at(0) == 'T' || card.at(0) == 'J' ||
16 card.at(0) == 'Q' || card.at(0) == 'K') {
17 score += 10;
18 } else {
19 // the value of the card is a digit,
20 // do some ASCII table math!
21 score += card.at(0) - '0';
22 }
23 }
24
25 // 3. While the user's score is above `21`
26 // and the number of `aces` is bigger than `0`:
27 // * Subtract `10` from the user's score and decrement `aces`.
28 while(score > 21 && aces > 0) {
29 score -= 10;
30 aces--;
31 }
32
33 return score;
34}
We should now see all the functionality of the blackjack game, except for the fact that the deck isn’t getting shuffled!
Thinking back to Wordle,
we will need to seed our random number generator
using the cstdlib and ctime libraries.
We’ll insert srand(time(0))
at the very beginning of the main function,
then use the rand function within shuffle to mix up our cards.
Problem 4.
Implement the shuffle function.
Solution
There’s lots of different ways to approach this, and I think the most natural way is to select a random card from the deck, remove it, and add it to an auxiliary vector. More specifically:
- Make a vector of strings called “shuffled”.
- While
deckis not empty:- Pick a random index between
0anddeck.length() - 1, inclusive. - Add the card at the randomly selected index to
shuffled. - Remove the card at the randomly selected index from
deck.
- Pick a random index between
- Set
deckequal toshuffled.
You may want to consult the erase function for vectors in C++. The syntax is a little funky, and although we may not understand every little piece, we can at least apply the code for our purposes.
In code, this looks like:
Problem 5.
Find a way to implement the shuffled function
without creating an auxiliary vector of strings.
We now have a fully functionaly game of blackjack!
Exercise 6.
Modify your code so that the dealer’s second card is hidden while the user is making their moves.
Exercise 7.
Modify your code so that the user is allowed to bet money from a balance and can play until they run out of money. (You can decide the payouts.)