15. Extending classes

In this chapter, we present a more comprehensive example of object-oriented programming.

Crazy Eights is a classic card game for two or more players. The main objective is to be the first player to get rid of all your cards. Here’s how to play:

  • Deal five or more cards to each player, and then deal one card face up to create the “discard pile”. Place the remaining cards face down to create the “draw pile”.

  • Each player takes turns placing a single card on the discard pile. The card must match the rank or suit of the previously played card, or be an eight, which is a “wild card”.

  • When players don’t have a matching card or an eight, they must draw new cards until they get one.

  • If the draw pile ever runs out, the discard pile is shuffled (except the top card) and becomes the new draw pile.

  • As soon as a player has no cards, the game ends, and all other players score penalty points for their remaining cards. Eights are worth 20, face cards are worth 10, and all others are worth their rank. You can read https://en.wikipedia.org/wiki/Crazy_Eights for more details, but we have enough to get started.

15.1. CardCollection

To implement Crazy Eights, we need to represent a deck of cards, a discard pile, a draw pile, and a hand for each player. And we need to be able to deal, draw, and discard cards.

The Deck and Pile classes from the previous chapter meet some of these requirements. But unless we make some changes, neither of them represents a hand of cards very well.

Furthermore, Deck and Pile are essentially two versions of the same code: one based on arrays, and the other based on vector. It would be helpful to combine their features into one class that meets the needs of both.

We will define a class named CardCollection and add the code we want one step at a time. Since this class will represent different piles and hands of cards, we’ll add a label attribute to tell them apart:

struct CardCollection {
    string label;
    vector<Card> cards;

    CardCollection(string label) {
        this->label = label;
        // this->cards is automatically initialized to an empty vector
    }
};

As with the Pile class, we need a way to add cards to the collection. Here is the add_card member function from the previous chapter:

void CardCollection::add_card(const Card& card)
{
    this->cards.push_back(card);
}

Until now, we have used this explicitly to make it easy to identify attributes. Inside add_card and other instance member functions, you can access instance variables without using the keyword this. So from here on, we will drop it:

void CardCollection::add_card(const Card& card)
{
    cards.push_back(card);
}

We also need to be able to remove cards from the collection. The following member functions takes an index, removes the card at that location, and shifts the following cards left to fill the gap:

Card CardCollection::pop_card()
{
    // remove last card
    Card toRemove = cards.back();
    cards.pop_back();
    return toRemove;
}

If we are dealing cards from a shuffled deck, we don’t care which card gets removed. It is most efficient to choose the last one, so we don’t have to shift any cards left. Here is an overloaded version of pop_card that removes and returns the last card:

Card CardCollection::pop_card(int i)
{
    Card toRemove = cards[i];
    cards.erase(cards.begin() + i);
    return toRemove;
}

CardCollection also provides is_empty, which returns true if there are no cards left, and size, which returns the number of cards:

bool CardCollection::is_empty()
{
    return cards.empty();
}
int CardCollection::size()
{
    return cards.size();
}

CardCollection also provides a wrapper for getting items from the vector. We don’t want to give users direct access to the vector because we want to control modifications.

Card CardCollection::get_card(int i)
{
    return cards[i];
}

last_card gets the last card (but doesn’t remove it):

Card CardCollection::last_card()
{
    return cards.back();
}

The only modifiers we provide to change the vector are the two versions of pop_card and the following version of swap_cards:

void CardCollection::swap_cards(int i, int j)
{
    Card temp = cards[i];
    cards[i] = cards[j];
    cards[j] = temp;
}

Finally, we use swap_cards to implement shuffle:

void CardCollection::shuffle()
{
    for (int i = cards.size() - 1; i > 0; i--) {
        int j = random_between(0, i + 1);
        swap_cards(i, j);
    }
}

15.2. Inheritance

At this point, we have a class that represents a collection of cards. It provides functionality common to decks of cards, piles of cards, hands of cards, and potentially other collections.

