Section 4 - Improving Deck with vector
7B.4.1 Moving Cards from Arrays to vectors
Now that we know about vectors, let's take a critical look at our Deck class more closely. Its primary member is the array of Cards:
// class Deck prototype ---------------------------------------- class Deck { // six full decks is enough for about any game static const int MAX_CARDS_PER_DECK = 6 * 52; static Card masterPack[52]; // one 52-card master for initializing decks private: Card cards[MAX_CARDS_PER_DECK]; int topCard; int numPacks;
You can already see an annoyance. We have to decide how large an array to allocate. If you think that the fix for this is to use a dynamic array, remember how much more trouble a dynamic array can be and how it is frought with memory leak dangers. Also, there is this "extra" member, topCard, which we seem to need but wish we didn't. Let's look at some of the implementation that resulted from the choice of array.
When we load up the deck in init() one of the double nested loops is:
// then transfer these values to our deck for (pack = 0; pack < numPacks; pack++) for (k = 0; k < 52; k++) cards[pack*52 + k] = masterPack[k]
The index expression in the last line is ugly. We're trying to turn two nested index values into a single index that tells us where the highest unused location is. This is programming 101, and you can do this in your sleep, but that doesn't make the sleep any more pleasant. A vector doesn't require such computation; we just use push_back() and we're done. Where else do cards[] and topCard come up?
Here is a fragment from dealCard():
if (topCard == 0) return errorReturn; else return cards[--topCard];
It's not horrible, but we have to remember to decrement topCard before we return. Not only that, but we can't use topCard-- (as it would give the wrong answer, obviously). Even though we know how and why, it would be nice to use a vector instead of an array, since push_back() and pop_back() present no such difficulties.
There are several places where the array nature of cards[] creates micro-challenges which increase the chance for error. So let's switch to vectors.
7B.4.2 A Vector of Cards
Inside Deck, we use a vector of cards, not an array, and when we want to deal out a card we simply call the vector's back() and pop_back() to remove it from the deck. The card will be gone and there is no need to do any bookkeeping on topCard. In fact, we no longer need topCard. That's already a plus.
Here is the start of the new prototype for our improved Deck:
// class Deck prototype ----------------------- class Deck { private: vector<Card> cards; int numPacks;
Already, it's cleaner. No array dimensioning, no MAX_CARDS_PER_DECK and no topCard. cards is now an (empty) vector, and it will grow and shrink though calls to push_back() and pop_back().
Let's see how this simplifies some of the implementation. The init() method's double nested loop now looks like this:
cards.clear(); // then transfer these values to our deck for (pack = 0; pack < numPacks; pack++) for (k = 0; k < 52; k++) cards.push_back( masterPack[k] );
Nicely done, right? What about dealCard()?
Card Deck::dealCard() { // always deal the top card (last in) Card errorReturn(0, Card::spades); // force errorFlag in return, in rare cases Card retCard; if (cards.size() == 0) return errorReturn; // else pop card from back retCard = cards.back(); cards.pop_back(); return retCard; }
Here is a summary of what we did, and what we still would need to do to completely transform Deck to use a vector instead of an array:
- Change cards[] from array to vector<Card>.
- Remove topCard and MAX_CARDS_IN_DECK.
- topCard value, when needed, is now cards.size(). But, you can still use a local topCard = cards.size() if you use topCard in a loop and don't want the overhead of the size() call inside the loop (or loop control).
- When adding to the deck (something we haven't done up to this point), instead of adding into cards[topCard++] use cards.push_back().
- When dealing a card, instead of returning cards[--topCard], we grab cards.back(), then call cards.pop_back().
7B.4.3 A Complete Listing
Here is a full listing of the transformed Deck class, along with Card and a test main()
// ====== main() Foothill.cpp ================= // CS 2B - LOCEFF // Client driver for Card, Hand, Deck classes ============ #include "CardSupportVec.h" #define MAX_HANDS 10 // main client -------------------------------------------------------- int main() { int k, numHands; Deck deck(1); Hand hands[MAX_HANDS]; // get the input from the user -------------------------- do { cout << "How many hands? (1 - " << MAX_HANDS << ", please): "; cin >> numHands; } while (numHands < 1 || numHands > MAX_HANDS); // deal deck, unshuffled -------------------------------- while (deck.getNumCards() > 0) { for (k = 0; k < numHands; k++) { if (deck.getNumCards() == 0) break; hands[k].takeCard( deck.dealCard() ); } } cout << "Here are our hands, from unshuffled deck:" << endl; for (k = 0; k < numHands; k++) { cout << hands[k].toString() << endl << endl; } cout << endl; // restock, deal deck, shuffled -------------------------- deck.init(1); deck.shuffle(); // clear hands for (k = 0; k < numHands; k++) hands[k].resetHand(); while (deck.getNumCards() > 0) { for (k = 0; k < numHands; k++) { if (deck.getNumCards() == 0) break; hands[k].takeCard( deck.dealCard() ); } } cout << "Here are our hands, from SHUFFLED deck:" << endl; for (k = 0; k < numHands; k++) { cout << hands[k].toString() << endl << endl; } return 0; } // ====== CardSupportVec.h =============== // CS 2B - LOCEFF // CardSupportVec.h Header File #pragma once #include <iostream> #include <ctime> #include <string> #include <cctype> #include <vector> using namespace std; // class Card prototype ---------------------------------------- class Card { public: enum Suit { clubs, diamonds, hearts, spades }; private: char value; Suit suit; bool errorFlag; public: Card(char value = 'A', Suit suit = spades); string toString(); bool set(char value = 'A', Suit suit = spades); char getVal() { return value; } Suit getSuit() { return suit; } bool getErrorFlag() { return errorFlag; } bool equals(Card card); // helpers private: bool isValid(char value, Suit suit); }; // class Hand prototype ---------------------------------------- class Hand { public: // need this in-line for array declaration static const int MAX_CARDS_PER_HAND = 50; // that should be enough for any game private: Card myCards[MAX_CARDS_PER_HAND]; int numCards; public: Hand() { resetHand(); } // mutators void resetHand() { numCards = 0; } bool takeCard(Card card); Card playCard(); // accessors string toString(); int getNumCards() { return numCards; } Card inspectCard(int k); }; // class Deck prototype ---------------------------------------- class Deck { static Card masterPack[52]; // one 52-card master for initializing decks private: vector<Card> cards; int numPacks; public: Deck(int numPacks = 1); void init(int numPacks = 1); int getNumCards() { return cards.size(); } void shuffle(); Card dealCard(); Card inspectCard(int k); string toString(); private: static void allocateMasterPack(); bool floatLargestToTop(int top); void swap(Card &a, Card &b); }; // ====== CardSupportVec.cpp =============== // CS 2B - LOCEFF // CardSupportVec.cpp source file #include "CardSupportVec.h" // beginning of Card method definitions ---------------------------------------- // constructor Card::Card(char value, Suit suit) { // because mutator sets errorFlag, we don't have to test set(value, suit); } // stringizer string Card::toString() { string retVal; char strVal[2]; if (errorFlag) return "** illegal **"; // convert char to a CString, then string strVal[0] = value; strVal[1] = '\0'; retVal = string(strVal); if (suit == spades) retVal += " of Spades"; else if (suit == hearts) retVal += " of Hearts"; else if (suit == diamonds) retVal += " of Diamonds"; else if (suit == clubs) retVal += " of Clubs"; return retVal; } // mutator bool Card::set(char value, Suit suit) { char upVal; // convert to uppercase to simplify (may need to #include <cctype>) upVal = toupper((int)value); if ( !isValid(upVal, suit)) { errorFlag = true; return false; } // else implied errorFlag = false; this->value = upVal; this->suit = suit; return true; } // helper bool Card::isValid(char value, Suit suit) { char upVal; // convert to uppercase to simplify (need #include <cctype>) upVal = toupper((int)value); // check for validity if ( upVal == 'A' || upVal == 'K' || upVal == 'Q' || upVal == 'J' || upVal == 'T' || (upVal >= '2' && upVal <= '9') ) return true; else return false; } bool Card::equals(Card card) { if (this->value != card.value) return false; if (this->suit != card.suit) return false; if (this->errorFlag != card.errorFlag) return false; return true; } // end of Card method definitions ---------------------------------------- // beginning of Hand method definitions ---------------------------------------- bool Hand::takeCard(Card card) { if (numCards >= MAX_CARDS_PER_HAND) return false; // don't just assign: mutator assures active/undeleted myCards[numCards++].set( card.getVal(), card.getSuit() ); return true; } Card Hand::playCard() { // always play highest card in array. client will prepare this position. // in rare case that client tries to play from a spent hand, return error Card errorReturn(0, Card::spades); // in rare cases if (numCards == 0) return errorReturn; else return myCards[--numCards]; } Card Hand::inspectCard(int k) { // return copy of card at position k. // if client tries to access out-of-bounds card, return error Card errorReturn(0, Card::spades); // force errorFlag in return, in rare cases if (k < 0 || k >= numCards) return errorReturn; else return myCards[k]; } string Hand::toString() { int k; string retVal = "Hand = ( "; for (k = 0; k < numCards; k++) { retVal += myCards[k].toString(); if (k < numCards - 1) retVal += ", "; } retVal += " )"; return retVal; } // end of Hand method definitions ------------------------------------ // beginning of Deck method definitions ------------------------------ Card Deck::masterPack[52]; // static definition Deck::Deck(int numPacks) { // place cards in the deck, in perfect order allocateMasterPack(); init(numPacks); } void Deck::allocateMasterPack() { int k, j; Card::Suit st; char val; // we're in a static method; only needed once per program: good for whole class static bool firstTime = true; if ( !firstTime ) return; firstTime = false; // first load an array of cards with values for (k = 0; k < 4; k++) { // set the suit for this loop pass st = (Card::Suit)k; // now set all the values for this suit masterPack[13*k].set('A', st); for (val='2', j = 1; val<='9'; val++, j++) masterPack[13*k + j].set(val, st); masterPack[13*k+9].set('T', st); masterPack[13*k+10].set('J', st); masterPack[13*k+11].set('Q', st); masterPack[13*k+12].set('K', st); } } // set deck from 1 to 6 packs, perfecly ordered void Deck::init(int numPacks) { int k, pack; if (numPacks < 1 || numPacks > 6) numPacks = 1; // when initing, reset the random num generator for shuffling srand(time(NULL)); cards.clear(); // then transfer these values to our deck for (pack = 0; pack < numPacks; pack++) for (k = 0; k < 52; k++) cards.push_back( masterPack[k] ); this->numPacks = numPacks; } void Deck::shuffle() { Card tempCard; int k, randInt, topCard; // for loop efficiency topCard = cards.size(); for (k = 0; k < topCard; k++) { randInt = rand() % topCard; // swap cards k and randInt (sometimes k == randInt: okay) tempCard = cards[k]; cards[k] = cards[randInt]; cards[randInt] = tempCard; } } Card Deck::dealCard() { // always deal the top card (last in). Card errorReturn(0, Card::spades); // force errorFlag in return, in rare cases Card retCard; if (cards.size() == 0) return errorReturn; // else pop card from back retCard = cards.back(); cards.pop_back(); return retCard; } Card Deck::inspectCard(int k) { // return copy of card at position k. // if client tries to access out-of-bounds card, return error Card errorReturn(0, Card::spades); // force errorFlag in return, in rare cases if (k < 0 || k >= cards.size()) return errorReturn; else return cards[k]; } // end of Hand method definitions ------------------------------------