3
\$\begingroup\$

Bist-o-yek (21) is a Persian card game very similar to blackjack, except it doesn't have things like double down and split. I did, though, change it a little. For example, added dealer dealing to 17.

First, there's a class called Cards that carries each card's name and score. Then, some functions for drawing, shuffling and dealing. At the end, a function called blackjack() bringing all together.

This code is a little clumsy and doesn't have input validation so be careful!

// BlackJack.cpp : Created by Chubak Bidpaa at 11/18/2018
//


#include <iostream>
#include <vector>
#include <string>
#include <ctime>
#include <algorithm>
#include <random>
#include <array>
#include <numeric>

struct Card
{

    std::string name;
    int score;

    Card(std::string card_name, int card_score) : name(card_name), score(card_score) {}
};

typedef std::vector<Card> card_vec;

card_vec deck()
{
    card_vec return_deck({ Card("Ace of Clubs", 11), Card("Ace of Diamods", 11),
                            Card("Ace of Hearts", 11), Card("Ace of Spades", 11),
                            Card("Two of Clubs", 2), Card("Two of Diamonds", 2),
                            Card("Two of Hearts", 2), Card("Two of Spades", 2),
                            Card("Three of Clubs", 3), Card("Three of Diamonds", 3),
                            Card("Three of Hearts", 3), Card("Three of Spades", 3),
        Card("Four of Clubs", 4), Card("Four of Diamonds", 4),
        Card("Four of Hearts", 4), Card("Four of Spades", 4),
        Card("Five of Clubs", 5), Card("Five of Diamonds", 5),
        Card("Five of Hearts", 5), Card("Five of Spades", 5),
        Card("Six of Clubs", 6), Card("Six of Diamonds", 6),
        Card("Six of Hearts", 6), Card("Six of Spades", 6),
        Card("Seven of Clubs", 7), Card("Seven of Diamonds", 7),
        Card("Seven of Hearts", 7), Card("Seven of Spades", 7), 
        Card("Eight of Clubs", 8), Card("Eight of Diamonds", 8),
        Card("Eight of Hearts", 8), Card("Eight of Spades", 8), 
        Card("Nine of Clubs", 9), Card("Nine of Diamonds", 9),
        Card("Nine of Hearts", 9), Card("Nine of Spades", 9),
        Card("Ten of Clubs", 10), Card("Ten of Diamonds", 10),
        Card("Ten of Hearts", 10), Card("Ten of Spades", 10), 
        Card("Jack of Clubs", 10), Card("Jack of Diamonds", 10),
        Card("Jack of Hearts", 10), Card("Jack of Spades", 10), 
        Card("Queen of Clubs", 10), Card("Queen of Diamonds", 10),
        Card("Queen of Hearts", 10), Card("Queen of Spades", 10), 
        Card("King of Clubs", 10), Card("King of Diamonds", 10),
        Card("King of Hearts", 10), Card("King of Spades", 10) });

    return return_deck;
}

int random(int min, int max)
{
    auto seed = std::time(NULL);
    std::mt19937 engine(seed);
    std::uniform_int_distribution<int> dist(min, max);

    return dist(engine);
}


void shuffle(card_vec &adeck)
{
    std::random_shuffle(adeck.begin(), adeck.end());
}

Card draw(card_vec &adeck)
{
    if (adeck.size() > 1)
    {
        shuffle(adeck);
    }

    int index = random(0, adeck.size() - 1);
    Card return_card = adeck[index];
    adeck.erase(adeck.begin() + index);

    return return_card;
}

void deal(card_vec &adeck, card_vec &dealers_cards, card_vec &player_cards)
{
    if (dealers_cards.size() == 0 && player_cards.size() == 0)
    {
        Card dealer_1 = draw(adeck);
        dealers_cards.push_back(dealer_1);
        std::random_shuffle(adeck.begin(), adeck.end());
        Card dealer_2 = draw(adeck);
        dealers_cards.push_back(dealer_2);
        Card player_1 = draw(adeck);
        player_cards.push_back(player_1);
        std::random_shuffle(adeck.begin(), adeck.end());
        Card player_2 = draw(adeck);
        player_cards.push_back(player_2);

    }
    else
    {
        Card dealer_3 = draw(adeck);
        dealers_cards.push_back(dealer_3);
    }
}

