Skip to main content
better DECKVAL
Source Link
Gareth Rees
  • 49.7k
  • 3
  • 129
  • 210
    RANK = 'A23456789TJQK'
    VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
    DECKVAL = {card:VALUES[RANKmin(1 + RANK.index(card[0])], 10) for card in DECK}
from itertools import product
from random import sample

SUIT = 'SCDH'
RANK = 'A23456789TJQK'
DECK = [''tuple(''.join(card) for card in product(RANK, SUIT)]
VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10])
DECKVAL = {card:VALUES[RANKmin(1 + RANK.index(card[0])], 10) for card in DECK}

class OutOfCards(Exception): pass

class Deck:
    """A shuffled deck of cards."""
    def __init__(self):
        self._cards = sample(DECK, len(DECK))

    def draw(self):
        """Draw one card from the deck and return it.
        Raise OutOfCards if the deck is out of cards.

        """
        if self._cards:
            return self._cards.pop()
        else:
            raise OutOfCards()

class Hand:
    """A hand of cards in blackjack."""
    def __init__(self, deck):
        self.cards = []
        for _ in range(2):
            self.draw(deck)

    def __str__(self):
        return ' '.join(self.cards)

    def draw(self, deck):
        """Draw one card from deck. Return the new card."""
        card = deck.draw()
        self.cards.append(card)
        return card

    @property
    def soft_value(self):
        """The "soft" value of the cards in the hand (counting aces as 1)."""
        return sum(DECKVAL[card] for card in self.cards)

    @property
    def value(self):
        """The "hard" value of the cards in the hand (counting aces as 11
        unless this would go bust, otherwise as 1).

        """
        v = self.soft_value
        if v <= 11 and any(card[0] == 'A' for card in self.cards):
            return v + 10
        else:
            return v

    @property
    def bust(self):
        """True if this hand is bust."""
        return self.soft_value > 21

    @property
    def blackjack(self):
        """True if this hand is a blackjack (ace plus ten or face card)."""
        return len(self.cards) == 2 and self.value == 21

def blackjack():
    """Play a hand of blackjack."""
    deck = Deck()
    hand = Hand(deck)
    dealer = Hand(deck)
    print("Dealer shows: {}".format(dealer.cards[0]))

    # Player logic
    while True:
        print("Your hand: {}".format(hand))
        if hand.bust:
            print("BUST!")
            return
        choice = input("Hit or stay? ").lower()
        if 'stay'.startswith(choice):
            break
        elif 'hit'.startswith(choice):
            card = hand.draw(deck)
            print("You drew: {}".format(card))

    # Dealer logic ("soft 17" variant)
    while True:
        print("Dealer's hand: {}".format(dealer))
        if dealer.soft_value >= 17:
            break
        card = dealer.draw(deck)
        print("Dealer drew: {}".format(card))
    if dealer.bust:
        print("Dealer is bust.")
    elif dealer.blackjack and not hand.blackjack:
        print("Dealer's blackjack beats your {}".format(hand.value))
    elif hand.blackjack and not dealer.blackjack:
        print("Your blackjack beats dealer's {}".format(dealer.value))
    elif dealer.value > hand.value:
        print("Dealer's {} beats your {}".format(dealer.value, hand.value))
    elif hand.value > dealer.value:
        print("Your {} beats dealer's {}".format(hand.value, dealer.value))
    else:
        print("Push: {} each".format(hand.value))
    RANK = 'A23456789TJQK'
    VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
    DECKVAL = {card:VALUES[RANK.index(card[0])] for card in DECK}
from itertools import product
from random import sample

SUIT = 'SCDH'
RANK = 'A23456789TJQK'
DECK = [''.join(card) for card in product(RANK, SUIT)]
VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
DECKVAL = {card:VALUES[RANK.index(card[0])] for card in DECK}

class OutOfCards(Exception): pass

class Deck:
    """A shuffled deck of cards."""
    def __init__(self):
        self._cards = sample(DECK, len(DECK))

    def draw(self):
        """Draw one card from the deck and return it.
        Raise OutOfCards if the deck is out of cards.

        """
        if self._cards:
            return self._cards.pop()
        else:
            raise OutOfCards()

