Object-Oriented Design of a Blackjack Game

Object-Oriented Design of a Blackjack Game

When it comes to designing a Blackjack game using object-oriented programming (OOP) principles, we can break down the game into several key classes that interact with each other. This approach allows for a modular, maintainable, and extensible codebase. Let’s explore the main classes we might use in our Blackjack game design.

Card Class

The Card class represents a single playing card. It would have properties such as:

  • suit: The suit of the card (Hearts, Diamonds, Clubs, Spades)
  • rank: The rank of the card (2-10, Jack, Queen, King, Ace)
  • value: The numerical value of the card in the game
class Card:
    def __init__(self, suit: str, rank: str):
        self.suit = suit
        self.rank = rank
        self.value = self._calculate_value()

    def _calculate_value(self):
        # Logic to determine card value
        if self.rank in ['Jack', 'Queen', 'King']:
            return 10
        elif self.rank == 'Ace':
            return 11
        else:
            return int(self.rank)

Deck Class

The Deck class represents a standard 52-card deck. It would contain a list of Card objects and methods to manipulate the deck:

  • shuffle(): Randomizes the order of cards in the deck
  • draw(): Removes and returns the top card from the deck
import random

class Deck:
    def __init__(self):
        self.cards = self._create_deck()

    def _create_deck(self):
        suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
        ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King', 'Ace']
        return [Card(suit, rank) for suit in suits for rank in ranks]

    def shuffle(self):
        random.shuffle(self.cards)

    def draw(self):
        if not self.cards:
            raise ValueError("No cards left in the deck")
        return self.cards.pop()

Hand Class

The Hand class represents a player’s or dealer’s hand. It would contain a list of Card objects and methods to manage the hand:

  • add_card(card: Card): Adds a card to the hand
  • calculate_value(): Calculates the total value of the hand
  • is_busted(): Checks if the hand value exceeds 21
class Hand:
    def __init__(self):
        self.cards = []

    def add_card(self, card: Card):
        self.cards.append(card)

    def calculate_value(self):
        value = sum(card.value for card in self.cards)
        num_aces = sum(1 for card in self.cards if card.rank == 'Ace')
        
        # Adjust for Aces
        while value > 21 and num_aces:
            value -= 10
            num_aces -= 1
        
        return value

    def is_busted(self):
        return self.calculate_value() > 21

Player Class

The Player class represents a player in the game. It would have properties such as:

  • name: The player’s name
  • hand: An instance of the Hand class
  • chips: The player’s current chip count
class Player:
    def __init__(self, name: str, initial_chips: int):
        self.name = name
        self.hand = Hand()
        self.chips = initial_chips

    def place_bet(self, amount: int):
        if amount > self.chips:
            raise ValueError("Not enough chips")
        self.chips -= amount
        return amount

    def hit(self, card: Card):
        self.hand.add_card(card)

    def stand(self):
        pass  # The action of standing doesn't require any change to the player's state

Dealer Class

The Dealer class, which could inherit from the Player class, represents the dealer in the game. It would have additional methods specific to dealer behavior:

  • reveal_hidden_card(): Reveals the dealer’s hidden card
  • play_hand(deck: Deck): Implements the dealer’s strategy for playing their hand
class Dealer(Player):
    def __init__(self):
        super().__init__("Dealer", float('inf'))  # Dealer has infinite chips
        self.hidden_card = None

    def reveal_hidden_card(self):
        if self.hidden_card:
            self.hand.add_card(self.hidden_card)
            self.hidden_card = None

    def play_hand(self, deck: Deck):
        self.reveal_hidden_card()
        while self.hand.calculate_value() < 17:
            self.hit(deck.draw())

Game Class

Finally, the Game class would orchestrate the entire game, managing the flow and rules:

  • start_round(): Begins a new round of play
  • deal_initial_cards(): Deals the initial two cards to each player and the dealer
  • check_for_blackjack(): Checks for any initial Blackjacks
  • play_player_turns(): Manages each player’s turn
  • play_dealer_turn(): Manages the dealer’s turn
  • determine_winners(): Determines the outcome of the round
class Game:
    def __init__(self, players: list[Player]):
        self.players = players
        self.dealer = Dealer()
        self.deck = Deck()

    def start_round(self):
        self.deck.shuffle()
        self.deal_initial_cards()
        self.check_for_blackjack()
        self.play_player_turns()
        self.play_dealer_turn()
        self.determine_winners()

    def deal_initial_cards(self):
        for _ in range(2):
            for player in self.players:
                player.hit(self.deck.draw())
            if _ == 0:
                self.dealer.hit(self.deck.draw())
            else:
                self.dealer.hidden_card = self.deck.draw()

    def check_for_blackjack(self):
        # Logic to check for Blackjack
        pass

    def play_player_turns(self):
        for player in self.players:
            while player.hand.calculate_value() < 21:
                action = input(f"{player.name}, do you want to hit or stand? ")
                if action.lower() == 'hit':
                    player.hit(self.deck.draw())
                elif action.lower() == 'stand':
                    break

    def play_dealer_turn(self):
        self.dealer.play_hand(self.deck)

    def determine_winners(self):
        dealer_value = self.dealer.hand.calculate_value()
        for player in self.players:
            player_value = player.hand.calculate_value()
            if player.hand.is_busted():
                print(f"{player.name} busts!")
            elif self.dealer.hand.is_busted() or player_value > dealer_value:
                print(f"{player.name} wins!")
            elif player_value < dealer_value:
                print(f"{player.name} loses!")
            else:
                print(f"{player.name} pushes!")

Conclusion

By breaking down our Blackjack game into these classes, we create a modular and flexible design. Each class has a single responsibility, following the Single Responsibility Principle of SOLID design. This approach allows for easy maintenance and extension of the game in the future.

For example, if we wanted to add new game variants or betting strategies, we could extend our existing classes or create new ones that interact with our current system. The separation of concerns also makes it easier to implement features like multiplayer support or different dealer behaviors.

Remember, this is a high-level design, and the actual implementation would involve more detailed methods and properties within each class. However, this structure provides a solid foundation for building a robust and scalable Blackjack game using object-oriented design principles in Python.