7
\$\begingroup\$

I'm still learning Python and especially Object Oriented Programming. The latter, though, seems to confuse me a lot: defining class object attributes vs instance attributes, passing arguments, using outside-of-the-class functions vs class methods, etc.

I understand it's a rather ambiguous question I'm asking, but based on the code below I'd appreciate if someone could help me organize it more and point to logic gaps/incorrect OOP way.

My goal is to learn OOP, blackjack game is just an example of what I've understood so far.

P.S. Please ignore the comments, if they don't make sense to you. (I made them for a non-Python friend to help me with the same problem).

import random


class Deck:
    """
    Creating class object attributes, that will be used to build game deck
    """
    ranks = ("Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King", "Ace")
    suits = ('Hearts', 'Diamonds', 'Spades', 'Clubs')
    values = {'Two': 2, 'Three': 3, 'Four': 4, 'Five': 5, 'Six': 6, 'Seven': 7, 'Eight': 8, 'Nine': 9, 'Ten': 10,
              'Jack': 10, 'Queen': 10, 'King': 10, 'Ace': 11}
    deck = []

    def __init__(self):
        """
        Building & shuffling the deck, whenever class instance is created
        """
        self.deck = []  # Resetting deck, whenever class instance is created
        for rank in self.ranks:
            for suit in self.suits:
                self.deck.append((rank, suit))
        random.shuffle(self.deck)
        print("\n********************\nNEW GAME BEGINS NOW!\n")


class Hand:
    """
    Hand class to control each player cards, values and aces count
    """

    def __init__(self):
        self.hand = []  # Starting with an empty hand for each player(class instance)
        self.value = 0  # Starting with 0 value for each hand
        self.aces_count = 0  # Starting with 0 aces for each hand

    def get_card(self, deck):
        """
        Removing last(top) card from the deck and adding it to the hand
        :param deck: Game deck object
        :return: None
        """
        self.hand.append(deck.pop())

    def get_hand_value(self):
        """
        Calculating value of the hand, adjusting to possible Aces
        :return: Hand value
        """
        self.value = 0  # Resetting hand value, when method is called
        self.aces_count = 0  # Resetting aces count, when method is called
        for rank, suit in self.hand:
            self.value += Deck.values[rank]
        for rank, suite in self.hand:
            if rank == 'Ace':
                self.aces_count += 1
        if self.aces_count >= 1 and self.value > 21:
                self.value = self.value - 10 * self.aces_count
        return self.value

    def reveal_cards(self, player="Player", hide=False):
        """
        Revealing card(s) of the Player/Dealer
        :param player: str reference to Player's or Dealer's hand
        :param hide: bool; Used to hide Dealer's last card
        :return: None
        """
        if hide is False:
            print("{} cards: {}".format(player, self.hand))
        else:
            print("{} cards: [{}, ('????', '????')]".format(player, self.hand[0:-1]))


class Bets:
    """
    Bets class to organize Player's chips, bets and addition/subtrcation when he wins/loses
    """

    def __init__(self, total):
        """
        Added attributes to __init__ method to leave an option (for future) to add 2nd instance of Bets class. e.g.
        dealer's bets
        """
        self.total = total  # Asking player how much $$ he wants to play on
        self.bet = 0  # Starting with 0 bet
        self.betting = 1  # Flag to continue asking for a bet

    def win_bet(self):
        """
        Adds bet amount to player's total
        :return: None
        """
        self.total += self.bet

    def lose_bet(self):
        """
        Subtracts bet amount from player's total
        :return: None
        """
        self.total -= self.bet

    def take_bet(self):
        """
        Validates, whether player has enough $$ to bet
        Reserves bet amount from player's total
        :return: None
        """
        if self.total != 0:
            while self.betting:
                    self.bet = int(input("Enter your bet: \n"))
                    if self.bet == 0:
                        print("Bet should be greater than 0!")
                        self.take_bet()
                    elif self.bet > self.total:
                        print("Bet cannot exceed your total amount of {}".format(self.total))
                        self.take_bet()
                    else:
                        print("Player bet is : {}".format(self.bet))
                        self.betting = 0
                        break
        else:
            print("Not enough money to play.\n")
            restart_game(bets)