std::string blackjack()
{
    card_vec player_cards;
    card_vec dealer_cards;
    card_vec adeck = deck();
    std::vector<int> player_scores;
    std::vector<int> dealer_scores;
    int input;

    deal(adeck, dealer_cards, player_cards);

    for (int i = 0; i < 2; ++i)
    {
        player_scores.push_back(player_cards[i].score);
        dealer_scores.push_back(dealer_cards[i].score);
    }

    auto player_card_1 = player_cards[0].name;
    auto player_card_2 = player_cards[1].name;
    auto dealer_card_1 = dealer_cards[0].name;
    auto dealer_card_2 = dealer_cards[1].name;

    std::cout << "Your cards are: " << player_card_1 << " and " << player_card_2 << std::endl;
    std::cout << "The dealer reveals: " << dealer_card_1 << std::endl;
    std::cout << "Press 1 to Hit or Press 2 to Stay" << std::endl;
    std::cin >> input;

    while (std::accumulate(player_scores.begin(), player_scores.end(), 0) <= 21)
    {
        if (input == 1)
        {
            Card new_card = draw(adeck);
            std::cout << "You were dealt a " << new_card.name << std::endl;
            player_scores.push_back(new_card.score);
            std::cin >> input;
        }
        else if (input == 2)
        {
            break;
        }

    }

    if (std::accumulate(player_scores.begin(), player_scores.end(), 0) > 21)
        return "You're busted!";
    else if (std::accumulate(player_scores.begin(), player_scores.end(), 0) == 21)
        return "Perfect win!";

    std::cout << "The dealer reveals: " << dealer_card_2 << std::endl;

    if (std::accumulate(dealer_scores.begin(), dealer_scores.end(), 0) > 17)
    {
        if (std::accumulate(dealer_scores.begin(), dealer_scores.end(), 0) >
            std::accumulate(player_scores.begin(), player_scores.end(), 0))
        {
            return "You lose!";
        }
        else
        {
            return "You win!";
        }

    }
    else
    {
        while (std::accumulate(dealer_scores.begin(), dealer_scores.end(), 0) <= 17)
        {
            Card new_card = draw(adeck);
            std::cout << "Dealer reveals: " << new_card.name << std::endl;
            dealer_scores.push_back(new_card.score);
        }
    }

    if (std::accumulate(dealer_scores.begin(), dealer_scores.end(), 0) >
        std::accumulate(player_scores.begin(), player_scores.end(), 0))
    {
        return "You lose!";
    }
    else
    {
        return "You win!";
    }


}

