14
\$\begingroup\$

To try and practice OOP, I've heard making a blackjack game can be really helpful; which it has been. I don't know how 'advanced' an implementation this is; but it has all the standard features, like being able to split/double. as well as actual card representations displayed in the terminal (made from ASCII characters). while my code does in fact in work, id love if somebody can take a look at it; if there's any bad practice going on, and what I can improve. Anything that can help me learn is more then welcome, so please don't hold back. I divided into two files: blackjack.py and visuals.py which has a variety of ways to display actual cards: at first it was all in one file, but I thought it would make more sense to split them into two for organization: each with its own clear purpose. is this frowned upon (making many files) or is that encouraged?

(I hope its okay that I link the GitHub BlackJack game click here as it would be a few hundred lines to copy and paste here.

The player has ability to wager on each hand, and keep playing until balance hits $0.0. As a future project, I'm thinking of maybe having a way to have user provide a login, to remember the balance/ and they can pick off where they left (stored in a database) but that's for the future. I also am new to GitHub, so I welcome any advice on how to utilize the readme: how descriptive I should or should not be/what's worth including etc.

I wonder if including assert to ensure the correct type is poorly used or if it is good practice, as it certainly can help in debugging. What is the standard when the project is done- are they left in (seen a few times in the Player class)?

I also wonder if I used super().__init__() correctly, when it came to inheritance. I provided an example screenshot of what the game would look like.

GitHub: BlackJack game click here

Follow-up post of Blackjack game

import random
import collections
import time
import os
import visuals

"""
BLACKJACK GAME:
visuals file imported: numerous pretty ways to display cards
"""

def clear():
    os.system('cls' if os.name == 'nt' else 'clear')

def validate_answer(question, choices):
    while True:
        answer = input(question)
        if answer in choices:
            return answer == choices[0]

yes_no   = ['y', 'n']


Card = collections.namedtuple('Card', ['value', 'suit'])

class Deck:

    values = [str(v) for v in range(2, 11)] + list('JQKA')
    suits = "Spades Diamonds Hearts Clubs".split()
    suit_symbols = ['♠','♦','♥','♣']

    def __init__(self, num_decks = 1):
        self.cards = [Card(value, suit) for suit in self.suits for value in self.values] * num_decks
        self.length = len(self)

    def __repr__(self):
        deck_cards = "Deck()\n"
        for card in self.cards:
            deck_cards += f"({card.value}-{card.suit})"
        return deck_cards

    def __len__(self):
        return len(self.cards)

    def __getitem__(self, position):
        return self.cards[position]

    def draw_card(self):
        return self.cards.pop()    

    def shuffle(self):
        random.shuffle(self.cards)
    
    #Shuffle when deck is < 50% full length
    def is_shuffle_time(self):
        return  len(self) < (self.length / 2)
    
    def shuffle_time(self):
        print("Reshuffling the Deck...\n")
        time.sleep(1)
        print("Reshuffling the Deck...\n")
        time.sleep(1)
        print("Reshuffling the Deck...\n")
        time.sleep(1)
        self.reset()
        self.shuffle()    

    def reset(self):
        self.cards = [Card(value, suit) for suit in self.suits for value in self.values] * num_decks

class Hand:

    def __init__(self):
        self.hand = []

    def __repr__(self):
        hand_cards = "Hand()\n"
        for card in self.hand:
            hand_cards += f"({card.value}-{card.suit})"
        return hand_cards

    def add_card(self, *cards):
        for card in cards:
            self.hand.append(card)

    def remove_card(self):
        return self.hand.pop()

    def hit(self, deck):
        assert isinstance(deck, Deck)
        card = deck.draw_card()
        self.add_card(card)

    def hand_score(self):
        self.card_val = [10 if card.value in ['J','Q','K'] else 1 if card.value == 'A'
                        else int(card.value) for card in self.hand]

        self.card_scores = dict(zip(self.hand, self.card_val))
        score = 0
        for card in self.hand:
            card_score = self.card_scores[card]
            score += card_score
        if any(card.value == 'A' for card in self.hand) and score <= 11:
            score += 10

        return score

    def card_visual(self):
        card_list = []
        for card in self.hand:
            card_vis = visuals.reg_card_visual(card)
            card_list.append(card_vis)
        visuals.print_cards(card_list)
        print(f"\nTotal of: {self.hand_score()}\n")
        time.sleep(1)

    def mini_card_visual(self):
        card_list = []
        for card in self.hand:
            card_vis = visuals.mini_card_visual(card)
            card_list.append(card_vis)
        visuals.print_cards(card_list)
        print(f"\nTotal of: {self.hand_score()}\n")
        time.sleep(1)

class Player(Hand):

    def __init__(self, chips, bet=0, split_cards = False):
        super().__init__()
        self.chips = chips
        self.bet = bet
        self.profit = 0
        self.alive = True
        self.split_cards = split_cards
        self.has_blackjack = False
    
    def deal_cards(self, deck):
        self.hit(deck)
        self.hit(deck)
        print_line('Player Cards')
        self.card_visual()  
        self.has_blackjack = self.check_for_blackjack()
        self.split_cards = self.check_for_split()
        self.apply_split(deck)

    def add_chips(self, chips):
        self.chips += chips

    def remove_chips(self, chips):
        self.chips -= chips

    def print_balance(self): 
        print(f"\nYour balance is currently: ${self.chips:,.2f}\n")

    def check_for_blackjack(self):
        return len(self.hand) == 2 and self.hand_score() == 21
    
    def check_for_split(self):
        if self.hand[0].value == self.hand[1].value:
            return validate_answer("Do you want to split your cards?: [y / n]: ", yes_no)            
        return False

    def wager(self):
        while True:
            self.print_balance()
            bet = input(f"How much would you like to bet?: $")
            if not bet.isdecimal():
                continue
            elif float(bet) > self.chips:
                print("sorry, you dont have enough chips. Try again")
            else:
                self.bet = float(bet)
                self.remove_chips(float(bet))
                break

    def added_wager(self):
        while True:
            self.print_balance()
            bet = input(f"Enter additional wager. You may bet up to your original ${self.bet} or less: $")
            if not bet.isdecimal() or float(bet) > self.bet:
                continue
            elif float(bet) > self.chips:
                print("You dont have enough chips. Try again")
            else:
                self.bet_two = float(bet)
                self.remove_chips(float(bet))
                break

    def confirm_double(self):
        return validate_answer("\nYou will only get 1 more card. Confirm you want to double down: [y / n]: ", yes_no)

    def double_down(self, deck):
        self.added_wager()
        self.bet += self.bet_two
        self.visual_move(deck)
        if self.hand_score() > 21:
            self.alive = False

    def apply_split(self, deck):
        if self.split_cards:
            self.added_wager()
            self.hand_two = Player(0, split_cards=True, bet=self.bet_two)

            transfer_card = self.remove_card()
            self.hand_two.add_card(transfer_card)
            self.hit(deck)
            self.hand_two.hit(deck)

            print("\nFirst Hand: ")
            self.mini_card_visual()
            self.player_move(deck)
            
            print("\nSecond Hand: ")
            self.hand_two.mini_card_visual()
            self.hand_two.player_move(deck)
            time.sleep(1)

    def visual_move(self, deck):
        self.hit(deck)
        if self.split_cards:
            self.mini_card_visual()
        else:
            self.card_visual()

    def player_move(self, deck):
        assert isinstance(deck, Deck)
        while True:
            if self.hand_score() > 21 or self.has_blackjack:
                self.alive = False
                break
            if self.hand_score() == 21:
                break
            if len(self.hand) == 2:
                action = input("Would you like to hit, stand, or double-down? Enter [h, s, or d]: ")
            else:
                action = input("Would you like to hit or stand: Enter [h or s]: ")
            if action == 'd':
                if len(self.hand) == 2:
                    if self.confirm_double():
                        self.double_down(deck)
                        break
            if action == "h":
                self.visual_move(deck)
            if action == "s":
                break

    def compute_results(self, dealer):
        assert isinstance(dealer, Dealer)
        if self.alive and dealer.alive:
            if self.hand_score() > dealer.hand_score():
                print("WINNER!\n")
                self.profit = 2
            elif self.hand_score() == dealer.hand_score():
                print("PUSH!\n")
                self.profit = 1
            else:
                print("LOSER! Dealer Wins\n")

        elif not self.alive:
            if self.has_blackjack:
                print("YOU HAVE BLACKJACK!\n")
                self.profit = 2.5
            else:
                print("BUST! LOSER!\n")
        else:
            print("DEALER BUSTS. YOU WIN!\n")
            self.profit = 2
        self.settle()

    def settle(self):
        self.add_chips(self.profit*self.bet)

    def reset(self):
        self.hand = []
        self.alive = True
        self.split_cards = False
        self.profit = 0
        self.bet, self.bet_two = 0, 0

class Dealer(Hand):

    def __init__(self):
        super().__init__()
        self.alive = True
    
    def deal_cards(self, deck):
        self.hit(deck)
        self.hit(deck)
        print_line('Dealer Cards')
        self.dealer_visual()

    def reset(self):
        self.hand = []
        self.alive = True

    def card_reveal(self):
        print_line('Dealer Cards')
        time.sleep(1)
        self.card_visual()

    def dealer_move(self, deck):
        self.card_reveal()
        while True:
            if self.hand_score() in range(17, 22):
                return True
            if self.hand_score() > 21:
                self.alive = False
                return False
            if self.hand_score() < 17:
                self.hit(deck)
                time.sleep(1)
                self.card_visual()

    def dealer_visual(self):
        card_list = []
        hidden_card = visuals.reg_hidden_card
        card_list.append(hidden_card)

        for card in self.hand[1:]:
            card_vis = visuals.reg_card_visual(card)
            card_list.append(card_vis)

        visuals.print_cards(card_list)
        time.sleep(1)


def play_again():
    if validate_answer("Would you like to play another round? [y / n]: ", yes_no):
        clear()
        return True
    return False

def print_line(word):
    print(f"\n______________________[{word}]______________________________\n")
    

def game():
    print_line('WELCOME TO BLACKJACK!!')

    num_decks    = 6
    player_chips = 1_000

    deck   =  Deck(num_decks)
    player =  Player(player_chips)
    dealer =  Dealer()

    deck.shuffle()

    while True:
        if player.chips == 0:
            print("You're out of money. Game Over")
            break
        print(f"Percentage of shoe not yet dealt: {len(deck)/(52*num_decks):.2%}")
        if deck.is_shuffle_time():
            deck.shuffle_time()        

        player.wager()
        dealer.deal_cards(deck)
        player.deal_cards(deck)        

        if not player.split_cards:
            player.player_move(deck)
            if player.alive:
                dealer.dealer_move(deck)
            player.compute_results(dealer)        
        # PLAYER SPLIT CARDS
        else:
            if player.alive or player.hand_two.alive:
                dealer.dealer_move(deck)
            print("HAND ONE:")
            player.compute_results(dealer)
            print("HAND TWO:")
            player.hand_two.compute_results(dealer)

            # Any chips won by second hand: Add it to total balance
            player.chips += player.hand_two.chips

        player.print_balance()
        if play_again():
            player.reset()
            dealer.reset()
            continue
        else:
            break

    print("Thanks for playing. Goodbye.")

if __name__ == "__main__":
    game()

Oh, and this is the visuals.py file (most of it):

"""

Variety of ways to nicely dislay card representations

Width of terminal may impact visual

"""


def print_cards(cardlist):
    for card in zip(*cardlist):
            print('   '.join(card))

def reg_card_visual(card):
    suits = "Spades Diamonds Hearts Clubs".split()
    suit_symbols = ['♠','♦','♥','♣']
    suit_pairs = dict(zip(suits, suit_symbols))

    v = card.value
    s = suit_pairs[card.suit]

    visual = [

        '  ╔════════════╗',
       f'  ║ {v:<5}      ║',
        '  ║            ║',
        '  ║            ║',
       f'  ║     {s:^3}    ║',
        '  ║            ║',
        '  ║            ║',
        '  ║            ║',
       f'  ║      {v:>5} ║',
        '  ╚════════════╝'
    ]

    return visual


def mini_card_visual(card):
    suits = "Spades Diamonds Hearts Clubs".split()
    suit_symbols = ['♠','♦','♥','♣']
    suit_pairs = dict(zip(suits, suit_symbols))

    v = card.value
    s = suit_pairs[card.suit]

    visual = [
        '╔══════╗',
       f'║ {v:<3}  ║',
       f'║      ║',
       f'║  {s:>3} ║',
        '╚══════╝'
        ]

    return visual


def large_card_visual(card):
    suits = "Spades Diamonds Hearts Clubs".split()
    suit_symbols = ['♠','♦','♥','♣']
    suit_pairs = dict(zip(suits, suit_symbols))

    v = card.value
    s = suit_pairs[card.suit]

    visual = [

        '   ┌─────────────────┐',
       f'   │ {v:<5}           │',
        '   │                 │',
        '   │                 │',
        '   │                 │',
        '   │                 │',
       f'   │        {s}        │',
        '   │                 │',
        '   │                 │',
        '   │                 │',
        '   │                 │',
        '   │                 │',
       f'   │           {v:>5} │',
        '   └─────────────────┘'

    ]

    return visual


reg_hidden_card = [
    '   ╔════════════╗',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ╚════════════╝'
    ]


v, s = 'V', 'S'

card_visuals = {

'small_card_vis' : [
    '╔══════╗',
   f'║ {v:<3}  ║',
   f'║ {s:>3}  ║',
   f'║      ║',
    '╚══════╝'
    ],


'mini_card_vis' : [
    '╔══════╗',
   f'║ {v:<3}  ║',
    '║      ║',
   f'║  {s:>3} ║',
    '╚══════╝'
    ],


'thick_border_vis' : [

    '  ╔════════════╗',
   f'  ║ {v:<5}      ║',
    '  ║            ║',
    '  ║            ║',
   f'  ║     {s:^3}    ║',
    '  ║            ║',
    '  ║            ║',
    '  ║            ║',
   f'  ║      {v:>5} ║',
    '  ╚════════════╝'
    ],


'thin_border_vis' : [

    '   ┌───────────┐',
   f'   │ {v:<5}     │',
    '   │           │',
    '   │           │',
    '   │           │',
   f'   │     {s}     │',
    '   │           │',
    '   │           │',
    '   │           │',
   f'   │     {v:>5} │',
    '   └───────────┘'
    ]

}

# print_card(card_visuals['thick_border_vis'])


hidden_cards = {


'mini_thick_hidden_card' : [
    '╔══════╗',
    '║░░░░░░║',
    '║░░░░░░║',
    '║░░░░░░║',
    '╚══════╝'
    ],


'reg_thick_hidden_card' : [
    '   ╔════════════╗',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ║░░░░░░░░░░░░║',
    '   ╚════════════╝'
    ],


'small_thin_hidden_card' : [

    '┌────────┐',
    '│░░░░░░░░│',
    '│░░░░░░░░│',
    '│░░░░░░░░│',
    '│░░░░░░░░│',
    '│░░░░░░░░│',
    '└────────┘'
    ],

'reg_thin_hidden_card' : [
    '   ┌───────────┐',
    '   │░░░░░░░░░░░│',
    '   │░░░░░░░░░░░│',
    '   │░░░░░░░░░░░│',
    '   │░░░░░░░░░░░│',
    '   │░░░░░░░░░░░│',
    '   │░░░░░░░░░░░│',
    '   │░░░░░░░░░░░│',
    '   │░░░░░░░░░░░│',
    '   │░░░░░░░░░░░│',
    '   └───────────┘'
    ],

'large_thin_hidden_card' : [
'   ┌─────────────────┐',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   │░░░░░░░░░░░░░░░░░│',
'   └─────────────────┘'
]

}

# print_card(hidden_cards['reg_thick_hidden_card'])

Ex screenshots:

enter image description here

enter image description here

\$\endgroup\$
5
\$\begingroup\$

system()

replace os.system with a call to subprocess, which for all intents and purposes replaces the former.

Type hints

def validate_answer(question, choices):

can be

def validate_answer(question: str, choices: Sequence[str]) -> str:

The Sequence is appropriate because you need choices to be both iterable and indexable.

Global variables

yes_no   = ['y', 'n']

can be

YES_NO = 'yn'

In other words, a string is itself a sequence of strings, each one character long.

Statics

These three:

values = [str(v) for v in range(2, 11)] + list('JQKA')
suits = "Spades Diamonds Hearts Clubs".split()
suit_symbols = ['♠','♦','♥','♣']

should probably all be tuples, since it's expected that none of them should change.

Also, for both values and suits, you should attempt to model them as Enums. They have a fixed set of valid values.

You re-declare these in mini_card_visual, large_card_visual etc. when you should not; just declare them once.

Don't lie to your user

    print("Reshuffling the Deck...\n")
    time.sleep(1)
    print("Reshuffling the Deck...\n")
    time.sleep(1)
    print("Reshuffling the Deck...\n")
    time.sleep(1)

I don't have very many pet peeves when it comes to user interface design, but this is one of them.

You're implying that during the sleep, something is actually happening, when it isn't. Don't lie to your user. Just output the message once, don't sleep, and do the shuffle().

Set membership

any(card.value == 'A' for card in self.hand)

is not bad. Another way to model this is

'A' in {card.value for card in self.hand}

Since the data are so small there won't really be an impact to performance either way.

Typo

sorry, you dont -> sorry, you don't

Similar for other instances of dont.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Appreciate all the feedback. thankyou! Your comments about TypeHints, Global Variables, and Statics, Typos are noted and will make the changes. I'm not familiar with enums, but I will certainly look into. To avoid redeclaring the statics in the card_visuals how would I reference them? regarding lying to user, my intention was far from that, though if that's how it comes off, ill certainly change. given that its played in terminal, everything prints very fast, so time.sleep() was a simply a way to slow the literal printing to make it a more seem more fluid for user. bc it does reshuffle \$\endgroup\$ – Zach Sonnenblick Jun 29 at 16:42
  • \$\begingroup\$ Sure! If the printing is so fast that the shuffling output is actually lost off-screen, one reasonable alternative is a press any key to continue prompt. Otherwise, it's best to simply trust your user and their ability to read text as it scrolls. \$\endgroup\$ – Reinderien Jun 29 at 16:45
  • 1
    \$\begingroup\$ You can keep clear; just replace os.system with something like subprocess.run. For more information read docs.python.org/3/library/subprocess.html . Particularly, for the motivation behind replacement, see python.org/dev/peps/pep-0324 \$\endgroup\$ – Reinderien Jun 29 at 16:48
  • 2
    \$\begingroup\$ A StackOverflow answer for the difference between os.system and subprocess here. \$\endgroup\$ – Linny Jun 29 at 16:48
  • 1
    \$\begingroup\$ It's fairly reasonable, though that kind of check isn't always strictly necessary. It's much more important IMO to add type hints to the actual parameters to let callers know what the function expects them to pass. \$\endgroup\$ – Reinderien Jun 29 at 17:02
5
\$\begingroup\$

Deck.reset bug

This function has no access to num_decks, so if you decide to call this function it will fail with a syntax error. A simple fix would be to define this as an instance variable in the constructor, i.e self.num_decks, and use that.

validate_answer

This function should be written like this:

from typing import List

def validate_answer(question: str, choices: List[str]) -> bool:
    while answer := input(question)[0].lower():
        if answer in choices:
            return answer == choices[0]

This makes use of the walrus operator, aka assignment expressions. This also fixes a bug. If a user enters "N" instead of "n", this function rejects that input and asks again. It's better to lower the input and analyze it that way.

For specific advice on type hints see Reinderien's answer.

Misc whitespace

You have many places like

yes_no =   ['y', 'n']

where there's extra whitespace. Try to keep one space before and after operators to make it readable without being extraneous.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Dude, I had no idea that PEP572 was implemented. This is a really cool thing to learn; thank you! \$\endgroup\$ – Reinderien Jun 29 at 16:55
  • \$\begingroup\$ thank you for taking the time to review! and oh wow, that's pretty neat. I wasn't aware of the walrus operator. additionally, thank you for spotting some errors that I managed to overlooked. will fix. Is type checking common practice/ encouraged? it certainly looks more readable; I imagine that's the goal? \$\endgroup\$ – Zach Sonnenblick Jun 29 at 17:01

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.