5
\$\begingroup\$

For the longest time I always felt lost designing a robust OOP program, especially in C++. I felt like I didn't know enough theory or algorithms so I would study and never write code, not to mention I was always afraid of criticism or looking amateurish. Recently I came to the realization that I been holding myself back by doing this and moving beyond my insecurities. I have taken more leaps by writing common C++ programs to build my knowledge of the language and architecture. Everyone told me writing a Blackjack game would help build my knowledge purely for practice for more ambitious projects and contributing to open source projects that is hard for me to understand.

TL;DR: I been creating a Blackjack program to help me learn programming and design at a more advanced level.

I tried utilizing things I never used before such as namespaces, enums, and maps with operator overloading. Right now it's a work in progress: it only handles 2 players, the dealer logic hasn't been developed yet until I get the game working correctly, thinking about making a derived class for Blackjack rules in case I want to make poker rules. I already know this is a mess, but I guess it's part of the learning process, huh?

main.cpp

#include <iostream>
#include <string>
#include "BlackJack.h"
#include "Player.h"

int main() {
    BlackJack game(2); // only two players for now, requires user for both
    game.play();
    return 0;
}

CardInfo.h

#pragma once
#include <string>
#include <unordered_map>

namespace Cards {
    enum class Rank {
        ONE = 1, TWO, THREE,
        FOUR, FIVE, SIX,
        SEVEN, EIGHT, NINE,
        TEN, JACK, QUEEN, KING, ACE
    }; 
    enum class Suit { 
        CLUBS, DIAMONDS, HEARTS, SPADES 
    };
};

namespace CardData {
    // TODO make a derived card class specifically for Blackjack rules
    struct Card {
        Cards::Rank rank;
        Cards::Suit suit;

        // TODO define this so the map works
        bool operator<(const CardData::Card &card) const {
            return this->rank < card.rank;
        }

    };

}

Player.h

#pragma once
#include <vector>
#include <string>

#include "CardInfo.h"

class Player {
    int points{ 0 };
    int numAces{ 0 }; // count aces in order to deduce 1 or 11 logic in updateScore
    int id;
    std::vector<CardData::Card> hand;

public:
    Player(int ID);
    void dealtCard(CardData::Card &drawnCard);
    void updateScore(int score); // call after every dealt card
    bool requestHit(); // request to exchange cards
    bool hasBust();
    int getScore();
    int getId();
};

Player.cpp

#include "Player.h"
#include <string>
#include <iostream>

Player::Player(int ID) : id(ID) {}

void Player::dealtCard(CardData::Card &drawnCard) {
    //Card drawnCard{ Cards::Rank::ACE, Cards::Suit::SPADES }; remove possibly
    hand.push_back(drawnCard); // test insert
}

void Player::updateScore(const int score) {
    if (score == 11)
        numAces++;

    points += score;

    if (points > 21)
        while (numAces > 0) {
            numAces--;
            points--;
        }
}

bool Player::requestHit() {
    char answer;
    std::cout << "Player " << id << ": Hit or stick? [y|n]" << std::endl;
    std::cin >> answer;
    return tolower(answer) == 'y';
}

bool Player::hasBust() {
    return getScore() > 21;
}

int Player::getScore() { return points; }
int Player::getId() { return id; }

BlackJack.h

#pragma once
#include <set>
#include <random>
#include "Player.h"
#include <string>

class BlackJack {
    int numPlayers;
    static std::unordered_map<Cards::Rank, int> score; 

    std::vector<Player> players;
    std::set<CardData::Card> inPlay; // drawn cards inserted here to prevent duplication 

    CardData::Card drawCard();
    void hitOrStick();
    void playHands();
    int enumerateCard(const CardData::Card &card) const; // converts cards to score, deals with Aces and Royals
    void deal(Player &player); 

public:
    BlackJack(int players);
    void play();
};

BlackJack.cpp

#include "BlackJack.h"
#include <iostream>
#include <string>

std::unordered_map<Cards::Rank, int> BlackJack::score{
    {Cards::Rank::ONE, 1},{Cards::Rank::TWO, 2},{Cards::Rank::THREE, 3},
    {Cards::Rank::FOUR, 4},{Cards::Rank::FIVE, 5},{Cards::Rank::SIX, 6},
    {Cards::Rank::SEVEN, 7},{Cards::Rank::EIGHT, 8},{Cards::Rank::NINE, 9},
    {Cards::Rank::TEN, 10},{Cards::Rank::JACK, 10},{Cards::Rank::QUEEN, 10},
    {Cards::Rank::KING, 10},{Cards::Rank::ACE, 11}
};

BlackJack::BlackJack(int players = 2) : numPlayers(players) {
    for (int i = 0; i < numPlayers; i++) {
        this->players.push_back(Player(i)); 
    }
}

void BlackJack::play() {
    // Shuffle workflow and distribute cards
    for (Player player : players) {
        std::cout << "Player \'" << player.getId() << "\' These are your cards: " << std::endl;
        for (int i = 0; i < 2; i++) {
            deal(player);
        }
    }

    hitOrStick();
    playHands();
}

