1
\$\begingroup\$

A little while back, I made a post on blackjack using object oriented programming. I've begun running simulations under it, it runs fairly quickly but I know there is always improvements to be made. I have a few questions this time around, how would I figure out the runtime complexity of my program? what suggestions do people have to make it cleaner? and my next step is to attempt to build a ML algorithm to design a strategy that'll hopefully work better than the fairly simple strategy I have at the moment. What parameters should I be worried about for that? and kinda just a bonus question, anyone got any recommendations on where I should go to learn that? I've been looking at kaggle for quite a while and a few tutorials on youtube, but I want to know if anyone has any better less obvious recommendations.

I have coded tests, however I don't feel those are useful to go into here, as they are just manual tests and the simulation is just a for loop. 3,000 games takes about 1.5 seconds to run on the laptop I am using, and 30,000,000 games takes about 50 minutes to run (I tried 300,000,000 but it took too long.). I apologize in advance for the long lines but other than the tests, I don't know what is and isn't important for anyone to help me.

The goal with the code is simply a game of blackjack, and the whole reason I'm doing this is to impress my mentor next time I see him. I'm looking to get into data architecture and other fields of data science in the future.


''' 

Basic Blackjack program 

Follows all rules as provided on wikipedia.com

'''

import random
import matplotlib.pyplot as plt

ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
suits = ['Spades', 'Hearts', 'Diamonds', 'Clubs']
data = []



class Card:

    '''

    Creating the 'Card' class

    this is used simply to evaluate information about a singular card instead of an entire deck

    '''

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



    def card_value(self):

        '''
        
        Evaluate the value of a card

        in blackjack if cards are any of the royals
        they are just a ten

        and if they are an Ace, they are either 1 or 11
        depending on if that would cause you to bust.
        which is checked for in the hand class

        '''

        # if the rank is is a king, queen, or jack, return 10 as the value of the card.
        if self.rank in ['J', 'Q', 'K']:
            return 10
        # if the rank is an ace return 1 and 11. This will later
        # let us choose whether we want the ace to be a one or eleven
        if self.rank == 'A':
            return 1, 11
        # if it's nothing special just return the number.
        return int(self.rank)


    def __str__(self):
        return self.rank + '-' + self.suit


class Deck:

    '''

    Creating our deck class

    mostly, this will be used for drawing cards,
    setting up a deck so we don't just draw cards at random
    resulting in us having 2 aces of spades, which is illegal in blackjack.

    '''


    def __init__(self):
        self.cards = []
        # Create a deck, suit first then rank.
        # This makes the deck follow what a brand new pack of cards looks like.
        for suit in suits:
            for rank in ranks:
                self.cards.append(Card(rank, suit))



    def shuffle(self):

        '''

        very simple, just using the random library's
        shuffle method to shuffle the array made in
        __init__

        '''

        random.shuffle(self.cards)


    def deal_card(self):

        '''

        simply deals a card by popping one
        from the first spot in the deck array

        '''

        # Check if there are no cards left in the deck
        if not self.cards:
            for suit in suits:
                for rank in ranks:
                    self.cards.append(Card(rank, suit))
                    self.shuffle()
        # As long as there are cards left in the deck, pop one from the top of the deck out.
        draw = self.cards.pop(0)
        # return the drawn card so it can be added to the hand
        return draw

    def __str__(self):
        return '[' + ', '.join(map(str, self.cards)) + ']'



class Hand:

    '''

    creating the 'Hand' class

    this is used simply to store each players hand
    give the value of the two hands
    show if either hand is soft
    and also to make it easier to draw cards

    '''


    def __init__(self):
        # start with two cards in hand.
        self.cards = [(deck.deal_card()), (deck.deal_card())]
        self.card_value = 0
        for c in self.cards:
            if c.rank == 'A':
                self.soft = True
            else:
                self.soft = False
        self.ace_count = 0



    def value(self):

        '''

        The method 'Value', is used to check the value
        of the players hand, this will allow us to check
        to see if the player has busted, has an ace, or
        just has a regular old hand

        '''

        total = 0
        ace_count = 0
        # Calculate the total value of the hand
        for card in self.cards:
            if card.rank == 'A':
                ace_count += 1
            else:
                total += card.card_value()
        # Determine the value of aces
        for _ in range(ace_count):
            if total + 11 <= 21:
                total += 11
                self.soft = True
            else:
                total += 1

        return total


    def reset(self):

        ''' For resetting the hand between games '''

        self.cards.clear()
        self.draw()
        self.draw()



    def draw(self):

        '''

        draw method, simply exists to make it easier
        for a player to draw cards

        '''

        self.cards.append(deck.deal_card())



    def __str__(self):
        hand = []
        for c in self.cards:
            hand.append(str(c))
        return str(hand)



