6
\$\begingroup\$

I feel the best way to learn is practice. I have written a simple Blackjack game in Python which supports multiple-players and functionality like Hit, Stand, Surrender, Split and Double_Down. I need some help to review my code, in order to remove any redundant code, simplify the implementation, and improve the code quality over all.

Please let me know your thoughts, any feedback is welcome.

import random

SUITS = ['Clubs', 'Spades', 'Hearts', 'Diamonds']
RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
MAX_PLAYERS = 8
MAX_BALANCE = 1000
chip_balance = 0


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

        if rank == 'A':
            self.point = 11
        elif rank in ['K', 'Q', 'J']:
            self.point = 10
        else:
            self.point = int(rank)

        self.hidden = False

    def __str__(self):
        if self.hidden:
            return '[X]'
        else:
            return '[' + self.suit + ' ' + self.rank + ']'

    def hide_card(self):
        self.hidden = True

    def reveal_card(self):
        self.hidden = False

    def is_ace(self):
        return self.rank == 'A'


class Deck(object):
    def __init__(self):
        self.cards = [Card(suit, rank) for suit in SUITS for rank in RANKS]
        self.shuffle()

    def __str__(self):
        cards_in_deck = ''
        for card in self.cards:
            cards_in_deck = cards_in_deck + str(card) + ' '
        return cards_in_deck

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

    def deal_card(self):
        card = self.cards.pop(0)
        return card


class Hand(object):
    def __init__(self):
        self.hand = []

    def add_card(self, card):
        self.hand.append(card)
        return self.hand

    def get_value(self):
        aces = 0
        value = 0
        for card in self.hand:
            if card.is_ace():
                aces += 1
            value += card.point
        while (value > 21) and aces:
            value -= 10
            aces -= 1
        return value


class Dealer(Hand):
    def __init__(self, name, deck):
        Hand.__init__(self)
        self.name = name
        self.deck = deck
        self.isBust = False

    def show_hand(self):
        for card in self.hand:
            print card,
        print

    def hit(self):
        print "Hitting..."
        self.add_card(self.deck.deal_card())
        return self.hand

    def stand(self):
        print "%s gets %d. Done." % (self.name, self.get_value())

    def check_bust(self):
        if self.get_value() > 21:
            self.isBust = True
            print "%s gets bust!" % self.name
        else:
            self.stand()


class Player(Dealer):
    def __init__(self, name, deck, bet):
        Dealer.__init__(self, name, deck)
        self.bet = bet
        self.isBust = False
        self.isSurrender = False
        self.isSplit = False
        self.split = []


def play(player, deck):
    print player.name + ':',
    player.show_hand()
    if player.name == 'Dealer':
        while player.get_value() < 17:
            player.hit()
            player.show_hand()
        player.check_bust()
    else:
        global chip_balance
        if chip_balance > player.bet and not player.isSplit:
            if player.hand[0].point == player.hand[1].point:
                choice = input_func("Hit, Stand, DoubleDown, Split or Surrender? (h/s/d/p/u) ", str.lower,
                                    range_=('h', 's', 'd', 'p', 'u'))
            else:
                choice = input_func("Hit, Stand, DoubleDown or Surrender? (h/s/d/u) ", str.lower,
                                    range_=('h', 's', 'd', 'u'))
        else:
            choice = input_func("Hit, Stand or Surrender? (h/s/u) ", str.lower, range_=('h', 's', 'u'))
        while choice == 'h':
            player.hit()
            player.show_hand()
            if player.get_value() > 21:
                player.isBust = True
                print "%s gets bust!" % player.name
                break
            choice = input_func("Hit or Stand? (h/s) ", str.lower, range_=('h', 's'))

        if choice == 's':
            player.stand()

        if choice == 'd':
            chip_balance -= player.bet
            player.bet *= 2
            print "New balance = %d" % chip_balance
            player.hit()
            player.show_hand()
            player.check_bust()

        if choice == 'u':
            player.isSurrender = True
            chip_balance += (player.bet - player.bet / 2)
            print "New balance = %d" % chip_balance

        if choice == 'p':
            chip_balance -= player.bet
            print "New balance = %d" % chip_balance
            player.split.append(Player(' Split_1', deck, player.bet))
            player.split.append(Player(' Split_2', deck, player.bet))
            for p in player.split:
                p.add_card(player.hand.pop(0))
                p.add_card(deck.deal_card())
                p.isSplit = True
                play(p, deck)


