4
\$\begingroup\$

I developed a blackjack game. I am relatively new to c++ and OOP, so I am asking for feedback on what could be improved. This game features betting, taking a card, standing, and splitting a hand. Besides, it also has a dealer that takes when his hand is under 17 and stands when it is over.

#include <bits/stdc++.h>

using namespace std;

class Card {
    private:
        int cardNum;
        int cardType;
        int value;
        string name;
    public:
        Card(int ct, int cn);
        ~Card() {}
        int getType() {return cardType;}
        int getNum() {return cardNum;}
        int getVal() {return value;}
        string getName() {return name;}
};

Card::Card(int ct, int cn) : cardNum(cn), cardType(ct) {
    if(cardNum == 1) {
        value = 11;
    } else if(cardNum >= 10) {
        value = 10;
    } else value = cardNum;
    switch(cardNum) {
        case 1 :
            name = "ace";
            break;
        case 11 :
            name = "jack";
            break;
        case 12 :
            name = "queen";
            break;
        case 13 :
            name = "king";
            break;
        default:
            name = to_string(cardNum);
            break;
    }
    switch(cardType) {
        case 1 :
            name.append(" of clubs");
            break;
        case 2 :
            name.append(" of diamonds");
            break;
        case 3 :
            name.append(" of hearts");
            break;
        case 4 :
            name.append(" of spades");
            break;
    }
}

class Deck {
    private:
        const int deckSize = 52;
        vector<Card*> deck;
        int pos;
    public:
        Deck();
        ~Deck();
        void shuffle(); //{random_shuffle(deck.begin(), deck.end());}
        Card* getCard();
};

Deck::Deck() : pos(0) {
    deck.reserve(deckSize);
    for(int i = 1; i <= 4; i++) {
        for(int j = 1; j <= 13; j++) {
            deck.push_back(new Card(i, j));
        }
    }
}

Deck::~Deck() {
    for(auto i : deck) {
        delete i;
    }
}

void Deck::shuffle() {
    for(long long unsigned i = 0; i < deck.size(); i++) {
        swap(deck[i], deck[rand()%deck.size()]);
    }
}

Card* Deck::getCard() {
    if(pos >= deckSize) {
        shuffle();
        pos = 0;
    }
    return deck[pos++];
}

class Hand {
    private:
        Deck* deck;
        vector<Card*> cards;
        int points;
        unordered_set<Card*> aces;
    public:
        Hand(Deck* d) : deck(d), points(0) {}
        ~Hand() {}
        void clearHand();
        void addCard();
        void addSpecificCard(Card* c);
        int getPoints() {return points;}
        Card* removeCard();
        vector<Card*> getCards() {return cards;}
};

void Hand::clearHand() {
    points = 0;
    aces = unordered_set<Card*>();
    cards = vector<Card*>();
}

void Hand::addCard() {
    cards.push_back(deck->getCard());
    if(cards.back()->getNum() == 1) aces.insert(cards.back());
    points += cards.back()->getVal();
    if(points > 21) {
        while(!aces.empty() && points > 21) {
            points -= 10;
            aces.erase(aces.begin());
        }
    }
}

void Hand::addSpecificCard(Card* c) {
    cards.push_back(c);
    if(c->getNum() == 1) aces.insert(c);
    points+=c->getVal();
}

Card* Hand::removeCard() {
    Card* lastCard = cards.back();
    points -= lastCard->getVal();
    if(lastCard->getNum() == 1) aces.erase(lastCard);
    cards.pop_back();
    return lastCard;
}

class Dealer{
    private:
        Hand* hand;
    public:
        Dealer(Deck* d) : hand(new Hand(d)) {}
        ~Dealer() {delete hand;}
        void getNewHand() {hand->clearHand();}
        void addCard() {return hand->addCard();}
        string makeTurn();
        int getScore() {return hand->getPoints();}
        Hand* getHand() {return hand;}
};

string Dealer::makeTurn() {
    if(hand->getPoints() < 17) {
        hand->addCard();
        return "hit";
    }
    return "stand";
}

class House {
    private:
        Deck* deck;
        vector<Hand*> hands;
        Dealer* dealer;

    private:
        float playNewGame();

    public:
        House() : deck(new Deck()), dealer(new Dealer(deck)) {}
        ~House() {delete deck; delete dealer;}
        void playBlackJack();
};

