Session 12 · Project: the number guessing game#
📖 Based on the blog posts Build a game where the user guesses a randomly generated number and Python Mini Projects Ideas.
The big idea#
Time to put everything together: variables, input(), conditions, loops, functions, lists and
files. You’ll build the same game four times, each version adding one feature:
Version |
Adds |
You practise |
|---|---|---|
A · Simple |
guess 1–100, too high / too low |
|
B · Levels |
difficulty, limited attempts, a hint, play again |
functions with several return values |
C · Leaderboard |
top-10 scores saved to a file |
files, lists of tuples, |
D · Window |
a Tkinter GUI |
classes and event handlers (a preview) |
Plan before you code#
Welcome the player and explain the rules.
Pick a secret number:
random.randint(1, 100)(both ends included).Ask for a guess, and keep asking until it’s a valid whole number.
Compare: too low, too high, or correct.
Repeat until correct (or out of attempts).
Finish: congratulate and show the number of attempts.
Version A · the simple game#
The heart of the game is one while True loop that only ends with break when the guess is right:
while True:
attempts += 1
guess = get_user_guess()
if guess < secret_number:
print("Too low! Try again.")
elif guess > secret_number:
print("Too high! Try again.")
else:
print(f"Congratulations! You guessed the number in {attempts} attempts.")
break
Validating input#
int(input()) crashes if the player types “abc”. Catch the error and ask again in a loop:
def get_user_guess():
# Keep asking until the input is a whole number
while True:
try:
return int(input("Enter your guess: "))
except ValueError:
print("Invalid input. Please enter a valid integer.")
Note
The blog’s version called get_user_guess() again inside except but threw away the result, so a
bad input followed by a good one crashed with UnboundLocalError. A loop is simpler and can’t run
out of stack.
A real session (secret number 18):
Welcome to the Number Guessing Game!
I'm thinking of a number between 1 and 100.
Enter your guess: 50
Too high! Try again.
Enter your guess: abc
Invalid input. Please enter a valid integer.
Enter your guess: 10
Too low! Try again.
Enter your guess: 18
Congratulations! You guessed the number in 3 attempts.
Version B · levels, attempts, hints, play again#
Level |
Range |
Attempts |
|---|---|---|
1 · Easy |
1–50 |
10 |
2 · Medium |
1–100 |
7 |
3 · Hard |
1–1000 |
5 |
A function returns all three settings at once (as a tuple, Session 8):
def get_min_max_total_attempts(difficulty):
if difficulty == 1:
min_value, max_value, max_attempts = 1, 50, 10
elif difficulty == 2:
min_value, max_value, max_attempts = 1, 100, 7
else:
min_value, max_value, max_attempts = 1, 1000, 5
return min_value, max_value, max_attempts
One input helper checks both “is it a number?” and “is it in range?”, and is reused for the menu and the guesses:
def get_valid_number(prompt, min_value, max_value):
while True:
try:
num = int(input(prompt))
if min_value <= num <= max_value:
return num
print(f"Please enter a number between {min_value} and {max_value}.")
except ValueError:
print("Invalid input. Please enter a valid integer.")
After half the attempts, the player gets a hint, using % from Session 4:
if attempts == max_attempts // 2 and not hints_given:
hint = "even" if secret_number % 2 == 0 else "odd"
print(f"Hint: The number is {hint}.")
hints_given = True
Tip
Why 7 attempts for 1–100? With the “halve the range” strategy (always guess the middle), 7 guesses are always enough for 100 numbers, because 2⁷ = 128. Hard mode (1–1000 in 5) needs luck: the strategy needs 10 guesses.
Version C · a leaderboard in a file#
Scores are saved as lines of name,attempts in leaderboard.txt, so they survive between games:
def load_leaderboard():
if os.path.exists(LEADERBOARD_FILE):
with open(LEADERBOARD_FILE, "r") as file:
leaderboard = [line.strip().split(",") for line in file.readlines()]
leaderboard = [(name, int(score)) for name, score in leaderboard]
return sorted(leaderboard, key=lambda x: x[1])
return []
def update_leaderboard(player_name, attempts):
leaderboard = load_leaderboard()
leaderboard.append((player_name, attempts))
leaderboard = sorted(leaderboard, key=lambda x: x[1])[:10] # keep the top 10
save_leaderboard(leaderboard)
sorted(..., key=lambda x: x[1]) sorts the (name, attempts) tuples by attempts: fewer is better.
Version D · a window with Tkinter#
The last version wraps the same logic in a window: radio buttons for the level, a text box for the
guess, and pop-up messages. The game state (secret number, attempts) lives on a class instance
instead of local variables, because button clicks call different methods. You don’t need classes
yet; read it as a preview of where Python goes next. Run it with python d_tkinter.py (Tkinter
comes with Python on Windows and macOS; on Ubuntu install python3-tk).
Ideas to extend the game#
Scoring: points =
max_attempts - attempts + 1, multiplied by the level.Warmer / colder: compare the new guess’s distance with the previous one.
Statistics: games played, win rate, average attempts, saved in a file.
Reverse game: you think of a number and the computer guesses it with binary search.
More mini projects#
From Python Mini Projects Ideas:
A calculator, then a calculator game (solve random sums against the clock)
A command-line to-do list (saved to a file)
Hangman
A Caesar cipher: encrypt and decrypt text by shifting letters
A Pomodoro timer
A simple key-value store (like a tiny Redis)
A grocery list
An alarm clock
Each one uses only what’s in this course.
Hands-on exercises#
Exercise 1 · Limit the attempts. Change version A so the player has only 7 attempts, and reveal the number when they run out.
Solution
MAX_ATTEMPTS = 7
for attempt in range(1, MAX_ATTEMPTS + 1):
guess = get_user_guess()
if guess < secret_number:
print("Too low!")
elif guess > secret_number:
print("Too high!")
else:
print(f"Correct in {attempt} attempts!")
break
else: # runs only if the loop didn't break
print(f"Out of attempts. The number was {secret_number}.")
A for … else is perfect here: the else runs only when the loop finished without break.
Exercise 2 · Warmer or colder. After the second guess, also print “warmer” if the guess is closer to the secret than the previous one, otherwise “colder”.
Solution
def warmer_or_colder(secret, previous, current):
if previous is None:
return ""
return "warmer" if abs(secret - current) < abs(secret - previous) else "colder"
print(warmer_or_colder(18, None, 50)) # (nothing on the first guess)
print(warmer_or_colder(18, 50, 30)) # warmer
print(warmer_or_colder(18, 30, 45)) # colder
Keep previous = None before the loop and set previous = guess at the end of each round.
Exercise 3 · Binary search. Write computer_guesses(secret, low=1, high=100) that guesses by
always picking the middle, and returns how many guesses it needed.
Solution
def computer_guesses(secret, low=1, high=100):
guesses = 0
while True:
guesses += 1
guess = (low + high) // 2
if guess == secret:
return guesses
if guess < secret:
low = guess + 1
else:
high = guess - 1
print(max(computer_guesses(n) for n in range(1, 101))) # 7: never more than 7
This proves the 7-attempt claim from the tip above.
Exercise 4 · A safer leaderboard. In version C, a player named “Ravi, Kumar” breaks the file (an extra comma). Fix it.
Solution
Use the csv module, which quotes commas for you:
import csv
def save_leaderboard(leaderboard, path="leaderboard.csv"):
with open(path, "w", newline="") as f:
csv.writer(f).writerows(leaderboard)
def load_leaderboard(path="leaderboard.csv"):
try:
with open(path, newline="") as f:
return sorted(((name, int(score)) for name, score in csv.reader(f)), key=lambda x: x[1])
except FileNotFoundError:
return []
save_leaderboard([("Ravi, Kumar", 4), ("Priya", 3)])
print(load_leaderboard()) # [('Priya', 3), ('Ravi, Kumar', 4)]
Exercise 5 · Pick a mini project. Build the Caesar cipher: encrypt(text, shift) and
decrypt(text, shift), keeping spaces and punctuation as they are.
Solution
def shift_char(ch, shift):
if ch.isupper():
return chr((ord(ch) - ord("A") + shift) % 26 + ord("A"))
if ch.islower():
return chr((ord(ch) - ord("a") + shift) % 26 + ord("a"))
return ch
def encrypt(text, shift):
return "".join(shift_char(c, shift) for c in text)
def decrypt(text, shift):
return encrypt(text, -shift)
secret = encrypt("Parotta Salna!", 3)
print(secret) # Sdurwwd Vdoqd!
print(decrypt(secret, 3)) # Parotta Salna!
Full source#
a_simple.py
import random
def get_user_guess():
# Keep asking until the input is a whole number
while True:
try:
return int(input("Enter your guess: "))
except ValueError:
print("Invalid input. Please enter a valid integer.")
def get_random_number():
secret_number = random.randint(1, 100)
return secret_number
def number_guessing_game():
"""
A simple number guessing game where the user tries to guess a randomly
generated number between 1 and 100.
"""
# Generate a random number between 1 and 100
secret_number = get_random_number()
attempts = 0
print("Welcome to the Number Guessing Game!")
print("I'm thinking of a number between 1 and 100.")
while True:
attempts += 1
guess = get_user_guess()
# Check if the guess is correct
if guess < secret_number:
print("Too low! Try again.")
elif guess > secret_number:
print("Too high! Try again.")
else:
print(f"Congratulations! You guessed the number in {attempts} attempts.")
break
if __name__ == "__main__":
number_guessing_game()
b_levels.py
import random
def print_menu():
"""
Function to print menu"""
print("Welcome to the Enhanced Number Guessing Game!")
# Select difficulty level
print("Select difficulty level:")
print("1. Easy (Range: 1-50, 10 attempts)")
print("2. Medium (Range: 1-100, 7 attempts)")
print("3. Hard (Range: 1-1000, 5 attempts)")
def get_min_max_total_attempts(difficulty):
# Set parameters based on difficulty level
if difficulty == 1:
min_value, max_value, max_attempts = 1, 50, 10
elif difficulty == 2:
min_value, max_value, max_attempts = 1, 100, 7
else:
min_value, max_value, max_attempts = 1, 1000, 5
return min_value, max_value, max_attempts
def play(secret_number, min_value, max_value, max_attempts):
attempts = 0
hints_given = False
print(f"\nI'm thinking of a number between {min_value} and {max_value}.")
print(f"You have {max_attempts} attempts to guess it.\n")
while attempts < max_attempts:
guess = get_valid_number(f"Attempt {attempts + 1}: Enter your guess: ", min_value, max_value)
attempts += 1
if guess < secret_number:
print("Too low! Try again.")
elif guess > secret_number:
print("Too high! Try again.")
else:
print(f"Congratulations! You guessed the number in {attempts} attempts.")
break
# Provide a hint after half the attempts if the user hasn't guessed correctly
if attempts == max_attempts // 2 and not hints_given:
hint = "even" if secret_number % 2 == 0 else "odd"
print(f"Hint: The number is {hint}.")
hints_given = True
if attempts == max_attempts and guess != secret_number:
print(f"\nSorry, you've used all {max_attempts} attempts. The correct number was {secret_number}.")
def get_valid_number(prompt, min_value, max_value):
"""Helper function to get a valid number within a specified range."""
while True:
try:
num = int(input(prompt))
if min_value <= num <= max_value:
return num
else:
print(f"Please enter a number between {min_value} and {max_value}.")
except ValueError:
print("Invalid input. Please enter a valid integer.")
def play_again():
# Ask the user if they want to play again
play_again = input("\nDo you want to play again? (yes/no): ").strip().lower()
if play_again == 'yes':
number_guessing_game()
else:
print("Thank you for playing! Goodbye.")
def number_guessing_game():
"""
An enhanced number guessing game where the user selects a difficulty level
and attempts to guess a randomly generated number within a range.
"""
print_menu()
difficulty = get_valid_number("Choose a difficulty level (1, 2, or 3): ", 1, 3)
min_value, max_value, max_attempts = get_min_max_total_attempts(difficulty)
secret_number = random.randint(min_value, max_value)
play(secret_number, min_value, max_value, max_attempts)
play_again()
if __name__ == "__main__":
number_guessing_game()
c_leaderboard.py
import os
import random
LEADERBOARD_FILE = "leaderboard.txt"
def load_leaderboard():
"""Load the leaderboard from a file."""
if os.path.exists(LEADERBOARD_FILE):
with open(LEADERBOARD_FILE, "r") as file:
leaderboard = [line.strip().split(",") for line in file.readlines()]
leaderboard = [(name, int(score)) for name, score in leaderboard]
return sorted(leaderboard, key=lambda x: x[1])
return []
def save_leaderboard(leaderboard):
"""Save the leaderboard to a file."""
with open(LEADERBOARD_FILE, "w") as file:
for name, score in leaderboard:
file.write(f"{name},{score}\n")
def display_leaderboard(leaderboard):
"""Display the top scores from the leaderboard."""
print("\n--- Leaderboard ---")
if leaderboard:
for i, (name, score) in enumerate(leaderboard[:10], start=1):
print(f"{i}. {name}: {score} attempts")
else:
print("No scores yet. Be the first to play!")
print("-------------------\n")
def update_leaderboard(player_name, attempts):
"""Update the leaderboard with the player's score."""
leaderboard = load_leaderboard()
leaderboard.append((player_name, attempts))
leaderboard = sorted(leaderboard, key=lambda x: x[1])[:10] # Keep only top 10 scores
save_leaderboard(leaderboard)
def print_menu():
"""
Function to print menu"""
print("Welcome to the Enhanced Number Guessing Game!")
# Select difficulty level
print("Select difficulty level:")
print("1. Easy (Range: 1-50, 10 attempts)")
print("2. Medium (Range: 1-100, 7 attempts)")
print("3. Hard (Range: 1-1000, 5 attempts)")
def get_min_max_total_attempts(difficulty):
# Set parameters based on difficulty level
if difficulty == 1:
min_value, max_value, max_attempts = 1, 50, 10
elif difficulty == 2:
min_value, max_value, max_attempts = 1, 100, 7
else:
min_value, max_value, max_attempts = 1, 1000, 5
return min_value, max_value, max_attempts
def play(secret_number, min_value, max_value, max_attempts):
attempts = 0
hints_given = False
print(f"\nI'm thinking of a number between {min_value} and {max_value}.")
print(f"You have {max_attempts} attempts to guess it.\n")
while attempts < max_attempts:
guess = get_valid_number(f"Attempt {attempts + 1}: Enter your guess: ", min_value, max_value)
attempts += 1
if guess < secret_number:
print("Too low! Try again.")
elif guess > secret_number:
print("Too high! Try again.")
else:
print(f"Congratulations! You guessed the number in {attempts} attempts.")
player_name = input("Enter your name for the leaderboard: ").strip()
update_leaderboard(player_name, attempts)
break
# Provide a hint after half the attempts if the user hasn't guessed correctly
if attempts == max_attempts // 2 and not hints_given:
hint = "even" if secret_number % 2 == 0 else "odd"
print(f"Hint: The number is {hint}.")
hints_given = True
if attempts == max_attempts and guess != secret_number:
print(f"\nSorry, you've used all {max_attempts} attempts. The correct number was {secret_number}.")
def get_valid_number(prompt, min_value, max_value):
"""Helper function to get a valid number within a specified range."""
while True:
try:
num = int(input(prompt))
if min_value <= num <= max_value:
return num
else:
print(f"Please enter a number between {min_value} and {max_value}.")
except ValueError:
print("Invalid input. Please enter a valid integer.")
def play_again():
# Ask the user if they want to play again
play_again = input("\nDo you want to play again? (yes/no): ").strip().lower()
if play_again == 'yes':
number_guessing_game()
else:
print("Thank you for playing! Goodbye.")
def number_guessing_game():
"""
An enhanced number guessing game where the user selects a difficulty level
and attempts to guess a randomly generated number within a range.
"""
display_leaderboard(load_leaderboard())
print_menu()
difficulty = get_valid_number("Choose a difficulty level (1, 2, or 3): ", 1, 3)
min_value, max_value, max_attempts = get_min_max_total_attempts(difficulty)
secret_number = random.randint(min_value, max_value)
play(secret_number, min_value, max_value, max_attempts)
display_leaderboard(load_leaderboard())
play_again()
if __name__ == "__main__":
number_guessing_game()
d_tkinter.py
import tkinter as tk
import tkinter.simpledialog
from tkinter import messagebox
import random
import os
LEADERBOARD_FILE = "leaderboard.txt"
class NumberGuessingGame:
def __init__(self, root):
self.root = root
self.root.title("Number Guessing Game")
self.min_value = 1
self.max_value = 100
self.max_attempts = 10
self.attempts = 0
self.secret_number = 0
self.player_name = ""
self.hints_given = False
self.create_widgets()
def create_widgets(self):
"""Create the main interface widgets."""
self.difficulty_label = tk.Label(self.root, text="Select Difficulty Level:")
self.difficulty_label.pack(pady=10)
self.difficulty_var = tk.IntVar(value=2)
self.difficulty_easy = tk.Radiobutton(self.root, text="Easy (1-50)", variable=self.difficulty_var, value=1, command=self.set_difficulty)
self.difficulty_medium = tk.Radiobutton(self.root, text="Medium (1-100)", variable=self.difficulty_var, value=2, command=self.set_difficulty)
self.difficulty_hard = tk.Radiobutton(self.root, text="Hard (1-1000)", variable=self.difficulty_var, value=3, command=self.set_difficulty)
self.difficulty_easy.pack()
self.difficulty_medium.pack()
self.difficulty_hard.pack()
self.start_button = tk.Button(self.root, text="Start Game", command=self.start_game)
self.start_button.pack(pady=10)
self.guess_label = tk.Label(self.root, text="Enter your guess:")
self.guess_entry = tk.Entry(self.root)
self.submit_button = tk.Button(self.root, text="Submit Guess", command=self.check_guess)
self.attempts_label = tk.Label(self.root, text="Attempts left: N/A")
self.leaderboard_button = tk.Button(self.root, text="Show Leaderboard", command=self.show_leaderboard)
self.leaderboard_button.pack(pady=10)
def set_difficulty(self):
"""Set the game difficulty based on user selection."""
difficulty = self.difficulty_var.get()
if difficulty == 1:
self.min_value, self.max_value, self.max_attempts = 1, 50, 10
elif difficulty == 2:
self.min_value, self.max_value, self.max_attempts = 1, 100, 7
else:
self.min_value, self.max_value, self.max_attempts = 1, 1000, 5
def start_game(self):
"""Start the game by generating a random number and resetting the attempts."""
self.secret_number = random.randint(self.min_value, self.max_value)
self.attempts = 0
self.hints_given = False
self.difficulty_label.pack_forget()
self.difficulty_easy.pack_forget()
self.difficulty_medium.pack_forget()
self.difficulty_hard.pack_forget()
self.start_button.pack_forget()
self.guess_label.pack(pady=10)
self.guess_entry.pack(pady=10)
self.submit_button.pack(pady=10)
self.attempts_label.pack(pady=10)
self.update_attempts_label()
def update_attempts_label(self):
"""Update the label that shows how many attempts are left."""
attempts_left = self.max_attempts - self.attempts
self.attempts_label.config(text=f"Attempts left: {attempts_left}")
def check_guess(self):
"""Check the user's guess and provide feedback."""
try:
guess = int(self.guess_entry.get())
if guess < self.min_value or guess > self.max_value:
messagebox.showerror("Error", f"Please enter a number between {self.min_value} and {self.max_value}.")
return
except ValueError:
messagebox.showerror("Error", "Please enter a valid integer.")
return
self.attempts += 1
self.update_attempts_label()
if guess < self.secret_number:
messagebox.showinfo("Result", "Too low! Try again.")
elif guess > self.secret_number:
messagebox.showinfo("Result", "Too high! Try again.")
else:
messagebox.showinfo("Congratulations!", f"You guessed the number in {self.attempts} attempts.")
self.record_score()
self.reset_game()
return
if self.attempts == self.max_attempts:
messagebox.showinfo("Game Over", f"Sorry, you've used all {self.max_attempts} attempts. The correct number was {self.secret_number}.")
self.reset_game()
if self.attempts == self.max_attempts // 2 and not self.hints_given:
hint = "even" if self.secret_number % 2 == 0 else "odd"
messagebox.showinfo("Hint", f"Hint: The number is {hint}.")
self.hints_given = True
def record_score(self):
"""Record the player's score and update the leaderboard."""
self.player_name = tkinter.simpledialog.askstring("Name", "Enter your name for the leaderboard:")
if not self.player_name:
self.player_name = "Anonymous"
leaderboard = self.load_leaderboard()
leaderboard.append((self.player_name, self.attempts))
leaderboard = sorted(leaderboard, key=lambda x: x[1])[:10] # Keep only top 10 scores
self.save_leaderboard(leaderboard)
def load_leaderboard(self):
"""Load the leaderboard from a file."""
if os.path.exists(LEADERBOARD_FILE):
with open(LEADERBOARD_FILE, "r") as file:
leaderboard = [line.strip().split(",") for line in file.readlines()]
return [(name, int(score)) for name, score in leaderboard]
return []
def save_leaderboard(self, leaderboard):
"""Save the leaderboard to a file."""
with open(LEADERBOARD_FILE, "w") as file:
for name, score in leaderboard:
file.write(f"{name},{score}\n")
def show_leaderboard(self):
"""Display the top scores from the leaderboard."""
leaderboard = self.load_leaderboard()
if leaderboard:
leaderboard_str = "\n".join([f"{i+1}. {name}: {score} attempts" for i, (name, score) in enumerate(leaderboard)])
else:
leaderboard_str = "No scores yet. Be the first to play!"
messagebox.showinfo("Leaderboard", leaderboard_str)
def reset_game(self):
"""Reset the game to the initial state."""
self.guess_label.pack_forget()
self.guess_entry.pack_forget()
self.submit_button.pack_forget()
self.attempts_label.pack_forget()
self.difficulty_label.pack(pady=10)
self.difficulty_easy.pack()
self.difficulty_medium.pack()
self.difficulty_hard.pack()
self.start_button.pack(pady=10)
if __name__ == "__main__":
root = tk.Tk()
app = NumberGuessingGame(root)
root.mainloop()
Downloads:
a_simple.py ·
b_levels.py ·
c_leaderboard.py ·
d_tkinter.py