# Rudimentary black jack game implementation

I started the 100 Days of Code: The Complete Python Bootcamp about a week ago. I finished day 11, and I wrote a rudimentary blackjack game. I'm just looking for some advise on how I did.

Am I showing any bad habits to avoid? Are there certain efficiencies I can look into to optimize my coding techniques? Keep in mind I'm still quite new to this, so I'm not going to be able to answer all of the questions. I did write this from scratch and didn't use any of the hints or code given in the course.

I mostly just want to make sure I'm on the right path and not starting to make any mistakes that could cause me problems down the line.

from art import blackjack
import random
import os

def deal():
"""Generates a random card from the list of available cards"""
cards = [11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
card = random.choice(cards)
return card

def dealer_check(d_cards):
"""Checks dealer hand for sum and deal more if less than 17 and stops if its between 17 and 21. Returns score for bust as well."""
while sum(d_cards) <= 21:
ace_check(d_cards)
if sum(d_cards) >= 21:
return sum(d_cards)
elif sum(d_cards) <= 16:
d_cards.append(deal())
elif sum(d_cards) > 16 and sum(d_cards) < 21:
return sum(d_cards)

def ace_check(hand):
"""Checks for ace putting score over 21 and will change it from 11 to 1"""
for card in range(len(hand)):
if hand[card] == 11 and sum(hand) > 21:
hand[card] = 1
return hand

another_game = "y"

while another_game == "y":
os.system('clear')
print(blackjack)
print("Welcome to Blackjack!")

game_over = False
p_cards = []
d_cards = []

for dealing in range(0, 2):
p_cards.append(deal())
d_cards.append(deal())

print(f"Your cards: {p_cards}, current score: {sum(p_cards)}")
print(f"Computer's first card: {d_cards[0]}")

while game_over == False:
another_card = input("Type 'y' to get another card, type 'n' to pass: ").lower()
if another_card == "y":
p_cards.append(deal())
ace_check(p_cards)
print(f"Your cards: {p_cards}, current score: {sum(p_cards)}")
if sum(p_cards) > 21:
print("You bust! Computer wins!")
game_over = True
elif another_card == "n":
d_score = dealer_check(d_cards)
print(f"Dealer's final cards: {d_cards} and their final score: {sum(d_cards)}")
if sum(d_cards) > 21:
print("Dealer busts, you win!")
game_over = True
elif sum(d_cards) == sum(p_cards):
print("We have a draw!")
game_over = True
elif sum(d_cards) > sum(p_cards):
print("Dealer wins!")
game_over = True
elif sum(d_cards) < sum(p_cards):
print("You win!")
game_over = True
another_game = input("Do you want to play another game? Type 'y' or 'n'").lower()

• Just a quick note: in dealer_check you compute sum(d_cards) repeatedly. Consider computing it once at the top of the function. Same in your main loop with sum(d_cards) and sum(p_cards). Commented Jul 20 at 17:19
• Ok, so just make a variable with the calculation once then it avoids redoing that over and over again. Thanks! Commented Jul 20 at 18:43

Don't be shy of functions. They help you scope (and even eliminate) local variables.

For example, the nested while loop plays a single round. Factor it out into a function, along the lines of

def play_single_round():
p_cards = deal_two_cards()
d_cards = deal_two_cards()
p_score = do_player_turn()
if p_score > 21:
return PLAYER_BUSTED
d_score = do_dealer_turn()
if d_score > 21:
return DEALER_BUSTED
if p_score > d_score:
return PLAYER_WON
elif p_score < d_score:
return DEALER_WON
else:
return DRAW


Notice how p_cards, d_cards, p_score, d_score are now confined to this function. How game_over is not needed anymore.

Similarly, another_card is now naturally belongs to do_player_turn.

The outer loop shall be a function too. Every loop deserves a name.

• How game_over is not needed anymore when you're using five variants of it as return values looks contradictory Commented Jul 21 at 6:33
• Yea, I'm trying to get myself into the mindset that instead of just doing something over and over, create a function to do it then I can just call the function. Thanks for the input. Commented Jul 21 at 7:00

## Potential bug

You should model a full deck of 52 cards. For example, a deck of 52 cards only has four cards with the value of 7. Although it is unlikely to happen, it is possible for your code to deal more than four 7's in one game. The deal function is called a minimum of 4 times per game, which means it is typically called several more times. The function simply picks a random value from the list of 13 values.

## DRY

In the nested while loop, you repeatedly calculate the sum of the 2 hands. Generally, it is more efficient to do the calculation once and store the result in a variable. This can also simplify the code. For example:

    elif another_card == "n":
d_score = dealer_check(d_cards)
d_sum = sum(d_cards)
p_sum = sum(p_cards)
print(f"Dealer's final cards: {d_cards} and their final score: {d_sum}")
if d_sum > 21:


## Simpler

In the nested while loop, there is no need to perform the final elif check. You can replace:

        elif sum(d_cards) < sum(p_cards):


with:

        else:


It simplifies the code and also better conveys the intent of the code. The elif implies there is another case to be considered, whereas there is not.

## Unused variable

Variable dealing is set but never used:

for dealing in range(0, 2):


It can be replaced with _. Also, there is no need for the 0 in the range call. This is simpler:

for _ in range(2):


## Long line

Long lines are hard to comprehend. Simply split up the long docstring into multiple lines:

"""
Checks dealer hand for sum and deal more if less than 17
and stops if its between 17 and 21. Returns score for bust as well.
"""


## Documentation

Add a docstring to the top of the code to describe its purpose.

"""
Interactive black jack game.  A user plays against the computer.
"""

• Thanks for your input. I forgot to mention that this game was only meant to simulate the function of Blackjack. We were told to just be working with an infinite deck of cards with no restrictions. I was thinking of limiting it to a single deck, or looking up exactly how many decks get used by a dealer in an official Casino-style game but we didn't need to for this. Commented Jul 20 at 16:25
• My understanding is that in casinos Blackjack is played with multiple decks shuffled together (wherein there could be many, many 7's) to thwart card counters. The OP's code models playing with an infinite number of decks, which would be ideal for the casino and makes card counting impossible. Commented Jul 20 at 16:39
• @Wazeewa: You're welcome. Details like this can be included in the header docstring to describe the code. Commented Jul 20 at 17:19

## Constants

Besides the potential bug that has been highlighted by @toolic I wouldn't write a function such as this:

def deal_slow():
cards = [11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
return np.random.choice(cards)

>>> %timeit deal_slow()
18.7 μs ± 505 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)


I would at least move the constant you have defined outside of the function. It gains marginal speed.

CARDS = [11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]

def deal_medium():
return np.random.choice(CARDS)

>>> %timeit deal_medium()
17.3 μs ± 432 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)


But when writing applications I generally try and adopt the simplest, most fundamental types that will satisfy the problem because it is usually the fastest.

CARDS = [11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]

def deal_fast():
return CARDS[np.random.randint(0, 11)]

>>> %timeit deal_fast()
3.26 μs ± 152 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)