int main()
{
    std::cout << blackjack() << std::endl;

    return 0;
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Being consistent

You have inconsistency between Description, filename and function name.

  • in your title and in your description, you talk about Bist-o-yek a card game similar to Blackjack
  • your main function and your file are just named Blackjack.

Try to be consistent between all your parts or you might lose users because of conflicting information. People can have to deal with unexpected behaviors because they might think that's a regular BlackJack.


Coding idiomatically

  • Prefer usingover typedef</u>:

    • the Standard recommended way.
    • more consistent with variables assignment (name on the left of the equa signl, attributes on the right)
    • more consistent with case where you make template aliases
    • make going from type alias to template alias easier.

    But, we'll see later that this alias is not even necessary

  • Try to don't use deprecated function :

    The std::random_shuffle is deprecated in C++14 and even removed in C++17. Instead you should use std::shuffle. (and if possible, with a good seeding, like explained here)

  • Consider inputs ill-formed

    Even if you said you knew it, you should always check the inputs as ill-formed. Especially that it's easy to check.

  • Avoid std::endl;

    It send the character '\n' to the stream and then flush the stream. If you want a flush, do it explicitly : .... << '\n' << std::flush;.

Going lazy for more efficiency

You do a lot of things for nothing:

  • Why shuffling each times the deck when drawing a card? Since the deck is already shuffled, just take the last card. Plus bonus, it's cheaper to remove the last element in a vector than a random one.
  • Why re-shuffling again two times in the deal() method
  • Give directly the draw()result to push_back, it will be cleaner and avoid temporaries.
  • You std::accumulate many many times even when player_score or dealer_score didn't change.

Defining et rethinking entities

Card

What's a Card?

A card is a set of two things:

  • the Suit, a value among a enumeration of four possibilities (Club, Diamond, Heart, Spade)
  • the Rank, depending on how you imagine it:
    • a value in the range of 1 to 13
    • a value among a enumeration of thirteen possibilities (As, Two, ..., Queen, King)
Displaying

A card may be displayed it in many ways:

  • Rank: full names (As, Two, Three, ..., Queen, King) or short names (A, 2, 3, ..., Q, K)
  • Suits : full names (Club, Diamond, Heart, Spade), first letter (C, D, H, S) or unicode symbols.
  • Card: Combination of theses possibilities, with maybe some decoration (Queen of Hearts, [1 **'s], ...)

In your current code, a card isn't really a card. It's just a string with a value associated and if you want to change to display of a card, you have to change a lot of things in your code. Making it more generic can make it less painful and more explicit.

struct Card {
    enum class Suit {
        Heart, Club, Spade, Diamond, INVALID
    };
    enum class Rank {
        Ace = 1, Two, Three, Four, Five, Six, Seven,
        Eight, Nine, Ten, Jack, Queen, King, INVALID
    };

    constexpr Card() : rank(Rank::INVALID), suit{Suit::INVALID} {}
    constexpr Card(Rank rank, Suit suit) : rank{rank}, suit{suit} {}

    bool valid() { return rank != Rank::INVALID && suit != Suit::INVALID; }

    Rank rank;
    Suit suit;
};

std::string to_string(Card::Rank rank) {
    switch (rank) {
        case Card::Rank::Ace:   return "A";
        case Card::Rank::Two:
        case Card::Rank::Three:
        case Card::Rank::Four:
        case Card::Rank::Five:
        case Card::Rank::Six:
        case Card::Rank::Seven:
        case Card::Rank::Eight:
        case Card::Rank::Nine:
        case Card::Rank::Ten:
            using std::to_string;
            return to_string(static_cast<std::underlying_type_t<Card::Rank>>(rank));
        case Card::Rank::Jack:  return "J";
        case Card::Rank::Queen: return "Q";
        case Card::Rank::King:  return "K";
        default: throw std::range_error("Rank is out of range");
    }
}
std::string to_string(Card::Suit suit) {
    switch (suit) {
        case Card::Suit::Heart:   return "\u2665";
        case Card::Suit::Club:    return "\u2667";
        case Card::Suit::Spade:   return "\u2664";
        case Card::Suit::Diamond: return "\u2666";
        default: throw std::range_error("Suit is out of range");
    }
}
std::string to_string(Card card) {
    return to_string(card.rank) + to_string(card.suit);   
}

Here, the two INVALID enum values and the `valid() method is just for convenience.

Deck

What's a Deck?

Some bunch of cards, with all cards possibles at beginning. Then, we draw cards from this deck, one by one, until no more. If it's not empty, we can also shuffle cards. We can also reset the deck to get a new complete one, shuffled.

Here, advantages are that if later, you want to do more rounds, without restarting the program, you don't have to rebuild the Deck, you just copy the static one, compile time computed, and shuffle it.

class Deck {
  public:
    Deck()
        : cards{new_deck.begin(), new_deck.end()}
    {
        shuffle();
    }
    void reset() {
        cards = {new_deck.begin(), new_deck.end()};
        shuffle();
    }
    bool empty() const {
        return cards.empty();
    }    
    Card draw() {
        if (empty()) return {Card::Rank::INVALID, Card::Suit::INVALID};
        Card last = cards.back();
        cards.pop_back();
        return last;
    }        
    void shuffle() {
        if (empty()) return;
        // from https://clang.llvm.org/extra/clang-tidy/checks/modernize-replace-random-shuffle.html
        std::shuffle(cards.begin(), cards.end(), []() {
            std::mt19937::result_type seeds[std::mt19937::state_size];
            std::random_device device;
            std::uniform_int_distribution<typename std::mt19937::result_type> dist;
            std::generate(std::begin(seeds), std::end(seeds), [&] { return dist(device); });
            std::seed_seq seq(std::begin(seeds), std::end(seeds));
            return std::mt19937(seq);
        }());
    }
  private:
    using R = Card::Rank;
    using S = Card::Suit;
    static constexpr std::array<Card, 52> new_deck {{
        {R::Ace,   S::Heart}, {R::Ace,   S::Club}, {R::Ace,   S::Spade}, {R::Ace,   S::Diamond},
        {R::Two,   S::Heart}, {R::Two,   S::Club}, {R::Two,   S::Spade}, {R::Two,   S::Diamond},
        {R::Three, S::Heart}, {R::Three, S::Club}, {R::Three, S::Spade}, {R::Three, S::Diamond},
        {R::Four,  S::Heart}, {R::Four,  S::Club}, {R::Four,  S::Spade}, {R::Four,  S::Diamond},
        {R::Five,  S::Heart}, {R::Five,  S::Club}, {R::Five,  S::Spade}, {R::Five,  S::Diamond},
        {R::Six,   S::Heart}, {R::Six,   S::Club}, {R::Six,   S::Spade}, {R::Six,   S::Diamond},
        {R::Seven, S::Heart}, {R::Seven, S::Club}, {R::Seven, S::Spade}, {R::Seven, S::Diamond},
        {R::Eight, S::Heart}, {R::Eight, S::Club}, {R::Eight, S::Spade}, {R::Eight, S::Diamond},
        {R::Nine,  S::Heart}, {R::Nine,  S::Club}, {R::Nine,  S::Spade}, {R::Nine,  S::Diamond},
        {R::Ten,   S::Heart}, {R::Ten,   S::Club}, {R::Ten,   S::Spade}, {R::Ten,   S::Diamond},
        {R::Jack,  S::Heart}, {R::Jack,  S::Club}, {R::Jack,  S::Spade}, {R::Jack,  S::Diamond},
        {R::Queen, S::Heart}, {R::Queen, S::Club}, {R::Queen, S::Spade}, {R::Queen, S::Diamond},
        {R::King,  S::Heart}, {R::King,  S::Club}, {R::King,  S::Spade}, {R::King,  S::Diamond},
    }};
    std::vector<Card> cards;
};
constexpr std::array<Card, 52> Deck::new_deck;

Hand

Both dealer and player have a bunch of card called a hand. Each times they take a new card drawing the deck, the points of this card being added to the score of their hand.

struct Hand {
    void take(Card card)
    {
        switch (card.rank) {
            case Card::Rank::Ace:
                score += 11;
                break;
            case Card::Rank::Two:
            case Card::Rank::Three:
            case Card::Rank::Four:
            case Card::Rank::Five:
            case Card::Rank::Six:
            case Card::Rank::Seven:
            case Card::Rank::Eight:
            case Card::Rank::Nine:
                score += static_cast<std::underlying_type_t<Card::Rank>>(card.rank);
                break;
            case Card::Rank::Ten:
            case Card::Rank::Jack:
            case Card::Rank::Queen:
            case Card::Rank::King:
                score += 10;
                break;
            default: throw std::range_error("Hand::take() - Rank is out of range");
        }
        cards.push_back(card);
    }
    int score{};
    std::vector<Card> cards{};
};

Example of usage:

auto deck = Deck{};

Hand dealer;
Hand player;

dealer.take(deck.draw());
player.take(deck.draw());
player.take(deck.draw());

std::cout << "You got: " ;
for(auto card : player.cards) {
   std::cout << to_string(card) << ' ';
}
std::cout << "(score: " << player.score <<")";
dealer.take(deck.draw());
//....

End Words

Try to don't return the final string to the main. Instead display it from your function. Or Even better, write a outputting function, and you pass it all output instead of calling explicitly std::cout << .. everywhere in your code, so, if you have to change output method, you only have to do in one place.

I wasn't exhaustive, but I tried to talk about most important things.

If you go on C++17 you can benefit of some features:
  • Returning std::optional<Card> the the draw() function, making the return type more explicit.

  • You can simplify the new_deck definition:

.    

static constexpr auto new_deck = [] {
    constexpr auto suits = std::array {
        Card::Suit::Heart, Card::Suit::Club, Card::Suit::Spade, Card::Suit::Diamond
    };
    constexpr auto ranks = std::array {
        Card::Rank::Ace,  Card::Rank::Two,   Card::Rank::Three, Card::Rank::Four, Card::Rank::Five,
        Card::Rank::Six,  Card::Rank::Seven, Card::Rank::Eight, Card::Rank::Nine, Card::Rank::Ten,
        Card::Rank::Jack, Card::Rank::Queen, Card::Rank::King
    };        
    std::array<Card, suits.size() * ranks.size()> cards{};
    for (std::size_t i = 0; i < cards.size(); ++i) {
        cards[i] = {ranks[i % ranks.size()], suits[i % suits.size()]};
    }
    return cards;
}();

What's next?

  • Sanitize inputs
  • Maybe try to implement the full game
  • With multiples rounds
  • Allowing bets
  • With multi-player
  • Also, I searched on google and didn't found talk relating this variant. Maybe a way to display the rules to users can be helpful.
\$\endgroup\$
1
  • \$\begingroup\$ Thank you. My father taught me this game, I'm not sure maybe it was one of his concoctions? \$\endgroup\$ Commented Nov 19, 2018 at 8:49

Your Answer

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

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