def hit_or_stand(deck, hand1, hand2, bets, limit):
    """
    Hit or Stand function, which determines whether Player/Dealer hits or stands
    :param deck: Game deck object
    :param hand1: Player's hand object
    :param hand2: Dealer's hand object
    :param bets: Player's bets object
    :param limit: Defines limit for each hand to stop hitting at, list of 2 integers
    :return: None
    """
    while hand1.get_hand_value() < int(limit[0]):
            h_or_s = str(input("Hit or Stand? H / S\n"))
            if h_or_s[0].lower() == "h":
                    print("Player hits >>> ")
                    hand1.get_card(deck.deck)
                    if busted(hand1, hand2, bets) is False and blackjack(hand1, hand2, bets) is False:
                        hand1.reveal_cards(player="Player", hide=False)
                        print("Player value: {}".format(hand1.get_hand_value()))
            elif h_or_s[0].lower() == "s":
                    print("Player stands...")
                    while hand2.get_hand_value() < int(limit[1]):
                        hand2.get_card(deck.deck)
                        if busted(hand1, hand2, bets) is False and blackjack(hand1, hand2, bets) is False:
                            print("\nDealer hits >>>")
                            hand2.reveal_cards(player="Dealer", hide=True)
                            continue
                    else:
                        define_winner(hand1, hand2, bets)
                        break


def drop_win(hand1, hand2, bets):
    """
    Function checks whether player or dealer got
    :param hand1: Player's hand object
    :param hand2: Dealer's hand object
    :param bets: Player's bets object
    :return: bool True, if player/dealer got blackjack (21)
    """
    if hand1.get_hand_value() == hand2.get_hand_value() == 21:
        print("BlackJack Tie on drop!")
        display_all_cards_and_values(hand1, hand2)
        continue_playing(bets)
        return True
    elif hand1.get_hand_value() == 21 and hand2.get_hand_value() < 21:
        print("Player BlackJack on drop!")
        display_all_cards_and_values(hand1, hand2)
        bets.win_bet()
        continue_playing(bets)
        return True
    elif hand2.get_hand_value() == 21 and hand1.get_hand_value() < 21:
        print("Dealer BlackJack on drop!")
        display_all_cards_and_values(hand1, hand2)
        bets.lose_bet()
        continue_playing(bets)
        return True
    else:
        return False


def busted(hand1, hand2, bets):
    """
    Function checks, whether player or dealer busted // got hand value over 21
    :param hand1: Player's hand object
    :param hand2: Dealer's hand object
    :param bets: Player's bets object
    :return: bool True if player/dealer busted
    """
    if hand1.get_hand_value() > 21:
        print("Player Busted :-|")
        display_all_cards_and_values(hand1, hand2)
        bets.lose_bet()
        continue_playing(bets)
        return True
    elif hand2.get_hand_value() > 21:
        print("Dealer Busted!")
        display_all_cards_and_values(hand1, hand2)
        bets.win_bet()
        continue_playing(bets)
        return True
    else:
        return False


def blackjack(hand1, hand2, bets):
    """
    Function checks, whether player or dealer got Blackjack (21)
    :param hand1: Player's hand object
    :param hand2: Dealer's hand object
    :param bets: Player's bets object
    :return: None
    """
    if hand1.get_hand_value() == 21 and hand2.get_hand_value() < 21:
        display_all_cards_and_values(hand1, hand2)
        print("Player BlackJack!")
        bets.win_bet()
        continue_playing(bets)
        return True
    elif hand2.get_hand_value() == 21 and hand1.get_hand_value() < 21:
        display_all_cards_and_values(hand1, hand2)
        print("Dealer BlackJack!")
        bets.lose_bet()
        continue_playing(bets)
        return True
    else:
        return False


