Skip to main content

Stage 8: Add a Timer and Win State

Course progressStage 8 of 10
~40 min
One game, one Trinket

Keep building in the workspace on the right.

This stage is part of the same Python Arcade project you started in Setup. Type each new code block into the Trinket rail and keep building on the last stage.

Build

a survival timer and a way to win

Learn

how to count seconds with a loop that runs in frames

Ship

a game that can be won, not just lost

The big idea

Up until now, every threat in the game has been a thing on the screen — something the player could see and shoot. Today we add the most dangerous threat of all: an invisible one. A clock.

A countdown timer changes the entire shape of the game. The player isn't just defending anymore — they're defending until time runs out and they get to declare victory. Stage 7 gave the player a way to lose. Today they finally get a way to win.

Stage 7: lives <= 0 → loop ends → GAME OVER
Stage 8: lives <= 0 → loop ends → GAME OVER
time = 0 → loop ends → YOU WIN

We're adding a second exit to the game loop. Same while game_running: line. Same game_running = False to stop. The only new part is which condition flipped the switch — and a result variable that remembers whether the ending was a win or a loss.

The math piece worth noting today: our game loop runs in frames, not seconds. With time.sleep(0.02) per frame, the loop runs about 50 times per second. To count seconds, we count frames and divide by 50. Python has a special operator for this: // is integer division, which throws away any fractional part. 100 // 50 = 2. 149 // 50 = 2. 150 // 50 = 3. Two full seconds becomes two; the half-second remainder gets ignored. That's exactly what a clock display wants.

Before you start

Your game should end when lives reach zero.

Build it

Step 1 — Track time in frames, and make a writer for it

We pick how many seconds the round should last (GAME_SECONDS), and we add a counter (frames) that ticks up once per frame. The new writer Turtle is positioned near the top-center of the screen — between score on the left and lives on the right.

Add these near your other variables:

GAME_SECONDS = 60
frames = 0

Then add the timer writer before the game loop:

timer_writer = turtle.Turtle()
timer_writer.hideturtle()
timer_writer.penup()
timer_writer.color("white")
timer_writer.setposition(-45, TOP - 40)

-45 instead of 0 shifts the writer slightly left of center so the text "Time: 60" looks balanced rather than starting in the middle.

Step 2 — Draw the timer

Same pattern as draw_score() and draw_status() from Stages 6 and 7. Clear, then write. The function takes the number of seconds left as an argument so the same function can show any value.

Write this function:

def draw_timer(seconds_left):
timer_writer.clear()
timer_writer.write("Time: {}".format(seconds_left), font=("Arial", 16, "bold"))

Call it once before the loop so the player sees Time: 60 at the start:

draw_timer(GAME_SECONDS)

Step 3 — Count frames and convert to seconds inside the loop

At the top of your while game_running: loop, add:

Think first

Frame math

If `frames` is `150`, what is `frames // 50`?

Check your thinking

It is `3`. At about 50 frames per second, 150 frames is about 3 seconds.

Your turn

Compute seconds left

Write the frames += 1 line first. Then write the line that computes seconds_left before checking the answer.

Need a hint?

Start with total time, then subtract elapsed seconds: `GAME_SECONDS - frames // 50`.

Stuck? Compare carefully
Answer check
Debug compare only

main.py

Write the frames += 1 line and the seconds_left line yourself first, then reveal to check.

frames += 1
seconds_left = GAME_SECONDS - frames // 50
draw_timer(seconds_left)

if seconds_left <= 0:
game_running = False
break

Read this top-to-bottom: bump the frame counter, compute how many seconds are left using integer division, redraw the timer, and if we're out of time end the game. The break exits the loop immediately, so the game does not keep moving aliens or checking collisions after the win condition has fired.

Run the game. The timer should count down once per second. Hold still until it hits zero — the game should stop.

Step 4 — Tell the player whether they won or lost

Right now, every ending says GAME OVER, even if the player survived the full minute. We need to remember which condition ended the game.

Before the game loop, add:

result = "GAME OVER"

This is the default — assume loss. Then, where we set game_running = False for the timer, set result too:

if seconds_left <= 0:
game_running = False
result = "YOU WIN"
break

Now, at the very end of the program, change the line that writes "GAME OVER" to use the result variable instead:

score_writer.setposition(0, 0)
score_writer.write(result, align="center", font=("Arial", 28, "bold"))

Run the game two ways: lose on purpose (stand still and let aliens reach you), then win on purpose (shoot well and let the clock run out). The ending should match.

File order checkpoint