class Player:

    '''

    creating the 'Player' class

    this will have a multitude of select-able values
    a strategy, a name, and a budget
    this will all be managed by the player throughout the game
    whether it's a bot or a person.

    '''

    def __init__(self, budget=100, strategy='none', name='Player'):
        self.name = name
        self.hand = Hand()
        self.hand.value()
        self.state = 'play'
        self.hand.soft = self.hand.soft
        self.strategy = strategy
        self.bid_amount = 0
        self.starting_bid = 10
        self.current_simulated_bid = self.starting_bid
        self.budget = budget
        self.broke = False




    def hit(self):

        '''

        Makes the player draw a card, and makes sure the player state
        is set to 'play' so that the player can keep playing

        '''

        self.hand.draw()
        self.state = 'play'


    def stay(self):

        '''

        Changes the player state to 'stay', which changes anything
        that runs based on the 'play' state and causes
        it to stop working.

        '''

        self.state = 'stay'


    def check_bust(self):

        '''

        Check if the player busted by seeing if the hand
        value is above 21

        '''

        if self.hand.value() > 21:
            self.state = 'bust'
            return True
        return False


    def check_broke(self):

        ''' check if the player is broke '''

        if self.budget < 1:
            self.state = 'broke'
            return True
        return False


    def bid(self, amount = 0):

        '''

        Get a bid from the player if the player is actually a player,
        otherwise automate the bid

        '''

        self.broke = self.check_broke()
        if self.strategy == 'none':
            if self.broke:
                print('You are out of money and cannot bid anymore')
            else:
                self.bid_amount = int(input('How much?\n'))
                if self.bid_amount > self.budget:
                    print(f'You don\'t have that much. You have: ${self.budget}')
                self.budget -= self.bid_amount
                return self.bid_amount
        else:
            self.bid_amount = amount
            self.budget -= amount
            return self.bid_amount



    def strategy_one(self, dealer_up_card):

        '''

        A random very basic strategy, generated by chatGPT
        and implemented, by no means is it great. but 
        it gets the job done for now.

        '''

        player_hand_value = self.hand.value()

        # Define the basic strategy decisions based
        # on the player's hand value and the dealer's up card
        if not self.hand.soft:
            if player_hand_value <= 11:
                self.hit()
            elif player_hand_value == 12:
                if dealer_up_card.rank in [2, 3, 7, 8, 9, 10, 'J', 'Q', 'K', 'A']:
                    self.hit()
                else:
                    self.stay()
            elif 13 <= player_hand_value <= 16:
                if dealer_up_card.rank in [2, 3, 4, 5, 6]:
                    self.stay()
                else:
                    self.hit()
            elif player_hand_value >= 17:
                self.stay()
        else:
            if player_hand_value <= 17:
                self.hit()
            elif player_hand_value == 18:
                if dealer_up_card.rank in [9, 10, 'J', 'Q', 'K', 'A']:
                    self.hit()
                else:
                    self.stay()
            else:
                self.stay()


    def __str__(self):
        return ('Name: ' + str(self.name) + '; Hand: ' + str(self.hand) +
            '; Budget: ' + str(self.budget) + '; Hand Value: ' + str(self.hand.value())
            + '; Hand is soft: ' + str(self.hand.soft) + '; Busted: '
            + str(self.check_bust()) + '; State: ' + str(self.state))