I fully understand that for your purpose here this is completely unnecessary. I am highlighting it for educational purposes - in my line of work these things crop up and these micro-second savings add up when routines are run millions of times.

## Other Performance

Are you sure that dealer_check is doing what you intend? If the two input cards are [11, 11], then the function will return None. You don't seem to strategically place the ace_check. I would probably restructure it as:

def dealer_check(d_cards):
"""Checks dealer hand for sum and deal more if less than 17 and stops if its between 17 and 21. Returns score for bust as well."""
score = sum(d_cards)

# Handle bust
if score > 21:
score = ace_check(d_cards)
if score > 21:
return score
return dealer_check(d_cards)

# Handle hit
elif score < 17:
d_cards.append(deal())
return dealer_check(d_cards)

# Handle stick
return score


Notice how this function has become recursive, and it also requires that ace_check return the modified score as well as mutating the hand. In your original ace_check returned the hand but that was not captured.

ace_check has similar performance slip ups, see if you can spot them.

• It gains marginal speed is so small as to be entirely negligible. Scope cleanliness is a more significant factor than speed in this case. Commented Jul 21 at 13:59
• I'm sure that, in your line of work, you know the difference between justified optimisation and premature optimisation. Commented Jul 21 at 14:00
• I do, thank you, hence why I wrote exactly what you have commented in my original post. It is very useful that you highlight it though so that learners (as was the OP) are promoted towards these considerations potentially making them think about design architecture other circumstance where it may matter more. Commented Jul 21 at 14:12
• The bug you dub "Other performance" can be found at other locations: if the player draws two aces, and answers "no" to the "another card" question, then his score is 22 but he doesn't bust simply because there is no check on his score being above 21.
– Anab
Commented Jul 21 at 21:23
• I really appreciate that you measured first ! So it is the first step into valid optimization (and not prematurely) and also an interesting showcase for learners. But of course optimizations in the range of microseconds does not justify any optimization effort (except for learning purposes of course) :) +1 from me. Commented Jul 22 at 6:19

# main guard

Please write the while loop in this way:

if __name__ == "__main__":
while another_game == "y":


Then an automated test suite can safely import without unwanted side effects, for example to exercise dealer_check().

