Stage 6: Detect Hits and Score Points
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.
collision detection, a score readout, and hit cleanup
how two game systems finally notice each other
a game where shooting an alien actually matters
The big idea
Until now, lasers and aliens have been strangers. They live in separate lists, get updated by separate loops, and never know the other exists. Today the two systems finally talk to each other — and the moment they do, the game gets its first real purpose. Pressing space stops being a button you mash; it becomes a button you aim.
The tool we use is collision detection. A collision is what we call it when two objects in the game overlap. Computers don't actually check pixel overlap — that would be slow and weird. Instead we check distance. If a laser and an alien are within, say, 25 pixels of each other, we treat it as a hit. Turtle has a built-in helper for this: laser.distance(alien) returns the gap between two Turtles.
To check every laser against every alien, we need a nested loop — a loop inside a loop. Picture it like this:
for each laser in lasers:
for each alien in aliens:
How far apart are they?
│
▼
Less than 25 pixels? ── → Hit! Remove both. Score +10.
The outer loop walks the laser list once. For each laser, the inner loop walks the whole alien list. So if you have 3 lasers and 5 aliens on screen, we do 3 × 5 = 15 distance checks per frame. That's fine here. Nested loops can get expensive when the lists get huge — keep that in the back of your mind for whenever you build something bigger after camp.
We also introduce the first piece of game state that isn't a list — the score. And we meet a new kind of Turtle: a writer Turtle, whose only job is drawing text on the screen.
Your game should have moving lasers and moving aliens.
Build it
Step 1 — Add the score and a writer to draw it
Until now, every Turtle in the game has been a thing the player sees as an object — a cannon, a laser, an alien. A writer Turtle is different: it's hidden, it doesn't move, and its only job is to call .write(...) to put text on the screen.
Add the score variable near your other lists:
score = 0
Then create the writer before the game loop:
score_writer = turtle.Turtle()
score_writer.hideturtle()
score_writer.penup()
score_writer.color("white")
score_writer.setposition(LEFT + 20, TOP - 40)
hideturtle() makes the Turtle itself invisible — we only see the text it writes. setposition(LEFT + 20, TOP - 40) puts it in the top-left corner, using the named edges from Stage 1.
Step 2 — Write the score every time it changes
We wrap the writing in a function so we can call it once now and again every time the score changes. The clear() call wipes the old text before drawing the new — otherwise "Score: 0" and "Score: 10" would stack on top of each other.
Write this function:
Why clear first?
What would happen if `draw_score()` wrote the new score but never called `score_writer.clear()`?
Check your thinking
Every score would draw on top of the old score, making smeared or overlapping text.
def draw_score():
score_writer.clear()
score_writer.write("Score: {}".format(score), font=("Arial", 16, "bold"))
"Score: {}".format(score) builds a piece of text and slots the value of score into the {} spot. So when score is 10, the text becomes "Score: 10". This format works cleanly in Trinket's free Python rail.
Call it once before the game loop so the player sees Score: 0 when the game starts:
draw_score()
Step 3 — Check every laser against every alien
This is the nested loop. Inside the game loop, after both movement loops, add:
Trace one collision check
- Pick one laser from
lasers[:]. - Compare it to each alien in
aliens[:]. - If the distance is less than 25, remove both and add points.
Write the hit condition
Write the if line and the four hide/remove lines before you add score += 10.
Need a hint?
The condition starts with `if laser.distance(alien) < 25:`. Everything inside that block is the consequence of a hit.
Stuck? Compare carefully
main.py
Write the if line and the hide/remove lines yourself first, then reveal to check.
for laser in lasers[:]:
for alien in aliens[:]:
if laser.distance(alien) < 25:
laser.hideturtle()
alien.hideturtle()
lasers.remove(laser)
aliens.remove(alien)
score += 10
draw_score()
break
Both lasers[:] and aliens[:] are copies — same trick from Stage 3, for the same reason. When a hit happens, four things happen together: hide the laser, hide the alien, remove them from their lists, bump the score.
The break at the end is important. Once a laser hits one alien, we stop checking that laser against any other alien — it's already gone. Without break, one laser could "double-kill" two aliens on the same frame just because they happened to be close together. Tiny detail, big difference.
Run the game and try to shoot an alien. Score should go up.
Step 4 — Respawn the fleet when it is cleared
Right now the game can run out of aliens. If the player shoots the whole fleet, or if the whole fleet drifts off the bottom, aliens becomes an empty list. An empty list means there is nothing left to shoot.
After the hit-detection block, but still before time.sleep(0.02), add:
if len(aliens) == 0:
spawn_alien_fleet()
This checks the alien list once per frame. When the list is empty, the game creates a fresh fleet at the top. The check goes near the end of the loop so cleanup and scoring finish before the next wave appears.
File order checkpoint
By the end of Stage 6, your new score and respawn code should live in four places:
score = 0near the other state variables and listsscore_writeranddraw_score()before the game loop- Hit detection inside the game loop, after lasers and aliens have moved
- The empty-list respawn check after hit detection and before
time.sleep(0.02)
Understand it
Three things in this stage are worth slowing down on.
Distance, not overlap. "Are they touching?" sounds simple, but it isn't — Turtle shapes are different sizes, drawing positions are at object centers, and pixel-perfect overlap math is fiddly. Distance under a threshold is a 90% solution that feels right to the player. The 25 is a tuning number, not a law: smaller feels stingy (the laser has to thread the needle), larger feels generous (the laser is basically a missile). Pick what feels fair.
Nested loops are how systems talk. Whenever you have list A and list B and you need to ask "for each thing in A, does anything in B affect it?" — nested loops are the answer. Game collision is the most familiar example. Search engines, recommendation systems, every "find the closest pair" problem in your future uses the same shape.
Score lives outside the lists. It's a single number, not a list of things, so it doesn't need a Turtle of its own. It's just state — data the game holds onto. Stage 7 adds another piece of state (lives), Stage 8 adds another (game time), and pretty soon the game has a small handful of state variables doing all the bookkeeping. This is normal.
Try this
Try this
Three short experiments. Predict before you run, then test your guess.
Change the hit threshold from 25 to 100. Predict before you run: will hits be easier, or so easy that aiming stops mattering? Try it. Was it as broken as you expected? Try 5 next. What's the smallest number that still feels like hitting?
Comment out the break line at the end of the nested loop. Now make the fleet tighter by moving the ALIEN_COLUMNS numbers closer together, then fire a single laser at a cluster. What happens? Is that better (super-shotgun feeling) or broken (one shot wipes a cluster unfairly)?
Stage 7 needs to know when an alien reaches the cannon — that triggers losing a life. Look at the alien movement loop you wrote in Stage 5. What's the existing line that's almost what Stage 7 needs?
Test your stage
Stuck? Compare carefully
required Stage 6 code
Where it goes: Compare this with your Stage 6 additions: `score` with the other state variables, `score_writer` before the game loop, `draw_score()` before it is called, hit detection inside the game loop after both movement loops, and the respawn check before the frame delay.
Use this only after tracing one laser against one alien.
score = 0
score_writer = turtle.Turtle()
score_writer.hideturtle()
score_writer.penup()
score_writer.color("white")
score_writer.setposition(LEFT + 20, TOP - 40)
def draw_score():
score_writer.clear()
score_writer.write("Score: {}".format(score), font=("Arial", 16, "bold"))
draw_score()
while True:
# Laser movement and alien movement stay here.
for laser in lasers[:]:
for alien in aliens[:]:
if laser.distance(alien) < 25:
laser.hideturtle()
alien.hideturtle()
lasers.remove(laser)
aliens.remove(alien)
score += 10
draw_score()
break
if len(aliens) == 0:
spawn_alien_fleet()
time.sleep(0.02)
screen.update()
- Firing at an alien removes the alien.
- The laser also disappears after a hit.
- Score goes up by 10 per hit.
- Missing a shot leaves both the laser and the alien intact.
- When the alien list is empty, a fresh fleet appears at the top.
- The score text stays in the top-left corner and doesn't smear.
- Design check. Play for thirty seconds. Does hitting feel rewarding? Does missing feel like it cost you something?
- From memory. Without looking, write the one
ifline that decides whether a laser hit an alien. Compare — did you uselaser.distance(alien) < 25?
If it breaks
- Python complains that
scoreis referenced before assignment. That happens whenscore += 10lives inside a function. The code in this stage changesscoredirectly in the main loop (module scope), so noglobalkeyword is needed. If you moved the hit logic into a function, addglobal scoreas the first line of that function. - Hits almost never count. The
25threshold is too tight for your laser/alien sizes. Try35or40. - One laser wipes out a whole cluster on impact. The
breakline is missing or in the wrong place. It needs to be the last line inside theif laser.distance(alien) < 25:block. - The game runs out of aliens. Check the respawn block. It should be after hit detection and before
time.sleep(0.02), and it should callspawn_alien_fleet()whenlen(aliens) == 0. - Score text stacks on top of itself.
score_writer.clear()is missing fromdraw_score(). The writer redraws fresh every time. - The game crashes after a hit. Probably a list mix-up — you may be removing from the wrong list (
lasers.remove(alien)or vice versa). Re-read the four.remove()and.hideturtle()lines and make sure each laser action targets a laser, each alien action targets an alien.