class Dealer(Player):

    '''

    A dealer is another player where 
    his strategy is automatic

    '''

    # initialize the dealer as a player
    def __init__(self, name='Dealer'):
        super().__init__(9999999999999999999999999999999999999999999999999999999999, 'dealer', name)
        self.up_card = self.hand.cards[0]

    def dealer_strategy(self):

        '''

        the dealers strategy is always:
        hit below 17 and on soft 17
        stay on hard 17 or above 17

        '''

        if self.hand.value() < 17 or (
            self.hand.value() == 17 and self.hand.soft):
            self.hit()
        else:
            self.stay()

    def __str__(self):
        return 'Name: ' + str(self.name) + '; up_card: [\'' + str(self.up_card) + '\']'



class Game:

    '''

    Create the 'Game' class

    This will be used to create and run the game as well as
    log it within a variable which we have set to 'data'


    '''

    game_count = 0

    def __init__(self, player):
        self.dealer = Dealer()
        self.deck = Deck()
        self.deck.shuffle()
        self.player = player
        self.pot = 0
        self.simulated = False
        Game.game_count += 1


    def log_game(self):

        '''

        as mentioned above, 
        the game class is in charge
        of logging the game.
        which is what this method does

        '''

        data.append((Game.game_count, self.player.budget))


    def player_turn(self):

        ''' player's turn function '''

        # get the move that the player wants to do
        if self.player.strategy == 'strategy_one':
            self.player.strategy_one(self.dealer.up_card)
            self.player.check_bust()


            # in this version we are using the 2 - 1 - 2 bidding strategy
            # which increments by 1 every win, then resets to 1 at a loss

            self.pot += self.player.bid(self.player.current_simulated_bid)

        else:
            self.player.budget -= 10
            self.pot += 10
            player_move = input("what would you like to do? \n1) Hit \n2) Stay \n3) Bid\n")
            # execute that move
            if player_move.upper() in ['1', 'HIT']:
                self.player.hit()
            elif player_move.upper() in ['2', 'STAY']:
                self.player.stay()
            elif player_move.upper() in ['3', 'BID']:
                self.pot += self.player.bid()
            else:
                print('Not a valid move')

            print(self.player.hand)
            print(self.player.hand.value())

            # Check if the player busted
            self.player.check_bust()
            if self.player.state == 'bust':
                print('Sorry, you busted.')


    def check_win(self):

        '''

        Check if the player has won the round
        this will be if the hand value is higher
        than the dealers
        and the hand has not busted
        or if the dealer has busted
        and the player hasn't

        '''

        if self.player.strategy == 'none':
            if (((self.player.hand.value() > self.dealer.hand.value()) and
                self.player.state != 'bust') or self.dealer.state == 'bust'):
                if self.player.state == 'bust' and self.dealer.state == 'bust':
                    print('You lost this round.')
                else:
                    print('You won this round!')
                    self.player.budget += self.pot
            else:
                print('You lost this round.')


    def change_bid(self):

        ''' 

        change the amount of the
        simulated bid depending on win/loss

        '''

        if (((self.player.hand.value() > self.dealer.hand.value()) and
            self.player.state != 'bust') or self.dealer.state == 'bust'):
            if self.player.state == 'bust' and self.dealer.state == 'bust':
                self.player.current_simulated_bid = self.player.starting_bid
            else:
                self.player.budget += self.pot
                self.player.current_simulated_bid += 2
        else:
            self.player.current_simulated_bid = self.player.starting_bid


    def run(self):

        '''
        
        this function actually runs the game,
        letting the player make their moves first
        then the dealer making theirs.

        '''

        self.pot = 0
        self.player.state = 'play'

        if self.player.strategy == 'none':
            print(self.player)
            print(self.dealer)
            print('Buy in is 10 bucks.')

        deck.shuffle()

        if self.dealer.hand.value() == 21:
            self.player.state = 'stay'

        # while the player is not staying, busted or out of budget
        while self.player.state == 'play':
            self.player_turn()


        # Run the dealer simulation
        while self.dealer.state == 'play':
            self.dealer.dealer_strategy()

        # add the dealers bet to the pot, which will always be equal to the pot.
        self.pot += self.dealer.bid(self.pot)

        # Check bust for both
        self.dealer.check_bust()
        self.player.check_bust()

        # Check win and loss
        if self.player.strategy == 'none':
            self.check_win()

        if self.player.strategy != 'none':
            self.change_bid()



        self.log_game()
        # print(data)
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I tried to clean your code, removed unused variables, and made dealer money infinite and deck creation faster.

Revising your it saw a big mistake: Player and Dealer used a different deck and fixed it.

I simplified Game creation and player strategies and added some argument-type and output-type indicators.

I may create a bug because, after some loop of Games with strategy one, the Game.run() stops printing.

To make your program faster without changing it, I suggest you use Pypy, which is 4.8 times faster (on overage).

Your project is so interesting, and if it is useful to you, I could help to the extent of my ability.

Here the Code:

from random import shuffle
from typing import Literal, Union
ranks = ('A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K')
suits = ('Spades', 'Hearts', 'Diamonds', 'Clubs')
data = []



class Card:

    '''

    Creating the 'Card' class

    this is used simply to evaluate information about a singular card instead of an entire deck

    '''

    def __init__(self, rank: tuple , suit: tuple) -> str:
        self.rank = rank
        self.suit = suit


    def card_value(self) -> int:

        '''

        Evaluate the value of a card

        in blackjack if cards are any of the royals
        they are just a ten

        and if they are an Ace, they are either 1 or 11
        depending on if that would cause you to bust.
        which is checked for in the hand class

        '''

        # if the rank is is a king, queen, or jack, return 10 as the value of the card.
        if self.rank in ['J', 'Q', 'K']:
            return 10
        # if the rank is an ace return 1 and 11. This will later
        # let us choose whether we want the ace to be a one or eleven
        if self.rank == 'A':
            return 1, 11
        # if it's nothing special just return the number.
        return int(self.rank)


    def __str__(self):
        return self.rank + '-' + self.suit


class Deck:

    '''

    Creating our deck class

    mostly, this will be used for drawing cards,
    setting up a deck so we don't just draw cards at random
    resulting in us having 2 aces of spades, which is illegal in blackjack.

    '''

    def __init__(self):
        # Create a deck, suit first then rank.
        # This makes the deck follow what a brand new pack of cards looks like.
        self.generate_deck() 

       
    def generate_deck(self):
        self.cards = [Card(rank, suit) for suit in suits for rank in ranks]
        shuffle(self.cards)


    def deal_card(self) -> classmethod:

        '''

        simply deals a card by popping one
        from the first spot in the deck array

        '''

        # Check if there are no cards left in the deck
        if not self.cards:
            self.generate_deck()
        # As long as there are cards left in the deck, pop one from the top of the deck out.
        # return the drawn card so it can be added to the hand
        return self.cards.pop(0)


    def __str__(self):
        return '[' + ', '.join(map(str, self.cards)) + ']'



class Hand:

    '''

    creating the 'Hand' class

    this is used simply to store each players hand
    give the value of the two hands
    show if either hand is soft
    and also to make it easier to draw cards

    '''


    def __init__(self):
        # start with two cards in hand.
        self.cards = [(Game.deck.deal_card()), (Game.deck.deal_card())]
        self.soft = any(c.rank == 'A' for c in self.cards)


    def value(self) -> int:

        '''

        The method 'Value', is used to check the value
        of the players hand, this will allow us to check
        to see if the player has busted, has an ace, or
        just has a regular old hand

        '''

        total = 0
        ace_count = 0

        # Calculate the total value of the hand
        for card in self.cards:
            if card.rank == 'A':
                ace_count += 1
            else:
                total += card.card_value()
        # Determine the value of aces

        for _ in range(ace_count):
            if total + 11 <= 21:
                total += 11
                self.soft = True
            else:
                total += 1

        return total


    def reset(self):

        ''' For resetting the hand between games '''

        self.cards.clear()
        self.draw()
        self.draw()


    def draw(self):

        '''

        draw method, simply exists to make it easier
        for a player to draw cards

        '''

        self.cards.append(Game.deck.deal_card())



    def __str__(self):
        return str([str(c) for c in self.cards])



