Project: a word-guessing game
Plan a game with a flowchart, then build it from tested functions and one input loop.
Create a free account Try part of it here
Why it matters
Every lesson so far taught one idea. Real programs combine many, and the hard part is deciding how to split the work. This project shows a split you'll use again and again, in games, command-line tools and agents alike: the rules live in small functions that return values, and one short loop handles talking to the person. The rules can then be checked without anyone typing.
What you'll be able to do
- Plan a whole program as a flowchart before writing code
- Split a game into small functions that return values, plus one short loop that talks to the player
- Build in checkpoints, checking each piece before adding the next
- Test a game that asks questions by replaying it with chosen answers
What it covers
- The brief
- Checkpoint 1: show the board
- Checkpoint 2: apply one guess
- Has the player won?
- Checkpoint 3: the loop that talks to the player
- Decisions behind the code
- Testing a game that asks questions
- Worked example: picking the word
- Go further
The brief
You'll build Guess the gear, a word game for the shop's website. The computer picks a piece of camping gear, like lantern, and shows one blank per letter: _ _ _ _ _ _ _. The player guesses one letter at a time:
- a right letter is revealed everywhere it appears;
- a wrong letter costs one of six lives;
- guessing the same letter twice costs nothing, and capitals count as small letters;
- the player wins when every letter is showing, and loses at zero lives.
That's the whole game, and it already uses lists, strings, loops, functions and input(). Before writing any code, it pays to draw the plan.
Each box becomes a function or a line of the loop. Writing the plan first makes the code's shape obvious.
Checkpoint 1: show the board
Store the guessed letters in a list. The board shows each letter of the word if it has been guessed, and an underscore if it hasn't. A comprehension builds the pieces, and " ".join puts spaces between them:
def masked(word, guessed): return " ".join(letter if letter in guessed else "_" for letter in word) print(masked("lantern", [])) print(masked("lantern", ["n", "a"])) print(masked("lantern", ["l", "a", "n", "t", "e", "r"]))
Notice what masked doesn't do: it doesn't print, and it doesn't ask anything. It takes a word and the guesses, and returns text. That makes it easy to check: you know exactly what it should give for any input.
def masked(word, guessed): return " ".join(letter if letter in guessed else "_" for letter in word) print(masked("tent", ["t"]))
Show the answer
t _ _ t
Both t's are revealed, because the comprehension checks every letter of the word against the guesses.
Checkpoint 2: apply one guess
A guess changes two things: the list of guessed letters, and maybe the lives. A function can hand back several values at once as a tuple, written with commas:
def apply_guess(word, guessed, lives, letter): letter = letter.lower() if letter in guessed: return guessed, lives, f"You already tried {letter}." guessed = guessed + [letter] if letter in word: return guessed, lives, f"Yes, {letter} is in the word." return guessed, lives - 1, f"No {letter}. {lives - 1} lives left." guessed, lives, message = apply_guess("lantern", [], 6, "N") print(guessed, lives, message) guessed, lives, message = apply_guess("lantern", guessed, lives, "z") print(guessed, lives, message)
The call guessed, lives, message = apply_guess(...) unpacks the three values into three names. guessed + [letter] builds a new list instead of changing the old one, so the caller's list is never changed by surprise. All three polish rules live here: lower-casing, the free repeat, and the life lost only for a wrong new letter.
Has the player won?
Write is_won(word, guessed): True when every letter of the word is in guessed. Then is_lost(lives): True at 0 lives or fewer.
def is_won(word, guessed): pass def is_lost(lives): pass
Checkpoint 3: the loop that talks to the player
Now the only part that uses input(). It's short, because the functions do the thinking:
def masked(word, guessed): return " ".join(letter if letter in guessed else "_" for letter in word) def apply_guess(word, guessed, lives, letter): letter = letter.lower() if letter in guessed: return guessed, lives, f"You already tried {letter}." guessed = guessed + [letter] if letter in word: return guessed, lives, f"Yes, {letter} is in the word." return guessed, lives - 1, f"No {letter}. {lives - 1} lives left." def play(word, lives=6): guessed = [] while True: print(masked(word, guessed)) letter = input("Letter: ") if len(letter) != 1 or not letter.isalpha(): print("Type one letter.") continue guessed, lives, message = apply_guess(word, guessed, lives, letter) print(message) if all(c in guessed for c in word): print("You win! It was", word) break if lives <= 0: print("Out of lives. It was", word) break play("lantern")
Play it, and try a number, a capital and a repeat. The continue skips back to the top for anything that isn't a single letter, so bad input never reaches apply_guess.
ab. What happens?Show the answer
It prints "Type one letter." and asks again, and no life is lost
len(letter) != 1 is true, so the loop prints the message and continue jumps back to the top before apply_guess runs.
Decisions behind the code
Small choices shape a program as much as big ones. Here are the ones this game makes, and why.
A list for the guesses. A list keeps the order the player guessed in, which you could show as "tried so far: n, q, a". A set would make letter in guessed faster, but with at most 26 letters that speed doesn't matter, and readability does. A plain string, like "nqa", also works, but a list says "a collection of separate guesses" more clearly.
New values instead of changes. apply_guess builds a new list and returns it. The caller then decides to keep it. If the function quietly changed the list it was given, a test that reused one list for several checks would see guesses leak from one check into the next.
Messages as return values. The function returns its message rather than printing it. The loop prints it here, but a web version of the game could show the same message on a page, and a test can check its wording.
One place for the settings. The six lives arrive as a default parameter, lives=6, and the words as one list near the top of the file. Making the game easier or harder is then a change in one obvious spot, not a hunt through the code.
Testing a game that asks questions
You could test by playing over and over, but that's slow and easy to get wrong. Because the rules are in functions, most checks don't need a player at all:
assert masked("tent", ["t"]) == "t _ _ t" guessed, lives, message = apply_guess("tent", ["t"], 6, "T") assert lives == 6 and "already" in message
For the loop itself, the academy's checks can run your whole program and type a chosen list of answers for you. Three short games cover it: one you win, one you lose, and one with a repeat, a capital and a stray digit. That's how the exercise below is checked. Professional teams test interactive programs the same way: they feed in prepared answers and compare what comes out.
Worked example: picking the word
The game should pick from a list of gear at random, but a test needs to know the word. The answer is to pass the random generator in.
import random GEAR = ["lantern", "carabiner", "headlamp", "bivvy", "compass", "stove"] def pick_word(rng): return rng.choice(GEAR) print(pick_word(random.Random(1)), pick_word(random.Random(1))) print(pick_word(random.Random(7)))
random.Random(1) is a separate random number generator started from the seed 1. The same seed always gives the same choices, so both calls on the first line pick the same word. In the real game you call pick_word(random.Random()), which starts from a different seed each time. In a test you pass a fixed seed and know exactly which word comes up. Handing a function what it depends on, instead of letting it reach out for it, is what makes code testable.
Go further
The game is finished, but it's a good base to build on. Some ideas, in rough order of difficulty:
- Show the lives as a row of lit and unlit lamps,
● ● ● ● ○ ○. - Let the player choose a category: gear, trail names or knots.
- Offer a hint that reveals one letter but costs a life.
- Keep a score across several rounds, and report the best one at the end.
Each idea is a new function, or a small change to one, and the loop barely moves. That's the real lesson of the project: a good split makes the next change easy.
Practice what you just read
- 5 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Why keep input() out of masked and apply_guess?