I am fairly new to programming, and I recently finished part of one of my projects, a guessing game.
The part I completed was the game mode where the player guesses the computer's number. I'm quite proud of it, but I have a strong feeling that it is hugely suboptimal, as I only really know the basics so far. I would like some feedback how to not only improve this program, but all programs I make in the future.
Please be specific when giving feedback, because there may be some terms that I do not know.
My code:
import random
import sys
import time
#controls typing speed (I got this from online)
typing_speed = 150 #wpm
def slow_print(t):
for l in t:
sys.stdout.write(l)
sys.stdout.flush()
time.sleep(random.random()*10.0/typing_speed)
print('')
#asks difficulty
def ask_guess_difficulty():
slow_print('Alright! I\'ll pick a number from 1 to 100, and you try to guess it!')
slow_print('''You can do easy mode (E), with infinite guesses; normal mode (N), with 10 guesses;
hard mode (H), with 5 guesses; or custom mode (C), where you pick how many guesses you get!''')
guess_difficulty = input()
guess_difficulty_lower = guess_difficulty.lower()
if guess_difficulty_lower == 'e':
guesses = 1000
return guesses
elif guess_difficulty_lower == 'n':
guesses = 11
return guesses
elif guess_difficulty_lower == 'h':
guesses = 6
return guesses
elif guess_difficulty_lower == 'c':
slow_print('How many guesses do you want?')
guesses = input()
if guesses.isnumeric() == True:
guesses = int(guesses) + 1
slow_print('Ok! You will have ', str(guesses - 1), ' guesses!')
return guesses
else:
slow_print('I don\'t understand. Please input a number for the amount of guess that you want.')
else:
slow_print('''I don\'t understand. Please input \"E\" for easy mode, \"N\" for
normal mode, \"H\" for hard mode, and \"C\" for custom mode''')
#plays the gamemode where the player guesses the computer's number
def player_guess(guesses):
guesses = int(guesses)
player_guess_win = False
slow_print('Ok! I\'m thinking of a number from 1 to 100')
slow_print('You\'ll have ', str(guesses - 1), ' guesses to get it.')
slow_print('Go ahead and guess when you\'re ready.')
total_tries = 0
player_guess_num = random.randint(1,100)
last_guess = None
low_high = None
while player_guess_win == False and guesses > 0:
guesses -= 1
total_tries += 1
if int(guesses) > 0:
current_guess = input()
if current_guess.isnumeric() == True and int(current_guess) <= 100 and int(current_guess) >= 1:
if int(current_guess) == last_guess:
slow_print('That was your last guess silly! Make sure to enter a new number!')
guesses += 1
continue
if low_high == 'high':
if int(current_guess) > last_guess:
slow_print('Your guess is...')
if int(current_guess) == player_guess_num:
slow_print('CORRECT!')
slow_print('Congratulations! You took ',str(total_tries),' guesses to get it right!')
slow_print('Do you want to play again? (Please input "yes" or "no".)')
while True:
yn_choice = input()
if yn_choice == 'yes' or yn_choice == 'y':
slow_print('Ok, let\'s do it!')
guess_the_number_start()
break
elif yn_choice == 'no' or yn_choice == 'n':
slow_print('Ok!')
exit()
else:
slow_print('I don\'t understand. Please input "yes" or "no".')
else:
if int(current_guess) < player_guess_num:
slow_print('Too low.')
last_guess = int(current_guess)
low_high = 'high'
continue
else:
slow_print('Too high')
last_guess = int(current_guess)
low_high = 'low'
continue
else:
slow_print('Oops! Make sure you enter a number that is greater than your last guess!')
guesses += 1
continue
elif low_high == 'low':
if int(current_guess) < last_guess:
slow_print('Your guess is...')
if int(current_guess) == player_guess_num:
slow_print('CORRECT!')
slow_print('Congratulations! You took ',str(total_tries),' guesses to get it right!')
slow_print('Do you want to play again? (Please input "yes" or "no".)')
while True:
yn_choice = input()
if yn_choice == 'yes' or yn_choice == 'y':
slow_print('Ok, let\'s do it!')
guess_the_number_start()
break
elif yn_choice == 'no' or yn_choice == 'n':
slow_print('Ok!')
exit()
else:
slow_print('I don\'t understand. Please input "yes" or "no".')
else:
if int(current_guess) < player_guess_num:
slow_print('Too low')
last_guess = int(current_guess)
low_high = 'high'
continue
else:
slow_print('Too high')
last_guess = int(current_guess)
low_high = 'low'
continue
else:
slow_print('Oops! Make sure you enter a number that is less than your last guess!')
guesses += 1
continue
else:
slow_print('Your guess is...')
if int(current_guess) == player_guess_num:
slow_print('CORRECT!')
slow_print('Congratulations! You took ',str(total_tries),' guesses to get it right!')
slow_print('Do you want to play again? (Please input "yes" or "no".)')
while True:
yn_choice = input()
if yn_choice == 'yes' or yn_choice == 'y':
slow_print('Ok, let\'s do it!')
guess_the_number_start()
break
elif yn_choice == 'no' or yn_choice == 'n':
slow_print('Ok!')
exit()
else:
slow_print('I don\'t understand. Please input "yes" or "no".')
else:
if int(current_guess) < player_guess_num:
slow_print('Too low')
last_guess = int(current_guess)
low_high = 'high'
continue
else:
slow_print('Too high')
last_guess = int(current_guess)
low_high = 'low'
continue
else:
slow_print('I don\'t understand. Please enter a number from 1 to 100.')
guesses += 1
continue
else:
slow_print('You ran out of guesses. My number was ',str(player_guess_num), '.')
slow_print('Would you like to play again?')
slow_print('(Please input "Yes" or "No".)')
play_again_gtn = input()
play_again_gtn_lower = play_again_gtn.lower()
if play_again_gtn_lower == 'y' or play_again_gtn_lower == 'yes':
slow_print('Oki doki!')
continue
elif play_again_gtn_lower == 'n' or play_again_gtn_lower == 'no':
slow_print('Oki doki! Bye!')
exit()
#computer guesses your number (not finished yet
def computer_guess():
slow_print('Ok, I\'ll guess!')
slow_print('First, we need to figure out the span of numbers I\'ll be guessing')
slow_print('What should the minimum be? The number will not be able to go any lower than this.')
#begins the program
def guess_the_number_start():
slow_print('Do you want to guess my number, or should I guess yours?')
slow_print('(If you want to guess, input \"I\". If I should guess, input \"U\".)')
who_guess = input()
if who_guess.lower() == 'i':
guesses = ask_guess_difficulty()
player_guess(guesses)
elif who_guess.lower() == 'u':
computer_guess()
else:
slow_print('I don\'t understand. Please input \"I\" if you should guess and input \"U\" if I should guess.')
#loops the program
while True:
guess_the_number_start()
2 Answers 2
Possible bug
When I run the code, and it executes this line:
slow_print('You\'ll have ', str(guesses - 1), ' guesses to get it.')
the code exits with an error. My input sequence is I
then H
to reach this code.
Perhaps you get a different result on the version of Python that you used.
I get no error when I use string concatenation:
slow_print('You\'ll have ' + str(guesses - 1) + ' guesses to get it.')
Escaping
There is no need to escape the double quotes:
slow_print('(If you want to guess, input \"I\". If I should guess, input \"U\".)')
This is simpler:
slow_print('(If you want to guess, input "I". If I should guess, input "U".)')
Comments
The following comment is unnecessary because it merely repeats the name of the function:
#asks difficulty
def ask_guess_difficulty():
The same is true for many other comments in the code.
For better describe a function, it is common to use docstrings instead of comments. For example, change:
#plays the gamemode where the player guesses the computer's number
def player_guess(guesses):
to:
def player_guess(guesses):
""" plays the gamemode where the player guesses the computer's number """
Return values
You can eliminate the guesses
variable since it is not used elsewhere
in the following if/elif
code:
if guess_difficulty_lower == 'e':
guesses = 1000
return guesses
This is simpler:
if guess_difficulty_lower == 'e':
return 1000
The same applies to other elif
cases.
True
It is simpler to omit the comparison to True
in the following code:
if guesses.isnumeric() == True:
Use:
if guesses.isnumeric():
Lint check
pylint identified a few issues. There are some long lines that can be shortened.
The code uses inconsistent indentation. The black program can be used automatically modify the code.
Naming
Variables named l
and t
are not too descriptive. Use longer names:
def slow_print(text):
for character in text:
sys.stdout.write(character)
-
1\$\begingroup\$ Thank you so much! This was incredibly helpful! Currently doing some research on docstrings! \$\endgroup\$ADAM WELLER-FAHY– ADAM WELLER-FAHY2024年05月21日 14:17:37 +00:00Commented May 21, 2024 at 14:17
-
\$\begingroup\$ I'm using Python Anywhere, because I'm on a school Chromebook so I have to use a web compiler. The issue is, I cannot figure out how to run Black or pylint on it. Do you know how I could solve this, or where I could go to figure this out? \$\endgroup\$ADAM WELLER-FAHY– ADAM WELLER-FAHY2024年05月21日 16:57:38 +00:00Commented May 21, 2024 at 16:57
-
\$\begingroup\$ @ADAMWELLER-FAHY: black and pylint are not necessary, but they are nice to have. You might be able to use this online Python formatter instead of black. I just found it by googling. \$\endgroup\$toolic– toolic2024年05月22日 10:31:04 +00:00Commented May 22, 2024 at 10:31
Style
typing_speed
is a constant (that is, a variable that is set and you never plan to change the value of while running the code), so most style guides recommend naming it in all caps, as TYPING_SPEED
Your first indentation is 3 spaces, while all your others are 4. Be consistent!
As toolic said, you don't need to escape your quotes. Python uses ''
and ""
for strings, and you can use the double quotes inside single quotes or vis versa without escaping. Only when you have both in a string do you need to escape (and even then you could use triple quotes)
I prefer f-strings over string concatenation. You can replace 'You\'ll have ' + str(guesses - 1) + ' guesses to get it.'
with f"You'll have {guesses-1} guesses to get it."
You have a variable named guesses
in ask_guess_difficulty()
, except that the value it holds isn't actually the number of guesses, it's the number of guesses + 1. I can see why you did it, but there are better ways of handling your flow than to make your variables misrepresent what they are.
The number of guesses at each difficulty level should be pulled out as constants. That way, you can easily change the values without having to change the value in multiple places. Here's a simple change to ask_guess_difficulty()
to use constants (changing nothing else, even if I complained about it)
EASY_COUNT = 1000
NORMAL_COUNT = 10
HARD_COUNT = 5
def ask_guess_difficulty():
slow_print('Alright! I\'ll pick a number from 1 to 100, and you try to guess it!')
slow_print(f'''You can do easy mode (E), with infinite guesses; normal mode (N), with {NORMAL_COUNT} guesses;
hard mode (H), with {HARD_COUNT} guesses; or custom mode (C), where you pick how many guesses you get!''')
guess_difficulty = input()
guess_difficulty_lower = guess_difficulty.lower()
if guess_difficulty_lower == 'e':
guesses = EASY_COUNT+1
return guesses
elif guess_difficulty_lower == 'n':
guesses = NORMAL_COUNT+1
return guesses
elif guess_difficulty_lower == 'h':
guesses = HARD_COUNT+1
return guesses
elif guess_difficulty_lower == 'c':
slow_print('How many guesses do you want?')
guesses = input()
if guesses.isnumeric() == True:
guesses = int(guesses) + 1
slow_print('Ok! You will have ', str(guesses - 1), ' guesses!')
return guesses
else:
slow_print('I don\'t understand. Please input a number for the amount of guess that you want.')
else:
slow_print('''I don\'t understand. Please input \"E\" for easy mode, \"N\" for
normal mode, \"H\" for hard mode, and \"C\" for custom mode''')
Try/Except
You will find plenty of opinions on the value of using try
/except
blocks over if
/then
blocks, but your guesses.isnumeric()
check is a classic example of where try
/except
is often recommended - it's a check that you expect to pass the majority of the time, and are only including to handle the exceptional case.
I'd replace
if guesses.isnumeric():
guesses = int(guesses) + 1
slow_print('Ok! You will have ', str(guesses - 1), ' guesses!')
return guesses
else:
slow_print('I don\'t understand. Please input a number for the amount of guess that you want.')
with
try:
guesses = int(guesses) + 1
except ValueError:
slow_print('I don\'t understand. Please input a number for the amount of guess that you want.')
else:
slow_print('Ok! You will have ', str(guesses - 1), ' guesses!')
return guesses
Flow
Your flow in the main gameplay loop is a bit of a mess. The first thing you do is decrement guesses
, but half the checks you do will force you to immediately reincrement it. You be better off doing the decrement later, after those checks are complete. You split your code on the low/high check, meaning that your victory check, which is the same either way, is needlessly duplicated. Your if
checks all check for success, rather than failure, and then handle the failure state in the else
portion. This means that the failure state is handled many lines down from the associated if
check, and tracking which else
is associated with each if
is confusing. The else
blocks are all short, and it is much better to reverse the if
s to handle them first. Additionally, doing it this way means you can combine them into an if
elif
else
stack.
Here's how you could implement the flow more concisely. I'm also going to go and implement it so that the guesses
argument is the total number of guesses, not the number of guesses + 1 as you have it implemented.
def player_guess(guesses):
#guesses = int(guesses) # This should have been validated before this
player_guess_win = False
slow_print('Ok! I\'m thinking of a number from 1 to 100')
slow_print(f"You'll have {guesses} guesses to get it.")
slow_print("Go ahead and guess when you're ready.")
total_tries = 0
player_guess_num = random.randint(1,100)
last_guess = None
low_high = None
while not player_guess_win and total_tries < guesses:
try:
current_guess = int(input())
except ValueError:
slow_print('I don\'t understand. Please enter a number from 1 to 100.')
continue
if current_guess > 100 or current_guess < 1:
slow_print('I don\'t understand. Please enter a number from 1 to 100.')
continue
if: current_guess == last_guess:
slow_print('That was your last guess silly! Make sure to enter a new number!')
continue
if: low_high == 'high' and current_guess < last_guess:
slow_print('Oops! Make sure you enter a number that is greater than your last guess!')
continue
if: low_high == 'low' and current_guess > last_guess:
slow_print('Oops! Make sure you enter a number that is less than your last guess!')
continue
# Valid Guess
total_tries += 1
if current_guess == player_guess_num:
slow_print('CORRECT!')
slow_print(f'Congratulations! You took {total_tries} guesses to get it right!')
slow_print('Do you want to play again? (Please input "yes" or "no".)')
while True:
yn_choice = input()
if yn_choice == 'yes' or yn_choice == 'y':
slow_print("Ok, let's do it!")
guess_the_number_start()
break
elif yn_choice == 'no' or yn_choice == 'n':
slow_print('Ok!')
exit()
else:
slow_print("""I don't understand. Please input "yes" or "no".""")
elif current_guess < player_guess_num:
slow_print('Too low.')
low_high = 'high'
elif current_guess > player_guess_num:
slow_print('Too high.')
low_high = 'low'
last_guess = current_guess
slow_print(f'You ran out of guesses. My number was {player_guess_num}.')
slow_print('Would you like to play again?')
slow_print('(Please input "Yes" or "No".)')
play_again_gtn = input()
play_again_gtn_lower = play_again_gtn.lower()
if play_again_gtn_lower == 'y' or play_again_gtn_lower == 'yes':
slow_print('Oki doki!')
elif play_again_gtn_lower == 'n' or play_again_gtn_lower == 'no':
slow_print('Oki doki! Bye!')
exit()
I think that a 63 line function with 4 levels of nesting is better a 130 line function with 8 levels of nesting, do you agree?
Further improvements
You need string inputs in several places, and while you do handle invalid inputs, you only reprompt for correct input in one place. All of these places could be simplified by a good validation function.
def validate(options, prompt_hint=''):
"""
Takes input from the user, and returns the lowercase first character
of that input if that character is among the options.
options : str
a string of valid result characters. Example: 'yn'
prompt_hint : str
a message to the user if they don't input correctly.
Example: "Please input 'yes' or 'no'."
"""
while True:
choice = input().lower()[0]
if choice not in options:
slow_print(f"I don't understand. {prompt_hint}")
else:
return choice
With validate()
, your ask_guess_difficulty()
function looks like this:
EASY_COUNT = 1000
NORMAL_COUNT = 10
HARD_COUNT = 5
def ask_guess_difficulty():
slow_print('Alright! I\'ll pick a number from 1 to 100, and you try to guess it!')
slow_print(f'''You can do easy mode (E), with infinite guesses; normal mode (N), with {NORMAL_COUNT} guesses;
hard mode (H), with {HARD_COUNT} guesses; or custom mode (C), where you pick how many guesses you get!''')
guess_difficulty = validate('enhc', 'Please input "E" for easy mode, '
'"N" for normal mode, "H" for hard mode, and "C" for custom mode.')
if guess_difficulty == 'e':
guesses = EASY_COUNT
elif guess_difficulty == 'n':
guesses = NORMAL_COUNT
elif guess_difficulty == 'h':
guesses = HARD_COUNT
elif guess_difficulty == 'c':
slow_print('How many guesses do you want?')
while True:
try:
guesses = int(input())
except ValueError:
slow_print("I don't understand. Please input a number for the amount of guess that you want.")
else:
break
slow_print(f'Ok! You will have {guesses} guesses!')
return guesses
and player_guess()
looks like this:
def player_guess(guesses):
player_guess_win = False
slow_print('Ok! I\'m thinking of a number from 1 to 100')
slow_print(f"You'll have {guesses} guesses to get it.")
slow_print("Go ahead and guess when you're ready.")
total_tries = 0
player_guess_num = random.randint(1,100)
last_guess = None
low_high = None
while not player_guess_win and total_tries < guesses:
try:
current_guess = int(input())
except ValueError:
slow_print('I don\'t understand. Please enter a number from 1 to 100.')
continue
if current_guess > 100 or current_guess < 1:
slow_print('I don\'t understand. Please enter a number from 1 to 100.')
continue
if: current_guess == last_guess:
slow_print('That was your last guess silly! Make sure to enter a new number!')
continue
if: low_high == 'high' and current_guess < last_guess:
slow_print('Oops! Make sure you enter a number that is greater than your last guess!')
continue
if: low_high == 'low' and current_guess > last_guess:
slow_print('Oops! Make sure you enter a number that is less than your last guess!')
continue
# Valid Guess
total_tries += 1
if current_guess == player_guess_num:
slow_print('CORRECT!')
slow_print(f'Congratulations! You took {total_tries} guesses to get it right!')
slow_print('Do you want to play again? (Please input "yes" or "no".)')
yn_choice = validate('yn', 'Please input "yes" or "no".')
if yn_choice == 'y':
slow_print("Ok, let's do it!")
guess_the_number_start()
break
elif yn_choice == 'n':
slow_print('Ok!')
exit()
elif current_guess < player_guess_num:
slow_print('Too low.')
low_high = 'high'
elif current_guess > player_guess_num:
slow_print('Too high.')
low_high = 'low'
last_guess = current_guess
slow_print(f'You ran out of guesses. My number was {player_guess_num}.')
slow_print('Would you like to play again?')
slow_print('(Please input "Yes" or "No".)')
play_again_gtn = validate('yn', 'Please input "yes" or "no".')
if play_again_gtn == 'y':
slow_print('Oki doki!')
elif play_again_gtn == 'n':
slow_print('Oki doki! Bye!')
exit()
def validate(options, prompt_hint=''):
"""
Takes input from the user, and returns the lowercase first character
of that input if that character is among the options.
options : str or None
a string of valid result characters. Example: 'yn'
prompt_hint : str
a message to the user if they don't input correctly.
Example: "Please input 'yes' or 'no'."
"""
while True:
choice = input().lower()[0]
if choice not in options:
slow_print(f"I don't understand. {prompt_hint}")
else:
return choice
EASY_COUNT = 1000
NORMAL_COUNT = 10
HARD_COUNT = 5
def ask_guess_difficulty():
slow_print('Alright! I\'ll pick a number from 1 to 100, and you try to guess it!')
slow_print(f'''You can do easy mode (E), with infinite guesses; normal mode (N), with {NORMAL_COUNT} guesses;
hard mode (H), with {HARD_COUNT} guesses; or custom mode (C), where you pick how many guesses you get!''')
guess_difficulty = validate('enhc', 'Please input "E" for easy mode, '
'"N" for normal mode, \"H\" for hard mode, and \"C\" for custom mode.')
if guess_difficulty == 'e':
guesses = EASY_COUNT+1
elif guess_difficulty == 'n':
guesses = NORMAL_COUNT+1
elif guess_difficulty == 'h':
guesses = HARD_COUNT+1
elif guess_difficulty == 'c':
slow_print('How many guesses do you want?')
while True:
try:
guesses = int(input())
except ValueError:
slow_print("I don't understand. Please input a number for the amount of guess that you want.")
else:
break
slow_print(f'Ok! You will have {guesses} guesses!')
return guesses
Dictionary as an If/Else
There's one final python trick you can use in ask_guess_difficulty()
, and that's using a dict
in place of an if/else block. This only works when the if checks are fairly simple, and the output is simply assigning to a variable, but it works here. This is what I mean:
EASY_COUNT = 1000
NORMAL_COUNT = 10
HARD_COUNT = 5
def ask_guess_difficulty():
slow_print('Alright! I\'ll pick a number from 1 to 100, and you try to guess it!')
slow_print(f'''You can do easy mode (E), with infinite guesses; normal mode (N), with {NORMAL_COUNT} guesses;
hard mode (H), with {HARD_COUNT} guesses; or custom mode (C), where you pick how many guesses you get!''')
difficulty = validate('enhc', 'Please input "E" for easy mode, '
'"N" for normal mode, \"H\" for hard mode, and \"C\" for custom mode.')
guesses = {'e':EASY_COUNT, 'n':NORMAL_COUNT, 'h':HARD_COUNT, 'c':None}[difficulty]
if guesses is None:
slow_print('How many guesses do you want?')
while True:
try:
guesses = int(input())
except ValueError:
slow_print("I don't understand. Please input a number for the amount of guess that you want.")
else:
break
slow_print(f'Ok! You will have {guesses} guesses!')
return guesses
The special treatment for 'custom'
has to be handled outside the dictionary, but since there was only one special case it's still worth using, and saves us about 5 lines. Whether this is preferable or not is situational and a matter of opinion, but it's a good trick to have in your back pocket.
Explore related questions
See similar questions with these tags.