Programming: Week 3 (cont. 2)

Ex. 6

Define a class that is able to describe any of the 52 cards in a pack of cards. Include a Print method that outputs the suit and the rank of a card.


Class Design Considerations

The class Card should contain the info required to uniquely identify a card, i.e., describe its suit (Hearts, Diamonds, Spades, Clubs) and rank (A, 1, 2, … 10, J, Q, K).

The class needs at least two member variables, one for suit and one for rank. You can uniquely identify a card by giving its suit and rank. However, it is more convenient to uniquely identify obejcts by using a single identifier as opposed to several identifiers, such as suit and rank.

Consider every card in the pack is numbered, the numbers running from 1 to 52.

cards

Such a number could serve as the single unique identifier. Let’s call it a card’s ID. The order of cards (and consequently, which card each of the 52 IDs refers to) is arbitrary. Were the cards arranged differently, the same ID (e.g., ID = 7) would refer to a different card. Good programming practice encourages to write as reusable and context-independent code as possible and now we are imposing a specific fixed order of cards on which to build the class.

However, numbering the cards in advance does not reduce generality. We need this fixed card order only inside the class so that we have some convenient way to identify each card; the user outside the class will not experience this fixed order because they will be interested in cards independently of one another. This will become more apparent when we define another class for the whole pack of cards and have that class manipulate individual cards via this class.

Now that we have card ID, the suit and the rank of a specific card become properties of that card and they are calculated based on the ID.

When you instantiate an object of the Card class, you bring one of the 52 cards into existence. That card should never be able to change its suit or rank (nor its ID), just as cards don’t change in the real world — a Two of Hearts (H2) will always remain a Two of Hearts. We should therefore equip this class with public GetSuit() and GetRank() methods but not SetSuit(), SetRank() or SetID(). A method GetID() isn’t necessary either because the user will only be interested in suit and rank of a card; ID should be handled internally in the class and hidden from the user.

Here is a visual representation of the class:

Ritning1

The class takes in one external value, id, and provides two values, suit and rank. So when a card object is instantiated, class constructor requests an id to be supplied so that the card can be uniquely identified and stay like that for the entire lifetime of the object. Public methods can then be called to obtain the suit and the rank of the card.

Class Code (Implementation)

Having discussed the design of the card class we can turn to the C++ implementation of the card class. Here is the code:

#include 
#include 
#include 

class Card
{
public:
	Card(int id)
	{
		m_id = id;
		SetSuitRank();
	};
	char GetSuit()
	{
		return m_suit;
	};
	std::string GetRank()
	{
		return m_rank;
	};
	void PrintCard()
	{
		std::cout << GetSuit() << GetRank();
	};

private:
	int m_id;
	char m_suit;
	std::string m_rank;
	void SetSuitRank()
	{
		switch (m_id / 13)
		{
		case 0:
			m_suit = 'H';
			break;
		case 1:
			m_suit = 'D';
			break;
		case 2:
			m_suit = 'S';
			break;
		case 3:
			m_suit = 'C';
			break;
		};
		switch (m_id % 13)
		{
		case 0:
			m_rank = "A";
			break;
		case 1:
			m_rank = "2";
			break;
		case 2:
			m_rank = "3";
			break;
		case 3:
			m_rank = "4";
			break;
		case 4:
			m_rank = "5";
			break;
		case 5:
			m_rank = "6";
			break;
		case 6:
			m_rank = "7";
			break;
		case 7:
			m_rank = "8";
			break;
		case 8:
			m_rank = "9";
			break;
		case 9:
			m_rank = "10";
			break;
		case 10:
			m_rank = "J";
			break;
		case 11:
			m_rank = "Q";
			break;
		case 12:
			m_rank = "K";
			break;
		};
	};
};

A few short comments about the class. The logic which assigns the suit and the rank to a card is in the private method SetSuitRank(). This method reads off the card’s ID from the member variable set by the constructor and calculates which row and column the card is in in the table above. Then suit and rank member variables are assigned corresponding values. This method is private because the user or outside functions should not be able to change the ID of the card once the card has been instantiated.

The public PrintCard() method outputs the suit and rank of the card and you can also access suit and rank separately via respective public get-methods.

Testing the class

We are going to test the class. Let’s create two different cards, e.g. an HQ and an S7. Looking at the table above, we can see that the numbers of these cards are 12 and 33, respectively. To create these cards we pass the values 11 and 32 to the constructor because the logic in SetSuitRank() follows the usual naming conventions whereby the first array element has the index 0.

Then we will call the PrintCard() method on both cards to verify they work correctly.

int main(int argc, char* argv[])
{
	Card card(11);
	Card card2(32);
	card.PrintCard();
	std::cout << std::endl;
	card2.PrintCard();
	std::cout << std::endl;
	return 0;
}

This is the output:

HQ
S7
Press any key to continue . . .

So the class works correctly. Notice that if the user wants to create a particular card, they have to know the card’s ID from the table above (using any other arrangements for IDs will create wrong cards). However, the user will not manually create cards using this class. We will write another class, Pack class, to describe the whole pack of cards and the pack class will use the card class to create all the different cards. In this way, we can see to it that the Pack class constructor calls the Card class constructor with every fo the 52 different IDs to guarantee the pack has been created correctly. This is the object of the next exercise.

Separating declarations from implementation

To better prepare for the next exercise and improve code readability, we will break the code we wrote in a *.cpp and a *.h file.

Card.h

#pragma once

class Card
{
public:
	Card(int id);

	char GetSuit();
	std::string GetRank();
	void PrintCard();
private:
	int m_id;
	char m_suit;
	std::string m_rank;
	
	void SetSuitRank();
};

Card.cpp

#include "stdafx.h"
#include "Card.h"

Card::Card(int id)
{
	m_id = id;
	SetSuitRank();
}

char Card::GetSuit()
{
	return m_suit;
}

std::string Card::GetRank()
{
	return m_rank;
}

void Card::PrintCard()
{
	std::cout << GetSuit() << GetRank();
}

void Card::SetSuitRank()
{
	switch (m_id / 13)
	{
		case 0:
			m_suit = 'H';
			break;
		case 1:
			m_suit = 'D';
			break;
		case 2:
			m_suit = 'S';
			break;
		case 3:
			m_suit = 'C';
			break;
	};
	switch (m_id % 13)
	{
		case 0:
			m_rank = "A";
			break;
		case 1:
			m_rank = "2";
			break;
		case 2:
			m_rank = "3";
			break;
		case 3:
			m_rank = "4";
			break;
		case 4:
			m_rank = "5";
			break;
		case 5:
			m_rank = "6";
			break;
		case 6:
			m_rank = "7";
			break;
		case 7:
			m_rank = "8";
			break;
		case 8:
			m_rank = "9";
			break;
		case 9:
			m_rank = "10";
			break;
		case 10:
			m_rank = "J";
			break;
		case 11:
			m_rank = "Q";
			break;
		case 12:
			m_rank = "K";
			break;
	};
}

main.cpp

#include 

#include "Card.h"


int main(int argc, char* argv[])
{
	//do things
}

NB: include directives for iostream and string have been moved to stdafx.h which is included in main.cpp.

About Rokas Paulauskas

2014  Programming