Source Code: Blackjack Game

okcancel20031010.gif

#! /usr/bin/python2.5
#
# $Id: blackjack.py 13 2008-02-22 11:58:32Z david $
# (c) 2008, David Chaves
#
# DESCRIPTION
#
#      This program implements a basic blackjack card game
#
#      In brief, each player is dealt 2 face up cards, and
#      the dealer is dealt 1 face up and one face down card.
#      In sequence, each player decides whether (s)he will take
#      another card (hit) or not (stand). If the new card makes
#      the total of that player's cards greater than 21,
#      that player has busted, and loses his/her bet. Otherwise,
#      the player may again choose to hit or stand
#
#      Jacks, queens and kings count as 10, and an ace may count
#      as either 1 or 11. After all of the players have gone,
#      the dealer reveals the down card and hits until the
#      dealer's total is greater than or equal to 17
#
#      If the dealer busts, all players who did not bust win;
#      otherwise, any player with a higher total than the deal win,
#      and lower totals lose. If a player has the same total as
#      the dealer, it is a push and the player neither wins nor loses
#
#      Here is a sample game:
#
#        |Number of players?
#        |3
#        |Player 1: J-D, 4-S
#        |Player 2: 7-H, 8-C
#        |Player 3: Q-H, 9-D
#        |Dealer : A-S, <down>
#        |Player 1, you have 14. Hit or stand?
#        |hit
#        |Player 1: J-D, 4-S, 3-D
#        |Player 1, you have 17. Hit or stand?
#        |h
#        |Player 1: J-D, 4-S, 3-D, 8-H - You busted!
#        |Player 2, you have 15. Hit or stand?
#        |s
#        |Player 3, you have 19. Hit or stand?
#        |s
#        |Dealer : A-S, 5-S
#        |Dealer has 16. Dealer hits.
#        |Dealer : A-S, 5-S, 8-D
#        |Dealer has 14. Dealer hits.
#        |Dealer : A-S, 5-S, 8-D, 3-C
#        |Dealer has 17. Dealer stands.
#        |Player 1: J-D, 4-S, 3-D, 8-H - You busted!
#        |Player 2: 7-H, 8-C - You lose!
#        |Player 3: Q-H, 9-D - You win!
#        |Dealer : A-S, 5-S, 8-D, 3-C
#
#     This program does not implement any of the special
#     betting rules of blackjack (splitting, blackjack,
#     insurance etc.) but conforms to the basic rules on
#     http://en.wikipedia.org/wiki/Blackjack
#
# KNOWN BUGS
#
#     We do not consider the case where the Deck becomes empty
#     in the middle of the game.  This situation should be fixed
#     using run-time exceptions thrown in Deck.deal() and 
#     catched in Game.__init__() and Game.play()
#
#     When the deck becomes empty, this program currently
#     crashes like this:
#
#         Traceback (most recent call last):
#           ...
#           File "blackjack.py"...
#             position_in_deck = random.randint(0, len(self.cards_)-1)
#           ...
#           File "blackjack.py"...
#             raise ValueError, "empty range for randrange() (%d,%d, %d)" % (istart, istop, width)
#
#     This program is also weak checking for valid inputs from end-users
#
#     Finally, this is our first Python code in our whole life.
#     In consecuence, the coding style can certantly be improved
#
# DISCLAIMER
#
#     This script was genetically modified from a Python script
#     originally written by an unknown author, but available 
#     at http://www.daniweb.com/code/snippet627.html
#     

import random

#--------------------------------------- Constants

# Every card has a rank and a face value
#
# The following Rank maps a rank into the value:
#   Ace 11 or 1
#   J, Q, K are 10
#   the rest of the cards 2 - 10 are face value

Ranks = {
    # Rank => Value
    '2'     :  2,
    '3'     :  3,
    '4'     :  4,
    '5'     :  5,
    '6'     :  6,
    '7'     :  7,
    '8'     :  8,
    '9'     :  9,
    '10'    : 10,
    'Jack'  : 10,
    'Queen' : 10,
    'King'  : 10,
    'Ace'   : 11
}

