3
\$\begingroup\$

I began creating blackjack in python; however, I'm looking to simulate it, so I'm thinking I'm going to need to optimize it so that I don't just leave my computer running for three hours waiting for just 3000 games to be simulated. I'm very new to this stuff so feel free to be as hard on me as you want. I'm looking to understand the changes as well so that I can improve my code in the future.

import random
ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
suits = ['Spades', 'Hearts', 'Diamonds', 'Clubs']
global deck
global data
data = []
class Hand(object):
 def __init__(self):
 # start with two cards in hand.
 self.cards = [(deck.deal_card()), (deck.deal_card())]
 self.cardValue = 0
 for c in self.cards:
 if c.rank == 'A':
 self.sοft = True
 else:
 self.soft = False
 def value(self):
 # setting two temporary variables and resetting them whenever the function is run.
 temp = 0
 self.aceCount = 0
 anothertemp = False
 for card in self.cards:
 # if the card is an ace. This works because our value for aces is (1, 11), declared in our card class.
 if card.value() == (1, 11): 
 # add an ace to the aceCount, aces needs to be accounted for at the end, because otherwise if you have an ace
 # in the first few cards they will count as 11 even if that would cause a bust (hand value greater than 21).
 self.aceCount += 1
 self.soft = True
 else:
 # add the card if it's not an ace
 temp += card.value()
 # Now we add the aces to our hand value, one ace can count as 11 while the other counts as one, so this works just fine
 for card in range(self.aceCount):
 # checking if the ace would cause a bust
 if temp + 11 > 21:
 # if it does, its value is only 1
 temp += 1
 self.soft = False
 else:
 # if it doesn't, its value is 11
 temp += 11
 self.soft = True
 anothertemp = True
 # finally set the cardValue to the temp variable to correctly return our hand value. 
 # I don't think I actually need the 'temp' variable if I just set self.cardValue to 0 in the beginning.
 self.soft = anothertemp
 self.cardValue = temp
 return str(self.cardValue)
 # just makes it easier to draw cards
 def draw(self):
 c = deck.deal_card()
 self.cards.append(c)
 # set up a string for display and testing purposes
 def __str__(self):
 hand = []
 for c in self.cards:
 hand.append(str(c))
 return str(hand)