float House::playNewGame() {
    deck->shuffle();
    Hand* curHand = new Hand(deck);
    hands.push_back(curHand);

    hands[0]->addCard();
    hands[0]->addCard();
    dealer->addCard();

    cout << "DEALERS first card: ";
    cout << dealer->getHand()->getCards().back()->getName() << endl;

    dealer->addCard();

    if(hands[0]->getPoints() ==  21) {
        if(dealer->getScore() == 21) {
            cout << "YOU and the DEALER have TIED ";
            for(auto i : hands[0]->getCards()) cout << i->getName() << "; ";
            cout << endl;
            for(auto i :  hands) {
                delete i;
            }
            hands.clear();
            dealer->getNewHand();
            return 1;
        } else {
            cout << "YOU have WON with hand: ";
            for(auto i : hands[0]->getCards()) cout << i->getName() << "; ";
            cout << endl;
            for(auto i :  hands) {
                delete i;
            }
            hands.clear();
            dealer->getNewHand();
            return 1.5;
        }
    }

    unordered_set<Hand*> validHands;

    validHands.insert(curHand);

    float multiplier = 0;

    string answer = "placeholder";

    long long unsigned iter = 0;

    while((answer != "no") && (iter < hands.size())) {
        if((hands[iter]->getCards()[0]->getNum() != 1) &&
           (hands[iter]->getCards()[1]->getNum() != 1) &&
           (hands[iter]->getCards()[0]->getNum() == hands[iter]->getCards()[1]->getNum())) {

            cout << "YOUR HAND: ";
            for(auto i : hands[iter]->getCards()) cout << i->getName() << "; ";
            cout << "SCORE = " << hands[0]->getPoints() << endl;
            answer = "placeholder";
            while(answer != "yes" && answer != "no") {
                cout << "split? yes/no" << endl;
                cin >> answer;
            }
            if(answer == "yes") {
                Hand* newHand = new Hand(deck);
                newHand->addSpecificCard(curHand->removeCard());
                newHand->addCard();
                curHand->addCard();
                hands.push_back(newHand);
                curHand = newHand;
                validHands.insert(curHand);
            }
        }
        iter++;
    }

    answer = "hit";

    bool isUnder = false;

    while(answer == "hit" || answer != "stand") {
        auto hand = validHands.cbegin();
        while(hand != validHands.end()) {
            cout << " YOUR HAND: ";
            for(auto i : (*hand)->getCards()) cout << i->getName() << "; ";
            cout << "with SCORE = " << (*hand)->getPoints() << endl;
            cout << "hit? hit/stand" << endl;
            cin >> answer;
            if(answer == "hit") {
                (*hand)->addCard();
                if((*hand)->getPoints() > 21) {
                    cout << "YOU have LOST with hand: ";
                    for(auto i : (*hand)->getCards()) cout << i->getName() << "; ";
                    cout << "with SCORE = " << (*hand)->getPoints() << endl;
                    hands.erase(find(hands.begin(), hands.end(), *hand));
                    multiplier -= 2;
                    hand = validHands.erase(hand);
                    if(hand == validHands.end()) answer = "stand";
                } else if((*hand)->getPoints() == 21) {
                    cout << "YOU have hand: ";
                    for(auto i : (*hand)->getCards()) cout << i->getName() << "; ";
                    cout << "with SCORE = " << (*hand)->getPoints() << endl;
                    hand = validHands.erase(hand);
                    isUnder = true;
                    if(hand == validHands.end()) answer = "stand";
                }
            } else hand++;
        }
    }

    if(validHands.empty() && !isUnder) return multiplier;

    answer = "hit";
    while(answer != "stand")
        answer = dealer->makeTurn();

    if(dealer->getScore() > 21) {
        cout << "DEALER has LOST with hand: ";
        for(auto i : dealer->getHand()->getCards()) cout << i->getName() << "; ";
        cout << "with SCORE: " << dealer->getScore() << endl;
        for(auto i :  hands) {
            delete i;
        }
        multiplier += 2* hands.size();
        hands.clear();
        dealer->getNewHand();
        return multiplier;
    }

    for(auto hand : hands) {
        cout << "your hand with hand SCORE = " << hand->getPoints() << " has ";
        if(dealer->getScore() == hand->getPoints()) {
            cout << "TIED with dealers hand: ";
            for(auto i : dealer->getHand()->getCards()) cout << i->getName() << "; ";
            cout << "with SCORE: " << dealer->getScore() << endl;
        } else {
            if(hand->getPoints() > dealer->getScore()) {
                cout << "WON";
                multiplier += 2;
            } else {
                cout << "LOST";
                multiplier -= 2;
            }
            cout << " against the dealers hand: ";
            for(auto i : dealer->getHand()->getCards()) cout << i->getName() << "; ";
            cout << "with SCORE: " << dealer->getScore() << endl;
        }
    }
    for(auto i :  hands) {
        delete i;
    }
    hands.clear();
    dealer->getNewHand();
    return multiplier;
}

