Skip to main content

Stage 2: Move the Cannon

Course progressStage 2 of 10
~35 min
One game, one Trinket

Keep building in your saved Python Arcade project.

This stage is part of the same game you started in Setup. Do the work in your own remixed Trinket — it’s the workspace on the right of this page.

Build

keyboard controls for the cannon

Learn

how a game reacts to the player without checking constantly

Ship

a cannon that moves left and right and cannot leave the screen

The big idea

Right now your cannon is a statue. It sits at FLOOR_LEVEL and does nothing. Today we teach the game to react when the player presses a key.

The way most games do this is called event-driven programming. The game does not stop and ask "did anyone press a key yet?" a thousand times per second. Instead, the screen listens in the background, and when something happens — a keypress, a click, a touch — it calls a function you wrote ahead of time. An event is a thing that happened (like a left-arrow press). An event handler is the function the screen calls in response.

Player presses ← → screen.onkeypress → move_left() → cannon moves
(the listener) (your handler)

We also need to stop the cannon from sliding off the edge of the screen. Last stage we named LEFT and RIGHT. Today they earn their keep: we use them as invisible walls.

A function is a named block of code we can call by writing its name. move_left and move_right will be the two functions the screen calls when the player presses an arrow.

Before you start

Your Stage 1 cannon should appear at the bottom of the screen.

Build it

Step 1 — Decide how fast the cannon moves

Speed and edges are tuning knobs. Pulling them into named constants at the top means we can change the feel of the game in one place instead of hunting through code.

Add these near your other constants:

CANNON_STEP = 20
CANNON_MARGIN = 40

CANNON_STEP is how many pixels the cannon shifts on each press. CANNON_MARGIN is how close to LEFT and RIGHT we let the cannon get before the invisible wall stops it.

Step 2 — Write the two movement functions

The screen will call one of these whenever the player presses an arrow. Each function checks the wall before moving — that way the cannon never lives outside the playfield, even for a single frame.

Add these functions above draw_cannon():

def move_left():
new_x = cannon.xcor() - CANNON_STEP
if new_x > LEFT + CANNON_MARGIN:
cannon.setx(new_x)
draw_cannon()

def move_right():
new_x = cannon.xcor() + CANNON_STEP
if new_x < RIGHT - CANNON_MARGIN:
cannon.setx(new_x)
draw_cannon()

Read the structure: figure out where the cannon would go → check the wall → only then commit the move. We always recompute the cannon's drawing with draw_cannon() so we don't leave a smear of old stamps behind.

Step 3 — Tell the screen which keys to listen for

The functions exist now, but nothing calls them yet. We connect them to the keyboard with screen.onkeypress(...). We also add a quick way to quit the game.

Add this before draw_cannon():

screen.onkeypress(move_left, "Left")
screen.onkeypress(move_right, "Right")
screen.onkeypress(turtle.bye, "q")
screen.listen()

screen.listen() is the line that actually starts the listening. Without it, you can press all the keys you want — nothing happens.

Run the game and use the left and right arrow keys. The cannon should slide back and forth and stop politely at each invisible wall.

Understand it

This stage swaps one mental model for another. In Stage 1 we wrote code that ran top-to-bottom once and then sat at turtle.done(). Now the game has a heartbeat made of events — the screen is awake in the background, and your move_left / move_right functions are the things it calls when the player presses an arrow.

Notice we kept the cannon as one Turtle (the same one from Stage 1) and just changed its X position. We did not create a new cannon for every keypress. That is the same "reuse one Turtle for decoration" idea from Stage 1, applied to a moving object — every Turtle has a cost, so we keep things small.

The wall check (if new_x > LEFT + CANNON_MARGIN) is a design choice, not a Python rule. We could have let the cannon slide off the screen. But a player who can lose their own cannon by holding a key feels punished by the controls, not the game. Walls keep the focus on the aliens.

Try this

Learning beat

Try this

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

Predict first

Change CANNON_STEP to 100. Before you run, write down what you expect to happen. Now run it. Did the cannon do what you predicted? What broke that you didn't expect?

Compare

Temporarily remove the if new_x > LEFT + CANNON_MARGIN: line in move_left (and the matching line in move_right). Run the game and hold an arrow. What happens at the edge? Put the if back when you're done — and notice how invisible good wall logic is when it works.

Connect

Stage 3 adds a fire button — the player presses space to shoot a laser. You will use screen.onkeypress(...) again. Look at the three lines you wrote in Step 3. Which one tells you the pattern you'll reuse?

Test your stage

  • Left arrow moves the cannon left.
  • Right arrow moves the cannon right.
  • The cannon redraws cleanly (no smear of old stamps).
  • The cannon stops short of the screen edges.
  • Pressing q closes the game.
  • Design check. Hold an arrow for a few seconds. Does the speed feel like the player is moving, or like they're chasing the cannon?

If it breaks

  • Nothing happens when I press keys. The screen.listen() line is missing or is in the wrong place. It needs to run after you call screen.onkeypress(...) for each key, and before turtle.done().
  • The cannon smears across the screen. The draw_cannon() function probably forgot its cannon.clear() first line. Without it, every move stamps a new cannon on top of the old ones.
  • Left arrow moves it right (or vice versa). Check the + and - signs in move_left and move_right. move_left should subtract from X; move_right should add. Coordinates: left is negative, right is positive.
  • The cannon slides right off the edge. Re-read the if line in both functions. move_left checks new_x > LEFT + CANNON_MARGIN (we want X to stay bigger than the left wall). move_right checks new_x < RIGHT - CANNON_MARGIN (we want X to stay smaller than the right wall).