def define_winner(hand1, hand2, bets):
    """
    Function defines winner, when both player and dealer stopped hitting and haven't busted or got blackjack
    :param hand1: Player's hand object
    :param hand2: Dealer's hand object
    :param bets: Player's bets object
    :return: None
    """
    print("\n********************\nDefining winner!\n")
    if hand1.get_hand_value() == hand2.get_hand_value():
        print("Tie!")
        display_all_cards_and_values(hand1, hand2)
        continue_playing(bets)
    elif hand1.get_hand_value() > hand2.get_hand_value():
        print("Player Wins!")
        display_all_cards_and_values(hand1, hand2)
        bets.win_bet()
        continue_playing(bets)
    else:
        print("Dealer Wins!")
        display_all_cards_and_values(hand1, hand2)
        bets.lose_bet()
        continue_playing(bets)


def continue_playing(bets):
    """
    Function asks player, whether he wants to continue playing, considering he has enough balance
    :param bets: Player's bets object
    :return: None
    """
    if bets.total > 0:
        answer = str(input("Your current balance is: {} . Want to continue playing? Y / N\n".format(bets.total)))
        if answer[0].lower() == 'y':
            pass
        elif answer[0].lower() == "n":
            global playing
            playing = 0
        else:
            print("Invalid Input")
            continue_playing(bets)
    else:
        print("Not enough money to play :(\n")
        restart_game(bets)


def restart_game(bets):
    """
    Function restarts the game from scratch, resetting total amount
    :param bets: Player's bets object
    :return: None
    """
    answer = str(input("Want to start a new game? Y / N\n"))
    if answer[0].lower() == 'y':
        bets.total = 100
        main()
    elif answer[0].lower() == "n":
        exit()
    else:
        print("Invalid Input")
        restart_game(bets)


def display_all_cards_and_values(hand1, hand2):
    """
    Function reveals all cards and prints each hand's value, when there's a winner
    :param hand1: Player's hand object
    :param hand2: Dealer's hand object
    :return: None
    """
    print("Player's cards : {}".format(hand1.hand))
    print("Player's value: {}".format(hand1.get_hand_value()))
    print("Dealer's cards: {}".format(hand2.hand))
    print("Dealer's value: {}".format(hand2.get_hand_value()))


def main():
    global playing
    playing = 1
    player_bets = Bets(total=100)
    while playing:
        game_deck = Deck()
        print("Player total is {}".format(player_bets.total))
        player_hand = Hand()
        dealer_hand = Hand()
        player_bets.take_bet()
        player_bets.betting = 1
        player_hand.get_card(game_deck.deck)
        player_hand.get_card(game_deck.deck)
        dealer_hand.get_card(game_deck.deck)
        dealer_hand.get_card(game_deck.deck)
        player_hand.reveal_cards(player="Player", hide=False)
        dealer_hand.reveal_cards(player="Dealer", hide=True)
        if drop_win(hand1=player_hand, hand2=dealer_hand, bets=player_bets) is False:
            print("Player value: {}".format(player_hand.get_hand_value()))
            hit_or_stand(deck=game_deck, hand1=player_hand, hand2=dealer_hand, bets=player_bets, limit=[21, 17])


if __name__ == '__main__':
    main()

\$\endgroup\$
1
  • \$\begingroup\$ Your question title should explain what your code does - and not what you want fixed with it; I suggest you remove the "need OOP advice" from the title. \$\endgroup\$ Commented Mar 6, 2020 at 12:18

3 Answers 3

2
\$\begingroup\$

