Tuesday, 21 January 2020

Object Oriented Design - Simple Blackjack

Object Oriented Design - Blackjack !

Simple Blackjack game design

Requirements :

  • Player should be able to place a bet
  • Dealer should be able to deal cards to player
  • Player can choose hit or stay only
  • Dealer can take more cards if sum <= 16 and declares the outcome

Actors and main use cases

  • Dealer
    • deal card
    • take bet
    • payout
    • declare outcome
  • Player
    • place bet
    • decide action
    • take payout

Classes in the system what responsibilities they have and how they interact

  • Card : represents a normal card
  • BlackJackCard : its a card but it know how it behaves in Blackjack game
  • Dealer : deals card, etc
  • Player : place bet, etc
  • Deck : a deck of cards
  • Hand : a set of cards
  • Game : orchestrates all of the objects, startes the game, asks player to place bet, makes dealer deal cards, asks player for action, asks dealer for action, declares outcome via dealer

bj_game.jpg

In [69]:
# K.I.S.S - keep things "very simple" in the beginning
# First code basic interfaces of the classes
# Build a client which uses it
# Implement details
# Then try to improve it

# *** Possible Improvements ***
#  print/io logic can be put into implemenations like CLIPlayer
#  Name can be asked from user at runtime
#  Rules can be abstracted out into a separate class
#  More actions can he handled like split, insurance
#  Dealer actions can be moved one level up and let game do it

import enum
import sys
from random import randint

class BlackJackGame(object):
    def __init__(self):
        self.player = self._make_player()
        self.dealer = self._make_dealer()
        self.deck = Deck()
    
    def play(self):
        # get bet from player
        # dealer deals cards
        # player gameplay
        # dealer gameplay
        # result and decide to replay
        while True:
            self.deck.shuffle()
            self.player.hand = Hand()
            self.dealer.hand = Hand()
            
            bet = self.player.get_bet()
            self.dealer.accept_bet(bet)
            
            self.dealer.deal_card(self.deck, self.player)
            self.dealer.deal_card(self.deck, self.player)
            self.dealer.deal_card(self.deck, self.dealer)
            # keep track of hole card to later make it visible
            hole_card = self.dealer.deal_card(self.deck, self.dealer, True)
            
            self.display()
            
            while not self._is_bust(self.player.hand):
                p_action = self.player.get_action(['hit', 'stand'])
                if p_action == 'hit':
                    self.dealer.deal_card(self.deck, self.player)
                    self.display()
                elif p_action == 'stand':
                    break
                    
            if self._is_bust(self.player.hand):
                result = 'Dealer wins, player is bust !'
                self.dealer.take_money(bet*2) 
            else:
                hole_card.hidden = False
                self.display()
                while self.dealer.hand.get_score() <= 16:
                    self.dealer.deal_card(self.deck, self.dealer)
                    self.display()

                if self._is_bust(self.dealer.hand):
                    result = 'Player wins, dealer is bust !'
                    self.player.take_money(bet*2)
                elif self.dealer.hand.get_score() == self.player.hand.get_score():
                    result = 'Push, its a draw !'
                    self.player.take_money(bet)
                    self.dealer.take_money(bet)
                elif self.dealer.did_player_win(self.dealer.hand, self.player.hand):
                    result = 'Player wins'
                    self.player.take_money(bet*2)
                else:
                    result = 'Dealer wins'
                    self.dealer.take_money(bet*2)
            
            self.publish_scores()    
            self._publish_result(result)
            
            
            if not self.player.wants_to_play():
                break
                
    def put_back_cards(self):
        all_cards = [].append(self.player.hand.cards).append(self.dealer.hand.cards)
        for card in all_cards:
            if card.hidden:
                card.hidden = False
            self.deck.add_card(card)
                
    def publish_scores(self):
        print('Dealer score : %s'%self.dealer.hand.get_score())
        print('Player score : %s'%self.player.hand.get_score())
           
    def _publish_result(self, res):
        print("**************************\n")
        print(res)
        print("\n")
        print("**************************\n")
        
    def display(self):   
        self.dealer.display()
        self.player.display()
        
    def _is_bust(self, hand):
        return hand.get_score() > 21
    
    def _make_player(self):
        return Player('Shaktiman', 100)
    
    def _make_dealer(self):
        return Dealer('Kilvish', 100)

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

class BlackJackCard(Card):
    def __init__(self, suit, number):
        super().__init__(suit, number)
        self.hidden = False

    def is_ace(self):
        return self.number == 1
    
    def value(self):
        if self.is_ace():
            raise NotImplementedError('Ace is handled separately')
        if self.number >= 10 and self.number <=13:
            return 10
        else:
            return self.number
    
    def to_string(self):
        if self.hidden:
            return 'XXX'
        else:
            return '%s:%d' % (self.suit, self.number)

class Player(object):
    def __init__(self, name, money):
        self.name = name
        self.money = money
        self.hand = Hand()
        
    def take_money(self, money):
        self.money = self.money + money
        
    def get_bet(self):
        amount =  int(input('Enter your bet amount %s : '% self.name))
        if amount > self.money:
            print('Cant bet more than what you have : %d\n'%self.money)
            return self.get_bet()
        self.money = self.money - amount
        return amount
    
    def wants_to_play(self):
        return 'y' == input('Want to play (y/n)?').lower()
    
    def display(self):
        print('Hand of %s : ' % self.name)
        for card in self.hand.cards:
            print('[%s]' % card.to_string())
    
    def get_action(self, options):
        action_index = input('Choose your action , press 1 for first , 2 for second etc : ' + ','.join(options))
        return options[int(action_index)-1]

class Dealer(Player):
    def __init__(self, name, money):
        super().__init__(name, money)
    
    def deal_card(self, deck, player, hidden=False):
        card = deck.remove_card()
        card.hidden = hidden
        player.hand.add_card(card)
        return card
        
    def accept_bet(self, money):
        self.money = self.money - money
        
    def did_player_win(self, dealer_hand, player_hand):
        d_score = dealer_hand.get_score()
        p_score = player_hand.get_score()
        return p_score > d_score
            


class Deck(object):
    def __init__(self):
        self.cards = []
        for suit in Suit:
            for number in range(1,14):
                # BlackJackCard can be passed via a strategy later on
                self.cards.append(BlackJackCard(suit, number))
    
    def shuffle(self):
        L = len(self.cards)
        for l in range(L-1):
            r = randint(l+1, L-1)
            self.cards[r], self.cards[l] = self.cards[l], self.cards[r]
    
    def remove_card(self):
        return self.cards.pop()
    
    def add_card(self, card):
        self.cards.append(card)
    
class Suit(enum.Enum):
    HEART = 1
    SPADE = 2
    DIAMOND = 3
    CLUB = 4

class Hand(object):
    def __init__(self):
        self.cards = []
        
    def add_card(self, bj_card):
        self.cards.append(bj_card)
        
    def get_score(self):
        # calculate the score of hand
        possible_scores = [0]
        for card in self.cards:
            new_possible_scores = []
            for score in possible_scores:
                if card.is_ace():
                    new_possible_scores.append(score+1)
                    new_possible_scores.append(score+11)
                else:
                    new_possible_scores.append(score+card.value())
            possible_scores = new_possible_scores
        possible_scores_less_than_21 = list(filter(lambda x : x<=21, possible_scores))
        if len(possible_scores_less_than_21) > 0 :
            return max(possible_scores_less_than_21)
        else:
            return min(possible_scores)
                
In [ ]:
game = BlackJackGame()
game.play()
In [ ]:
 

No comments:

Post a Comment