2
\$\begingroup\$

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()
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

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)
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Thank you so much! This was incredibly helpful! Currently doing some research on docstrings! \$\endgroup\$ Commented May 21 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\$ Commented May 21 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
    Commented May 22 at 10:31
2
\$\begingroup\$

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

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.