class Player(object):
 def __init__(self, strategy = 'none', name='Player', budget=100):
 self.name = name
 self.hand = Hand()
 self.budget = budget
 self.state = 'play'
 self.handValue = self.hand.value()
 self.handIsSoft = self.hand.soft
 self.strategy = strategy
 # strategy sheets
 # Neither player has a soft hand
 self.strategy1NoSoft = [
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay', 'stay']
 ]
 # Only dealer has a soft hand
 self.strategy1DealerSoft = [
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','stay'],
 ['hit','hit','stay','stay','stay'],
 ['hit','stay','stay','stay','stay'],
 ['stay','stay','stay','stay','stay'],
 ['stay','stay','stay','stay','stay'],
 ['stay','stay','stay','stay','stay'],
 ['stay','stay','stay','stay','stay'],
 ['stay','stay','stay','stay','stay'],
 ['stay','stay','stay','stay','stay'],
 ['stay','stay','stay','stay','stay']
 ]
 # only the player has a soft hand
 self.strategy1SelfSoft = [
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
 ['stay', 'stay', 'stay', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay', 'hit', 'hit', 'hit', 'hit', 'stay', 'hit', 'hit', 'hit', 'stay'],
 ['stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','hit','hit','stay'],
 ['stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','hit','stay'],
 ['stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay','stay']
 ]
 # both have a soft hand
 self.strategy1BothSoft = [
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','hit'],
 ['hit','hit','hit','hit','stay'],
 ['stay','stay','stay','stay','stay',],
 ['stay','stay','stay','stay','stay',],
 ['stay','stay','stay','stay','stay',]
 ]
 # add a card to hand
 def hit(self):
 self.hand.draw()
 self.handValue = self.hand.value()
 self.handIsSoft = self.hand.soft
 # change the state to stay to stop all functions revolving around 'play' state
 def stay(self):
 self.state = 'stay'
 # Check if the player busted
 def check_bust(self):
 # if your hand is over 21 you bust
 if int(self.handValue) > 21:
 self.state = 'bust'
 return True
 else:
 return False
 # Check the budget
 def check_broke(self):
 if int(self.budget < 1):
 self.state = 'broke'
 return True
 else:
 return False
 # bid an amount
 def bid(self):
 self.broke = self.check_broke()
 if self.broke:
 print('You are out of money and cannot bid anymore')
 else:
 self.bidAmount = int(input('How much?\n'))
 self.budget -= self.bidAmount
 # the strategy function, checks for each soft/not soft hand and responds according to the strategy sheet
 def strategy_one(self, dealer):
 try: 
 if not self.handIsSoft and not dealer.handIsSoft:
 if self.strategy1NoSoft[int(self.handValue)][int(dealer.handValue)] == 'hit':
 self.hit()
 elif self.strategy1NoSoft[int(self.handValue)][int(dealer.handValue)] == 'stay':
 self.stay()
 elif not self.handIsSoft and dealer.handIsSoft and dealer.handValue >= 12 and dealer.handValue <= 16:
 if self.strategy1DealerSoft[int(self.handValue)][int(dealer.handValue) - 12] == 'hit':
 self.hit()
 elif self.strategy1DealerSoft[int(self.handValue)][int(dealer.handValue) - 12] == 'stay':
 self.stay()
 elif self.handIsSoft and not dealer.handISSoft and dealer.handValue >= 4:
 if self.strategy1SelfSoft[int(self.handValue)][int(dealer.handValue) - 4] == 'hit':
 self.hit()
 elif self.strategy1SelfSoft[int(self.handValue)][int(dealer.handValue) - 4] == 'stay':
 self.stay()
 elif self.handIsSoft and dealer.handIsSoft and self.handValue >= 13 and dealer.handValue >= 12 and dealer.handValue <= 16:
 if self.strategy1BothSoft[int(self.handValue)-13][int(dealer.handValue) - 12] == 'hit':
 self.hit()
 elif self.strategy1BothSoft[int(self.handValue)-13][int(dealer.handValue) - 12] == 'stay':
 self.stay()
 except:
 self.stay()
 def __str__(self):
 # player string
 return str('Name: ' + str(self.name) + '; Hand: ' + str(self.hand) + '; Budget: ' + str(self.budget) + 
 '; Hand Value: ' + str(self.handValue) + '; Hand is soft: ' + str(self.handIsSoft) + '; Busted: ' + str(self.check_bust()) + '; State: ' + str(self.state))
class Dealer(Player):
 # initialize the dealer as a player
 def __init__(self, name='Dealer'):
 super().__init__('None',name)
 # automate the dealer strategy. His strategy is nice and simple, hit below 17 stay at 17 or higher if the hand is 17 and soft, hit.
 def dealer_strategy(self):
 if int(self.handValue) < 17 or int(self.handValue) == 17 and self.handIsSoft:
 self.hit()
 else:
 self.stay()
class Game(object):
 def __init__(self, strategy = 'none', name = 'Player'):
 self.dealer = Dealer()
 self.deck = Deck()
 self.deck.shuffle()
 self.player = Player(strategy, name)
 # this will be used later for running a simulation
 global gameCount
 gameCount += 1
 # this will later be used to save the data of the game
 def log_game(self):
 data.append((gameCount, self.player.name, self.player.budget))
 # run the game
 def run(self):
 print(self.player)
 print(self.dealer)
 if self.dealer.handValue == 21 and self.dealer.hand.aceCount > 0:
 self.player.state = 'stay'
 # while the player is not staying, busted or out of budget
 while not self.player.state in ['stay', 'bust']:
 # if a strategy is already selected then automate the strategy
 if self.player.strategy == 'strategy1':
 self.player.strategy_one(self.dealer)
 print(self.player.hand, self.player.handValue)
 else:
 # get the move that the player wants to do
 playerMove = input("what would you like to do? \n1) Hit \n2) Stay \n3) Bid\n")
 # execute that move
 if playerMove.upper() in ['1', 'HIT']:
 self.player.hit()
 print(self.player.hand)
 print(self.player.handValue)
 elif playerMove.upper() in ['2', 'STAY']:
 self.player.stay()
 elif playerMove.upper() in ['3', 'BID']:
 self.player.bid()
 else:
 print('Not a valid move')
 # Check if the player busted
 self.player.check_bust()
 if self.player.state == 'Bust':
 print('Sorry, you busted.')
 # Run the dealer simulation
 while not self.dealer.state in ['stay', 'bust']:
 self.dealer.dealer_strategy()
 print(self.dealer.hand, self.dealer.handValue)
 # Check bust for both
 self.dealer.check_bust()
 self.player.check_bust()
 # Check win and loss
 if ((self.player.handValue > self.dealer.handValue) 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!')
 else:
 print('You lost this round.')
 self.log_game()
 print(data)
 
class Deck(object):
 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:
 c = Card(rank, suit)
 self.cards.append(c) 
 def shuffle(self):
 # use the random library to shuffle the cards
 random.shuffle(self.cards)
 def deal_card(self):
 # Check if there are no cards left in the deck
 if not self.cards:
 raise Exception('No more cards in deck!')
 # 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 
 # Set up another string so that the deck can be displayed whenever needed
 def __str__(self):
 deck = [] 
 for c in self.cards:
 deck.append(str(c))
 return str(deck)
class Card(object):
 def __init__(self, rank, suit):
 self.rank = rank
 self.suit = suit
 def value(self):
 # 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
 else: 
 # if it's nothing special just return the number.
 return int(self.rank)
 def suit(self):
 return self.suit
 def rank(self):
 return self.rank
 # another string declaration to make it easy to display
 def __str__(self):
 return self.rank + '-' + self.suit
# <------------- TESTS ------------->
def cardTest():
 # manually create 4 cards to make sure all the special things work
 card1 = Card('9', 'Spades')
 card2 = Card('3', 'Hearts')
 card3 = Card('K', 'Clubs')
 card4 = Card('A', 'Diamonds')
 print(card1, card2, card3, card4)
def deckTest():
 # create a deck, display the deck to test the str then shuffle and re-display to test the shuffle method
 deck = Deck()
 print('<----------- Before Shuffle ----------->')
 print(deck)
 deck.shuffle()
 print('<----------- After Shuffle ----------->')
 print(deck)
def handTest():
 # It's safe to use global because the deck variable will be global, make a new hand and print it and its value.
 global deck
 deck = Deck()
 deck.shuffle()
 hand = Hand()
 print(hand)
 print(hand.value())
 print(hand.soft)
def playerTest():
 # make sure the player class and each of there moves are working.
 player1 = Player('Andrea', 200)
 player2 = Player()
 player3 = Player('Rob')
 player4 = Player('Tara', 300)
 print(player1, '\n' + str(player2), '\n' + str(player3), '\n' + str(player4))
 player4.hit()
 print(player4)
def dealerTest():
 dealer = Dealer()
 print(str(dealer))
 while dealer.state not in ['stay', 'bust']:
 dealer.dealer_strategy()
 print(str(dealer)) 
def gameTest():
 print('<--------------------------------------- GAME --------------------------------------->')
 global gameCount
 gameCount = 0
 game = Game('None') # 'None', 'strategy1'
 game.run()
cardTest()
deckTest()
handTest()
playerTest()
dealerTest()
gameTest()
toolic
14.6k5 gold badges29 silver badges204 bronze badges
asked Apr 23, 2024 at 16:30
\$\endgroup\$

2 Answers 2

8
\$\begingroup\$

This submission is about performance, but it does not include any profiling measurements.

language choice

If you're looking for raw speed, interpreted python usually wouldn't be the first choice. Maybe you'd be happier with Rust, zig, or C++.

main guard

cardTest()
deckTest()
handTest()
playerTest()
dealerTest()
gameTest()

Please enclose those within the customary if __name__ == "__main__": idiom, so unit tests can safely import this module without delay or side effects.

module-level global

global deck
global data
data = []

Excessive coupling leads to trouble.

Prefer self.foo over global foo.

python2

class Hand(object):

Please don't tell us Hand inherits from object. Yes, we know that, everything inherits from object. As written this looks like python2 code which is trying to distinguish between the old- and new-style MRO. Simply write class Hand:

Similarly for Player and Game.

docstrings

 def __init__(self):
 # start with two cards in hand.
 ...
 def value(self):
 # setting two temporary variables and resetting them whenever

These # comments would be more helpful as """docstrings""". Please get in the habit of putting one at the start of each class or method.

identifiers

 self.cardValue = 0

Pep-8 asked you nicely to spell it card_value, ace_value, another_temp, etc. Writing in the style of some non-python language is distracting and makes your code harder to read.

odd loop

 for c in self.cards:
 if c.rank == 'A':
 self.sοft = True
 else:
 self.soft = False

That can't be right -- consider the case where just card zero is an Ace.

 self.soft = False
 for c in self.cards:
 if c.rank == 'A':
 self.sοft = True

Or use self.soft = any((c.rank == 'A' for c in self.cards)).

ToC

 def value(self):
 ...
 self.aceCount = 0

Please don't spring that on me by surprise. While not necessary, it is polite to introduce all attributes up in the __init__ constructor, even if only to assign None, as sort of a Table of Contents. That way the Gentle Reader has a heads up on what state matters to the Hand object. Similarly for self.bid_amount, and especially for self.broke which there's no need for storing as an attribute.

Also, if card.value() == (1, 11): is unexpected and weird, but fine, we'll roll with it. Naming it plural card.values() would have led to less surprise. Prefer a [ ] list for variable-length datastructures, rather than the ( ) tuple we see here.

throwaway name

This is clearly the wrong name:

 if temp + 11 > 21:

You intended total here.

Names really matter. They affect how we think about code. Choose them carefully.

stringly typed

class Player(object):
 def __init__( ... ):
 ...
 self.state = 'play'

Consider defining an Enum for that.

Also, not sure why you're replicating things here:

 self.handValue = self.hand.value()
 self.handIsSoft = self.hand.soft

Now we have to worry about them getting out of sync, for example down in the hit() method. Create a @property if you feel you need a convenience method.

cite your reference

 # Neither player has a soft hand
 self.strategy1NoSoft = [
 ['hit', 'hit', 'hit',
 ...
 # Only dealer has a soft hand
 self.strategy1DealerSoft = [
 ...
 # only the player has a soft hand
 self.strategy1SelfSoft = [
 ...
 # both have a soft hand
 self.strategy1BothSoft = [

These datastructures are really quite hard to read and interpret. If you copied them from a book or web page, please cite it. If there's some code which generated these, show us the code rather than its output.

As written, the Gentle Reader has no reason to believe that these are valid or winning strategies. You have not offered us the tools to reason about this datastructure or its correctness.

temporary expression

In strategy_one() the repeated int(self.handValue) conversion is distracting. Assign an integer hand_value local variable, and use that.

There's no need for four self.hit() calls. Phrase it in this way:

 if not self.handIsSoft and ...
 strategy = self.strategy1NoSoft...
 elif not self.handIsSoft and ...
 strategy = self.strategy1DealerSoft...
 elif self.handIsSoft and ...
 strategy = self.strategy1SelfSoft...
 elif self.handIsSoft and ...
 strategy = self.strategy1BothSoft...

Now that you've assigned a strategy, call it. Ideally we'd have a function object already, rather than a string. We could do what the OP was doing before:

 if strategy == 'hit':
 self.hit()
 else:
 self.stay()

Or we could use a lookup table:

 str_to_fn = {
 'hit': self.hit,
 'stay': self.stay,
 }
 str_to_fn[strategy]()

bare except

 except:
 self.stay()

No, we don't do that, not ever. Prefer except Exception:, so you don't accidentally disable some important items like CTRL/C handling. Use ruff check or pylint to help you notice such things.

Also, it's a bit unsettling that we silently ignore the error without logging it. Writing something specific like except IndexError: would be a boon to the Gentle Reader. As written, a # comment becomes necessary, explaining why we anticipate an error and why it's harmless to silently discard it.

f-string

 def __str__(self): ...
 return str('Name: ' + str(self.name) + '; Hand: ' + str(self.hand) + '; Budget: ' + ...

You compute a string expression and then apply str() to it, which is harmless but unnecessary.

Prefer the more compact format-string notation:

 return f'Name: {self.name}; Hand: {self.hand}' ...

none

class Player(object):
 def __init__(self, strategy = 'none', ... ):
...
class Dealer(Player): ...
 def __init__(self, ... ):
 super().__init__('None', name)

You told us the usual spelling is lowercase 'none', then used titlecase 'None'. Idiomatic python would pass in a None object rather than a str. Please use an Enum for this already!

Similar remarks for the 'bust' / 'Bust' inconsistency.

namedtuple

This is very nice:

 def log_game(self):
 data.append((gameCount, self.player.name, self.player.budget))

I'm glad to see the use of a 3-tuple, rather than a list.

Consider making it self-explanatory by naming the three fields:

from collections import namedtuple
...
LogRecord = namedtuple('LogRecord', 'game_count, name, budget')

overly broad error

 raise Exception('No more cards in deck!')

Consider making this a ValueError. Or define your own app-specific error.

As written, a caller could not e.g. catch just an anticipated ZeroDivisionError.

join

Turning a deck into a str works just fine:

 deck = [] 
 for c in self.cards:
 deck.append(str(c))
 return str(deck)

Consider using .join() there:

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

If you dislike map(), at least go for a list comprehension:

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

elide getters

 def suit(self):
 return self.suit
 def rank(self):
 return self.rank

This isn't java. We're all grownups here. Just let the caller access those attributes.

Oh, wait, you didn't say def get_suit. You overwrote it, yikes! So in the class namespace suit is a callable method, and in the object namespace suit is a str. That's not helpful. Yeah, you really need to delete this pair of methods.

automated tests

I really like that you're thoroughly exercising the target code. But an automated test suite "knows the right answer", so it can display a Green bar.

def cardTest():
 ...
 print(card1, card2, card3, card4)

Rather than manually eyeballing the results to see if they look right, prefer to create a TestCase, using e.g. self.assertEqual().

answered Apr 23, 2024 at 18:43
\$\endgroup\$
4
\$\begingroup\$

Overview

The code layout is good, and you made good use of classes, functions and comments.

Here are some suggestions to consider.

DRY

There is a lot of unnecessary repetition in lines like this:

['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],
['hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'hit', 'stay'],

Firstly, the 3 lines are identical (your code actually has 11 lines).

Secondly, the same string (hit) appears many times.

Loops are one way to reduce the repetition. It also has the benefit of eliminating long lines of code.

Another example of repetition is in lines like the following:

if self.strategy1NoSoft[int(self.handValue)][int(dealer.handValue)] == 'hit':
 self.hit()
elif self.strategy1NoSoft[int(self.handValue)][int(dealer.handValue)] == 'stay':

It seems like every handValue is converted to int. Perhaps handValue could be an int in the class, or your class could have another variable which is an int.

Simpler

You can eliminate the temporary variable. To simplify the code, change:

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

to:

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

Documentation

Add docstrings for the classes and functions. These should describe the purpose of the code as well as inputs and return values.

Unicode

In the Hand __init()__ function, the following line has a Unicode character instead of o:

self.soft = True

I recommend changing to an o. It was a syntax error for me when I ran the code.

Output

When I run the code, it generates a lot of output to the screen, some of which is very wide and wraps. This makes the output very hard to understand.

I recommend adding an option to print only necessary information by default. Let the user opt in for more verbose output.

Lint check

pylint identified a few issues. There are some long lines that can be shortened, especially the comments.

Refer to the PEP 8 style guide for function and variable names.

answered Apr 23, 2024 at 17:24
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.