Specific suggestions:

  1. Parameter defaults are not relevant for reveal_cards - it's always called with hide set.
  2. Boolean parameters are a code smell. Usually they should be replaced by two methods which each encapsulate just their difference, usually calling a third method to do the common work.
  3. The ranks and suits and their values should probably be an enumeration. That way any reference to for example the ace would be a reference to an enumeration value rather than a magic string which happens to be connected to a number elsewhere.
  4. The value of the ace is either 1 or 11, depending on what's best for the player. That could be encapsulated by a method on the ace enum value.
  5. Try to get rid of comments by improving the code where possible. For example, renaming hand1 to player_hand removes the need for mapping that in a comment.
  6. Mixing output and logic is usually a bad idea. Pulling out the printing code to just print the current state after each event should make the logic easier to read.

Tool support suggestions

  1. black can automatically format your code to be more idiomatic. It'll do things like adjusting the vertical and horizontal spacing, while keeping the functionality of the code unchanged.
  2. isort can group imports (built-ins first, then libraries and finally imports from within your project) and sort them.
  3. flake8 can give you more hints to write idiomatic Python:

    [flake8]
    max-complexity = 4
    ignore = W503,E203
    
  4. I would then recommend validating your type hints using a strict mypy configuration:

    [mypy]
    check_untyped_defs = true
    disallow_any_generics = true
    disallow_untyped_defs = true
    ignore_missing_imports = true
    no_implicit_optional = true
    warn_redundant_casts = true
    warn_return_any = true
    warn_unused_ignores = true
    

    This ensures that anyone reading the code (including yourself) understand how it's meant to be called, which is very powerful in terms of modifying and reusing it.

\$\endgroup\$
2
\$\begingroup\$

You're going the right path, but to master OOP think about the logical division of things. Hands and Decks can indeed be seen as objects, but you've kept functions like display_all_cards_and_values, restart_game, and continue_playing separate---these sound like aspects of a Game to me. Some of your classes also contain methods and data that don't seem to relate to them; print("\n********************\nNEW GAME BEGINS NOW!\n") doesn't seem like a very Deck thing. The initial shuffle of the deck (in __init__ sounds like a Deck method: def shuffle(self) -> None: random.shuffle(self.deck).

So what I would say is to try to turn (almost) everything into objects. If you have functions that just format strings or that transform sequences somehow, then make them separate functions, but as you force yourself to think of more things as objects it's going to get increasingly easy to structure things, and you'll realise that there are a lot of natural pairings to be made. You can also look for parts where you repeat yourself, and try to generalise as much as you can out of those parts (e.g. your if-statements in drop_win, which contain the same 4–5 lines with only minor differences).

Smaller advice:

  • your values is basically an enum, so you can use an enum instead.

  • list comprehensions are fast, use them:

    self.deck = []
    for rank in self.ranks:
        for suit in self.suits:
            self.deck.append((rank, suit))
    

    ->

    self.deck = [rank, suit for itertools.product(ranks, suits)]
    
  • if you're using Python version >= 3.6, utilize f-strings!

    print("{} cards: {}".format(player, self.hand))
    

    ->

    print(f"{player} cards: {self.hand}")
    

    and you can even add conditionals

    print(f"{player} cards: {self.hand if not hide else self.hand[0:-1]}")
    
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thank you to all of you guys: ades, l0b0, C. Harley It's definitely a lot to swallow for me, but i'll keep practicing and use all of your advises to improve my OOP. I'll post new code later. \$\endgroup\$
    – mrsnakes
    Commented Jun 16, 2020 at 14:07
  • \$\begingroup\$ Best of luck! Don't forget that you can, and should, accept (the check mark under votes) your preferred answer once you're happy with the replies. \$\endgroup\$
    – ades
    Commented Jun 20, 2020 at 14:36
2
\$\begingroup\$

I think the reason why you're having a problem with classes and OOP is because you've not fully understood the things around you. For instance - you start with Deck, because that's your the focus of the game. You're thinking about the game - and not the objects themselves.

defining class object attributes vs instance attributes,

Think of a class as a set of architectural plans - it's not a real thing until you actually instantiate it. Let's OOP the start of your program.

What's a Deck made up of? Cards. Can you go smaller than a card? No. Okay, so that's the first object you should create.

Create your first class as Card. Now describe the Card. What does a card have? It has a Rank and a Suit. What else can you use to describe the card? You could describe the quality of the paper and the thickness, as well as the colours used to paint the card - but we discard those attributes because they serve no purpose for what we're trying to achieve, which is playing the game (and they don't matter for our program).