class Player:

    '''

    creating the 'Player' class

    this will have a multitude of select-able values
    a strategy, a name, and a budget
    this will all be managed by the player throughout the game
    whether it's a bot or a person.

    '''

    def __init__(self, budget: int, strategy: str, name: str):
        self.name = name
        self.hand = Hand()
        self.state = 'play'
        self.strategy = strategy
        self.bid_amount = 0
        self.starting_bid = 10
        self.current_simulated_bid = self.starting_bid
        self.budget = budget
        self.broke = False


    # player action
    def hit(self):

        '''

        Makes the player draw a card, and makes sure the player state
        is set to 'play' so that the player can keep playing

        '''

        self.hand.draw()

    def stay(self):

        '''

        Changes the player state to 'stay', which changes anything
        that runs based on the 'play' state and causes
        it to stop working.

        '''

        self.state = 'stay'


    def bid(self, amount = 0) ->int :

        '''

        Get a bid from the player if the player is actually a player,
        otherwise automate the bid

        '''

        if self.strategy is None :
            if self.check_broke():
                print('You are out of money and cannot bid anymore')
            else:
                self.bid_amount = int(input('How much?\n'))
                while self.bid_amount > self.budget:
                    self.bid_amount = int(input('How much?\n'))
                    print(f'You don\'t have that much. You have: ${self.budget}')
        else:
            self.bid_amount = amount

        self.budget -= amount
        return self.bid_amount


    #status ceck point
    def check_bust(self) -> bool:

        '''

        Check if the player busted by seeing if the hand
        value is above 21

        '''

        if self.hand.value() > 21:
            self.state = 'bust'
            return True
        return False


    def check_broke(self) -> bool:

        ''' check if the player is broke '''

        if self.budget < 1:
            self.state = 'broke'
            return True
        return False


    # strategies
    def strategy_player(self, strdealer_up_card: classmethod):

        '''

        A random very basic strategy, generated by chatGPT
        and implemented, by no means is it great. but
        it gets the job done for now.

        '''
        dealer_up_card = strdealer_up_card
        player_hand_value = self.hand.value()
        
        if self.strategy == "strategy_one":
            # Define the basic strategy decisions based
            # on the player's hand value and the dealer's up card
            if self.hand.soft:
                if player_hand_value <= 17:
                    self.hit()
                elif player_hand_value == 18 and dealer_up_card.rank in [9, 10, 'J', 'Q', 'K', 'A']:
                    self.hit()
                else:
                    self.stay()
            else:
                if player_hand_value <= 11:
                    self.hit()
                elif player_hand_value == 12 and dealer_up_card.rank in [2, 3, 7, 8, 9, 10, 'J', 'Q', 'K', 'A']:
                    self.hit()
                elif 13 <= player_hand_value <= 16:
                    if dealer_up_card.rank in [2, 3, 4, 5, 6]:
                        self.stay()
                    else:
                        self.hit()
                elif player_hand_value >= 17:
                    self.stay()




    def __str__(self):
        return ('Name: ' + str(self.name) + '; Hand: ' + str(self.hand) +
            '; Budget: ' + str(self.budget) + '; Hand Value: ' + str(self.hand.value())
            + '; Hand is soft: ' + str(self.hand.soft) + '; Busted: '
            + str(self.check_bust()) + '; State: ' + str(self.state))



class Dealer(Player):

    '''

    A dealer is another player where
    his strategy is automatic

    '''

    # initialize the dealer as a player
    def __init__(self):
        super().__init__(float('inf'), 'dealer', 'Dealer')
        self.up_card = self.hand.cards[0]

    def dealer_strategy(self):

        '''

        the dealers strategy is always:
        hit below 17 and on soft 17
        stay on hard 17 or above 17

        '''

        if self.hand.value() < 17 or (
            self.hand.value() == 17 and self.hand.soft):
            self.hit()
        else:
            self.stay()

    def __str__(self):
        return 'Name: ' + self.name + '; up_card: [\'' + str(self.up_card) + '\']'