void BlackJack::deal(Player &player) {
    CardData::Card drawn;
    drawn = drawCard();
    while (inPlay.find(drawn) != inPlay.end())
        drawn = drawCard();

    std::cout << score[drawn.rank] << std::endl;
    player.dealtCard(drawn);
    player.updateScore(enumerateCard(drawn));
    inPlay.insert(drawn);
}

CardData::Card BlackJack::drawCard() {

    std::uniform_int_distribution<> suitDistrib(0, 3);
    std::uniform_int_distribution<> rankDistrib(1, 14);
    std::random_device rd;
    std::mt19937 generator(rd());
    int newRank = rankDistrib(generator);
    int newSuit = suitDistrib(generator);

    return CardData::Card{ static_cast<Cards::Rank>(newRank), static_cast<Cards::Suit>(newSuit) };
}

int BlackJack::enumerateCard(const CardData::Card &card) const {
    return score[card.rank];
}

void BlackJack::hitOrStick() {
    for (Player player : players) 
        while (!player.hasBust() && player.requestHit()) 
            deal(player);
}

void BlackJack::playHands() {
    int winner = -1;
    int maxScore = -1;
    bool push = false;

    for (Player player : players) {
        std::cout << "Player " << player.getId() << ": " << player.getScore() << std::endl;
        if (player.getScore() > maxScore) {
            push = false;
            winner = player.getId();
            maxScore = player.getScore();
        } else if (maxScore == player.getScore()) {
            push = true;
        }
    }

    if (push)
        std::cout << "Push!" << std::endl;
    else if (winner == 0)
        std::cout << "House wins!" << std::endl;
    else
        std::cout << "Player " << winner << " wins!" << std::endl;
}

Any advice on what I need to improve or design/clean code faux pas I'm breaking? If you also having any suggestions on how I should focus on my dealer AI or operator< overload, I really appreciate. Here's to getting better every day!

\$\endgroup\$
  • 3
    \$\begingroup\$ Right now it's a work in progress Does it work? \$\endgroup\$ – yuri Jun 7 at 20:47
  • \$\begingroup\$ @yuri each function works as intended, but to completion it doesn't. Right now I'm trying to figure out the overload for bool operator<(const CardData::Card &card) const so that the comparisons work correctly for the set and map containers. \$\endgroup\$ – RinSu Jun 7 at 20:52
  • 1
    \$\begingroup\$ Deja vu! I just reviewed another Blackjack game probably a week ago! I think I like your design better. Anyway, if you want to see how someone else tried to accomplish the same thing, here you go. \$\endgroup\$ – Chipster Jun 8 at 5:38
  • \$\begingroup\$ @Chipster thanks, just having other references is helpful enough to see what may work or be easier. \$\endgroup\$ – RinSu Jun 9 at 22:27
3
\$\begingroup\$

Recently I came to the realization that I been holding myself back by doing this and moving beyond my insecurities. I have taken more leaps by writing common C++ programs to build my knowledge of the language and architecture.

Learning by doing is a good approach IMO. Hopefully you can improve your game and submit it again for review!


main

Is Player.h needed in main?

Turn 2 into a named constant:

int constexpr players{2};
BlackJack game{players}; 

This makes it much clearer what is being passed to your game.

While some people really don't like this, you can omit return 0 from main if you don't depend on it. The compiler will generate it for you.


cardinfo

Personally I don't mind #pragma once, just remember it's not standard.

[Subjective] Don't indent after namespace.

[Subjective] Don't write more than one statement per line in your enums.

If you want to use an unordered container with a custom key you need to provide hash and compare functions. Have a look at this SO question and this link from cppreference.


player

Class interfaces should go from least restricted to most restricted (i.e. public, protected, private). Reason being that when someone reads your interface he is most likely interested in the public functions that can be worked with.

[Subjective] Don't omit braces as that can lead to bugs down the line. E.g.:

// bad
if (foo) 
    // code here

// good
if (foo)
{
    // code here
}

Prefer prefix (++foo) over postfix (foo++) operator.

Prefer using \n over std::endl

I'm not familiar with blackjack but if you ask "hit or stick" should the answer options really be "yes/no" not maybe "hit/stick"?


blackjack

Reuse your RNG. Have a look at this question to see how it could be done.

You can and should use brace initialization when possible.

\$\endgroup\$
  • \$\begingroup\$ Thank you @yuri this was more than I was expecting. This should put me on the right track. This pretty much covers everything I been having uncertainty with. I'll be sure to submit my updates again. \$\endgroup\$ – RinSu Jun 10 at 16:49
  • \$\begingroup\$ I guess I'm in the crowd that doesn't like not returning from main. I think doing that might technically be undefined behavior, although since it's main, there may be an exception for that. I don't remember. \$\endgroup\$ – Chipster Jun 14 at 15:36
  • \$\begingroup\$ Nvm. I found where it said that in the standard. I guess I still don't like it, though :) \$\endgroup\$ – Chipster Jun 14 at 15:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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