5. Demo : Secret Number Game#
We have now covered enough concepts to create our first program. This will also give us the opportunity to delve deeper into additional good programming practices such as code structuring and variable naming conventions.
The program we will be developing is a guessing game. The goal is to determine a secret number in as few attempts as possible. The computer will randomly select a number between 0 and 100, and the user will try to guess this number. With each attempt, the program will indicate whether the guess is too low or too high. When the user guesses the correct number, the program will display a congratulatory message and terminate.
To guide our development, we will outline the different steps of the program in comments. Here’s a preliminary version of our pseudocode:
# 1. Generate a random number
# 2. Read a number from the user
# 3. If the number is the secret number
# Then display a victory message and stop the program
# Otherwise display a hint and read a new number
# 4. Repeat from step 2.
It’s clear that step 4 involves a loop, which we can explicitly define using a while loop:
# 1. Generate a random number
# 2. Read a number from the user
# 3. While the user’s guess is not the secret number
# Display a hint
# Read a new number
# 4. Display a victory message and stop the program
This pseudocode will serve as our foundation. Initially, it can be helpful to write this out when tackling a complex problem. With experience, this process becomes second nature, and often, we can skip it. For now, we will rely on it.
5.1. Importing Functions#
To accomplish step 1, we need to generate a random number. This operation is common in programming, so most languages include a function for this purpose in their standard libraries (see glossary Standard Librairies).
Python is no exception and provides the randint(a, b)
function, which generates a pseudo-random (see glossary Pseudo-random) integer between a
(inclusive) and b
(inclusive). However, this function needs to be imported (see glossary Import). It is part of a module called random
. To import it, we use the following command:
from random import randint
Convention dictates that imports are grouped at the top of the script file. Once the import is done, we can generate a random number and store it in a variable. This variable should have a descriptive name to help the programmer understand its purpose. Here, we will use secret_number
.
Here’s what our script looks like at this stage:
from random import randint
# 1. Generate a random number
secret_number = randint(0, 100)
# 2. Read a number from the user
# 3. While the user’s guess is not the secret number
# Display a hint
# Read a new number
# 4. Display a victory message and stop the program
The variable name secret_number
and the function randint
are sufficiently descriptive, making the comment redundant. We can therefore remove it:
from random import randint
secret_number = randint(0, 100)
# 2. Read a number from the user
# 3. While the user’s guess is not the secret number
# Display a hint
# Read a new number
# 4. Display a victory message and stop the program
5.2. Reading a Number#
To read an integer from the user, we use the input
function combined with int
conversion. However, it’s crucial to ensure that the number is between 0
and 100
. Therefore, we will create a function that repeatedly prompts the user until a valid number is entered.
def get_user_guess():
choice = -1
while choice < 0 or choice > 100:
print("Enter a number between 0 and 100.")
choice = int(input())
return choice
The variable choice
is initialized to -1
to ensure the loop executes at least once. While this function is functional, it has some design issues that should be addressed to avoid potential bugs or unnecessary modifications later. Specifically, the range limits (0
and 100
) are hard-coded. If we decide to change the range of the secret number to 0
–1000
, we would need to update these values in multiple places, which can be tedious and one occurence may be missed.
A better approach is to define constants for these values. This way, changing the range in one place updates it throughout the program. Here’s how we can adjust our script:
from random import randint
MIN_SECRET_VALUE = 0
MAX_SECRET_VALUE = 100
def get_user_guess():
choice = -1
while choice < MIN_SECRET_VALUE or choice > MAX_SECRET_VALUE:
print(f"Enter a number between {MIN_SECRET_VALUE} and {MAX_SECRET_VALUE}.")
choice = int(input())
return choice
secret_number = randint(MIN_SECRET_VALUE, MAX_SECRET_VALUE)
user_guess = get_user_guess()
# 3. While the user’s guess is not the secret number
# Display a hint
# Read a new number
# 4. Display a victory message and stop the program
Notice how the script begins with imports, followed by global constants and function definitions. The main instructions of the program come afterward.
5.3. Error Handling#
To ensure that our program is robust and can handle unexpected or invalid inputs gracefully, it’s crucial to implement error handling (see glossary [](glossary:Error Handling)). For instance, when using the int function to convert a string to an integer, there’s a potential risk of encountering a ValueError if the string cannot be interpreted as a valid integer. To address this, we can use Python’s exception handling mechanism, which involves try
and except
blocks. Here’s how it works:
Try Block: The
try
block contains the code that might potentially raise an exception. In our case, this would be the code where we attempt to convert user input to an integer.Except Block: The
except
block follows thetry
block and contains the code that should be executed if an exception occurs. This block can handle the error gracefully, such as by displaying a user-friendly message or prompting the user to enter the input again.
def get_user_guess():
choice = -1
while choice < MIN_SECRET_VALUE or choice > MAX_SECRET_VALUE:
print(f"Enter a number between {MIN_SECRET_VALUE} and {MAX_SECRET_VALUE}.")
try:
choice = int(input())
except ValueError:
print("The input must be an integer!")
choice = -1
return choice
In the except
block, by resetting choice to -1
, we ensure that the loop will continue until a valid input is received.
from random import randint
MIN_SECRET_VALUE = 0
MAX_SECRET_VALUE = 100
def get_user_guess():
choice = -1
while choice < MIN_SECRET_VALUE or choice > MAX_SECRET_VALUE:
print(f"Enter a number between {MIN_SECRET_VALUE} and {MAX_SECRET_VALUE}.")
try:
choice = int(input())
except ValueError:
print("The input must be an integer!")
choice = -1
return choice
secret_number = randint(MIN_SECRET_VALUE, MAX_SECRET_VALUE)
user_guess = get_user_guess()
# 3. While the user’s guess is not the secret number
# Display a hint
# Read a new number
# 4. Display a victory message and stop the program
5.4. Game Loop#
We can now implement the game loop, corresponding to step 3. To improve readability, we will create a function specifically for providing hints.
from random import randint
MIN_SECRET_VALUE = 0
MAX_SECRET_VALUE = 100
def get_user_guess():
choice = -1
while choice < MIN_SECRET_VALUE or choice > MAX_SECRET_VALUE:
print(f"Enter a number between {MIN_SECRET_VALUE} and {MAX_SECRET_VALUE}.")
try:
choice = int(input())
except ValueError:
print("The input must be an integer!")
choice = -1
return choice
def print_hint(guess, secret):
if guess < secret:
print("The secret number is higher...")
else:
print("The secret number is lower...")
secret_number = randint(MIN_SECRET_VALUE, MAX_SECRET_VALUE)
user_guess = get_user_guess()
while user_guess != secret_number:
print_hint(user_guess, secret_number)
user_guess = get_user_guess()
# 4. Display a victory message and stop the program
5.5. Victory message with f-string#
Assuming we have a variable named nb_of_tries
that represents the number of attempts a user has made, displaying a message with this information using two separate print statements can be done as follows:
print("Bravo! You win in")
print(nb_of_tries)
However, this approach lacks elegance. To enhance readability and streamline the output, we can use a more sophisticated method known as f-strings. F-strings, or formatted string literals, are a feature introduced in Python 3.6 that simplify string formatting.
An f-string (see glossary F-string) is a string literal prefixed with the letter f
that contains expressions inside curly braces {}
. These expressions are evaluated at runtime and formatted using the format specified in the f-string. This allows for concise and readable string formatting. For our specific case, instead of printing messages and variables separately, we can combine them into a single, formatted string:
print(f"Bravo! You win in {nb_of_tries} tries!")
In this example, {nb_of_tries}
is an expression inside the f-string that gets replaced by the value of the nb_of_tries
variable when the string is evaluated. This not only makes the code cleaner but also more efficient.
F-strings are highly versatile and support a variety of formatting options, making them a powerful tool for string manipulation. We will explore their advanced features and formatting capabilities in more detail later in the course.
from random import randint
MIN_SECRET_VALUE = 0
MAX_SECRET_VALUE = 100
def get_user_guess():
choice = -1
while choice < MIN_SECRET_VALUE or choice > MAX_SECRET_VALUE:
print(f"Enter a number between {MIN_SECRET_VALUE} and {MAX_SECRET_VALUE}.")
try:
choice = int(input())
except ValueError:
print("The input must be an integer!")
choice = -1
return choice
def print_hint(guess, secret):
if guess < secret:
print("The secret number is higher...")
else:
print("The secret number is lower...")
secret_number = randint(MIN_SECRET_VALUE, MAX_SECRET_VALUE)
user_guess = get_user_guess()
nb_of_tries = 1
while user_guess != secret_number:
print_hint(user_guess, secret_number)
user_guess = get_user_guess()
nb_of_tries += 1
print(f"Congratulations! You guessed the number in {nb_of_tries} tries!")
5.6. Cleaning before leaving#
In Python programming, having a clear and organized structure in your code is crucial. Even if your code works correctly, it’s beneficial to refactor it into a more organized format before finalizing it. One key organizational practice is using a main function. Let’s explore why and how to do this.
In many programming languages, including Python, the main
function serves as the entry point for program execution. While Python doesn’t require a main
function, using one is a convention that improves the readability and maintainability of your code.
A main
function helps structure your code. It acts as a starting point, making it clear where the program execution begins. By encapsulating the primary execution logic within main
, you make it easier for others (or yourself in the future) to understand the program’s flow without wading through all the details. It helps separate the logic of your program (inside main
) from the implementation details (inside other functions). This separation makes your code cleaner and easier to debug or extend.
from random import randint
MIN_SECRET_VALUE = 0
MAX_SECRET_VALUE = 100
def get_user_guess():
choice = -1
while choice < MIN_SECRET_VALUE or choice > MAX_SECRET_VALUE:
print(f"Enter a number between {MIN_SECRET_VALUE} and {MAX_SECRET_VALUE}.")
try:
choice = int(input())
except ValueError:
print("The input must be an integer!")
choice = -1
return choice
def print_hint(guess, secret):
if guess < secret:
print("The secret number is higher...")
else:
print("The secret number is lower...")
def main():
secret_number = randint(MIN_SECRET_VALUE, MAX_SECRET_VALUE)
user_guess = get_user_guess()
nb_of_tries = 1
while user_guess != secret_number:
print_hint(user_guess, secret_number)
user_guess = get_user_guess()
nb_of_tries += 1
print(f"Congratulations! You guessed the number in {nb_of_tries} tries!")
main()
5.7. Conclusion#
It is important to note that the program is developped incrementally. At each step it can be run even if it doesn’t yet perform all tasks. This approach helps avoid accumulating syntax errors, which can be discouraging when correcting them all at once. By developing the program function by function, you can test each part independently and identify bugs more easily.