2
\$\begingroup\$

This is a follow-up to my original post of Blackjack with card representation visuals

I was given some very helpful advice by @Reinderien and @Linny which I greatly appreciate.

I have done my best to implement the changes. However, I am unsure if I actually implemented these changes in the best way, and would love if anybody can take a look/ provide any feedback. I'm always looking to learn more and improve, so I greatly appreciate any feedback or additional comments.

With regards to typing, is it common practice to include type annotations on every function? I can't help but feel it makes the file more cluttered?
But if so, what is the best practice with class methods where the only parameter is the instance self ?

Additionally, some of my methods don't actually return anything, but rather, they update the state of something. in these cases, did I handle them correctly, with the type hint of -> None

I also added from __future__ import annotations in order to use type hints for classes I created. Is this the intended usage of the library/ correct way to handle these cases?

To implement the various changes, I ended up needing to import a few more modules; but I'm curious if it's frowned upon to have too many imports? At a certain point, can it actually impact performance?

Here is the link to Github Blackjack game which also contains a visuals.py file.

Thank you so much in advance:
The updated code:

from __future__ import annotations
import random
import collections
import time
import os
from typing import Sequence
import subprocess as sp 
import visuals


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

def clear():
    sp.run(('cls' if os.name == 'nt' else 'clear'), shell=True)

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

YES_NO = 'yn'


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.num_decks = num_decks
        self.cards = [Card(value, suit) for suit in self.suits for value in self.values] * self.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):
        clear()
        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] * self.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: tuple) -> None:
        for card in cards:
            self.hand.append(card)

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

    def hit(self, deck: Deck) -> None:
        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")

    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")

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: Deck) -> None:
        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: float) -> None:
        self.chips += chips

    def remove_chips(self, chips: float) -> None:
        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 don't 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 don't 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: Deck) -> None:
        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: Deck) -> None:
        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()
            time.sleep(1)
            self.player_move(deck)
            print("\nSecond Hand: ")
            self.hand_two.mini_card_visual()
            time.sleep(1)
            self.hand_two.player_move(deck)
            time.sleep(1)

    def visual_move(self, deck: Deck) -> None:
        self.hit(deck)
        if self.split_cards:
            self.mini_card_visual()
        else:
            self.card_visual()

    def player_move(self, deck: Deck) -> None:
        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: Dealer) -> None:
        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: Deck) -> None:
        self.hit(deck)
        self.hit(deck)
        print_line('Dealer Cards')
        self.dealer_visual()
        time.sleep(1)

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

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

    def dealer_move(self, deck: Deck) -> None:
        self.card_reveal()
        while True:
            if self.hand_score() in range(17, 22):
                break
            if self.hand_score() > 21:
                self.alive = False
                break
            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)


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: str) -> None:
    print(f"\n______________________[{word}]______________________________\n")
    

def game():
    print_line('WELCOME TO BLACKJACK!!')
    num_decks = 6
    player_chips = 1_000

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

    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()
\$\endgroup\$
  • \$\begingroup\$ Are you using Python 3? \$\endgroup\$ – Reinderien Jun 30 at 14:22
  • \$\begingroup\$ @Reinderien yes. python 3.8. is there anything I'm doing that suggests an older version? \$\endgroup\$ – Zach Sonnenblick Jun 30 at 14:36
  • \$\begingroup\$ __future__. You should just be using typing. \$\endgroup\$ – Reinderien Jun 30 at 14:36
  • \$\begingroup\$ I was trying to understand the usage of it. it seems to be a feature not introduced until 3.7 though I might be wrong. but if i don't include it, i get an error "NameError: name 'Dealer' is not defined" as an example, used in typing. as that is a class I created. \$\endgroup\$ – Zach Sonnenblick Jun 30 at 14:53
  • 1
    \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$ – Reinderien Jun 30 at 15:25
1
\$\begingroup\$

Default Parameters

There should be no spaces before or after the =.

def __init__(self, chips, bet=0, split_cards=False):

Hand.__repr__

You can utilize the .join function to append to strings.

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

Hand.card_visual

You can use list comprehension to shorten this method.

def card_visual(self):
    card_list = [visuals.reg_card_visual(card) for card in self.hand]
    visuals.print_cards(card_list)
    print(f"\nTotal of: {self.hand_score()}\n")
| improve this answer | |
\$\endgroup\$

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.