class Game:

    '''

    Create the 'Game' class

    This will be used to create and run the game as well as
    log it within a variable which we have set to 'data'


    '''

    game_count = 0
    deck = Deck()

    def __init__(self,  budget: int =100, strategy: Union[None, Literal['strategy_one']] = None , name: str ='Player'):

        self.dealer = Dealer()
        self.player = Player(budget, strategy, name)
        self.pot = 0
        self.simulated = False
        Game.game_count += 1


    def log_game(self):

        '''

        as mentioned above,
        the game class is in charge
        of logging the game.
        which is what this method does

        '''

        data.append((Game.game_count, self.player.budget))


    def player_turn(self):

        ''' player's turn function '''

        if self.player.strategy is None:

            self.player.budget -= 10
            self.pot += 10
            option = ('1', 'HIT', '2', 'STAY','3', 'BID')
            while player_move not in option:
                player_move = input("what would you like to do? \n1) Hit \n2) Stay \n3) Bid\n").uppper()
                print('Not a valid move')
                
            # execute that move
            if player_move in option[:2]:
                self.player.hit()
            elif player_move in option[2:4]:
                self.player.stay()
            elif player_move in option[4:]:
                self.pot += self.player.bid()

            print(self.player.hand)
            print(self.player.hand.value(), "/n")

            # Check if the player busted
            self.player.check_bust()
            if self.player.state == 'bust':
                print('Sorry, you busted.')


        # get the move that the player wants to do
        else:
            self.player.strategy_player(self.dealer.up_card)
            self.player.check_bust()

            # in this version we are using the 2 - 1 - 2 bidding strategy
            # which increments by 1 every win, then resets to 1 at a loss

            self.pot += self.player.bid(self.player.current_simulated_bid)
        
        
    def check_win(self) -> bool:

        '''

        Check if the player has won the round
        this will be if the hand value is higher
        than the dealers
        and the hand has not busted
        or if the dealer has busted
        and the player hasn't

        '''

        if (((self.player.hand.value() > self.dealer.hand.value()) and
            self.player.state != 'bust') or self.dealer.state == 'bust'):
            if self.player.state == 'bust' and self.dealer.state == 'bust':
                return False
            else:
                self.player.budget += self.pot
                return True
        else:
            return False


    def change_bid(self):

        '''

        change the amount of the
        simulated bid depending on win/loss

        '''

        if self.check_win():
            self.player.budget += self.pot
            self.player.current_simulated_bid += 2
        else:
            self.player.current_simulated_bid = self.player.starting_bid


    def run(self):

        '''

        this function actually runs the game,
        letting the player make their moves first
        then the dealer making theirs.

        '''

        self.pot = 0
        self.player.state = 'play'

        if self.player.strategy is None:
            print(self.player)
            print(self.dealer)
            print('Buy in is 10 bucks.')

        if self.dealer.hand.value() == 21:
            self.player.state = 'stay'

        # while the player and is not staying, busted or out of budget
        while 'play' in (self.player.state, self.dealer.state):
            self.player_turn()
            self.dealer.dealer_strategy()
           
        # add the dealers bet to the pot, which will always be equal to the pot.
        self.pot += self.dealer.bid(self.pot)

        # Check bust for both
        self.dealer.check_bust()
        self.player.check_bust()

        # Check win and lost
        if self.player.strategy is None:
            self.check_win()
        else: 
            self.change_bid()

        self.log_game()
        print(data[-1])

while True:
  game = Game(strategy= "strategy_one")
  game.run()
\$\endgroup\$
3
  • \$\begingroup\$ Dude this is awesome, thank you very much. I appreciate the help a lot. I found the bug you were talking about and solution was nice and simple, there's nothing to run self.stay() when checking a hand value of 12, but I fixed that with and else: self.stay() at the end of the whole string. this catches any irregularities and just stays. I ended up just replacing Elif hand.value() = 17 with the else: because it works just fine for our case. But yeah, thank you for the help. I'm going to look at pypy once I get out of school and have access to a good computer. I would love your help w/ this. \$\endgroup\$ Commented May 1 at 18:57
  • \$\begingroup\$ I have a good pc, whit pypy. You could contact me on Discord, my username is: lorito_39408. \$\endgroup\$
    – AsrtoMichi
    Commented May 1 at 19:03
  • \$\begingroup\$ Alright, sent one your way, .ze.ro \$\endgroup\$ Commented May 1 at 19:43

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.