# BlackJack in python

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):
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
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)

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()
gameTest()


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()
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 = []


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):
...

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.

        # 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')


            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().

## 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.