Consider packaging the while loop within a function, such as def main():

# ANSI escape sequence

Forking off a child process is relatively expensive.

    os.system('clear')


Printing ESC [ 2 J would be simpler.

# testing a boolean

    while game_over == False:


Prefer while not game_over:, as that makes it more readable for a future maintenance engineer. Sure, it's just a bit we're testing, the same bit. But we're trying to phrase it in higher level language, so it reads more like a story, less like computer details.

• Thanks for the feedback. I'm trying to make sure I don't develop bad habits while coding so anything that uses more CPU than necessary or is bad practice I'd like to avoid. Just out of curiosity though, is using the NOT condition the same as testing the boolean? Is saying while game_over == False: not just the same as while not gave_over:? Commented Jul 20 at 16:27
• Suppose we had assigned in_progress = True. Then the natural way to express the loop would be while in_progress:. We wouldn't compare the value to True, producing a new boolean expression, since a bool is already a boolean quantity. Having chosen an alternate definition, we need to negate that quantity with not. // Separate item: never define a predicate function def is_bust(): with if x > 21: return True else: return False. Prefer to simply return x > 21, as that is already an expression which is either True or False. Similar concept. CPU cycles? Not relevant.
– J_H
Commented Jul 20 at 18:48
• Ok, so its more about just being efficient with your code and avoiding unnecessary steps. That's kind of what I'm trying to learn. Thanks! I'll try to keep that in mind as I move forward. I'm sure I have a lifetime of mistakes and "why did I do that??" thoughts ahead of me hahaha Commented Jul 20 at 18:51

## Input safety

This isn't crucial, but your code makes some assumptions about user input. The following inputs will result in strange behavior:

• yes, Yes, YES, etc. ("Yes" with more than one character)
• maybe, potato, foo, etc. (Neither yes nor no)
• y are you doing this to me (Neither yes nor no, but begins with y)

You can correct for this by defining a new yes/no input function that accounts for some variations. This is a fairly rudimentary variation (it doesn't fix the third case) - there's plenty of room for changes depending on how far you want to take this.

def yes_no_input(message: str) -> bool:
reply = input("Sorry, I didn't understand that.")

• We haven't gotten too much into user input and making sure its correct. At this time its just assuming what is entered is correct. I'm sure further along there will be ways that we can ensure its correct without needing assumptions. This is still quite early in the course so we aren't getting too deep into things quite yet. But thank you for the input, I'll be sure to take it into account in the future. Commented Jul 21 at 6:59

I'm surprised nobody mentioned it already, but you would go bankrupt pretty quickly if you would run a casino with this code. The reason is this implementation:

def deal():
"""Generates a random card from the list of available cards"""
cards = [11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10]
card = random.choice(cards)
return card


random.choice uses Mersenne Twister by default and it is not very hard to reconstruct the internal state of that PRNG if you now some of the previous outputs. With the internal state, an attacker can easily predict the next outputs, i.e. which cards are dealt next.

Luckily, there is an easy fix: just use secrets.choice instead.

• Ok, sounds good. I'm mostly just following the course instructor right now and haven't gone in depth too much yet. Mostly just trying to get syntax and the correct mindset down. Once I'm good with that, then I'll be sure to look into more secure functions. Thank you! Commented Jul 22 at 21:02

Just from a first glance:

• Well formatted
• Good choice of identifiers for variables and subroutines (except d_cards and p_cards; should be dealer_cards, player_cards or dealer_hand, player_hand if applicable)
• Code structure is easy to read/follow (I would also encourage extracting sub routines, as mentioned in the other reviews)
• Little use of comments (which is a good sign, it means you can express yourself in code, not comments alone)
• No fancy pantsy stuff on a simple program, goes straight to the point
• Minor code smell: You use magic numbers like "21" for "MAX_POINTS" (or "BLACKJACK"), "11" for "ACE_OVERFLOW", "1" for "ACE_RESET", "16" (IDK), "y" for "YES", "n" for "NO" for example.
• You might want to extract all the Strings to constants with a descriptive name. For example: "You bust! Computer wins!" could be GAME_OVER. This is less distracting when reading the code, shortens the line lengths and improves readability. Additionally it enables your program to be translated into different languages more easily. But this can be considered a new feature, not a code improvement alone.
• Large downside would be the lack of code tests. But this is an exercise for the future.
• Keep up the good work! :)
• Thanks for the input. I'm still in the very early stages so I'm glad that I've got some good things going on so far. I'm just trying to avoid any bad habits or practices and figured the time to do that is now when I start instead of trying to correct them later down the line. Much appreciated! Commented Jul 22 at 21:01