class Hand:
    """A hand of cards in blackjack."""
    def __init__(self, deck):
        self.cards = []
        for _ in range(2):
            self.draw(deck)

    def __str__(self):
        return ' '.join(self.cards)

    def draw(self, deck):
        """Draw one card from deck. Return the new card."""
        card = deck.draw()
        self.cards.append(card)
        return card

    @property
    def soft_value(self):
        """The "soft" value of the cards in the hand (counting aces as 1)."""
        return sum(DECKVAL[card] for card in self.cards)

    @property
    def value(self):
        """The "hard" value of the cards in the hand (counting aces as 11
        unless this would go bust, otherwise as 1).

        """
        v = self.soft_value
        if v <= 11 and any(card[0] == 'A' for card in self.cards):
            return v + 10
        else:
            return v

    @property
    def bust(self):
        """True if this hand is bust."""
        return self.soft_value > 21

    @property
    def blackjack(self):
        """True if this hand is a blackjack (ace plus ten or face card)."""
        return len(self.cards) == 2 and self.value == 21

def blackjack():
    """Play a hand of blackjack."""
    deck = Deck()
    hand = Hand(deck)
    dealer = Hand(deck)
    print("Dealer shows: {}".format(dealer.cards[0]))

    # Player logic
    while True:
        print("Your hand: {}".format(hand))
        if hand.bust:
            print("BUST!")
            return
        choice = input("Hit or stay? ").lower()
        if 'stay'.startswith(choice):
            break
        elif 'hit'.startswith(choice):
            card = hand.draw(deck)
            print("You drew: {}".format(card))

    # Dealer logic ("soft 17" variant)
    while True:
        print("Dealer's hand: {}".format(dealer))
        if dealer.soft_value >= 17:
            break
        card = dealer.draw(deck)
        print("Dealer drew: {}".format(card))
    if dealer.bust:
        print("Dealer is bust.")
    elif dealer.blackjack and not hand.blackjack:
        print("Dealer's blackjack beats your {}".format(hand.value))
    elif hand.blackjack and not dealer.blackjack:
        print("Your blackjack beats dealer's {}".format(dealer.value))
    elif dealer.value > hand.value:
        print("Dealer's {} beats your {}".format(dealer.value, hand.value))
    elif hand.value > dealer.value:
        print("Your {} beats dealer's {}".format(hand.value, dealer.value))
    else:
        print("Push: {} each".format(hand.value))
    RANK = 'A23456789TJQK'
    DECKVAL = {card:min(1 + RANK.index(card[0]), 10) for card in DECK}
from itertools import product
from random import sample

SUIT = 'SCDH'
RANK = 'A23456789TJQK'
DECK = tuple(''.join(card) for card in product(RANK, SUIT))
DECKVAL = {card:min(1 + RANK.index(card[0]), 10) for card in DECK}

class OutOfCards(Exception): pass

class Deck:
    """A shuffled deck of cards."""
    def __init__(self):
        self._cards = sample(DECK, len(DECK))

    def draw(self):
        """Draw one card from the deck and return it.
        Raise OutOfCards if the deck is out of cards.

        """
        if self._cards:
            return self._cards.pop()
        else:
            raise OutOfCards()

class Hand:
    """A hand of cards in blackjack."""
    def __init__(self, deck):
        self.cards = []
        for _ in range(2):
            self.draw(deck)

    def __str__(self):
        return ' '.join(self.cards)

    def draw(self, deck):
        """Draw one card from deck. Return the new card."""
        card = deck.draw()
        self.cards.append(card)
        return card

    @property
    def soft_value(self):
        """The "soft" value of the cards in the hand (counting aces as 1)."""
        return sum(DECKVAL[card] for card in self.cards)

    @property
    def value(self):
        """The "hard" value of the cards in the hand (counting aces as 11
        unless this would go bust, otherwise as 1).

        """
        v = self.soft_value
        if v <= 11 and any(card[0] == 'A' for card in self.cards):
            return v + 10
        else:
            return v

    @property
    def bust(self):
        """True if this hand is bust."""
        return self.soft_value > 21

    @property
    def blackjack(self):
        """True if this hand is a blackjack (ace plus ten or face card)."""
        return len(self.cards) == 2 and self.value == 21

