Project 2 — Number Guessing Game
Guess the secret number with too-high/too-low hints. Add difficulty levels (attempts limit) and a high-score leaderboard.
What you'll learn
- 1Generate a secret number with random.randint
- 2Count attempts and enforce a limit by difficulty
- 3Give meaningful feedback on each guess
- 4Track and display high scores
Project Overview
- Three difficulty levels: Easy (1–50, 10 tries), Normal (1–100, 7 tries), Hard (1–200, 5 tries)
- Hints: Too high / Too low / Very close (within 5)
- Score = remaining attempts × difficulty multiplier
- Keep a top-5 leaderboard for the session
Sample Session
Difficulty (1=Easy 2=Normal 3=Hard): 2
Guess the number (1–100), 7 attempts.
Guess: 50 → Too low
Guess: 75 → Too high
Guess: 62 → Very close!
Guess: 63 → Correct! (4 attempts, score=30)
--- Leaderboard ---
1. Alice 30
2. Bob 24Design — Where State Lives
Two ideas carry this game. First, keep settings like difficulty in one place (a dict) instead of branching with if/elif. Second, wrap a single round in its own function so the main loop only orchestrates: play, show the board, ask to play again. That keeps the code easy to extend with scores, stats, or tests later.
- LEVELS dict — maps each difficulty to (max_number, tries, multiplier). Adding a level never adds a branch.
- play(leaderboard) — generates the secret, gives hints, counts attempts, and records the score on a win.
- show_board(lb) — sorts and prints the top 5; sorting happens at display time, not on every append.
- the main loop — only decides: play a round, show the board, play again? It doesn't know the details (separation of concerns).
Have functions return values (a score, a result) instead of only printing — it makes adding a leaderboard or automated tests far easier.
Reading the Key Code
1) Difficulty as a dict — data, not branches
Branching difficulty with if/elif grows every time you add a level. Pack (max, tries, multiplier) into a dict and pull it out in one line.
LEVELS = {1: (50, 10, 1), 2: (100, 7, 2), 3: (200, 5, 3)}
hi, tries, mult = LEVELS.get(d, LEVELS[2]) # default to Normal
secret = random.randint(1, hi) # both ends inclusive2) Attempt loop with a 'very close' hint
range(1, tries + 1) numbers the attempts 1..tries. Compare the guess to the secret; when the gap is small (<= 5) give a warmer 'very close' hint before the plain high/low.
for attempt in range(1, tries + 1):
guess = int(input(f"Guess [{tries-attempt+1} left]: "))
if guess < secret:
print(" Too low!" if secret - guess > 5 else " Very close! (low)")
elif guess > secret:
print(" Too high!" if guess - secret > 5 else " Very close! (high)")
else:
score = (tries - attempt) * mult * 10
return score3) Leaderboard — sort on display
Append (name, score) as the game runs, then sort only when you print. Sorting by -score gives descending order; slicing [:5] keeps the top five.
def show_board(lb):
for i, (n, s) in enumerate(sorted(lb, key=lambda x: -x[1])[:5], 1):
print(f" {i}. {n:<15} {s}")int(input()) raises ValueError on non-numeric input. Wrap it in try/except and re-prompt, and decide whether a bad entry should cost an attempt (usually it shouldn't).
Common Mistakes (FAQ)
Q. Does random.randint(1, 100) include 100?
Yes — both ends are inclusive (1 and 100 are both possible). Use random.randrange(1, 100) if you want to exclude the upper bound.
Q. The program crashes when I type letters.
int(input()) raises ValueError. Wrap it in try/except ValueError and continue so the game re-prompts instead of crashing.
Q. How is 'Very close' decided?
By the gap: abs(secret - guess) <= 5. Check it inside the too-low / too-high branch so you still tell the player the direction.
Q. My leaderboard isn't sorted.
Sort at display time with sorted(lb, key=lambda x: -x[1])[:5]. Sorting on every append is wasteful and easy to get wrong — keep append cheap and sort once when showing the board.
Wrap-up
The takeaways are the same three you'll reuse in the next projects (calculator, to-do): reduce branches with data (a dict), wrap one unit of work in a function that returns a result, and never trust raw input.
- Keep settings in a dict so new levels need no new branches
- Return values (scores, results) instead of only printing
- Validate int conversions with try/except and decide the attempt-cost policy
💻 Examples
Run these examples and check the output yourself.
import random
LEVELS = {1: (50, 10, 1), 2: (100, 7, 2), 3: (200, 5, 3)}
def play(leaderboard):
d = int(input("Difficulty (1=Easy 2=Normal 3=Hard): "))
hi, tries, mult = LEVELS.get(d, LEVELS[2])
secret = random.randint(1, hi)
print(f"Guess the number (1–{hi}), {tries} attempts.")
for attempt in range(1, tries + 1):
guess = int(input(f"Guess [{tries-attempt+1} left]: "))
if guess < secret:
print(" Too low!" if secret-guess > 5 else " Very close! (low)")
elif guess > secret:
print(" Too high!" if guess-secret > 5 else " Very close! (high)")
else:
score = (tries - attempt) * mult * 10
print(f" Correct! ({attempt} attempts, score={score})")
name = input(" Your name: ")
leaderboard.append((name, score))
return
print(f" Out of attempts! The answer was {secret}.")
def show_board(lb):
print("\n--- Leaderboard ---")
for i, (n, s) in enumerate(sorted(lb, key=lambda x: -x[1])[:5], 1):
print(f" {i}. {n:<15} {s}")
lb = []
while True:
play(lb)
show_board(lb)
if input("\nPlay again? (y/n): ").lower() != "y": break
📝 Exercises
Try them yourself first, then open the solution to compare.
Build the guessing game
Goal: Implement difficulty levels, hints, and a session leaderboard.
- 3 difficulty levels with different ranges and attempt limits
- Too-high/too-low/very-close hints
- Score calculation
- Top-5 leaderboard
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