PEP-8
First off, some minor things your variable names aren't compliant with PEP-8, which recommends using:
UPPER_SNAKE
for top-level variables
PascalCase
for classes
lower_snake
for ordinary functions and variables
It also has recommendations on indent levels, etc. I suggest you look into getting a linter such as flake8 or pylint and run your code through them to standardise it against others' codes.
You also do something called "shadowing" of functions. Which means using the name of something declared elsewhere and giving it a new meaning. Here:
def difficultysetting(input):
you use the name input
which is an internal Python function (and indeed one you've used), but override it with the variable passed into the function. This means that if we wanted to use input
in the function, we couldn't. In this case it's not catastrophic, but doing it is bad practice and confusing.
In this case we might want to change it to choice
, for example. Now you should note that there exists a function called choice
in the random
library. However, because we're importing it with its name, in our local namespace, it is called random.choice
, so we aren't shadowing it. This is also why importing *
can be a bad idea, we might use names which conflict with some functions defined in libraries we've imported.
Handle user input smartly
At the moment, you are putting a lot of trust in the user to enter sensible values to each of your input
s. Realistically, we want to be prepared to handle whatever the user throws at us. Consider your guess:
guess = int(input("Pick a number between 1-100: "))
What happens if a user provides "a" as their guess?
At the moment you crash out with:
ValueError: invalid literal for int() with base 10: 'a'
We can be smarter than this. Consider:
while True:
guess = input("Pick a number between 1-100: ")
try:
guess = int(guess)
except ValueError:
print(f"Invalid guess ({guess}), must be valid number")
continue # Go back to start of loop and get a new guess
if 1 <= guess <= 100:
break
else: # Go back to start of loop and get a new guess
print(f"Invalid guess ({guess}), must be between 1 and 100")
This can even be stuck in a function so it doesn't clutter up our main code.
Functions (and how to use them)
Functions in Python (and most languages) take arguments and return some answers. The idea being that they can be used in multiple different places and be useful. A common structure of a function looks like:
def function_name(argument):
""" Describe function, what it does
Describe function arguments
Describe what function returns
"""
do stuff to argument(s)
return result
N.B. This is not always the case, there are some cases where you might have no arguments or want a function which doesn't return anything, e.g. to change the input argument or change global settings.
So, taking your difficulty selection as an example:
# It is sometimes recommended to name functions as verbs where appropriate
def get_difficulty(choice: str) -> int: # Type-hints give information to a user about how your code is meant to be used
""" Returns difficulty to 1-3 depending on user choice, otherwise raises a ValueError
:param choice: string containing user-entered choice
:returns: 1,2 or 3 representing difficulty
:rtype: int
"""
choice = choice.lower() # Changing the argument here doesn't change the value outside the function as `lower` returns a new string
if choice == "e":
difficulty = 1
elif choice == "m":
difficulty = 2
elif choice == "h":
difficulty = 3
else: # Consider what happens if a user enters e.g. "really hard"
raise ValueError(f"Invalid difficulty, must be one of e,m or h. Received: {choice}")
return difficulty
Using the right variable for the job
You have a lot of if
blocks which all just check against difficulty, e.g. to get which score, to get which max number, how many attempts, etc. This might be best done using arrays/tuples and we simply index into the array with difficulty. N.B.: This does mean the difficulties become 0
, 1
, 2
as arrays are indexed from 0
I.e.
SCORES = [9999999999] * 3
DIFFICULTY_RANGES = [100, 1000, 10000]
...
while isPlaying:
...
# You could set a variable to store DIFFICULTY_RANGES[difficulty] when you get difficulty, so you don't have to use the full name every time
number = random.randint(1, DIFFICULTY_RANGES[difficulty])
while guess != number:
guess = int(input(f"Pick a number between 1-{DIFFICULTY_RANGES[difficulty]}: "))
...
if len(attempts) < SCORES[difficulty]:
SCORES[difficulty] = len(attempts)
Also, your attempts
is a list of prior guesses, but as it is, you don't use it as a list, you just check against its length. This means that it could probably just be an integer which counts up each time.
Summary
Putting all this together along with a few other minor changes we end up with:
"""
Play number guessing game
"""
import random
# Max number for difficulty
DIFFICULTY_RANGES = [100, 1000, 10000]
def get_difficulty() -> int:
""" Returns difficulty to 0-2 depending on user choice
:returns: 0, 1 or 2 representing difficulty
:rtype: int
"""
difficulty = -1
while difficulty < 0:
choice = input("Choose a difficulty, (E)asy, (M)edium, (H)ard: ").lower()
if choice == "e":
difficulty = 0
elif choice == "m":
difficulty = 1
elif choice == "h":
difficulty = 2
else:
print(f"Invalid difficulty choice ({choice}) must be one of e, m or h")
return difficulty
def get_guess(max_number: int) -> int:
""" Get valid user guess, i.e. is a number and in range
:returns: User guess
:rtype: int
"""
while True:
guess = input(f"Pick a number between 1-{max_number}: ")
try:
guess = int(guess)
except ValueError:
print(f"Invalid guess ({guess}), must be valid number")
continue
if 1 <= guess <= max_number:
return guess
print(f"Invalid guess ({guess}), must be between 1 and {max_number}")
def play_game(max_number: int) -> int:
""" Play a round of the guessing game
:param max_number: Maximum value in range to guess
:returns: Number of attempts
:rtype: int
"""
number = random.randint(1, max_number)
guess = -1
attempts = 0
# Prompting user which numbers to guess between per difficulty
while guess != number:
guess = get_guess(max_number)
# Lets user know if they've guessed to high or too low
if guess < number:
print("Higher")
attempts += 1
elif guess > number:
print("Lower")
attempts += 1
else:
print("That's Correct!")
print("Guesses: ", attempts)
return attempts
def main():
# Default highscores for difficulties
scores = [9999999999] * 3
is_playing = True
while is_playing:
difficulty = get_difficulty()
max_number = DIFFICULTY_RANGES[difficulty]
attempts = play_game(max_number)
# Checking if new highscore is lower than old highscore and updating the variable if so
if attempts < scores[difficulty]:
print("New high score!")
scores[difficulty] = attempts
print(f"Current Highscore: {scores[difficulty]}")
# Asking user if they'd like to play again and ending while loop if no
while True:
play_again = input("Do you want to play again? (Y/N): ").lower()
if play_again == "n":
is_playing = False
break
if play_again == "y":
break
print(f"Invalid option ({play_again}), must be Y/N")
if __name__ == '__main__': # Main guard means that code will only run if called as `python guessing_game.py` and not when imported as a package
main()
There's certainly some more room for expansion, e.g. saving and loading high scores from a file, if you want some extra challenge.