However, each kind of collection will be slightly different. Rather than add every possible feature to CardCollection, we can use inheritance to define subclasses. A subclass is a class that “extends” an existing class; that is, it has the attributes and member functions of the existing class, plus more.

Here is the complete definition of our new and improved Deck class:

struct Deck: CardCollection {
    Deck(string label) : CardCollection(label) {
        for (int suit = 0; suit <= 3; suit++) {
            for (int rank = 1; rank <= 13; rank++) {
                add_card(Card(rank, suit))
            }
        }
    }
};

The first line uses a colon to indicate that Deck extends the class CardCollection. That means a Deck object has the same instance variables and member functions as a CardCollection. Another way to say the same thing is that Deck “inherits from” CardCollection. We could also say that CardCollection is a superclass, and Deck is one of its subclasses. In C++, classes may extend multiple superclasses.

Constructors are not inherited, but all other public attributes and member functions are. The only additional item in Deck, at least for now, is a constructor. So you can create a Deck object like this:

Deck deck = Deck("Deck");

The definition of the constructor uses a colon then the constructor from the superclass. This colon invokes the constructor of the base class before continuing.

So in this case, the colon invokes the CardCollection constructor, which initializes the attributes label and cards. When it returns, the Deck constructor resumes and populates the (empty) vector with Card objects.

That’s it for the Deck class. Next we need a way to represent a hand, which is the collection of cards held by a player, and a pile, which is a collection of cards on the table. We could define two classes, one for hands and one for piles, but there is not much difference between them. So we’ll use one class, called Hand, for both hands and piles. Here’s what the definition looks like:

struct Hand: CardCollection {
    Hand(string label): CardCollection(label) { }

    void display() {
        cout << get_label() << ": " << endl;
        for (int i = 0; i < size(); i++) {
            cout << get_card(i).to_string() << endl;
        }
        cout << endl;
    }
};

Like Deck, the Hand class extends CardCollection. So it inherits member functions like get_label, size, and get_card, which are used in display. Hand also provides a constructor, which invokes the constructor of CardCollection.

In summary, a Deck is just like a CardCollection, but it provides a different constructor. And a Hand is just like a CardCollection, but it provides an additional member function, display.

15.3. Dealing Cards

To begin the game, we need to deal cards to each of the players. And during the game, we need to move cards between hands and piles. If we add the following member function to CardCollection, it can meet both of these requirements:

void CardCollection::deal(CardCollection& that, int n)
{
    for (int i = 0; i < n; i++) {
        Card card = pop_card();
        that.add_card(card);
    }
}

The deal member function removes cards from the collection it is invoked on, this, and adds them to the collection it gets as a parameter, that. The second parameter, n, is the number of cards to deal. We will use this member function to implement deal_all, which deals (or moves) all of the remaining cards:

void CardCollection::deal_all(CardCollection& that)
{
    int n = cards.size();
    deal(that, n);
}

At this point, we can create a Deck and start dealing cards. Here’s a simple example that deals five cards to a hand, and deals the rest into a draw pile:

Deck deck = Deck("Deck");
deck.shuffle();

Hand hand = Hand("Hand");
deck.deal(hand, 5);
hand.display();

Hand draw_pile = Hand("Draw Pile");
deck.deal_all(draw_pile);
cout << "Draw Pile has " << draw_pile.size() << "cards." << endl;

Because the deck is shuffled randomly, you should get a different hand each time you run this example. The output will look something like this:

Hand:
5 of Diamonds
Ace of Hearts
6 of Clubs
6 of Diamonds
2 of Clubs

Draw Pile has 47 cards.

If you are a careful reader, you might notice something strange about this example. Take another look at the definition of deal. Notice that the first parameter is supposed to be a CardCollection. But we invoked it like this:

Hand hand = Hand("Hand");
deck.deal(hand, 5);

The argument is a Hand, not a CardCollection. So why is this example legal?

It’s because Hand is a subclass of CardCollection, so a Hand object is also con- sidered to be a CardCollection object. If a function expects a CardCollection, you can give it a Hand, a Deck, or a CardCollection.