def input_func(prompt, type_=None, min_=None, max_=None, range_=None):
    value = ''
    while True:
        value = raw_input(prompt)
        if type_ is not None:
            try:
                value = type_(value)
            except ValueError:
                print "Sorry I don't understand."
                continue
        if min_ is not None and value < min_:
            print "Sorry your input can not be less than %d!" % min_
        elif max_ is not None and value > max_:
            print "Sorry your input can not be more than %d!" % max_
        elif range_ is not None and value not in range_:
            print "You must select from", range_
        else:
            break
    return value


def report(player, dealer):
    global chip_balance
    if player.isSurrender:
        tag = 'surrender'
    elif player.isBust:
        tag = 'lose'
    elif len(player.hand) == 2 and player.get_value() == 21 and not player.isSplit:
        tag = 'blackjack'
        chip_balance += player.bet * 3
    elif dealer.isBust or (player.get_value() > dealer.get_value()):
        tag = 'win'
        chip_balance += player.bet * 2
    elif player.get_value() == dealer.get_value():
        tag = 'push'
        chip_balance += player.bet
    else:
        tag = 'lose'
    print "%s: %-*s Balance = %d" % (player.name, 10, tag, chip_balance)


def game():
    players = []
    global chip_balance
    deck = Deck()

    player_num = input_func("\nPlease enter the number of players: (1-8) ", int, 1, MAX_PLAYERS)

    print "\nLet's get started...\n"

    for i in range(player_num):
        if chip_balance > 0:
            player_name = 'Player_' + str(i + 1)
            print "%s:" % player_name
            player_bet = input_func("Please bet. The minimal bet is 1 chip. ", int, 1, chip_balance)
            chip_balance -= player_bet
            print "Balance updated. New balance is %d." % chip_balance
            player = Player(player_name, deck, player_bet)
            players.append(player)
        else:
            print "\nThe actual number of player is %d. There's no balance to support more players." % (len(players))
            break

    dealer = Dealer('Dealer', deck)

    for i in range(2):
        for player in (players + [dealer]):
            player.add_card(deck.deal_card())

    dealer.hand[1].hide_card()
    print "\nDealer:"
    dealer.show_hand()
    print
    dealer.hand[1].reveal_card()

    for player in (players + [dealer]):
        play(player, deck)
        print

    print "...Final result...\n"

    for player in players:
        if not player.split:
            report(player, dealer)
        else:
            print "%s: split" % player.name
            for p in player.split:
                report(p, dealer)

    print "\nFinal chip balance is %d.\n" % chip_balance


if __name__ == '__main__':

    chip_balance = input_func("\nWelcome to BlackJack! Please enter the chip balance: (1-1000) ", int, 1, MAX_BALANCE)
    while True:
        game()
        if chip_balance < 1:
            print "You don't have enough balance to proceed. Game over."
            break
        proceed = input_func("Do you want to continue? (y/n) ", str.lower, range_=('y', 'n'))
        if proceed == 'n':
            print "\nThank you for playing! See you next time."
            break

Below is the result:

Welcome to BlackJack! Please enter the chip balance: (1-1000) 100

Please enter the number of players: (1-8) 4

Let's get started...

Player_1:
Please bet. The minimal bet is 1 chip. 2
Balance updated. New balance is 98.
Player_2:
Please bet. The minimal bet is 1 chip. 2
Balance updated. New balance is 96.
Player_3:
Please bet. The minimal bet is 1 chip. 2
Balance updated. New balance is 94.
Player_4:
Please bet. The minimal bet is 1 chip. 2
Balance updated. New balance is 92.

