7
\$\begingroup\$

I am new to Python (and coding in general) and after about a week of reading "Thinking Like a Computer Scientist: Learning with Python" I decided to try and build a version the classic "guessing game". I added some extra features such as counting the number of guesses the user takes, and playing against a simulated "computer" player to make the program slightly more interesting. Also, the number of guesses the computer takes is based on the mean number of guesses needed to guess a number in a given range (which is logarithmic of base 2 for range n) and varies according to standard deviation.

Any feedback on the structure of my code or the way I generate the number of guesses the computer takes would be much appreciated!

# Number guessing game in Python
# Taylor Wright
# July 27 2016

import random


def get_number(level):                  #selects a random number in range depending on difficulty selected
    if level == "e":
        number = random.randint(1,20)
    if level == "m":
        number = random.randint(1,100)
    if level == "h":
        number = random.randint(1,1000)
    elif level != "e" and level != "m" and level != "h":
        print ("Invalid input!")
        get_number()
    return number


def select_level():    #prompts the user to select a difficulty to play on
    while True:
        level = str(input("Would you like to play on easy, medium, or hard? \n"
                      "Type 'e' for easy, 'm' for medium, or 'h' for hard!\n"))
        if level != "e" and level != "m" and level != "h":
            print("Invalid input!\n")
        if level == "e" or level == "m" or level == "h":
            break
    return level


def guess_number(level):        #function that prompts the user to guess within range depending on chosen difficulty
    if level == "e":
        guess = int(input("Guess a number between 1 and 20:\n"))
    if level == "m":
        guess = int(input("Guess a number between 1 and 100:\n"))
    if level == "h":
        guess = int(input("Guess a number between 1 and 1000:\n"))
    return guess


def check_guess(guess,number):         #processes the guess and tells the user if it is too high, too low, or bang on
    if guess > number:
        print ("your guess is too high! Try again! \n")
    if guess < number:
        print ("your guess is too low! Try again! \n")
    if guess == number:
        print("\n{0} was the number!".format(number))


def com_num_guesses(level):          #function to get the number of guesses taken by the computer
    if level == "e":
        com_guesses = round(random.normalvariate(3.7,1.1))
    if level == "m":
        com_guesses = round(random.normalvariate(5.8,1.319))
    if level == "h":
        com_guesses = round(random.normalvariate(8.99,1.37474))
    print("The computer guessed the number in {0} guesses! Can you beat that?".format(com_guesses))
    return com_guesses


def mainloop():
    level = select_level()
    number = get_number(level)
    com_guesses = com_num_guesses(level)
    num_guesses = 0
    while True:                 #tells program how to handle guesses after the first guess
        guess = guess_number(level)
        check_guess(guess,number)
        num_guesses += 1
        if guess == number:
            print( "You got it in {0} guesses.".format(num_guesses))
            if num_guesses == com_guesses:
                print("It took the computer {0} guesses too!\nIt's a tie!\n".format(com_guesses))
            if num_guesses > com_guesses:
                print("It took the computer {0} guesses.\nThe computer wins!\n".format((com_guesses)))
            if num_guesses < com_guesses:
                print("It took the computer {0} guesses.\nYou win!\n".format(com_guesses))
            play_again = str(input("To play again type 'yes'. To exit type 'no'. \n"))
            if play_again == "yes":
                mainloop()
            if play_again == "no":
                raise SystemExit(0)
            break


mainloop()
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

You need to avoid duplicating code and use the if/elif/else logic a bit more.

I've added an example of how your code could look to make it more extendable and cleaner

import random

class Level: #make a level class that you can extend
    def __init__(self, difficulty, computer):
        self.difficulty = difficulty
        self.computer = computer

leveldict = { #a dictionary to store your levels
    "e" : Level(20, (3.7, 1.1)),
    "m" : Level(100, (5.8, 1.319)),
    "h" : Level(1000, (8.99,1.37474)),
    }

def get_number(level):
    return random.randint(1, leveldict[level].difficulty)

def select_level(): 
    print("Would you like to play on easy, medium, or hard?\nType 'e' for easy, 'm' for medium, or 'h' for hard!")
    level = str(input())
    while level not in leveldict.keys(): #check for errors in select_level not in get_number
        print ("Invalid input!")
        level = str(input("Type 'e' for easy, 'm' for medium, or 'h' for hard!"))
    return level


def guess_number(level):
    print("Guess a number between 1 and {0}:\n".format(leveldict[level].difficulty))
    # a try/except block to check if the user really gives a number
    # you could add a check to see if the number is in the given range (e.g. 1-20)
    try: 
        n = int(input())
    except ValueError:
        print("Invalid input!")
        n = guess_number(level)
    return n


def check_guess(guess, number):
    #use if, elif, else logic
    if guess > number:
        print ("your guess is too high! Try again! \n")
    elif guess < number:
        print ("your guess is too low! Try again! \n")
    else:
        print("\n{0} was the number!".format(number))


def com_num_guesses(level):
    # the * in "leveldict[level].computer" is to unpack your tuple with the normalvariate range
    com_guesses = round(random.normalvariate(*leveldict[level].computer))
    print("The computer guessed the number in {0} guesses! Can you beat that?".format(com_guesses))
    return com_guesses