#--------------------------------------- class Card

class Card:
    '''Represent a single card'''

    def __init__(self, rank = 'Ace', suite = 'Heart'):
        '''Constructor'''
        self.rank_ = rank
        self.suite_ = suite

    def __int__(self):
        '''Return the value for this card'''
        return Ranks[self.rank_]

    def __repr__(self):
        '''Return the string representation for this card'''
        return '%s-%s' % (self.rank_, self.suite_)

    def is_ace(self):
        '''Return True if this card is an Ace'''
        return self.rank_ == 'Ace'

#--------------------------------------- class Deck

class Deck:
    '''Represent a deck of cards'''

    def __init__(self):
        '''Constructor'''

        # populate an initial list 
        # containing all possible cards

        self.cards_ = []
        for suite in [ 'Heart', 'Diamond', 'Spade', 'Club' ]:
            for rank in Ranks.keys():
                card = Card(rank, suite)
                self.cards_.append(card)

        # normally we would need to shuffle the deck
        # being created, but this is not necessary
        # since that we will draw the cards in
        # randomly order

    def deal(self):
        '''Take the next card from the deck'''

        # select a random card in the deck
        # @see http://docs.python.org/lib/module-random.html
        position_in_deck = random.randint(0, len(self.cards_)-1)

        # take the card from the deck
        card = self.cards_[position_in_deck]
        self.cards_.pop(position_in_deck)
        return card

#--------------------------------------- class Hand

class Hand:
    '''Represent a hand of cards'''

    def __init__(self, owner = 'Dealer'):
        '''Constructor'''
        self.cards_ = []
        self.owner_ = owner

    def hit(self, deck):
        '''One card will be cut from the deck and copied to the hand'''
        self.cards_.append(deck.deal())

    def tally(self):
        '''Return the total the value of the cards in this hand'''

        total = aces = 0
        for card in self.cards_:
            total += int(card)  # count the value of the hand
            if card.is_ace():   # counting ACE(s) as 11
                ++aces

        # to complicate things a little the ace can be 11 or 1
        # this little while loop figures it out for you

        while aces > 0 and total > 21:
            # you have gone over 21 but there is an ace
            # this will switch the ace from 11 to 1
            total -= 10
            aces -= 1

        return total

    def is_busted(self):
        '''Return true if this hand is busted'''
        return self.tally() > 21

    def might_want_to_hit(self):
        '''Return true if this hand might want to hit another card'''

        # This function is used by the class Game to define the
        # dealer strategy: s/he generally stands around 17 or 18
        #
        # However, it is made available to all players becuase
        # we might want to hint them in the future... should we?

        return self.tally() < 18

    def owner(self):
        '''Pretty print the owner name'''
        return self.owner_

    def cards(self):
        '''Pretty print the cards in this hand'''
        text = ''
        for card in self.cards_:
            if text:
                text += ', '
            text += '%s' % card
        return text

    def tally_and_cards(self):
        '''Pretty print the value and the cards in this hand'''
        return '%d: %s' % (self.tally(), self.cards())

    def tally_and_first_card(self):
        '''Pretty print the value and the first card in this hand'''
        return '%d: %s, (DOWN)' % (self.tally(), self.cards_[0])

    def win_or_lose(self, dealer):
        '''Pretty print the win/lose final result'''

        # If the dealer busts, all players
        # who did not bust win; otherwise,
        # any player with a higher total than
        # the deal win, and lower totals lose.
        # If a player has the same total as the dealer,
        # it is a push and the player neither wins nor loses.

        if self.is_busted():
            return 'you busted'
        if dealer.is_busted():
            return 'you win'

        total_hand = self.tally()
        total_dealer = dealer.tally()

        if total_dealer > total_hand:
            return 'you lose'
        elif total_dealer < total_hand:
            return 'you win'
        else:
            return 'this is a push'

#--------------------------------------- class Player

