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!
1 Answer 1
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.
-
\$\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– RinSu2019年06月10日 16:49:22 +00:00Commented Jun 10, 2019 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– Chipster2019年06月14日 15:36:03 +00:00Commented Jun 14, 2019 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– Chipster2019年06月14日 15:46:34 +00:00Commented Jun 14, 2019 at 15:46
Explore related questions
See similar questions with these tags.
Right now it's a work in progress
Does it work? \$\endgroup\$bool operator<(const CardData::Card &card) const
so that the comparisons work correctly for the set and map containers. \$\endgroup\$