By the end of Stage 8, the timer and result code should live in these places:

  1. GAME_SECONDS, frames, and result before the writer setup
  2. timer_writer, draw_timer(), and draw_timer(GAME_SECONDS) before the loop
  3. frames += 1, seconds_left = ..., and the win check with break at the top of while game_running:
  4. The final result text after the loop exits

Understand it

The clever piece is frames // 50. We can't ask Python for "how many seconds have passed" — it has no idea, because nothing in the game talks to a real clock. What we can do is count loop passes and divide. Since each pass takes 20 milliseconds (because of time.sleep(0.02)), 50 passes equals one second. Frames per second is the conversion rate.

Integer division (//) is the right choice here, not regular division (/). frames / 50 gives a fraction like 2.7, which the player doesn't want to read. frames // 50 gives 2, which is what a clock shows. This is the first time the course has cared about types of numbers — fractions vs. whole numbers — but it won't be the last in your coding career.

The result variable is a small but important pattern. It's a piece of state that remembers a fact about the game's path: did time run out, or did the player run out of lives? Both flip the same switch (game_running = False), but they tell different stories. As your future games get more complex, "what happened?" becomes its own thing the game has to track, separate from "is it over?"

We deliberately default result = "GAME OVER" and only change it on a win. That's defensive code again, same idea as <= 0 from Stage 7. If we forget to set the result for some new ending we add later, the player at least sees a sensible message instead of a crash or a blank screen.

Try this

Learning beat

Try this

Three short experiments. Predict before you run, then test your guess.

Predict first

Set GAME_SECONDS = 10. Predict before you play: is a ten-second round more intense or too short to be a game? Now play. Where exactly is the line between "thrilling" and "rushed"?

Compare

Change frames // 50 to frames // 100. Predict what will happen to the timer, then run. Now try frames // 25. Why does this number have to match the game's actual framerate?

Connect

Stage 9 ramps difficulty as the game goes on — aliens get faster over time. Look at the lines you just wrote. Which variable tells you how long the game has been running? Stage 9 will use exactly that.

Test your stage

Stuck? Compare carefully
Answer check
Debug compare only

required Stage 8 code

Where it goes: Compare this with your Stage 8 changes: timer variables and `result` before writers, `draw_timer()` before the loop, frame math at the top of `while game_running:`, and the final result writer after the loop.

Use this only after doing the frame math by hand.

GAME_SECONDS = 60
frames = 0
result = "GAME OVER"

timer_writer = turtle.Turtle()
timer_writer.hideturtle()
timer_writer.penup()
timer_writer.color("white")
timer_writer.setposition(-45, TOP - 40)

def draw_timer(seconds_left):
timer_writer.clear()
timer_writer.write("Time: {}".format(seconds_left), font=("Arial", 16, "bold"))

draw_timer(GAME_SECONDS)

while game_running:
frames += 1
seconds_left = GAME_SECONDS - frames // 50
draw_timer(seconds_left)

if seconds_left <= 0:
game_running = False
result = "YOU WIN"
break

# Movement, life loss, hit detection, and guarded respawn stay here.
time.sleep(0.02)
screen.update()

score_writer.setposition(0, 0)
score_writer.write(result, align="center", font=("Arial", 28, "bold"))
screen.update()
turtle.done()
  • The timer appears near the top of the screen.
  • The timer counts down once per second.
  • Surviving until zero shows YOU WIN.
  • Losing all lives still shows GAME OVER.
  • The timer doesn't flicker or overlap itself.
  • Design check. Play a full round. Did the timer pressure change your strategy? Did you take more risks near the end?
  • From memory. Without scrolling up, write the line that turns the frame counter into seconds left. Compare — did you use GAME_SECONDS - frames // 50?

If it breaks

  • The timer counts too fast. Your game's framerate is faster than 50. Increase the divisor: try frames // 60 or frames // 100. The right number is whatever matches your real loop speed.
  • The timer counts too slowly (or doesn't reach zero). Lower the divisor — try frames // 30. Or shorten GAME_SECONDS while you tune.
  • Every ending says GAME OVER, even after a clean survival. The line result = "YOU WIN" isn't running. Check that it sits inside the if seconds_left <= 0: block, next to game_running = False.
  • Every ending says YOU WIN, even after losing all lives. You probably set result = "YOU WIN" before the loop and never reset it on a loss. The default should be "GAME OVER"; only winning should override it.
  • The timer overlaps the score or lives text. The position (-45, TOP - 40) is between them — but if your score writer extends past -45, they'll collide. Move one of the writers further out.