Now, let's think about actions (methods/functions) of the card - does the card have any actions? No. We know they are dealt by the dealer, but by themselves, they don't do anything. So that tells you it's an object with data/state, but no methods.

Now each card is different, and we know there are 52 cards (ignoring the Joker) typically in each deck. Do you build all 52 cards manually? You can. That's a brute-force approach, which you did. Do you know what computers are good at? Giving them a set of instructions and letting them repeat until the goal is achieved - this is the basis of Machine Learning.

So, we create the Card, and we know when we create the card, it must have 2 attributes at minimum:

class Card:
    def __init__(self, rank, suit):
        self.rank, self.suit = rank, suit

But we know we have to get information from the card - we want to get a value, we want to get the rank, and we want to display the card information itself, in a human-readable way, such as 7 of Spades, or 7S. Let's go for the shorter-version - but you can change that later on that single line.

class Card:
    def __init__(self, rank, suit):
        self.rank, self.suit = rank, suit
    def __str__(self):
        return f"{self.rank}{self.suit}"

Ah, but if we think of an Ace - it represents 2 values depending on how it is used by the player. So let's add the two states - hard and soft values to the class. We don't know how we're going to address that (such as the player having 4 aces with all of them together only representing a hand value of 4), so this is okay for the moment. Let's add hard and soft values:

class Card:
    def __init__(self, rank, suit):
        self.rank, self.suit = rank, suit
    def __str__(self):
        return f"{self.rank}{self.suit}"
    def get_hard_value(self):
        return self.rank
    def get_soft_value(self):
        return self.rank

Now let's consider how we're going to create a card with a special rule. For cards 2-9, they're straight-forward, we don't need anything special. Aces and Face cards however do require different rules or behavior. As the Ace only has a single rule (either 1 or 11) - let's do that first. We inherit Card as the base class object, and specify the unique rules:

class Ace(Card):
    """ Aces can be either 1 or 11"""
    def __init__(self, rank, suit):
        super().__init__(rank, suit)
    def __str__(self):
        return f"A{self.suit}"
    def get_hard_value(self):
        return 1
    def get_soft_value(self):
        return 11

And now look at the Face Cards. Like the Ace, we know the face cards value should be hard-coded to 10, but we are faced with a slightly different issue (pun intended). The card is either K, Q, or J but keeps a value of 10. So, if we get an initial value that specifies that it is a K/Q/J - such as 11,12,13? Let's keep that initial value as an extra property, and override the value back to 10.

class FaceCard(Card):
    """ Face cards have a value of 10 and a must be either of King, Queen or Jack"""
    def __init__(self, rank, suit):
        super().__init__(rank, suit)
        self.special = rank
        self.rank = 10
    def __str__(self):
        label = ["K", "Q", "J"][self.special - 11]
        return f"{label}{self.suit}"

Now we have the architecture of the card, with the specific cases where the card changes slightly based on the rules of the cards. We have used inheritance to create a base class, and inherit Card with special overriding rules to show how - even though an Ace is a Card - it is slightly different.

This explanation should demonstrate the architecture of what a class object is - it is not yet instantiated - we don't have an instance object for you to see the difference - so let's create a set of instances of card, face card and ace. How do we do that? by creating the deck.