def blackjack():
    """Play a hand of blackjack."""
    deck = Deck()
    hand = Hand(deck)
    dealer = Hand(deck)
    print("Dealer shows: {}".format(dealer.cards[0]))

    # Player logic
    while True:
        print("Your hand: {}".format(hand))
        if hand.bust:
            print("BUST!")
            return
        choice = input("Hit or stay? ").lower()
        if 'stay'.startswith(choice):
            break
        elif 'hit'.startswith(choice):
            card = hand.draw(deck)
            print("You drew: {}".format(card))

    # Dealer logic ("soft 17" variant)
    while True:
        print("Dealer's hand: {}".format(dealer))
        if dealer.soft_value >= 17:
            break
        card = dealer.draw(deck)
        print("Dealer drew: {}".format(card))
    if dealer.bust:
        print("Dealer is bust.")
    elif dealer.blackjack and not hand.blackjack:
        print("Dealer's blackjack beats your {}".format(hand.value))
    elif hand.blackjack and not dealer.blackjack:
        print("Your blackjack beats dealer's {}".format(dealer.value))
    elif dealer.value > hand.value:
        print("Dealer's {} beats your {}".format(dealer.value, hand.value))
    elif hand.value > dealer.value:
        print("Your {} beats dealer's {}".format(hand.value, dealer.value))
    else:
        print("Push: {} each".format(hand.value))
Source Link
Gareth Rees
  • 49.7k
  • 3
  • 129
  • 210

1. General discussion

The organization of the classes needs work.

  1. The logic for handling the deck is distributed all over the place: some of it in shuffle, some in Hand.__init__, and some in Hand.draw. It would be better to keep this together, for example in a Deck class.

  2. The Hand class mixes two things: (i) a persistent data structure representing a hand of cards; (ii) gameplay logic for the player. It would be better to separate these concerns.

  3. The Dealer class adds no data to the Hand class, nor does it change the behaviour of any of the methods of the Hand class. So this class is not needed.

  4. The Game class does nothing: it has no data and no methods other than __init__. Again, this class is not needed.

2. Other comments

  1. There's some missing game logic: normally a "blackjack" (ace plus a ten or face card) beats a 21 made from three or more cards.

  2. This seems rather long-winded:

     RANK = '23456789TJQKA'
     VAL = []
     for a in range(9):
         VAL += [a+2] * 4
     for _ in range(3):
         VAL += [10] * 4
     VAL += [1] * 4
     DECKVAL = dict(zip(DECK, VAL))
    

compared to something like this:

    RANK = 'A23456789TJQK'
    VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
    DECKVAL = {card:VALUES[RANK.index(card[0])] for card in DECK}
  1. The Hand class has a poorly designed interface. To get the value of a hand requires two steps to be carried out in sequence: (i) call sum_hand(); (ii) read the hand_sum attribute. If you omit step (i) then the hand_sum attribute might have the wrong value.

For example, the total method omits step (i). Is this correct? It's impossible to tell by reading the method in isolation: you have to look at the logic for all the callers (and the callers of the callers, and so on) to see if they all remembered to call sum_hand().

