I've implemented a card game called Macau. This is a follow-up from this topic, where I tried to get the game's design right before actually implementing it.
Even if the game is presented on that topic, I will give a brief explanation as it follows. The game consists of a standard-deck that contains 52 cards. At the very beginning, every player receives 5 cards from the deck. (The first player that runs out of cards is the winner). Then, a card is put on the top of the pile. A player can put on the pile any card that is compatible with the top one. Two cards are compatible if they have the same suit or the same rank. There also exist several special cards:
- Rank: 2 or 3. This will require the next player to receive 2/3 cards from the deck if he doesn't have a 4 card with the same suit.
- Rank: A (Ace). This will simply skip the next player's turn.
Please note that, even if I mentioned 7 and the Joker as special cards in that topic, in the current state of the game they are not implemented. More precisely, the Jokers are disabled and the 7 is considered a normal card.
Here is the diagram of my classes.
And here is the code:
CardProperties.hpp
#pragma once
#include <array>
#include <string>
constexpr unsigned int numberOfSuits{ 6 };
constexpr unsigned int numberOfRanks{ 14 };
const std::array<std::string, numberOfSuits> suits{"Diamonds","Clubs"
, "Hearts","Spades" , "Red", "Black"};
const std::array<std::string, numberOfRanks> ranks{ "A", "2","3","4","5","6","7","8","9","10", "J","Q","K","Joker" };
enum class Suit
{
Diamonds,
Clubs,
Hearts,
Spades,
Red,
Black,
};
enum class Rank
{
Ace = 0,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
Joker,
};
Card.hpp
#pragma once
#include <ostream>
#include "CardProperties.hpp"
class Game;
class Card
{
public:
Card(const Suit& suit, const Rank& rank);
Card(const Rank& rank, const Suit& suit);
bool isCompatibleWith(const Card& other) const;
bool operator != (const Card& other) const;
friend std::ostream& operator<<(std::ostream& os, const Card& card);
//needed for std::set
bool operator == (const Card& other) const;
bool operator < (const Card& other) const;
friend class Game;
private:
bool invalidCardProperties(const Suit& suit, const Rank& rank) const;
Suit _suit;
Rank _rank;
};
Exceptions.hpp
#pragma once
#include <exception>
#include <string>
class InvalidCardException : public std::exception
{
public:
InvalidCardException(const std::string& errorMessage);
const char* what() const override;
private:
std::string _errorMessage;
};
class EmptyDeckException : public std::exception
{
public:
EmptyDeckException(const std::string& errorMessage);
const char* what() const override;
private:
std::string _errorMessage;
};
class NotEnoughCardsException : public std::exception
{
public:
NotEnoughCardsException(const std::string& errorMessage);
const char* what() const override;
private:
std::string _errorMessage;
};
class InvalidNumberOfPlayersException : public std::exception
{
public:
InvalidNumberOfPlayersException(const std::string& errorMessage);
const char* what() const override;
private:
std::string _errorMessage;
};
Card.cpp
#include "Card.hpp"
#include "Exceptions.hpp"
bool Card::invalidCardProperties(const Suit& suit, const Rank& rank) const
{
if (rank == Rank::Joker && (suit != Suit::Black && suit != Suit::Red))
return true;
if (rank != Rank::Joker && (suit == Suit::Black || suit == Suit::Red))
return true;
return false;
}
Card::Card(const Suit& suit, const Rank& rank)
: _suit(suit),
_rank(rank)
{
if (invalidCardProperties(suit, rank))
throw InvalidCardException("The Joker can only be red or black.\n");
}
Card::Card(const Rank& rank, const Suit& suit)
: Card(suit,rank)
{}
bool Card::isCompatibleWith(const Card& other) const
{
return _suit == other._suit || _rank == other._rank;
}
bool Card::operator< (const Card& other) const
{
return (static_cast<int>(_rank) < static_cast<int>(other._rank));
}
bool Card::operator == (const Card& other) const
{
return _rank == other._rank && _suit == other._suit;
}
bool Card::operator!= (const Card& other) const
{
return !((*this) == other);
}
std::ostream& operator << (std::ostream& os, const Card& card)
{
return os << '[' << suits[static_cast<int>(card._suit)] << " " << ranks[static_cast<int>(card._rank)] << ']';
}
Deck.hpp
#pragma once
#include <vector>
#include "Card.hpp"
class Deck
{
public:
Deck();
const Card& dealCard();
std::vector<Card> dealCards(const unsigned int& numberOfCards);
void refill(const std::vector<Card>& cards);
void refill(const Card& card);
private:
void fill();
void shuffle();
std::vector<Card> _cards;
};
Deck.cpp
#include "Deck.hpp"
#include "Exceptions.hpp"
#include <random>
Deck::Deck()
{
fill();
shuffle();
}
const Card& Deck::dealCard()
{
if (_cards.empty())
throw EmptyDeckException("");
Card& topCard(_cards.back());
_cards.pop_back();
return topCard;
}
std::vector<Card> Deck::dealCards(const unsigned int& numberOfCards)
{
if (_cards.size() < numberOfCards)
throw NotEnoughCardsException("");
std::vector<Card> requestedCards;
auto it{ std::prev(_cards.end(), numberOfCards) };
std::move(it, _cards.end(), std::back_inserter(requestedCards));
_cards.erase(it, _cards.end());
return requestedCards;
}
void Deck::fill()
{
constexpr unsigned int numberOfSuitsWithoutJokerSpecificOnes{ 4 };
constexpr unsigned int numberOfRanksWithoutJokers{ 13 };
for (unsigned int index1 = 0; index1 < numberOfRanksWithoutJokers; ++index1)
{
for (unsigned int index2 = 0; index2 < numberOfSuitsWithoutJokerSpecificOnes; ++index2)
{
Card card(static_cast<Suit>(index2), static_cast<Rank>(index1));
_cards.push_back(card);
}
}
/*Card redJoker(Suit::Red, Rank::Joker);
_cards.push_back(redJoker);
Card blackJoker(Suit::Black, Rank::Joker);
_cards.push_back(blackJoker);*/
}
void Deck::shuffle()
{
std::mt19937 randomNumberGenerator{ std::random_device{}() };
std::shuffle(_cards.begin(), _cards.end(), randomNumberGenerator);
}
void Deck::refill(const std::vector<Card>& cards)
{
_cards.insert(_cards.end(), cards.begin(), cards.end());
shuffle();
}
void Deck::refill(const Card& card)
{
_cards.push_back(card);
shuffle();
}
Pile.hpp
#pragma once
#include "Card.hpp"
#include <vector>
class Pile
{
public:
void addCard(const Card& card);
const Card& eraseTopCard();
const Card& getTopCard();
friend std::ostream& operator << (std::ostream& os, const Pile& pile);
std::vector<Card> releaseAllCardsButFirst();
private:
std::vector<Card> _alreadyPlayedCards;
};
Pile.cpp
#include "Pile.hpp"
#include "Exceptions.hpp"
#include <algorithm>
constexpr unsigned int minimumValueOfPlayedCards{ 1 };
constexpr unsigned int lastElementOffset{ 1 };
void Pile::addCard(const Card& card)
{
_alreadyPlayedCards.push_back(card);
}
std::vector<Card> Pile::releaseAllCardsButFirst()
{
std::vector<Card> releasedCards;
auto it{ std::next(_alreadyPlayedCards.begin(), _alreadyPlayedCards.size() - lastElementOffset) };
std::move(_alreadyPlayedCards.begin(), it, std::back_inserter(releasedCards));
_alreadyPlayedCards.erase(_alreadyPlayedCards.begin(), it);
return releasedCards;
}
const Card& Pile::getTopCard()
{
return _alreadyPlayedCards.back();
}
const Card& Pile::eraseTopCard()
{
const Card& requestedCard{ _alreadyPlayedCards.back() };
_alreadyPlayedCards.pop_back();
return requestedCard;
}
std::ostream & operator<<(std::ostream & os, const Pile & pile)
{
return os << pile._alreadyPlayedCards.back();
}
Hand.hpp
#pragma once
#include <vector>
#include "Card.hpp"
class Hand
{
public:
void addCard(const Card& card);
void addCards(const std::vector<Card>& cards);
void removeCard(const Card& card);
void removeCard(const unsigned int& cardNumber);
std::vector<Card>::iterator findCard(const Card& card);
const Card& getCard(const Card& card);
const Card& getCard(const unsigned int& cardNumber);
const unsigned int numberOfCards() const;
friend std::ostream& operator << (std::ostream& os, const Hand& hand);
private:
std::vector<Card> _cards;
};
Hand.cpp
#include "Hand.hpp"
#include <algorithm>
constexpr unsigned int indexOffset{ 1 };
void Hand::addCard(const Card& card)
{
_cards.push_back(card);
}
void Hand::removeCard(const Card& card)
{
auto it{ findCard(card) };
_cards.erase(it);
}
void Hand::addCards(const std::vector<Card>& cards)
{
_cards.insert(_cards.end(), cards.begin(), cards.end());
}
void Hand::removeCard(const unsigned int& cardNumber)
{
_cards.erase(std::next(_cards.begin(), cardNumber - indexOffset));
}
std::vector<Card>::iterator Hand::findCard(const Card& card)
{
return std::find(_cards.begin(), _cards.end(), card);
}
const Card& Hand::getCard(const Card& card)
{
auto it{ findCard(card) };
return *it;
}
const Card& Hand::getCard(const unsigned int& cardNumber)
{
return _cards[cardNumber - indexOffset];
}
const unsigned int Hand::numberOfCards() const
{
return _cards.size();
}
std::ostream& operator << (std::ostream& os, const Hand& hand)
{
for (unsigned int index = 0; index < hand._cards.size(); ++index)
os << "Card number: " << index + indexOffset <<" "<< hand._cards[index] << '\n';
return os << '\n';
}
Player.hpp
#pragma once
#include "Hand.hpp"
#include <vector>
class Player
{
public:
Player(const std::string& name = "undefined_name");
const Card& putCard(const Card& card);
Card putCard(const unsigned int& cardNumber);
const Card& getCard(const unsigned int& cardNumber);
void receive(const Card& card);
void receive(const std::vector<Card>& cards);
const unsigned int numberOfCards() const;
friend std::ostream& operator << (std::ostream& os, const Player& player);
const std::string name() const;
private:
Hand _handOfCards;
std::string _name;
};
Player.cpp
#include "Player.hpp"
Player::Player(const std::string& name)
: _name(name)
{}
const Card& Player::putCard(const Card& card)
{
const Card& requestedCard{ _handOfCards.getCard(card) };
_handOfCards.removeCard(card);
return requestedCard;
}
Card Player::putCard(const unsigned int& cardNumber)
{
Card requestedCard{ _handOfCards.getCard(cardNumber) };
_handOfCards.removeCard(cardNumber);
return requestedCard;
}
const Card& Player::getCard(const unsigned int& cardNumber)
{
return _handOfCards.getCard(cardNumber);
}
void Player::receive(const Card& card)
{
_handOfCards.addCard(card);
}
void Player::receive(const std::vector<Card>& cards)
{
_handOfCards.addCards(cards);
}
std::ostream& operator << (std::ostream& os, const Player& player)
{
return os << player._name << "'s cards.\n" << player._handOfCards;
}
const unsigned int Player::numberOfCards() const
{
return _handOfCards.numberOfCards();
}
const std::string Player::name() const
{
return _name;
}
Game.hpp
#pragma once
#include "Deck.hpp"
#include "Pile.hpp"
#include "Exceptions.hpp"
#include "Player.hpp"
#include <set>
class Game
{
public:
Game(const unsigned int& numberOfPlayers = 2);
void run();
private:
void dealInitialCardsToEachPlayer();
void initializePlayerNames();
void receiveCardsFromDeck(Player& player, const unsigned int& numberOfCards);
void putCardToPile(Player& player, unsigned int& cardNumber);
void normalCardPlayerTurn(Player& player);
void specialCardPlayerTurn(Player& player);
void printInformationAboutThePlayerAndTheTopCardFromPile(const Player& player) const;
void printInformationAboutThePlayerAndTheTopCardFromPile(const Player& player, const Rank& rank) const;
const Player& findWinner();
bool isSpecialCard(const Card& card) const;
void validatePlayerCardCompatibilityWithPileTopCard(Player& player, unsigned int& cardNumber);
std::vector<Player> _players;
unsigned int _numberOfPlayers;
Deck _deck;
Pile _pile;
bool _isRunning;
const std::set<Card> _specialCards{ { Suit::Clubs, Rank::Two },{ Suit::Diamonds, Rank::Two },{ Suit::Hearts, Rank::Two },{ Suit::Spades, Rank::Two }
,{ Suit::Clubs, Rank::Three },{ Suit::Diamonds, Rank::Three },{ Suit::Hearts, Rank::Three },{ Suit::Spades, Rank::Three },
/*,{ Suit::Clubs, Rank::Seven },{ Suit::Diamonds, Rank::Seven },{ Suit::Hearts, Rank::Seven },{ Suit::Spades, Rank::Seven },*/
{ Suit::Clubs, Rank::Ace },{ Suit::Diamonds, Rank::Ace },{ Suit::Hearts, Rank::Ace },{ Suit::Spades, Rank::Ace } };
};
Game.cpp
#include "Game.hpp"
#include "GameConstants.hpp"
#include <set>
#include <iostream>
#include <algorithm>
void Game::initializePlayerNames()
{
std::string currentPlayerName;
for (unsigned int index = 0; index < _numberOfPlayers; ++index)
{
std::cout << "Name: ";
std::getline(std::cin, currentPlayerName);
Player player(currentPlayerName);
_players.push_back(player);
}
}
Game::Game(const unsigned int& numberOfPlayers)
: _numberOfPlayers(numberOfPlayers),
_isRunning(true)
{
if (numberOfPlayers > maximumNumberOfPlayers || numberOfPlayers < minimumNumberOfPlayers)
throw InvalidNumberOfPlayersException("The minimum number of players is 2, while the maximum number of players is 9.");
initializePlayerNames();
}
void Game::printInformationAboutThePlayerAndTheTopCardFromPile(const Player& player) const
{
std::cout << "\nTop card from pile: " << _pile << "\n";
std::cout << player << "\n";
std::cout << "What card would you like to put? If you don't have any compatible card or you wish to skip the turn, enter 0.\n";
}
void Game::printInformationAboutThePlayerAndTheTopCardFromPile(const Player& player, const Rank& rank) const
{
switch (rank)
{
case Rank::Two:
{
std::cout << "\nTop card from pile: " << _pile << "\nThis is 2 card and you will need to receive 2 cards from the deck.\n"
<< "If you have a 4 card that has the same suit, then you can stop it.\n";
std::cout << "What card would you like to put? If don't have a compatible 4 card, then enter 0.\n" << player << "\n";
break;
}
case Rank::Three:
{
std::cout << "\nTop card from pile: " << _pile << "\nThis is 3 card and you will need to receive 3 cards from the deck.\n"
<< "If you have a 4 card that has the same suit, then you can stop it.\n";
std::cout << "What card would you like to put? If don't have a compatible 4 card, then enter 0.\n" << player << "\n";
break;
}
case Rank::Seven:
{
std::cout << "\nTop card from pile: " << _pile << "\nThis is a 7 card and you will need to put down a card that has the specified suit. If you have a Joker, then"
<< " you can put it. Enter 0 if you don't have such a compatible card.\n" << player << "\n";
break;
}
case Rank::Ace:
{
std::cout << "\nTop card from pile: " << _pile << "\nThis is an A card."<< player.name() <<"'s turn was skipped.\n";
break;
}
}
}
void Game::receiveCardsFromDeck(Player& player, const unsigned int& numberOfCards)
{
try
{
player.receive(_deck.dealCards(numberOfCards));
}
catch (const NotEnoughCardsException& error)
{
_deck.refill(_pile.releaseAllCardsButFirst());
player.receive(_deck.dealCards(numberOfCards));
}
catch (const EmptyDeckException& error)
{
_deck.refill(_pile.releaseAllCardsButFirst());
player.receive(_deck.dealCards(numberOfCards));
}
}
void Game::putCardToPile(Player& player, unsigned int& cardNumber)
{
_pile.addCard(player.putCard(cardNumber));
}
bool Game::isSpecialCard(const Card& card) const
{
return (_specialCards.find(card) != _specialCards.end());
}
const Player& Game::findWinner()
{
auto it{ std::find_if(_players.begin(), _players.end(),
[&](const Player& player) { return !player.numberOfCards(); }) };
return *it;
}
void Game::dealInitialCardsToEachPlayer()
{
std::for_each(_players.begin(), _players.end(),
[this](Player& player) {player.receive(_deck.dealCards(initialNumberOfCardsPerPlayer)); });
}
void Game::validatePlayerCardCompatibilityWithPileTopCard(Player& player, unsigned int& cardNumber)
{
while (!player.getCard(cardNumber).isCompatibleWith(_pile.getTopCard()))
{
std::cout << "This card is incompatible.\nEnter another card or enter 0 to skip your turn.\n";
std::cin >> cardNumber;
if (!cardNumber)
break;
}
}
void Game::normalCardPlayerTurn(Player& player)
{
unsigned int cardNumber{ 0 };
printInformationAboutThePlayerAndTheTopCardFromPile(player);
std::cin >> cardNumber;
if (!cardNumber)
receiveCardsFromDeck(player, defaultNumberOfCardsToPick);
else
{
validatePlayerCardCompatibilityWithPileTopCard(player, cardNumber);
if (!cardNumber)
receiveCardsFromDeck(player, defaultNumberOfCardsToPick);
else
putCardToPile(player, cardNumber);
}
if (!player.numberOfCards())
_isRunning = false;
}
void Game::specialCardPlayerTurn(Player& player)
{
unsigned int cardNumber{ 0 };
switch (_pile.getTopCard()._rank)
{
case Rank::Two:
{
printInformationAboutThePlayerAndTheTopCardFromPile(player, Rank::Two);
std::cin >> cardNumber;
if (!cardNumber)
receiveCardsFromDeck(player, neededCardsToPickForTwo);
else if (player.getCard(cardNumber)._rank == Rank::Four && player.getCard(cardNumber).isCompatibleWith(_pile.getTopCard()))
putCardToPile(player, cardNumber);
else
{
validatePlayerCardCompatibilityWithPileTopCard(player, cardNumber);
if (!cardNumber)
receiveCardsFromDeck(player, neededCardsToPickForTwo);
else
putCardToPile(player, cardNumber);
}
break;
}
case Rank::Three:
{
printInformationAboutThePlayerAndTheTopCardFromPile(player, Rank::Three);
std::cin >> cardNumber;
if (!cardNumber)
receiveCardsFromDeck(player, neededCardsToPickForThree);
else if (player.getCard(cardNumber)._rank == Rank::Four && player.getCard(cardNumber).isCompatibleWith(_pile.getTopCard()))
putCardToPile(player, cardNumber);
else
{
validatePlayerCardCompatibilityWithPileTopCard(player, cardNumber);
if (!cardNumber)
receiveCardsFromDeck(player, neededCardsToPickForThree);
else
putCardToPile(player, cardNumber);
}
break;
}
case Rank::Ace:
{
printInformationAboutThePlayerAndTheTopCardFromPile(player, Rank::Ace);
break;
}
}
if (!player.numberOfCards())
_isRunning = false;
}
void Game::run()
{
dealInitialCardsToEachPlayer();
_pile.addCard(_deck.dealCard());
while (isSpecialCard(_pile.getTopCard()))
{
_deck.refill(_pile.eraseTopCard());
_pile.addCard(_deck.dealCard());
}
Card lastCard{ _pile.getTopCard() };
bool specialCardHadEffect{ false };
while (_isRunning)
{
for (auto& currentPlayer : _players)
{
if (!isSpecialCard(_pile.getTopCard()))
normalCardPlayerTurn(currentPlayer);
else
{
if (isSpecialCard(_pile.getTopCard()) && !specialCardHadEffect)
{
specialCardPlayerTurn(currentPlayer);
specialCardHadEffect = true;
}
else
normalCardPlayerTurn(currentPlayer);
}
if (_pile.getTopCard() != lastCard)
{
specialCardHadEffect = false;
lastCard = _pile.getTopCard();
}
if (!_isRunning)
break;
}
}
std::cout << "\tThe winner is " << findWinner().name() << ".\n";
}
main.cpp
#include "Game.hpp"
#include <iostream>
int main()
{
unsigned int numberOfPlayers{ 0 };
std::cout << "Number of players.\n";
std::cin >> numberOfPlayers;
std::cin.ignore();
try
{
Game game(numberOfPlayers);
game.run();
}
catch (const InvalidNumberOfPlayersException& error)
{
std::cerr << error.what() << "\n";
std::cin >> numberOfPlayers;
Game game(numberOfPlayers);
game.run();
}
return 0;
}