def mainloop():
    level = select_level()

    number = get_number(level)
    com_guesses = com_num_guesses(level)
    guess = guess_number(level)
    check_guess(guess, number)

    num_guesses = 1

    # use a statement for the while loop, it's cleaner in this case than "while True: (...) break"
    # and you have less duplicate code
    while guess != number:
        guess = guess_number(level)
        check_guess(guess,number)
        num_guesses += 1

    print("You got it in {0} guesses.".format(num_guesses))
    print("It took the computer {0} guesses".format(com_guesses), end="")

    #use if/elif/else logic and remove the duplicate code
    if num_guesses > com_guesses:
        print(".\nThe computer wins!\n".format((com_guesses)))
    elif num_guesses < com_guesses:
        print(".\nYou win!\n".format(com_guesses))
    else:
        print(" too!\nIt's a tie!\n")

    #you dont need the if "no" because it will exit anyways if the input is not "yes"
    play_again = str(input("To play again type 'yes'. To exit type 'no'. \n"))
    if play_again == "yes":
        mainloop()

mainloop()
\$\endgroup\$
1
  • 2
    \$\begingroup\$ It might be better to selectively show the code that can be improved along with the the code you think should replace it. You will get higher scoring answers if you discuss the code more and not do a whole alternate solution. See codereview.stackexchange.com/help/how-to-answer. \$\endgroup\$
    – pacmaninbw
    Jul 25, 2016 at 16:04
5
\$\begingroup\$

Without changing the global structure of your code, here are a few things that could make it better.

NB: I will try to cover as much as I can, skipping objects and if-reduction, as it has already been covered in another answer

Docstrings

You use regular comments at the beginning of your functions. Their purpose is to indicate what the function does, therefore they should be docstrings. The difference is that it is stored in the __doc__ special variable.

def get_number(level):
    """selects a random number in range depending on difficulty selected"""
    if level == "e":
        # rest of the function

About get_number

You could use a dictionary of levels to make it shorter and easier to understand. Also more flexible when you want to change levels later, or add new levels.

levels = {'e': 20, 'm': 100, 'h': 1000}

The content of the function is then something like:

if level != "e" and level != "m" and level != "h":
    print ("Invalid input!")
    get_number()
else:
    number = random.randint(1, levels[level])
return number

Or even better, try and catch a KeyError that is raised when the key does not exist. Final get_number function:

levels = {'e': 20, 'm': 100, 'h': 1000}

def get_number(level):
    """selects a random number in range depending on difficulty selected"""
    try:
        number = random.randint(1, levels[level])
    except KeyError:
        print ("Invalid input!")
        get_number()
    return number

About guess_number

Golden rule : never rely on a user input (you can read about it here for example). As we've seen before, the user does not always enter what you asked for, and this can cause your game to crash. Fortunately, using try/except can solve this problem. I've kept the use of the levels dictionary here.

def guess_number(level):
    """function that prompts the user to guess within range
    depending on chosen difficulty"""
    try:
        guess = int(input("Guess a number between 1 and "+str(levels[level])+":\n"))
    except KeyError:
        print('This should never have happened. "level" is not part of the levels')
    except (ValueError, NameError, SyntaxError):
        print('Invalid input\n')
        guess = guess_number(level)
    return guess

Naming arguments

Sometimes functions can be obscure, it can be helpful for others (and future you) to name arguments, as Python allows nicely.

if level == "e":
    com_guesses = round(random.normalvariate(mu=3.7, sigma=1.1))

This makes it way easier to understand.

Magic numbers

Speaking of the line of code just above, 3.7 and 1.1 seem to come out of nowhere. Two solutions to get this straight:

  • Create constants (not my favorite)

For instance,

EASY_MU = 3.7
EASY_SIGMA = 1.1
MEDIUM_MU = 5.8
# ...

and the line becomes much more understandable

if level == "e":
    com_guesses = round(random.normalvariate(mu=EASY_MU, sigma=EASY_SIGMA))
  • Use dictionaries (again)

We could improve the levels dictionary to make it contain what we want for later.

levels = {'e': {'size': 20, 'mu': 3.7, 'sigma': 1.1},
          'm': {'size': 100, 'mu': 5.8, 'sigma': 1.319},
          'h': {'size': 1000, 'mu': 8.99, 'sigma': 1.37474}
          }

Then the whole method becomes way more readable

def com_num_guesses(level):
    """function to get the number of guesses taken by the computer"""
    try:
        com_guesses = round(random.normalvariate(mu=levels[level]['mu'], sigma=levels[level]['sigma']))
    except KeyError:
        print('This should never have happened. "level" is not part of the levels')
    print("The computer guessed the number in {0} guesses! Can you beat that?".format(com_guesses))
    return com_guesses

Also note that in get_number and guess_number, we will now have calls to levels[level]['size'] since the dictionary has changed.

Lauching

It is recommended to encapsulate your mainloop function within an if __name__ == '__main__' condition. That way, it will not get run if this module is imported by someone else. More information on that here.

if __name__ == '__main__':
    mainloop()

Global remarks

You used a lot of recursive calls. This is OK, since the number of calls remains small (once per user mistake). But I personally have a preference for while True loops that break as soon as the user input is correct. It uses less memory.

This answer is already quite long, and I've given you pieces of information to improve your code, not revolutionized the whole structure, that's why I didn't copy the whole code at the end.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thanks for tour feedback about Docstrings! I always wondered why some explanations about the code go in triple quotes while others go in comments \$\endgroup\$ Jul 25, 2016 at 8:33
  • \$\begingroup\$ You're welcome. See updated answer for more :) \$\endgroup\$
    – BusyAnt
    Jul 25, 2016 at 9:12

Your Answer

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

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