The deck will be a grouping of cards in a certain way depending on the rules of the game. Currently, your Deck class is very specific to vanilla Blackjack - if you wanted to add a Joker as a wild-card - you'd need to rewrite the Deck class. What if you wanted to switch back? Your Deck class is broken for a vanilla Blackjack game, and you'd need to rewrite it again, losing all the changes you created for a Jokers wild Blackjack game.

We learn the value of inheritance by this specific case. deck = BlackJackDeck() or deck = BlackJackJokersWildDeck() allow you to create different games with slightly different sets of cards.

So - analysis of the deck of cards for the vanilla Blackjack game. What is it? It's a set (unique items, no duplicates) of cards. What attributes does it have? It contains a specific size (52) of cards. Any other attributes? Not really. What about actions? Yes, it has an action of dealing a card - which reduces the available deck by 1. What happens when the deck is exhausted? Typically you should not reach this edge case unless you exceed a certain number of players.

We've covered class objects and their attributes, now we look at instantiated/concrete objects from the class, and passing arguments. We will cover those with the Deck class.

You also wanted clarification on class functions verses functions outside classes. This is more about the game itself - Blackjack or Texas Hold'em - the game has specific actions (functions/methods) which act on the deck of cards.

The cards don't care what game you're playing. They exist, they are dealt. That is the functionality combining the cards and the deck. The game - that is those "outside functions" with different scoring rules and rules of the game like of the number of players per game. Does that help? What something is verses what you do with it.

If an object has an action (like giving a card when it is asked for) then it belongs with the object. If you do something with that card - then the action belongs outside the object. Take some time to think about it. Until then, we dig into the Deck object (Which I've renamed as BlackJackDeck because it's more specific of what it is. Correct naming of variables and classes help other programmers to understand your code easier).

from random import shuffle

class BlackJackDeck:
    """ A set of cards suitable for a Blackjack game which can deal a random card"""
    def __init__(self):
        self.cards = []
        for suit in ["H", "D", "C", "S"]:
            self.cards += [Card(rank, suit) for rank in range(2,10)]
            self.cards += [FaceCard(rank, suit) for rank in range(11,14)]
            self.cards += [Ace(11, suit)]
        shuffle(self.cards)

    def deal(self):
        for card in self.cards:
            yield card

I'll leave the rest of the game up to you, but essentially now we have a set of code where we have a Deck of cards specific to a Blackjack game, where we can create a deck for each round, and deal a card when requested.

This creating of objects, from the smallest thing and getting larger, mimics real life is what OOP is all about. Learning the S.O.L.I.D principals can help you improve your OOP code too. Looking further at your code, I see drop_win busted and blackjack do calculations of the value of players hand - all this belongs in one place - the Player class. It is the player which holds the hand of cards, it is the player that plays, bets, gets another card, stands or hits, and leaves the table with their winnings.

I'll import the deck into my iPython CLI and show you how to use it - notice calling deck.deal() gives the same deck? This is because the shuffle is only called when creating a new Deck.

In [52]: deck = BlackJackDeck()

In [53]: for card in deck.deal():
    ...:     print(f"{card}   {card.rank}")
    ...:
9H   9
7C   7
3H   3
(snip)

In [54]: cards = deck.deal()

In [55]: next(cards)
Out[55]: <__main__.Card at 0x6089c50>

In [56]: print(next(cards))
7C

In [57]: print(next(cards))
3H

In [58]: a = next(cards)

In [59]: print(a)
AD

In [60]: dir(a)
Out[60]:
[(snip)
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'get_hard_value',
 'get_soft_value',
 'rank',
 'suit']

In [61]: a.rank
Out[61]: 11

In [62]: a.get_hard_value()
Out[62]: 1

In [63]: a.get_soft_value()
Out[63]: 11

I hope this helps to clarify objects a little further. It's an interesting way to represent things for coding - and not the only way - classes work well for data objects and object-specific actions - but I find components of functional programming safer. As mentioned, learn SOLID principals and they will help your coding get better.

Good luck and keep up the coding!

\$\endgroup\$

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.