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

random, while, if/elif/else, try/except

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, sorted with a key

D · Window

a Tkinter GUI

classes and event handlers (a preview)

The guessing game loop Pick a secret number, then loop: read a valid guess, count the attempt, compare, and give a hint, until the guess is right or the attempts run out. secret = randint(1, 100) read guess validated int compare with secret lower “Too low!” higher “Too high!” equal You win! 🎉 attempts left? try again
Pick a secret number, then loop: read a valid guess, count the attempt, compare, and give a hint, until the guess is right or the attempts run out.

Plan before you code#

  1. Welcome the player and explain the rules.

  2. Pick a secret number: random.randint(1, 100) (both ends included).

  3. Ask for a guess, and keep asking until it’s a valid whole number.

  4. Compare: too low, too high, or correct.

  5. Repeat until correct (or out of attempts).

  6. 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:

  1. A calculator, then a calculator game (solve random sums against the clock)

  2. A command-line to-do list (saved to a file)

  3. Hangman

  4. A Caesar cipher: encrypt and decrypt text by shifting letters

  5. A Pomodoro timer

  6. A simple key-value store (like a tiny Redis)

  7. A grocery list

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