Dealer:
[Clubs A] [X]

Player_1: [Diamonds J] [Clubs 2]
Hit, Stand, DoubleDown or Surrender? (h/s/d/u) h
Hitting...
[Diamonds J] [Clubs 2] [Hearts A]
Hit or Stand? (h/s) h
Hitting...
[Diamonds J] [Clubs 2] [Hearts A] [Diamonds 10]
Player_1 gets bust!

Player_2: [Clubs 7] [Hearts 5]
Hit, Stand, DoubleDown or Surrender? (h/s/d/u) d
New balance = 90
Hitting...
[Clubs 7] [Hearts 5] [Hearts 4]
Player_2 gets 16. Done.

Player_3: [Spades 3] [Spades 9]
Hit, Stand, DoubleDown or Surrender? (h/s/d/u) d
New balance = 88
Hitting...
[Spades 3] [Spades 9] [Diamonds 2]
Player_3 gets 14. Done.

Player_4: [Spades K] [Spades 5]
Hit, Stand, DoubleDown or Surrender? (h/s/d/u) h
Hitting...
[Spades K] [Spades 5] [Hearts K]
Player_4 gets bust!

Dealer: [Clubs A] [Spades 8]
Dealer gets 19. Done.

...Final result...

Player_1: lose       Balance = 88
Player_2: lose       Balance = 88
Player_3: lose       Balance = 88
Player_4: lose       Balance = 88

Final chip balance is 88.

Do you want to continue? (y/n) n

Thank you for playing! See you next time.
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

The code generally looks good.


The inheritance hierarchy is weird: Dealer is a Hand? I would rather say that a dealer has a hand. Player is a Dealer? That sounds backwards — rather, a dealer is a player.

The difference in naming convention between properties (e.g. isBust) and method calls (e.g. get_value()) is annoying. You should use the is_bust convention uniformly. I would expect card.is_ace to be a property, rather than card.is_ace().


For Deck.__str__(), you could write

def __str__(self):
    return ' '.join(str(card) for card in self.cards)

… which also gets rid of the extra space at the end of any non-empty deck.

Deck.deal_card() should just be return self.cards.pop(). Dealing from the start of the array requires every card to be shifted over — that's a lot of unnecessary copying.


For Hand.get_value(), you should probably use sum():

def get_value(self):
    value = sum(card.point for card in self.cards)
    aces = sum(card.is_ace for card in self.cards)
    while (value > 21) and aces:
        value -= 10
        aces -= 1
    return value

The function name report() suggests that it merely prints the outcome. It's a bit surprising that it does more: it also awards the winnings. Therefore, award_winnings() would be a better name.

Winning double your bet for a Blackjack is unusually generous. Customarily, you only earn 1.5× your bet.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks!. The reason I let player inherit dealer is dealer is a more general type, it has some functions such as hit/stand that can be re-used by player, but player has more functions that dealer doesn't require, like surrender/split and attributes like isSplit etc. I don't want to initialize a object (dealer) that includes too many unused attributes and methods. Usually blackjack wins 1.5 x bet, but i wanted to avoid the floats, same reason for surrender: usually we get half bet back but in my program we will get (rounded) half, for example, 2 of 3 chips will be back once we surrender. \$\endgroup\$
    – yren
    Commented Apr 4, 2015 at 6:16
0
\$\begingroup\$

You forgot to put parentheses on all of the print statements.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Welcome to Code Review! The OP did not mention which version of Python they were using, nor was version 2.x or 3.x tagged, though given the age it may well be the case that it was intended for version 2. In Python 2.x print is actually a special statement and not a function*.. Might there be any other observations or recommendations about the code to mention that hasn’t been brought up in the other answer? \$\endgroup\$ Commented Nov 1, 2022 at 3:02

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.