void House::playBlackJack() {
    float totalMoney = 1000;
    cout << "your total money = " << totalMoney << endl;
    string cashOut = "no";
    while((totalMoney > 0) && (cashOut == "no" || cashOut != "yes")) {
        int bet = INT_MAX;
        while(bet > totalMoney) {
            cout << "bet number?" << endl;
            cin >> bet;
        }
        totalMoney += bet*playNewGame()/2;
        cout << "your total money = " << totalMoney << endl;
        if(totalMoney > 0) {
            cout << "cash out? yes/no" << endl;
            cin >> cashOut;
        }
    }
}

int main()
{
    srand((time(0)));
    House h;
    h.playBlackJack();
    return 0;
}
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

General Observations

Playing is fun. There one upgrade could be allowing the player to double down their bet. The program could use some error handling, when the program prompted me with "cash out? yes/no" I entered y for yes, and it took me into another hand, it should either report an error or accept y for yes. Some other interesting options might be to allow the player to play 2 hands against the dealer. The blackjack tables in Las Vegas allow 5 or 6 players at a time, you could allow for other players at the table.

Have a minimum bet, I bet zero once and it was allowed.

Real casinos very rarely have one deck blackjack games, the shoe used at most Las Vegas casinos contains 6 decks. This prevents card counting, which is one way to cheat at blackjack. The dealing machines use more decks than 6.

General Code Observations

The classes are broken up in good manner.

Code Observations

Avoid using namespace std;

If you are coding professionally you probably should get out of the habit of using the using namespace std; statement. The code will more clearly define where cout and other identifiers are coming from (std::cin, std::cout). As you start using namespaces in your code it is better to identify where each function comes from because there may be function name collisions from different namespaces. The identifiercout you may override within your own classes, and you may override the operator << in your own classes as well. This stack overflow question discusses this in more detail.

Include Files

I generally only see #include <bits/stdc++.h> on programming challenge sites, where they want to allow the users to use any standard template library include, professionals will only include what they need to include. I was able to get the program to compile with the following:

#include <iostream>
#include <string>
#include <vector>
#include <unordered_set>

One of the reasons for including only what is necessary is that it allows others who have to maintain the code to know what they are dealing with. A second reason is that #include means exactly that it is including a file into the current file, the more that gets including, the longer the compile time is. To keep build times down only include what is necessary.

Code and File Organization

It seems that all of the code is currently in one file, while this works in C++, multiple classes in a single file doesn't work in some other object oriented languages. A better reason to break the code into multiple files, one header and one source for each class is that it is easier to maintain the code. Header files provide the interface and should rarely change, C++ source files may change often for bug fixes or for additional functionality. There is no reason to recompile the house class when you make changes in card.cpp, you just need to re-link. Having one class per header file and source file allows whoever needs to maintain the code to immediately find the code they need to update.

Complexity

The function House::playNewGame() is too complex (does too much). It is 158 lines of code, that is almost 3 screens. A general rule in programming is that no function should be larger than a single screen (usually about 55 lines of code). Anything larger than one screen is very difficult to maintain because you can't see what is going on in the rest of the function. It is also important to limit the scope of the variables. There are several logical blocks of code in playNewGame() that could become their own functions.

There is also a programming principle called the Single Responsibility Principle that applies here. The Single Responsibility Principle states:

that every module, class, or function should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by that module, class or function.

Due to the complexity of the function House::playNewGame() it is very hard to review, I may have missed some things.

Object Oriented Programming Principles

SOLID is 5 object oriented design principles. SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable. This will help you design your objects and classes better.

  1. The Single Responsibility Principle - A class should only have a single responsibility, that is, only changes to one part of the software's specification should be able to affect the specification of the class.
  2. The Open–closed Principle - states software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
  3. The Liskov Substitution Principle - Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
  4. The Interface segregation principle - states that no client should be forced to depend on methods it does not use.
  5. The Dependency Inversion Principle - is a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.