But it doesn’t work the other way around: not every CardCollection is a Hand, so if a function expects a Hand, you have to give it a Hand, not a CardCollection or a Deck.

If it seems strange that an object can belong to more than one type, remember that this happens in real life too. Every cat is also a mammal, and every mammal is also an animal. But not every animal is a mammal, and not every mammal is a cat.

15.4. The Player Class

The Deck and Hand classes we have defined so far could be used for any card game; we have not yet implemented any of the rules specific to Crazy Eights. And that’s probably a good thing, since it makes it easy to reuse these classes if we want to make another game in the future.

But now it’s time to implement the rules. We’ll use two classes: Player, which encapsulates player strategy, and Eights, which creates and maintains the state of the game. Here is the beginning of the Player definition:

struct Player {
    string name;
    Hand hand;

    Player(string name) {
        this->name = name;
        this->hand = Hand(name);
    }
};

A Player has two attributes: a name and a hand. The constructor takes the player’s name as a string and saves it in an instance variable. In this example, we have to use this to distinguish between the instance variable and the parameter with the same name.

The primary member function that Player provides is play, which decides which card to discard during each turn:

Card Player::play(Eights& eights, const Card& prev)
{
    Card card;
    try {
        card = search_for_match(prev);
    } catch(const char* msg) {
        // no match found; player draws a card
        card = draw_for_match(eights, prev);
    }
    return card;
}

The first parameter is a reference to the Eights object that encapsulates the state of the game (coming up in the next section). The second parameter, prev, is the card on top of the discard pile.

play invokes two helper member functions: search_for_match and draw_for_match. Since we have not written them yet, this is an example of top-down design.

Here’s search_for_match, which looks in the player’s hand for a card that matches the previously played card:

Card Player::search_for_match(const Card& prev)
{
    for (int i = 0; i < hand.size(); i++) {
        Card card = hand.get_card(i);
        if (card_matches(card, prev)) {
            return hand.pop_card(i);
        }
    }
    throw "No match found";
}

The strategy is pretty simple: the for loop searches for the first card that’s legal to play and returns it. If there are no cards that match, it throws an exception. In that case, we have to draw cards until we get a match, which is what draw_for_match does:

Card Player::draw_for_match(Eights& eights, const Card& prev)
{
    while (true) {
        Card card = eights.draw_card();
        cout << name << " draws " << card.to_string() << endl;
        if (card_matches(card, prev)) {
            return card;
        }
        hand.add_card(card);
    }
}

The while loop runs until it finds a match (we’ll assume for now that it always finds one). The loop uses the Eights object to draw a card. If it matches, draw_for_match returns the card. Otherwise it adds the card to the player’s hand and repeats.

Both search_for_match and draw_for_match use card_matches, which is a member function, also defined in Player. This member function is a straightforward translation of the rules of the game:

bool Player::card_matches(const Card& card1, const Card& card2)
{
    return card1.suit == card2.suit
        || card1.rank == card2.rank
        || card1.rank == 8;
}

15.5. The Eights Class

. TODO: Find the right chapter number In Section 13.2, we introduced top-down design. In this way of developing pro- grams, we identify high-level goals, like shuffling a deck, and break them into smaller problems, like choosing a random element or swapping two elements.

In this section, we present bottom-up design, which goes the other way around: first we identify simple pieces we need and then we assemble them into more-complex algorithms.

Looking at the rules of Crazy Eights, we can identify some of the member variables. we’ll need:

  • Create the deck, the players, and the discard and draw piles. Deal the cards and set up the game. (Eights constructor)

  • Check whether the game is over. (is_done)

  • If the draw pile is empty, shuffle the discard pile and move the cards into the draw pile. (reshuffle)

  • Draw a card, reshuffling the discard pile if necessary. (draw_card)

  • Keep track of whose turn it is, and switch from one player to the next. (next_player)

  • Display the state of the game, and wait for the user before running the next turn. (display_state)

Now we can start implementing the pieces. Here is the beginning of the class definition for Eights, which encapsulates the state of the game:

