Stage 3: Fire Lasers
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 list of lasers, a fire button, and a game loop
how a game updates many moving objects every frame
lasers that launch upward from the cannon and disappear at the top
The big idea
Up until now, every object in the game has been created once and lived forever — one screen, one cannon. Lasers are different. They are temporary: a laser is born when the player presses space, climbs up the screen, and disappears when it leaves the playfield. The game might have zero lasers, or one, or three, all at the same time.
To handle "an unknown number of similar things," we use a list. A list in Python is a container that holds many items in order. You can add to it, walk through it, and remove items as the game changes.
lasers = [ laser0 , laser1 , laser2 ]
│ │ │
▼ ▼ ▼
(each one is its own Turtle, climbing the screen)
We also need to introduce the game loop. A game loop is a piece of code that runs over and over — many times per second — and on each pass it updates every moving thing in the game. One pass of the loop is called a frame. Lasers move because every frame, the loop walks the list and nudges each one upward.
The list pattern from this stage is the most reused pattern in the whole course — aliens, hits, and game-over checks all use it. Today is the day to understand it.
Your cannon should move left and right with the arrow keys.
Build it
Step 1 — Set up the laser list and limits
A list starts empty. We also pick a top speed and a cap on how many lasers can be on screen at once. Caps keep the game readable — a screen full of lasers is a screen the player can't see through.
Add these near your other constants:
LASER_SPEED = 18
MAX_LASERS = 3
lasers = []
lasers = [] creates an empty list. By the end of the stage it will fill up and drain as the player shoots.
Step 2 — Write the fire function
Pressing space should create a new laser at the cannon's current position — unless there are already too many. We check the cap first, then build a fresh Turtle and add it to the list.
Build this function above the keyboard setup:
Why a list?
Why does the game need `lasers = []` instead of one variable called `laser`?
Check your thinking
There can be many lasers on screen at the same time. A list lets the game remember all of them.
Write the guard first
Type only the first three lines of fire_laser() before looking at the rest. Explain to a partner what problem those lines prevent.
Need a hint?
The first two lines inside `fire_laser()` should check `len(lasers)` and return early if there are already too many.
Stuck? Compare carefully
main.py
Write the guard and the laser yourself first, then reveal to check.
def fire_laser():
if len(lasers) >= MAX_LASERS:
return
laser = turtle.Turtle()
laser.penup()
laser.color("red")
laser.shape("triangle")
laser.setheading(90)
laser.setposition(cannon.xcor(), FLOOR_LEVEL + 30)
lasers.append(laser)
len(lasers) is how many items the list currently holds. lasers.append(laser) adds the new laser to the end of the list. From this moment on, the game owns that laser through the list.
Step 3 — Bind the space key
Same pattern as Stage 2 — the screen listens, and when it sees the space bar it calls fire_laser.
Wire this with your key bindings:
screen.onkey(fire_laser, "space")
Run the game and press space. You'll see red triangles appear at the cannon — but they will not move yet. That's the next step.
Step 4 — Build the game loop
Now we make the lasers move. We add a while True: loop at the bottom of the program that walks the laser list every frame, nudges each one upward, and removes any that have left the screen.
First, add this at the top of the file with your imports:
import time
Then add this before your key bindings so animation is smooth:
screen.tracer(0)
screen.tracer(0) tells Turtle to stop drawing automatically. We will tell it when to draw, once per frame, with screen.update().
Now replace your final turtle.done() with this loop:
Trace one frame
- The loop visits each laser in a copy of the list.
- Each laser moves upward by
LASER_SPEED. - If a laser passes
TOP, it hides and leaves the list.
while True:
for laser in lasers[:]:
laser.sety(laser.ycor() + LASER_SPEED)
if laser.ycor() > TOP:
laser.hideturtle()
lasers.remove(laser)
time.sleep(0.02)
screen.update()
Run the game. Press space — lasers should now climb the screen, hit the top, and disappear.
File order checkpoint
By the end of Stage 3, your main.py should be ordered like this:
#!/bin/python3, thenimport turtleandimport time- Screen setup and constants, including
LASER_SPEED,MAX_LASERS, andlasers - Cannon setup,
move_left(),move_right(),draw_cannon(), andfire_laser() screen.tracer(0), key bindings,screen.listen(), anddraw_cannon()- The
while True:game loop at the very bottom
Understand it
The piece worth slowing down on is lasers[:]. That little [:] is not decoration — it makes a copy of the list. When we walk through a list with for laser in ... and remove items from the original list at the same time, the loop loses track of where it is and skips items. Walking a copy means we can safely modify the real list mid-loop. This is a real Python gotcha, and you'll see it again in Stage 5.
The screen.tracer(0) / screen.update() pair is a speed choice, not a correctness one. By default, Turtle redraws the screen after every single change — which is slow when you have lots of moving things. Turning off auto-draw and updating manually once per frame is how games keep their framerate up.
time.sleep(0.02) is the other half of that choice. Trinket runs in the browser, so an unlimited while True: loop can run too fast and make the editor feel stuck. A tiny pause gives the browser time to breathe and makes the game speed predictable.
We deliberately picked MAX_LASERS = 3 instead of allowing unlimited shots. A cap makes the game better: it forces the player to aim instead of mash space, and it keeps the screen readable. Game designers call this a constraint that creates skill — the same idea that makes a chess board only 8×8.
Try this
Try this
Three short experiments. Predict before you run, then test your guess.
Change LASER_SPEED to 2. Run the game and fire a few lasers. Does the game still feel like an arcade shooter? Write one sentence about why the speed matters this much.
Comment out screen.tracer(0) (put a # in front of it) and run. Watch what happens to the cannon and the lasers as the screen tries to draw every change. Put it back when you're done.
Stage 4 spawns aliens. Aliens are also temporary game objects that need to live in a list and move every frame. Look at your laser code. What three pieces will the alien code reuse almost word-for-word?
Test your stage
Stuck? Compare carefully
required Stage 3 code
Where it goes: Compare this with your Stage 3 additions: `import time` at the top, laser constants with the other constants, `fire_laser()` before key bindings, and the loop at the bottom replacing `turtle.done()`.
Use this only after tracing your own loop and checking the key binding.
import time
LASER_SPEED = 18
MAX_LASERS = 3
lasers = []
def fire_laser():
if len(lasers) >= MAX_LASERS:
return
laser = turtle.Turtle()
laser.penup()
laser.color("red")
laser.shape("triangle")
laser.setheading(90)
laser.setposition(cannon.xcor(), FLOOR_LEVEL + 30)
lasers.append(laser)
screen.tracer(0)
screen.onkey(fire_laser, "space")
while True:
for laser in lasers[:]:
laser.sety(laser.ycor() + LASER_SPEED)
if laser.ycor() > TOP:
laser.hideturtle()
lasers.remove(laser)
time.sleep(0.02)
screen.update()
- Pressing space creates a laser at the cannon.
- Lasers move upward smoothly.
- Lasers disappear when they reach the top.
- You cannot fill the screen with more than three lasers at once.
- The cannon still moves while lasers are flying.
- Design check. Hold the space bar for two seconds. Does the rate of fire feel right, or does the cap make the game feel slow?
- From memory. Close this page and rewrite the two lines that stop a fourth laser from being created. Reopen and compare — did you remember the
>=guard and the earlyreturn?
If it breaks
- The game freezes and the window stops responding.
screen.update()is probably missing from inside thewhile True:loop, or it's outside the loop. The screen needs to be updated every frame, not once. - Lasers appear but don't move. Check that
LASER_SPEEDis a number, not zero, and that the linelaser.sety(laser.ycor() + LASER_SPEED)is inside thefor laser in lasers[:]:loop. - Lasers fly forever and the game gets slower. The
if laser.ycor() > TOP:block is what cleans them up. If a laser passes the top and never gets removed, it stays in the list and the loop keeps moving it. Double-check the>direction. - Lasers come from the wrong place.
cannon.xcor()is what aims them — it asks the cannon "where are you right now?" If lasers always start in the middle, you may have used a number like0instead ofcannon.xcor(). - I press space and nothing happens. Re-read your key bindings.
screen.onkey(fire_laser, "space")must come beforescreen.listen().