Stage 9: Tune the Difficulty
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.
a difficulty ramp and playtest notes
the difference between coding a game and designing one
a game that feels fair to someone who isn't you
The big idea
For eight stages you've been coding — adding a mechanic, making it work, shipping it. Today you do the missing 50% of game development: design. You stop asking "does this run?" and start asking "does this feel right to someone who didn't write it?"
Every constant you've added over the course is a tuning knob. Most game studios call this collection the balance sheet — and changing those numbers is most of what makes a game fun.
Your tuning knobs
CANNON_STEP ── how fast the player moves
LASER_SPEED ── how fast shots travel
MAX_LASERS ── how many shots can be on screen
ALIEN_COLUMNS ── how many aliens fit across the fleet
ALIEN_ROWS ── how many rows the fleet has
ALIEN_SPEED ── how fast aliens fall
GAME_SECONDS ── how long a round lasts
SCORE_TO_WIN ── (if you did Stage 8's hard stretch)
Every console game on every shelf has the same kind of list. Players never see it, but designers stare at it for months.
Today we also introduce a difficulty curve — instead of one fixed alien speed for the whole round, aliens get faster as time goes on. A good curve starts easy enough that a new player gets a footing, then climbs steadily so the experienced player stays engaged. Easy at the start, hard at the end is the most reused arc in arcade design.
The other big new idea today isn't code at all. It's playtesting — watching someone else play your game without explaining anything, and treating what you see as data, not criticism. Playtesting is the only way to find out whether the player feels what you intended.
Your game should have scoring, lives, a timer, and a win-or-lose screen.
Build it
Step 1 — Pull your tuning knobs into one block
Open your code and find every constant we've added over the course. Move them together near the top of the file, in this order:
CANNON_STEP = 20
LASER_SPEED = 18
MAX_LASERS = 3
ALIEN_COLUMNS = [-240, -120, 0, 120, 240]
ALIEN_ROWS = [TOP - 40, TOP - 90, TOP - 140]
ALIEN_SPEED = 2
GAME_SECONDS = 60
This block is now your balance sheet. When a playtester says "the cannon is too slow," you know exactly which line to touch. When they say "there are too many aliens," you know which row or column list to tune. Knowing where the dials are is half the design battle.
Step 2 — Make aliens get faster as the round goes on
Right now ALIEN_SPEED = 2 is fixed for the whole game. We'll keep that as the starting speed but compute a current speed that rises with elapsed time.
Inside the game loop, after seconds_left = GAME_SECONDS - frames // 50, add:
Difficulty curve
If `time_played // 20` is `0` at the start and grows later, what happens to `current_alien_speed` over the round?
Check your thinking
It starts at `ALIEN_SPEED` and slowly increases as more time passes.
time_played = GAME_SECONDS - seconds_left
current_alien_speed = ALIEN_SPEED + time_played // 20
time_played is just the inverse of seconds_left — how many seconds the game has been running. current_alien_speed starts at 2 (when time_played is 0) and gains 1 every 20 seconds. So at 0s it's 2, at 20s it's 3, at 40s it's 4, at 60s it's 5. A gentle climb.
Now change the alien movement line to use current_alien_speed instead of ALIEN_SPEED:
alien.sety(alien.ycor() - current_alien_speed)
Run the game and play a full minute. The early game should feel similar to Stage 5; the last 15 seconds should feel noticeably harder.
Step 3 — Playtest with one person
Find someone who hasn't seen your code — a Code Coach, a camper at another table, a parent stopping by. Don't explain anything. Hand them the keyboard, tell them "see what you can do," and watch silently for 60 seconds.
While they play, write down — without correcting them:
- Did they figure out how to move the cannon? In how many seconds?
- Did they figure out how to fire? Did they discover it on their own, or look at you?
- Did they notice the timer and the lives counter?
- Did the game feel too easy, too hard, or fair? You don't need to ask — watch their face.
- Did they understand why they won or lost? Did the ending make sense to them?
The hardest playtest rule: resist the urge to defend your game. Every time you find yourself saying "oh, but it's supposed to —", stop. The fact that they didn't see what you intended is the data. The game has to teach itself.
Step 4 — Change one thing
From your notes, pick exactly one balance knob to adjust. Just one. Resist the urge to fix everything you saw.
Pick from:
- Cannon speed (
CANNON_STEP) — slower if they over-shot, faster if they felt sluggish - Laser cap (
MAX_LASERS) — higher if they wanted to spam, lower if they did and it felt cheap - Fleet width (
ALIEN_COLUMNS) — fewer columns if it felt crowded, more columns if it felt empty - Fleet height (
ALIEN_ROWS) — fewer rows if it felt overwhelming, more rows if players cleared it too easily - Alien starting speed (
ALIEN_SPEED) — slower if they got hit before they could aim - Difficulty ramp rate (
time_played // 20) — change20to30for gentler,15for steeper - Lives (
lives = 3) — lower for more tension, higher for more forgiveness - Round length (
GAME_SECONDS) — shorter for a punchier demo, longer for harder survival
Make the change. Run it. Try to feel the difference as the playtester, not as the coder who wrote it.
Why only one change? Because if you change three things and the game feels better, you don't know which change did it. Designers move one slider at a time on purpose.
File order checkpoint
By the end of Stage 9, most of the file stays where it already was. Only two areas change:
- The tuning constants are grouped together near the top of
main.py - The difficulty math runs near the top of
while game_running:, before the alien movement loop usescurrent_alien_speed
Understand it
What changed today isn't the code — it's your job. Until Stage 8, your job was "make the thing work." From Stage 9 onward, the job is "make the thing feel right." These are different skills. Most coders are stronger at the first; great coders learn the second.
Difficulty curves are everywhere once you start looking for them. Mario's first level teaches jumping over a pit by putting a pit right where you'd run. Tetris speeds up the falling pieces as your score grows. Online matchmaking pairs you with players slightly above your skill so you have to just stretch. The same principle drives all of them: a player who's stuck quits, a player who's bored quits, a player who's pushed just hard enough keeps playing.
The playtest discipline is harder than it looks. Watching someone struggle with something you built feels personal — your instinct is to jump in and explain. But every word of explanation is a place where the game failed to communicate. Quiet observation is the most valuable tool a designer has.
Changing one thing at a time is the same principle as a controlled experiment in science. If you tune MAX_LASERS, change only MAX_LASERS, then playtest again. The next session's reaction tells you whether that one change helped. Bundle three changes together and you've learned nothing about any of them.
Try this
Try this
Three short experiments. Predict before you run, then test your guess.
Predict what would happen if you set ALIEN_SPEED = 6 and ran the game without the difficulty ramp from Step 2. Now actually set it to 6 (and comment out the ramp) and play 30 seconds. How does starting hard feel different from getting hard?
Try time_played // 10 instead of time_played // 20. (Aliens get faster twice as often.) Play a full round. Was the ramp exciting, or did it spike into unplayability? What does that tell you about the slope of a good difficulty curve?
Stage 10 is the parent demo. Imagine a parent watching your game for 30 seconds with no context. Which of the six tuning knobs above matters most for that 30 seconds? Why?
Test your stage
Stuck? Compare carefully
required Stage 9 code
Where it goes: Compare this with your constants block near the top and the first few lines inside `while game_running:`. Only the alien movement line should switch from `ALIEN_SPEED` to `current_alien_speed`.
Use this only after explaining the ramp in your own words.
CANNON_STEP = 20
LASER_SPEED = 18
MAX_LASERS = 3
ALIEN_COLUMNS = [-240, -120, 0, 120, 240]
ALIEN_ROWS = [TOP - 40, TOP - 90, TOP - 140]
ALIEN_SPEED = 2
GAME_SECONDS = 60
while game_running:
frames += 1
seconds_left = GAME_SECONDS - frames // 50
time_played = GAME_SECONDS - seconds_left
current_alien_speed = ALIEN_SPEED + time_played // 20
# Laser movement stays here.
for alien in aliens[:]:
alien.sety(alien.ycor() - current_alien_speed)
# Stage 7 life-loss block stays here.
# Hit detection and guarded respawn stay here.
# Frame delay and screen update stay last.
- Your tuning constants are grouped together near the top of the file.
- Aliens visibly get faster as the timer counts down.
- You watched at least one person play without explaining the controls.
- You wrote down what you saw.
- You changed exactly one balance knob, not more.
- Design check. Re-play after your one change. Does the game feel better, worse, or different but not better? "Different but not better" is the most useful answer — it means you learned what doesn't work.
- From memory. Without looking, write the line that makes aliens speed up as the round goes on. Compare — did you use
current_alien_speed = ALIEN_SPEED + time_played // 20?
If it breaks
- The alien speed jumps in huge steps. Increase the divisor —
time_played // 30makes the ramp gentler. The math here is: everydivisorseconds, the speed bumps by1. - The speed never changes during the round. Add a temporary line after
current_alien_speed = ...liketimer_writer.write(str(current_alien_speed))(just for debugging) — that prints the current speed on screen. If it's stuck at the starting value, the issue is thattime_playedisn't growing. Double-check thatseconds_leftis being recomputed every frame. - The game crashes after a few seconds. Make sure you use
current_alien_speedonly in the alien movement loop, not by accident in the laser loop. They each have their own speed variable. - It feels worse after your one change. That's still data. Revert and try a different single change. Three reverts is normal in real design work.