Learning Goals
3 minBy the end of this lesson you can:
- Use an
if/elif/elseon a menu choice to set three constants together (low, high, max_tries). - End the game in two ways from one loop — win (correct guess) and lose (tries used up).
- Wrap the difficulty-pick + game loop inside a function so the main program reads like a menu.
Warm-Up
5 minHow many guesses should each difficulty allow? Use the "halving" intuition (binary search). Each guess can rule out half the remaining range, so the number of guesses needed is roughly:
range size guesses needed 1-50 ~6 1-100 ~7 1-200 ~8
If we give the player exactly the binary-search count, only the most perfect play wins. If we give them generous wiggle room, the game is trivially easy. The choice is a game-design call:
- Easy: 1-50, generous 10 tries (4 extra) — beginners.
- Medium: 1-100, 7 tries (zero extra) — solid players.
- Hard: 1-200, 8 tries (zero extra) — masochists.
Game tuning is what separates "works" from "fun". The numbers above aren't in Python yet — they're design choices, made on paper, before any code. Build the habit now.
New Concept · Menus and Two-Exit Loops
14 minOne if, three constants
level = input("Pick (easy / medium / hard): ").strip().lower() if level == "easy": low, high, max_tries = 1, 50, 10 elif level == "medium": low, high, max_tries = 1, 100, 7 elif level == "hard": low, high, max_tries = 1, 200, 8 else: print("Defaulting to medium.") low, high, max_tries = 1, 100, 7
One menu pick, three values set together. The notation low, high, max_tries = 1, 50, 10 is tuple unpacking — assign three variables in one line. We'll meet it properly in Level 2 (PY-L2-04), but it's natural to use here.
Two-exit loop
The game ends either when the guess is right, or when tries reaches max_tries. Same shape as Hangman's win/lose loop from PY-L1-36:
while tries < max_tries: # ...ask for guess, validate... tries = tries + 1 if guess == secret: print("Won in", tries, "tries!") break elif guess < secret: print("higher") else: print("lower") # after the loop if tries == max_tries and guess != secret: print("Out of tries. The number was", secret)
The while tries < max_tries condition does the "ran out" case automatically. The break inside handles the "won" case. After the loop, one if tells the two endings apart — exactly like Hangman.
Wrap it in a function
The game function takes the level's constants and runs the round. That hides all the messy logic and lets the main program read like a menu:
def play_round(low, high, max_tries): # ... random pick, loop, win/lose ... return True if won else False # main print("Welcome to Number Guesser Deluxe!") while True: level = input("Easy / Medium / Hard / Quit? ").strip().lower() if level == "quit": break low, high, tries = pick_level(level) # imagined helper play_round(low, high, tries)
Today we'll keep the function fairly thin — pass in the constants, run one round, print the result. Tomorrow we'll extract the level-picker too.
When several things change together (range size, try cap, difficulty label), set them all in one if branch. Keeps related values in one place — anyone reading the menu sees the whole rule at once.
Worked Example · Number Guesser v2
14 minThe full file
Update guesser.py:
Code
# guesser.py — Number Guesser v2 (difficulty levels) import random def pick_difficulty(): print() print("Difficulty:") print(" easy — 1-50, 10 tries") print(" medium — 1-100, 7 tries") print(" hard — 1-200, 8 tries") while True: choice = input("Pick: ").strip().lower() if choice == "easy": return 1, 50, 10 elif choice == "medium": return 1, 100, 7 elif choice == "hard": return 1, 200, 8 else: print("Type easy, medium, or hard.") def play_round(low, high, max_tries): secret = random.randint(low, high) tries = 0 print() print("I picked a number between", low, "and", high, ". You have", max_tries, "tries.") while tries < max_tries: raw = input("Guess: ").strip() if not raw.isdigit(): print("Whole numbers only.") continue guess = int(raw) if guess < low or guess > high: print("Out of range.") continue tries = tries + 1 remaining = max_tries - tries if guess < secret: print("⬆️ Higher.", "Tries left:", remaining) elif guess > secret: print("⬇️ Lower.", "Tries left:", remaining) else: print("🎉 Correct! Got it in", tries, "out of", max_tries, "tries.") return True print() print("💀 Out of tries. The number was", secret, ".") return False # main program print("=" * 40) print(" Number Guesser — v2") print("=" * 40) while True: low, high, cap = pick_difficulty() won = play_round(low, high, cap) again = input("Play again? (y/n) ").strip().lower() if again != "y": print("Goodbye.") break
Sample run · a hard-difficulty win
========================================
Number Guesser — v2
========================================
Difficulty:
easy — 1-50, 10 tries
medium — 1-100, 7 tries
hard — 1-200, 8 tries
Pick: hard
I picked a number between 1 and 200 . You have 8 tries.
Guess: 100
⬆️ Higher. Tries left: 7
Guess: 150
⬇️ Lower. Tries left: 6
Guess: 125
⬆️ Higher. Tries left: 5
Guess: 137
⬇️ Lower. Tries left: 4
Guess: 130
⬆️ Higher. Tries left: 3
Guess: 134
⬇️ Lower. Tries left: 2
Guess: 132
🎉 Correct! Got it in 7 out of 8 tries.
Play again? (y/n) n
Goodbye.Sample run · running out
Pick: medium I picked a number between 1 and 100 . You have 7 tries. Guess: 50 ... (random unlucky game) ... Guess: 41 ⬆️ Higher. Tries left: 0 💀 Out of tries. The number was 47 .
Three things to notice
pick_difficultyuses a sub-loop. If the player types something invalid, it keeps re-asking. The function only returns when it has a valid difficulty.play_roundreturnsTrue/False. The main program doesn't use the result today, but it sets us up for Part 3 — the win counter and hi-score table will need to know whether the round was a win.remaining = max_tries - triesis computed every iteration so we can tell the player how much longer they've got. Tiny UX win, two extra lines of code.
One does menu choosing, one does game playing. Tomorrow we'll add a third — save_score. Three small focused functions are easier to reason about than one giant play_game that does everything.
Try It Yourself
13 minThree tasks. Each adds one piece — menu, max-tries cap, lose-condition.
Add a difficulty prompt to your Part 1 file. Use if/elif/else to set low, high, and max_tries for three levels. Print the three values right after so you can see they were set.
Hint
level = input("easy / medium / hard? ").strip().lower() if level == "easy": low, high, max_tries = 1, 50, 10 elif level == "medium": low, high, max_tries = 1, 100, 7 elif level == "hard": low, high, max_tries = 1, 200, 8 else: print("Going with medium.") low, high, max_tries = 1, 100, 7 print("Settings:", low, high, max_tries)
Type each level once. The print should show 1, 50, 10 then 1, 100, 7 then 1, 200, 8. Confirm before adding the loop.
Change your while True loop into while tries < max_tries. Move the win break inside the "correct guess" branch. Don't add a lose-message yet — just verify that the loop ends automatically when the cap is hit.
Hint
tries = 0 while tries < max_tries: # validation as before tries = tries + 1 # comparisons as before if guess == secret: print("Correct in", tries, "!") break
Test by picking hard (8 tries) and deliberately guessing wrong each time. After your 8th wrong guess the loop should exit cleanly without crashing. We'll add the lose-message in task 3.
After the while loop, add an if that checks whether the player won. If they didn't, print Out of tries! The number was <secret>.
Hint
# after the while loop if guess != secret: print("Out of tries! The number was", secret)
If you used break inside the correct branch, guess still equals secret when you escaped — so the if is False and the lose message doesn't print. Without break, the loop ends naturally when tries == max_tries and guess is the last (wrong) guess. Either way the check works.
Mini-Challenge · Ali's One-Try Easy Mode
8 minAli's game lets you guess only once on every difficulty. Find the bug.
# ali_guesser.py — buggy
import random
level = input("easy / hard? ").strip().lower()
if level == "easy":
low, high, max_tries = 1, 50, 10
elif level == "hard":
low, high, max_tries = 1, 200, 8
secret = random.randint(low, high)
tries = 1 # bug
while tries < max_tries:
guess = int(input("Guess: "))
if guess == secret:
print("Win!")
break
tries = tries + 1- Bug.
tries = 1instead oftries = 0. The counter starts at 1, so on the first attempt the loop runs but tries jumps to 2, then 3... when the player's on attempt 9 of 10 on easy,triesis already 10 and the loop exits early. Effectively they get fewer tries than promised. - Stretch bug. Same crash risk on bad input as Hana's —
int(input(...))withoutisdigit.
Show the fix
tries = 0 # start at zero while tries < max_tries: raw = input("Guess: ").strip() if not raw.isdigit(): print("Numbers only.") continue guess = int(raw) tries = tries + 1 if guess == secret: print("Won in", tries, "tries!") break
Counter off-by-one is the most common bug in any cap-based loop. Decide once and for all: tries is "how many valid guesses have happened so far". Before any guess: 0. After the first: 1. The loop condition tries < max_tries then reads as "keep going while we haven't used them all". Easy.
Recap
3 minDifficulty levels are an if/elif/else on a menu choice that sets three constants together — range low, range high, and max tries. The game becomes a two-exit loop: win on correct (break), lose on tries-cap (loop ends naturally), with one if after the loop to decide which message to print. We wrapped the round in a play_round function and the menu in pick_difficulty, and the main program reads like a menu — pick, play, ask again, repeat. Next lesson — Part 3 — we add a hi-score table that remembers the player's best three runs.
Vocabulary Card
- difficulty level
- A named bundle of game constants — range and max-tries together. Changing the name changes all three.
- tuple unpacking (preview)
a, b, c = 1, 2, 3— assign three variables in one statement. Proper deep-dive in Level 2 (PY-L2-04).- two-exit loop
- A loop that ends in one of two ways: condition becomes false, or
breakfires. After the loop, oneifdecides which message to print. play_round- The conventional name for a function that runs exactly one game. Returns whether the round was a win.
- game tuning
- Choosing constants (range size, try cap, damage range) to make a game feel fun. A design skill, not a coding one.
Homework
4 minTwo upgrades for tonight.
- Add a fourth difficulty impossible — 1-500, 9 tries. Play it once. (You should usually lose. That's the point.)
- Refactor your level-picker to return a list instead of three values:
return [low, high, max_tries]. Then unpack in the main program:settings = pick_difficulty(); low, high, cap = settings[0], settings[1], settings[2]. (Tuple unpacking in Level 2 will let you skip the index dance.) - Bonus: have
play_roundalso returntriesso the main program can announce the score:print("That took you", tries, "guesses."). Hint:return True, triesreturns two values; capture aswon, used = play_round(...).
Bring guesser.py next class. PY-L1-47 adds the hi-score table — your wins get saved, and we print a leaderboard.
Sample · guesser.py with fourth level & two-value return
# guesser.py — v2.1 def pick_difficulty(): print("\nDifficulty: easy / medium / hard / impossible") while True: c = input("Pick: ").strip().lower() if c == "easy": return [1, 50, 10] if c == "medium": return [1, 100, 7] if c == "hard": return [1, 200, 8] if c == "impossible": return [1, 500, 9] print("Try easy / medium / hard / impossible.") def play_round(low, high, cap): secret = random.randint(low, high) tries = 0 while tries < cap: raw = input("Guess: ").strip() if not raw.isdigit(): continue guess = int(raw) if guess < low or guess > high: continue tries = tries + 1 if guess == secret: print("Won in", tries, "!") return True, tries elif guess < secret: print("higher") else: print("lower") print("Out of tries. Secret was", secret) return False, tries # main import random while True: s = pick_difficulty() low, high, cap = s[0], s[1], s[2] won, used = play_round(low, high, cap) if won: print("That took you", used, "guesses.") if input("Again? ").strip().lower() != "y": break
Two ideas worth pinning down. (1) return True, tries returns two values at once — Python wraps them in a tuple and lets you unpack with won, used = .... The capstone uses this pattern a lot. (2) Indexing the returned list as s[0], s[1], s[2] works, but feels clunky — that's exactly the problem tuple unpacking in PY-L2-04 solves elegantly.