class Player(Hand):
    '''Represent a human player (not a dealer)'''

    def __init__(self, owner = 'Player'):
        '''Constructor'''
        Hand.__init__(self, owner)

    def header(self):
        '''Pretty print the header message'''
        return '    %s has %s' % (self.owner(), self.tally_and_cards())

    def summary(self):
        '''Pretty print the final, summary message'''
        return '    %s has %s' % (self.owner(), self.tally_and_cards())

    def hit_or_stand(self):
        '''Pretty print the message used while this player plays'''
        return '%s, you have %s' % (self.owner(), self.tally_and_cards())

#--------------------------------------- class Dealer

class Dealer(Hand):
    '''Represent a dealer player (the computer)'''

    def __init__(self):
        '''Constructor'''
        Hand.__init__(self, 'Dealer')

    def header(self):
        '''Pretty print the header message'''
        return '    The Dealer has %s' % self.tally_and_first_card()

    def summary(self):
        '''Pretty print the final, summary message'''
        return '    Dealer has %s' % self.tally_and_cards()

    def hit_or_stand(self):
        '''Pretty print the message used while the dealer plays'''
        return 'Dealer has %s' % self.tally_and_cards()

#--------------------------------------- class Game

class Game:
    '''Represent a game in progress'''

    def __init__(self, players = range(1, 1)):
        '''Constructor'''

        self.deck_ = Deck()

        self.players_ = {}
        for player in players:
            hand = Player('Player %s' % player)
            # draw 2 cards for the player to start
            hand.hit(self.deck_)
            hand.hit(self.deck_)
            self.players_[player] = hand

        self.dealer_ = Dealer()
        # draw 2 cards for the dealer to start
        self.dealer_.hit(self.deck_)
        self.dealer_.hit(self.deck_)

    def players(self):
        '''Syntatic sugar: return the list of player keys'''
        return self.players_.keys()

    def print_header(self):
        '''Print the messages used at the beginning of the game'''
        dealer = self.dealer_
        for player in self.players():
            hand = self.players_[player]
            print hand.header()
        print dealer.header()
        print

    def print_summary(self):
        '''Print the messages used at the very end of the game'''
        dealer = self.dealer_
        for player in self.players():
            hand = self.players_[player]
            print '%s - %s!' % (hand.summary(), hand.win_or_lose(dealer))
        print dealer.summary()
        print

    def play_player_loop(self, player):
        '''This is the complete loop where a player... well, plays'''

        hand = self.players_[player]
        while True:
            if hand.is_busted():
                print '%s - You busted!' % hand.hit_or_stand()
                break

            # @todo -- we might want to wait until the player
            # enters a valid answer here, but it is no worth the effort

            # for now, an empty answer (just pressing [ENTER], for example)
            # will be handled the same as "Hit, please"

            question = '%s - Hit or stand? ' % hand.hit_or_stand()
            answer = raw_input(question).lower()
            if 's' in answer:
                break

            hand.hit(self.deck_)
        print

    def play_dealer_loop(self):
        '''This is the complete loop where the dealer plays'''

        # the dealer reveals the down card and hits until the
        # dealer's total is greater than or equal to 17.

        dealer = self.dealer_
        while True:
            if dealer.is_busted():
                break
            elif dealer.might_want_to_hit():
                print '%s - Dealer hits' % dealer.hit_or_stand()
                dealer.hit(self.deck_)
            else:
                break
        print '%s - Dealer stands' % dealer.hit_or_stand()
        print

    def play(self):
        '''Play a complete game, from the beginning to the end'''
        self.print_header()
        for player in self.players():
            self.play_player_loop(player)
        self.play_dealer_loop()
        self.print_summary()

#--------------------------------------- Main Body

if __name__ == '__main__':

    # if this file is executed from the command line,
    # then we will ask the number of players and
    # then we do play a complete run

    print 
    print 'Welcome to BlackJack'
    number_of_players = int(raw_input('Number of players? '))
    print

    players = range(1, number_of_players + 1)
    blackjack = Game(players)
    blackjack.play()

#--------------------------------------- The End

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License