Index Ai Academy
Phase 7 · Software engineering · Python projects

Project: a word-guessing game

Plan a game with a flowchart, then build it from tested functions and one input loop.

About 40 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. The brief
  2. Checkpoint 1: show the board
  3. Checkpoint 2: apply one guess
  4. Has the player won?
  5. Checkpoint 3: the loop that talks to the player
  6. Decisions behind the code
  7. Testing a game that asks questions
  8. Worked example: picking the word
  9. 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.

Try it now

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"]))
It runs right here in your browser, with nothing to install.

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.

Quick check
What does this print?
def masked(word, guessed):
    return " ".join(letter if letter in guessed else "_" for letter in word)

print(masked("tent", ["t"]))
  1. _ e n _
  2. t _ _ t
  3. t _ _ _
  4. t 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.

Drill

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
In the lesson, your code is checked as you write it.

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.

Quick check
In the game above, the player types ab. What happens?
  1. It takes a life, because ab isn't in the word
  2. It prints "Type one letter." and asks again, and no life is lost
  3. It reveals any a or b in the word
  4. The game ends with an error
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

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)))
In the lesson you can change this and run it.

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

Create a free account

Try a quiz question

Why keep input() out of masked and apply_guess?

  1. So those functions can be checked with known inputs, without anyone typing
  2. Because input() only works inside while loops
  3. To make the program run faster
  4. Because functions can't call input()

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson