There's good news and bad news.
For beginner code, this is fairly well written. The graphics are intuitive and the controls straightforward.
However, the site you've used (trinket.io) allows a kind of Python I haven't seen before. Your code will fail both in Python 2 and in 3. Before we do anything else, let's make sure it works on at least one of those versions so you're no longer restricted to an odd flavour from an odd site.
Note: If anyone happens to know more about this flavour of Python, please leave a comment.
This will be a review in 3 parts:
- Make it work in 'normal' Python.
- Iron out the odd things and inconsistencies in the code.
- Remarks from a user-experience point of view.
Don't be discouraged by the length of the review. Most of the points mentioned in section 2 and 3 are only there because they'll hurt when you start writing larger programs.
At the end of the review a partially fixed piece of code is included to illustrate some of the points. I'll leave it up to you to find the differences and implement the rest of the points mentioned.
Make it work
The code looks more like Python 3 than 2, so let's stick with that one for now. The main reason your code won't work in Python 3, is because of your print
statements. In 2, print
is an exotic statement. In 3, print()
is a proper function. So let's make them all function calls.
That's all!
Odd things and inconsistencies
Repeat after me:
Consistent code is readable code.
Readable code is maintainable code.
Maintainable code is good code.
Most of the things I'll mention are written down as guidelines in the Python Enhancement Proposals (PEP). Read more about those here.
Main issues (in order of appearance, not of importance):
- Bugs
- Input
- Aces
- Random cards
- Imports
- Order
- Blanket imports
- Whitespace
- Indentation
- Blank lines
- Operators
- Comments
- Comparison
- Strings
- Naming
- Illegal names
- Functions and variables
- Miscellaneous
Bug
Input
A bug in both the original and the Python 3 version is apparent when you want to bet more money than available:
ValueError: invalid literal for int() with base 10: '' on line 142 in main.py
Or when the input isn't an integer at all (but, for example, a float or string):
ValueError: invalid literal for int() with base 10: '150.1' on line 140 in main.py
Your program will crash at invalid input. 2 possible ways out: validate input before trying to put a square block through a round hole, or keep trying till a round block is used.
Aces
When an Ace is drawn, you check whether the player wants the ace to count as 11 or 1 point. I'm not sure what version of Blackjack you're used to, but around here the value of the Ace can change at will and doesn't have to be set at draw. For example, if I initially draw an Ace and 7, I'll consider the hand to be 18 and 8 at the same time. Should I decide to draw another card and it's a 3, the Ace is 11 and the hand 21. If it's any higher than a 3, the Ace turns into a 1 to avoid going bust. Your program doesn't account for this.
Random cards
At one point in the game both the dealer and I had the Ace of Diamonds. Your random selector should account for that. Now I know for certain the dealer is cheating.
Part of the game is to know what cards are in play so you can calculate your chances. If the dealer has a King and you have 2 Kings, what are the odds of the dealer having another King? This gets even better when playing with multiple players at the same table. The more cards are on the table, the less you have to guess about the cards remaining.
I don't expect a beginner program to track the cards over multiple rounds (just assume the cards will be shuffled after every round and it doesn't matter anymore), but at the moment your program allows to have 5 Aces at the same time on the field.
If you're truly interested in Blackjack, I recommend reading "Bringing down the house" by Ben Mezrich (2002). Or watch the movie adaptation "21" (2008). The book is much better.
Order
The order of imports:
- Standard library imports
- Third party imports
- Local application imports
Does it matter when you only have 2 import lines? No. Does it keep things easier when you have 20 of them? Definitely.
from turtle import *
import random
Turns into:
import random
from turtle import *
Blanket imports
Do you know what you're importing when you use from turtle import *
? The entire turtle. You don't need the entire turtle. This is what you need:
from turtle import Turtle, ht, speed, write, clear, up, down, left, right, goto, forward
And yes, that's quite a mouthful. And longer than 80 characters. So, for small programs which only import from 1 library this way, there's no problem. Import 6 different libraries like that and you're importing a lot of stuff you don't need. What would happen if 2 or 3 libraries have a clear
function? By writing out which ones you need, you immediately realize the problem should such a thing happen.
Of-course, the obvious solution is simply importing like this:
import random
import turtle
Yes, this will lead to somewhat longer calls. updater = Turtle()
turns into updater = turtle.Turtle()
and ht()
turns into turtle.ht()
. But the moment you get multiple from lib import *
statements in your code, you got a problem.
Whitespace
Indentation
In Python, indentation is important. Forget to indent a line of code the proper amount and your code will do something different altogether (if it still does anything at all). To notice indentation problems, it helps if the indentation is enough to be easily noticeable but not so much that it will make the lines unnecessarily long. 4 spaces is good. If you prefer using tabs during programming, set your editor tabs-to-spaces with a tab length of 4. If you're not sure what editor to use, give Atom a go. Runs on everything.
The amount of blank lines between pieces of code help to see in just a glance where you are in the program. While not as important as indentation (blank lines, or vertical whitespace, won't make or break your program), every bit helps. Remember what I said about consistency?
For example, between imports and the rest of your code there'll always be 2 blank lines. Between methods? A single blank line. Between global functions? 2 lines. Between class declaration and the first method? Zero lines.
Operators
All operators get spaces around them, except around the =
when assigning default values. After a comma there's always a space, but never more than one. So this:
number = random.choice(['A',2,3,4,5,6,7,8,9,10,'J','Q', 'K'])
Should've been:
number = random.choice(
['A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K']
)
Comments
After the #
to start a comment, leave a space before writing down the actual comment. Leave (at least) 2 spaces before the #
.
some_var = 'foo' # like this
Comparison
"Comparisons to singletons like None should always be done with 'is' or 'is not', never the equality operators."
As explained here, here and here.
A class is free to implement comparison any way it chooses, and it can choose to make comparison against None mean something (which actually makes sense; if someone told you to implement the None object from scratch, how else would you get it to compare True against itself?).
Strings
Roughly speaking, Python has 3 methods of indicating a string.
`keys` and single characters go between single quotes.
"sentences go between double quotes"
"""docstrings and multi-line strings
go between tripled double quotes"""
Also nicely illustrated here.
And whatever you do, be consistent. So no more of this:
suit = random.choice(['D', "S", 'C', 'H'])
That S
goes between single quotes, just like the rest of them.
Naming
Illegal names
The following is quite important.
Don't EVER name a variable or function after a built-in. It confuses everyone including (possibly, although you usually get away with it) the interpreter.
round
sum
You got those as function and variable names. Don't.
Consider naming something after a built-in illegal.
Function, method and variable names are all snake_case
. Lowercase with underscores as separators. If you feel the need to indicate a function is part of a group of functions, like you did with the dealer functions, consider making them methods part of a class. Problem solved.
Global variables which will never changed (think constants, except Python doesn't have them) are in UPPER_CASE
.
I don't know what bob
and joe
are doing in your program, but names should be effective in telling the world what they mean. Those don't cut it.
joe = [0, 1]
Not a clue.
QuitorNah()
Dubious at best. Don't try to make your function names funny. We got comments for that.
z = bust(x, playerMoney)
if z == 0:
return playerMoney
Why is that called z
? Only use z
if it has a meaning, like in a 3D graph as third axis. Here, it's a bad name. Might even want to drop the z entirely and simply return when bust(x, playerMoney)
is False.
Miscellaneous
There's a lot of code in there that looks like it's not doing anything.
def round(playerMoney):
PcardUpdate(0, 0, playerMoney)
playerMoney = playerMoney # <----
When you run out of money, the game will halt. Not really a problem, but somewhat unexpected. After all, you keep asking me after every round whether I want to quit. Why not asking me at the end of each game whether I want a new one?
On top of that, consider leaving a trailing space or newline behind a question, so the answer isn't typed directly next to the question. Makes it all just a little neater.
I know most people have a nice big monitor nowadays, but it's recommended to stay below 80 characters wide when writing Python. Better to use a couple of lines more than to have a line of 200 characters wide. Scrolling up and down is easy, scrolling sideways is more tedious.
While I mentioned being consistent in your code, being consistent in what you show the user is perhaps just as important. Use proper capitalisation and spelling whenever possible.
Oh, and is your Card.getNumber()
doing what you think it's doing?
def getNumber(self):
return self.number[1]
self.number
shouldn't be a list.
There's plenty more, but I think I gave you enough examples on how to improve your code. Here's your exact code, just a little prettier. Good luck implementing the rest of the points and I might write next review about actual code flow :-)
import random
import turtle
updater = turtle.Turtle()
updater.ht()
updater.speed(0)
updater.up()
updater.goto(30, 0)
turtle.speed(0)
playerMoney = 1000
turtle.ht()
class Card:
def __init__(self, number=None, suit=None):
if suit is None:
suit = random.choice(['D', 'S', 'C', 'H'])
number = random.choice(
['A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K']
)
if number in ['K', 'Q', 'J']:
number = [number, 10]
else:
number = [number, number]
self.suit = suit
self.number = number
def getSuit(self):
return self.suit
def getNumber(self):
return self.number[1]
def writing(self, x, y):
turtle.up()
turtle.goto(x, y)
turtle.down()
turtle.write(self.number[0])
turtle.up()
turtle.right(90)
turtle.forward(25)
turtle.left(90)
turtle.write(self.suit)
turtle.left(90)
turtle.forward(50)
turtle.right(90)
def ace(self):
if 'A' in self.number:
x = int(input("Would you like your ace to equal 11 or 1?"))
if x == 11:
self.number[1] = 11
elif x == 1:
self.number[1] = 1
else:
ace(self)
def setup(x, y):
turtle.up()
turtle.goto(x, y)
turtle.down()
for _ in range(2):
for _ in range(2):
turtle.forward(50)
turtle.right(90)
turtle.forward(100)
turtle.right(90)
turtle.forward(50)
def drawcard(x, y):
turtle.up()
turtle.goto(x-25, y+30)
turtle.down()
for _ in range(2):
turtle.forward(50)
turtle.right(90)
turtle.forward(100)
turtle.right(90)
def PcardUpdate(x, card_sum, playerMoney):
updater.clear()
updater.goto(30, 20)
updater.write("Player Card sum: " + str(x))
updater.goto(30, 0)
updater.write("Dealer Card sum: " + str(card_sum))
updater.goto(30, -20)
updater.write("Player Money: " + str(playerMoney))
def bust(x, playerMoney):
if x > 21:
print("Bust!")
return 0
def is_hit(current_player):
x = -25 + (current_player * 50)
choice = input('Hit or stand?')
while choice.lower() not in ['hit', 'stand']:
choice = input('Hit or stand')
if choice.lower() == 'Hit':
print('Hit it is')
current_player += 1
XtraCard = Card()
XtraCard.writing(x, -55)
drawcard(x, -55)
XtraCard.ace()
return [XtraCard.getNumber(), current_player]
elif choice.lower() == 'stand':
print('Ok')
return [0, 0]
else:
print("I didn't understand")
is_hit(current_player)
def DHit(current_player, playerhand, card_sum):
x = -25 + (current_player * 50)
current_player += 1
XtraCard = Card()
XtraCard.writing(x-55, 100)
drawcard(x, 120)
if XtraCard.getNumber() == 'A':
if card_sum + 11 > 21:
return [1, current_player]
elif card_sum + 11 == 21:
return [11, current_player]
else:
return [11, current_player]
return [XtraCard.getNumber(), current_player]
def Dchecker(card_sum, x):
if card_sum > 21:
return 'dealer bust'
elif card_sum > x and card_sum < 21:
return 'dealer win'
elif card_sum == x:
return 'push'
elif card_sum == 21:
return 'dealer win'
def howmuch(player1): # how much the player wants to bet
bet = int(input("How much would you like to bet? (whole number)"))
while bet > player1:
bet = int(
input("You can't bet more than you have.")
)
while bet < 0:
bet = int(
input("How much would you like to bet? (whole positive number)")
)
while bet == 0:
print("No empty bets allowed.")
bet = int(
input("How much would you like to bet? (whole positive number)")
)
return bet
def want_to_quit():
choice = input("Would you like to quit?")
while choice.lower() not in ['yes', 'no']:
print("Sorry I didn't understand.")
choice = input("Would you like to quit")
if choice.lower() == 'yes':
print("Ok, bye")
return 'bye'
elif choice.lower() == 'no':
print("Ok")
return
def play_round(playerMoney):
PcardUpdate(0, 0, playerMoney)
playerMoney = playerMoney
bet = howmuch(playerMoney)
PcardUpdate(0, 0, playerMoney)
playerMoney -= bet
counter = [0, 1]
joe = [0, 1]
setup(-100, 150)
setup(-100, -25)
# dealer cards
Dcard1 = Card()
Dcard1.writing(-75, 100)
# player cards
Pcard1 = Card()
Pcard2 = Card()
# writes the card down
Pcard1.writing(-75, -55)
Pcard2.writing(-25, -55)
# checks to see if Pcard is ace
Pcard1.ace()
Pcard2.ace()
x = Pcard1.getNumber() + Pcard2.getNumber()
if x == 21:
print("Blackjack!")
playerMoney = playerMoney + 2 * bet
return playerMoney
PcardUpdate(x, Dcard1.getNumber(), playerMoney)
while counter != [0, 0]:
counter = is_hit(counter[1])
x += counter[0]
PcardUpdate(x, Dcard1.getNumber(), playerMoney)
z = bust(x, playerMoney)
if z == 0:
return playerMoney
card_sum = Dcard1.getNumber()
if card_sum == 'A':
card_sum = 11
result = 'current_player'
while result not in ['dealer bust', 'dealer win', 'push']:
joe = DHit(joe[1], x, card_sum)
card_sum += joe[0]
PcardUpdate(x, card_sum, playerMoney)
result = Dchecker(card_sum, x)
if result == 'dealer bust':
print('Dealer bust!')
playerMoney += 2*bet
return playerMoney
elif result == 'dealer win':
print('Dealer win!')
return playerMoney
elif result == 'push':
print('Push!')
playerMoney += bet
return playerMoney
while True:
playerMoney = play_round(playerMoney)
if playerMoney == 0:
print("You ran out of money :(")
break
last_round = want_to_quit()
turtle.clear()
if last_round == 'bye':
break
SyntaxError
, but on Python 2 after the 'Hit or stand?' prompt, aNameError
is raised. Did you mean to useraw_input
? \$\endgroup\$