struct Eights {
    Player one;
    Player two;
    Hand draw_pile;
    Hand discard_pile;
}

In this version, there are always two players. One of the exercises at the end of the chapter asks you to modify this code to handle more players. The Eights class also includes a draw pile, and a discard pile.

. TODO: Find the correct chapter The constructor for Eights initializes the instance variables and deals the cards, similar to Section 14.3. The next piece we’ll need is a member variables that checks whether the game is over. If either hand is empty, we’re done:

bool Eights::is_done()
{
    return one.hand.is_empty() || two.hand.is_empty();
}

When the draw pile is empty, we have to shuffle the discard pile. Here is a member function for that:

void Eights::reshuffle()
{
    Card prev = discard_pile.pop_card();
    discard_pile.deal_all(draw_pile);
    discard_pile.add_card(prev);
    draw_pile.shuffle();
}

The first line saves the top card from discard_pile. The next line transfers the rest of the cards to draw_pile. Then we put the saved card back into discard_pile and shuffle draw_pile. We can use reshuffle as part of the draw member function:

Card Eights::draw_card()
{
    if (draw_pile.is_empty()) {
        reshuffle();
    }
    return draw_pile.pop_card();
}

The next_player member function takes the current player as a parameter and returns the player who should go next:

Player* Eights::next_player(const Player* current)
{
    if (current == (&one)) {
        return &two;
    } else {
        return &one;
    }
}

The last member function from our bottom-up design is display_state. It displays the hand of each player, the contents of the discard pile, and the number of cards in the draw pile. Finally, it waits for the user to press the Enter key:

void Eights::display_state()
{
    one.display();
    two.display();
    discard_pile.display();
    cout << "Draw pile:" << endl;
    cout << draw_pile.size() << " cards" << endl;
    
    // wait for user input before continuing
    getchar();

    cout << "--------\n" << endl;
}

Using these pieces, we can write take_turn, which executes one player’s turn. It reads the top card off the discard pile and passes it to player.play, which you saw in the previous section. The result is the card the player chose, which is added to the discard pile:

void Eights::take_turn(Player* player)
{
    Card prev = discard_pile.last_card();
    Card next = (*player).play(*this, prev);
    discard_pile.add_card(next);
    cout << (*player).name << " plays " << next.to_string() << endl << endl;
}

Finally, we use takeTurn and the other member functions to write play_game:

void Eights::play_game()
{
    Player* player = &one;
    // keep playing until there's a winner
    while (!is_done()) {
        display_state();
        take_turn(player);
        player = next_player(player);
    }
    // display the final score
    one.display();
    two.display();
}

Done! The result of bottom-up design is similar to top-down: we have a high- level member function that calls helper member functions. The difference is the development process we used to arrive at this solution.

15.6. Class Relationships

This chapter demonstrates two common relationships between classes:

composition: Instances of one class contain references to instances of another class. For example, an instance of Eights contains references to two Player objects, and two Hand objects.

inheritance: One class extends another class. For example, Hand extends CardCollection, so every instance of Hand is also a CardCollection

Composition is also known as a HAS-A relationship, as in “Eights has a Hand”. Inheritance is also known as an IS-A relationship, as in “Hand is a CardCollection”. This vocabulary provides a concise way to talk about an object-oriented design.

. TODO: What chapter number? There is also a standard way to represent these relationships graphically in UML class diagrams. As you saw in Section 10.7, the UML representation of a class is a box with three sections: the class name, the attributes, and the member functions. The latter two sections are optional when showing relationships.

UML diagram for the classes in this chapter.

Relationships between classes are represented by arrows: composition arrows have a standard arrow head, and inheritance arrows have a hollow triangle head (usually pointing up). Figure 16.1 shows the classes defined in this chapter and the relationships among them.

UML is an international standard, so almost any software engineer in the world could look at this diagram and understand our design. And class diagrams are only one of many graphical representations defined in the UML standard.

15.7. Glossary

15.8. Exercises