Skip to main content

Stage 9: Tune the Difficulty

Course progressStage 9 of 10
~35 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 difficulty ramp and playtest notes

Learn

the difference between coding a game and designing one

Ship

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.

Before you start

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:

Think first

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) — change 20 to 30 for gentler, 15 for 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:

  1. The tuning constants are grouped together near the top of main.py
  2. The difficulty math runs near the top of while game_running:, before the alien movement loop uses current_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

Learning beat

Try this

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

Predict first

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?

Compare

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?

Connect

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
Answer check
Debug compare only

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 // 30 makes the ramp gentler. The math here is: every divisor seconds, the speed bumps by 1.
  • The speed never changes during the round. Add a temporary line after current_alien_speed = ... like timer_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 that time_played isn't growing. Double-check that seconds_left is being recomputed every frame.
  • The game crashes after a few seconds. Make sure you use current_alien_speed only 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.