This is my attempt at a terminal based blackjack game. Currently, the game does not support betting, although I intend to add it in the future.
I would very much appreciate general feedback, but I am especially interested in the following:
Comments on docstrings and type hints, as this is my first time trying to properly use either
Advice about printing long strings to the terminal. I am at present using \ line continuation characters to avoid overly long lines in the code, but this seems to either require weird indentation or printing excess whitespace to the terminal.
I intend to, in the future, use many of these classes to write a GUI based blackjack game with tkinter. If I've made any decisions that will make that kind of reuse challenging, advice would be great!
import random
SUITS_DICT = {
'spades': 0x1f0a0,
'hearts': 0x1f0b0,
'diamonds': 0x1f0c0,
'clubs': 0x1f0d0,
}
KNIGHT_RANK = 12
class Card():
"""A class used to represent a playing card"""
def __init__(self, number: int, suit: str):
self.suit = suit
self.number = number
def __repr__(self) -> str:
return f'Card({self.number}, {self.suit})'
def __str__(self) -> str:
cardNo = SUITS_DICT[self.suit] + self.number
return chr(cardNo) if self.number < KNIGHT_RANK else chr(cardNo + 1)
def blackjack_value(self) -> int:
"""Returns the card's value in blackjack"""
return self.number if self.number < 10 else 10
CUT_CARD = Card(0, 'spades')
class CardCollection(list):
"""A subclass of list for collections of playing cards"""
def __str__(self) -> str:
outStr = ''
for card in self:
outStr += str(card) + ' '
return outStr
class Deck(CardCollection):
"""A class to represent a deck (or multiple decks) of cards"""
def __init__(self, deckMultiple: int):
self.deckMultiple = deckMultiple
self.fill_deck()
def cut(self):
random.shuffle(self)
self.insert(random.randrange(-75, -60), CUT_CARD)
def fill_deck(self):
"""Fills the deck with cards"""
for i in range(self.deckMultiple):
for suit in SUITS_DICT:
for n in range(1, 14):
self.append(Card(n, suit))
def refresh(self):
"""Removes any cards remaining in the deck,
and fills it with fresh cards"""
self.clear()
self.fill_deck()
def deal_to(self, hand: CardCollection):
"""Removes a card from the deck, and adds it to another card
collection. If the card drawn is a card used to mark where
the deck is cut, refreshes the deck."""
card = self.pop(0)
if card == CUT_CARD:
self.refresh()
hand.append(self.pop(0))
else:
hand.append(card)
class BlackjackHand(CardCollection):
"""A Class to represent a hand of cards in blackjack"""
def __init__(self, isDealer: bool = False):
self.isDealer = isDealer
def score(self) -> int:
"""Returns the hands value in blackjack"""
aceCount = 0
total = 0
for card in self:
if card.number == 1:
aceCount += 1
else:
total += card.blackjack_value()
for i in range(aceCount):
if total + 11 > 21:
total += 1
else:
total += 11
return total
def evaluate(self):
"""Returns the cards reuslt in blackjack"""
if self.score() == 21 and len(self) == 2:
return 'blackjack'
elif self.score() < 22:
return self.score()
else:
return 'bust'
class HandList(list):
"""A class to represent a group of hands"""
def __str__(self) -> str:
strList = []
for hand in self:
strList.append(str(hand))
return ' '.join(strList)
class Blackjack():
"""A class to represent a game of blackjack"""
def __init__(self, noDecks: int = 6):
self.reset_hands()
self.shoe = Deck(noDecks)
self.shoe.cut()
def reset_hands(self):
"""Removes all hands associated with the game and creates empty ones"""
self.playerHands = HandList()
self.playerHands.append(BlackjackHand())
self.dealerHand = BlackjackHand(True)
def hit(self, hand: BlackjackHand):
"""Moves one hand from the shoe to a hand"""
self.shoe.deal_to(hand)
def split(self, hand: BlackjackHand):
"""Splits a hand containing two cards of the same value into two hands"""
if (len(hand) == 2 and
hand[0].blackjack_value() == hand[-1].blackjack_value()):
self.playerHands.append(BlackjackHand())
self.playerHands[-1].append(hand.pop(-1))
self.hit(hand)
self.hit(self.playerHands[-1])
else:
raise ValueError
# Doubling down will be implemented with betting
# def doubleDown(self, hand: BlackjackHand):
# """"""
# if len(hand) == 2:
# self.hit(hand)
# else:
# raise ValueError
def player_actions_text(self) -> list:
"""Allows the player to make their moves,
returning a list of the results"""
results = []
for hand in self.playerHands:
print(f'Your hand is {hand}, which scores {hand.score()}')
while hand.score() < 22:
playerAction = input('What would you like to do? ').lower()
match playerAction:
case 'hit':
self.hit(hand)
# Doubling down will be implemented with betting
# case 'double down':
# try:
# game.doubleDown(hand)
# break
# except ValueError:
# print('You can only double down on the initial deal!')
case 'split':
try:
self.split(hand)
except ValueError:
print('You can only split two cards with \
the same value!')
case 'stand':
break
case _:
print("That's not a valid move!")
print(f'Your hand is {hand}, which scores {hand.score()}.')
else:
print(f"Your hand is {hand}, which scores {hand.score()}. \
You're bust!")
results.append(hand.evaluate())
return results
def dealer_actions_text(self):
"""Performs the dealer's moves, and returns the dealer's result"""
while self.dealerHand.score() < 17:
print("That's less than 17, so the dealer hits")
self.hit(self.dealerHand)
print(f"The dealer's hand is {self.dealerHand}, \
which scores {self.dealerHand.score()}.")
else:
if self.dealerHand.score() > 21:
print(f"The dealer's hand is {self.dealerHand}, \
which scores {self.dealerHand.score()}. They're bust!")
else:
print(f"The dealer's hand is {self.dealerHand}. \
They stand on {self.dealerHand.score()}.")
return self.dealerHand.evaluate()
def declare_results(playerResults: list, dealerResult: list):
"""Prints the results of a game to the terminal"""
for i in range(len(playerResults)):
handNumber = i+1
if (playerResults[i] == 'blackjack' and
dealerResult == 'blackjack'):
print(f"Hand {handNumber} and the dealer both got blackjack. \
It's a push!")
elif playerResults[i] == 'blackjack':
print(f"Hand {handNumber} got blackjack and won!")
elif playerResults[i] == 'bust':
print(f"Hand {handNumber} went bust and lost!")
elif dealerResult == 'blackjack':
print(f"Hand {handNumber} scored {playerResults[i]} \
and the dealer got blackjack. Hand {handNumber} lost!")
elif dealerResult == 'bust':
print(f"Hand {handNumber} scored {playerResults[i]} \
and the dealer went bust. Hand {handNumber} won!")
else:
print(f"Hand {handNumber} scored {playerResults[i]} \
and the dealer scored {dealerResult}")
if playerResults[i] == dealerResult:
print("It's a push!")
elif playerResults[i] > dealerResult:
print(f"Hand {handNumber} wins!")
elif playerResults[i] < dealerResult:
print(f"Hand {handNumber} loses!")
else:
raise Exception
def main():
my_game = Blackjack()
while True:
for i in range(2):
for hand in my_game.playerHands:
my_game.hit(hand)
my_game.hit(my_game.dealerHand)
print(f"The dealer's facing card is {my_game.dealerHand[0]}")
playerResults = my_game.player_actions_text()
print(f"The dealer's hand is {my_game.dealerHand}, \
which scores {my_game.dealerHand.score()}.")
dealerResult = my_game.dealer_actions_text()
declare_results(playerResults, dealerResult)
if input('Do you want to play another round? (y/n) ').lower() == 'y':
my_game.reset_hands()
else:
break
if __name__ == '__main__':
main()
1 Answer 1
Run your code through a PEP8 linter. The most frequent issue is variable naming for which you use lowerCamelCase but this needs to be changed to lower_snake_case.
SUITS_DICT
is a neat trick, but has a few issues:
- you should add documentation, something like
Suit = Literal[
'spades', 'diamonds', 'clubs', 'hearts',
]
# This is a dictionary of Unicode suit codepoints. The codepoints themselves
# don't represent anything and need to be added to the card number.
SUITS_DICT: dict[Suit, int] = {
'spades': 0x1f0a0, # then 🂡 (+1, ace) through 🂮 (+14, king)
'hearts': 0x1f0b0, # similarly for the other suits
'diamonds': 0x1f0c0,
'clubs': 0x1f0d0,
}
- it's not very legible or accessible. The card values and suits are extremely small and require zooming in; and there's more risk that the user's font cannot render these symbols. So as neat of a trick as it is, it's probably not as well-advised as to simply print the value in regular numerals aside the suit symbol.
You're missing several type hints. Anything (including constructors) that doesn't have a return value needs to be marked -> None
.
()
after a class declaration is redundant and should be deleted.
CardCollection.__str__
is likely to suffer from O(n^2) runtime due to repeated string concatenation. Options to solve this include a join
on a generator. HandList
is slightly better, but needn't construct an in-memory list; just use a bare generator i.e. ' '.join(str(hand) for hand in self)
.
isDealer
seems unused so delete it.
The user interface is a little hostile to usability. When you ask what the user would like to do, you should print the available options.
This:
print('You can only split two cards with \
the same value!')
is unlikely to do your intent. After the newline-escape, the string will include all of the indentation spaces. Instead,
print('You can only split two cards with '
'the same value!')
for i in range(2)
should replace i
with _
since it's unused.
Explore related questions
See similar questions with these tags.
Deck.__init__()
should immediately raise fatal error ifdeck_multiple < 2
. Else we have a ticking time bomb wherecut()
will blow up with IndexError. If app developer wrote a bug it should be easy to find, pointing blame at the immediate cause rather than waiting for some innocent caller to trigger the bomb. Rename the class to pluralDecks
, or toShoe
. RenamenoDecks
tonum_decks
for clarity. Havingevaluate
returnstr
orint
wouldn't be my first choice. Rather than'bust'
, consider returning0
,float('NaN')
, orNone
. Spell the parameter:blackjack_hand
\$\endgroup\$[player, dealer]
list of hands makes sense until we split(). Consider instead modeling it as a dict with keys of'player'
,'dealer'
, and after a split'player2'
. \$\endgroup\$