This is no way to structure a program! Ideally, we should be able to check the correctness of each method in isolation. There are two alternative ways to achieve this: either make hand_sum into a property that always returns the correct value (regardless of what methods you've called beforehand); or ensure that hand_sum is always up to date by re-computing it whenever a card is drawn.

The first of these approaches is the most reliable, and it could easily be implemented using the @property decorator.

  1. We could improve the code in hand_sum by:
  • writing a docstring;
  • using a local variable for init_sum (I chose v for value) instead of an attribute (since this is not needed outside this method);
  • using the built-in sum function to add up the card values;
  • using the built-in any to simplify the aces test;
  • avoiding the superfluous and self.cards condition (if the hand contains an ace then obviously it must have at least one card);
  • using a bit of math to simplify v + 10 <= 21 to v <= 11;
  • stating the ace rule instead merely alluding to it (this means that a reader can check the code is correct without having to be a blackjack expert);
  • putting the (cheap) test v <= 11 before the (less cheap) ace test, so that if the former fails we skip the latter.

See below for the revised code.

  1. The dealer logic has a comment saying "makes sure the dealer hits soft 17" but the code does not implement this. The sum_hand method always computes the hard value of the hand, and so the dealer will stay on soft 17. The solution is for a hand to have two value properties: one for the soft value and one for the hard value.

  2. Drawing a card from the deck requires three steps in sequence: (i) check that counter is less than the length of the deck (otherwise there are no cards remaining to draw); (ii) read s_deck[counter]; (iii) add one to counter. Again, it would be very easy to forget one of these steps. And as far as I can see, step (i) has in fact been forgotten.

With just two players, and the deck being shuffled after every hand, it's not possible to run out of cards. But if you wanted to shuffle less often, or add more players, then the issue would come up.

See the Deck class below for one approach.

3. Revised code

from itertools import product
from random import sample

SUIT = 'SCDH'
RANK = 'A23456789TJQK'
DECK = [''.join(card) for card in product(RANK, SUIT)]
VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
DECKVAL = {card:VALUES[RANK.index(card[0])] for card in DECK}

class OutOfCards(Exception): pass

class Deck:
    """A shuffled deck of cards."""
    def __init__(self):
        self._cards = sample(DECK, len(DECK))

    def draw(self):
        """Draw one card from the deck and return it.
        Raise OutOfCards if the deck is out of cards.

        """
        if self._cards:
            return self._cards.pop()
        else:
            raise OutOfCards()

class Hand:
    """A hand of cards in blackjack."""
    def __init__(self, deck):
        self.cards = []
        for _ in range(2):
            self.draw(deck)

    def __str__(self):
        return ' '.join(self.cards)

    def draw(self, deck):
        """Draw one card from deck. Return the new card."""
        card = deck.draw()
        self.cards.append(card)
        return card

    @property
    def soft_value(self):
        """The "soft" value of the cards in the hand (counting aces as 1)."""
        return sum(DECKVAL[card] for card in self.cards)

    @property
    def value(self):
        """The "hard" value of the cards in the hand (counting aces as 11
        unless this would go bust, otherwise as 1).

        """
        v = self.soft_value
        if v <= 11 and any(card[0] == 'A' for card in self.cards):
            return v + 10
        else:
            return v

    @property
    def bust(self):
        """True if this hand is bust."""
        return self.soft_value > 21

    @property
    def blackjack(self):
        """True if this hand is a blackjack (ace plus ten or face card)."""
        return len(self.cards) == 2 and self.value == 21

def blackjack():
    """Play a hand of blackjack."""
    deck = Deck()
    hand = Hand(deck)
    dealer = Hand(deck)
    print("Dealer shows: {}".format(dealer.cards[0]))

    # Player logic
    while True:
        print("Your hand: {}".format(hand))
        if hand.bust:
            print("BUST!")
            return
        choice = input("Hit or stay? ").lower()
        if 'stay'.startswith(choice):
            break
        elif 'hit'.startswith(choice):
            card = hand.draw(deck)
            print("You drew: {}".format(card))

    # Dealer logic ("soft 17" variant)
    while True:
        print("Dealer's hand: {}".format(dealer))
        if dealer.soft_value >= 17:
            break
        card = dealer.draw(deck)
        print("Dealer drew: {}".format(card))
    if dealer.bust:
        print("Dealer is bust.")
    elif dealer.blackjack and not hand.blackjack:
        print("Dealer's blackjack beats your {}".format(hand.value))
    elif hand.blackjack and not dealer.blackjack:
        print("Your blackjack beats dealer's {}".format(dealer.value))
    elif dealer.value > hand.value:
        print("Dealer's {} beats your {}".format(dealer.value, hand.value))
    elif hand.value > dealer.value:
        print("Your {} beats dealer's {}".format(hand.value, dealer.value))
    else:
        print("Push: {} each".format(hand.value))