Learning Goals
3 minBy the end of this lesson you can:
- Use
actor.collidepoint(pos)insideon_mouse_downto detect a click on an actor. - Respawn an actor at a random screen edge using
random.randint. - Run a complete Bug Squasher game that tracks and displays a score.
Warm-Up · collidepoint Preview
5 minIn PZ-15 you used a distance formula to test if a click landed on a circle. Actors have a shortcut built in. Predict what this prints (imagine a 100×100 actor centred at (200, 150)):
# Hypothetical — not a runnable pgzrun file # Actor centred at (200, 150), roughly 64x64 pixels # collidepoint tests if a point is inside the actor's bounding rectangle inside = True # pretend (215, 140) hits the 64x64 box outside = False # pretend (50, 50) misses it print("click on bug:", inside) print("click missed:", outside)
Show the answer
Output
click on bug: True click missed: False
actor.collidepoint(pos) returns True if the position pos falls inside the actor's bounding rectangle. We will use it in on_mouse_down to detect squashes.
New Concept · collidepoint & Random Respawn
12 minThink of collidepoint like a stamp pad test: you press a finger (the click position) against the bug's rectangular stamp. If the finger lands on ink, it's a hit.
actor.collidepoint(pos)
def on_mouse_down(pos, button): if button == mouse.LEFT: if bug.collidepoint(pos): print("Squashed!")
pos is the (x, y) tuple Pygame Zero passes in. The actor's bounding box is calculated from its image size and current .pos.
Random respawn at an edge
After squashing, send the bug to a random position on one of the four edges so it drifts back in:
import random def respawn(actor): edge = random.choice(["top", "bottom", "left", "right"]) if edge == "top": actor.x = random.randint(0, WIDTH) actor.y = -30 elif edge == "bottom": actor.x = random.randint(0, WIDTH) actor.y = HEIGHT + 30 elif edge == "left": actor.x = -30 actor.y = random.randint(0, HEIGHT) else: actor.x = WIDTH + 30 actor.y = random.randint(0, HEIGHT)
Why it matters
Random respawning keeps the game unpredictable — the player cannot predict where the next bug will come from. collidepoint saves you writing the distance formula by hand every time.
Worked Example · Bug Squasher
12 minThe story
Priya is building a reaction-speed game for her school fair. Three bugs drift across the screen. Click one — score +1, bug respawns. Save as bug_squasher.py:
# bug_squasher.py — part 1: setup import pgzrun import random WIDTH = 600 HEIGHT = 400 TITLE = "Bug Squasher" BUG_SPEED = 2 score = 0 def make_bug(): b = Actor("ladybug") # images/ladybug.png or coloured circle fallback respawn(b) return b def respawn(actor): edge = random.choice(["top", "bottom", "left", "right"]) if edge == "top": actor.x = random.randint(20, WIDTH - 20) actor.y = -30 actor.dx = random.uniform(-1, 1) actor.dy = BUG_SPEED elif edge == "bottom": actor.x = random.randint(20, WIDTH - 20) actor.y = HEIGHT + 30 actor.dx = random.uniform(-1, 1) actor.dy = -BUG_SPEED
elif edge == "left": actor.x = -30 actor.y = random.randint(20, HEIGHT - 20) actor.dx = BUG_SPEED actor.dy = random.uniform(-1, 1) else: actor.x = WIDTH + 30 actor.y = random.randint(20, HEIGHT - 20) actor.dx = -BUG_SPEED actor.dy = random.uniform(-1, 1) bugs = [make_bug() for _ in range(3)]
# bug_squasher.py — part 2: draw / update / click def draw(): screen.fill("lawngreen") for bug in bugs: bug.draw() screen.draw.text(f"Score: {score}", topleft=(10, 10), fontsize=30, color="darkgreen") def update(): for bug in bugs: bug.x += bug.dx bug.y += bug.dy off = bug.x < -60 or bug.x > WIDTH + 60 off = off or bug.y < -60 or bug.y > HEIGHT + 60 if off: respawn(bug) def on_mouse_down(pos, button): global score if button == mouse.LEFT: for bug in bugs: if bug.collidepoint(pos): score += 1 respawn(bug) break pgzrun.go()
What you'll see
Replace each bug.draw() with screen.draw.filled_circle((int(bug.x), int(bug.y)), 14, "red"). The Actor still provides collidepoint and stores dx/dy as custom attributes.
Try It Yourself
13 minChange BUG_SPEED to 3 and add a fifth bug to the list. Notice how the difficulty jumps — one constant controls all the bugs.
Hint
BUG_SPEED = 3 bugs = [make_bug() for _ in range(5)]
If the player clicks on the background (not on any bug), subtract 1 from the score. The score should never go below 0. Display a brief "Miss!" message by setting a global miss_timer and decrementing it in update().
Hint
miss_timer = 0 def on_mouse_down(pos, button): global score, miss_timer if button == mouse.LEFT: hit = False for bug in bugs: if bug.collidepoint(pos): score += 1 respawn(bug) hit = True break if not hit: score = max(0, score - 1) miss_timer = 60 # show for 60 frames
Mini-Challenge 🔥 · Countdown Timer
8 minAdd a 30-second countdown timer to Bug Squasher. Use update(dt) (which receives the elapsed seconds since the last frame) to decrease a time_left float. When it hits zero, stop the bugs and display a "Game Over!" message with the final score. Combine today's game with the conditional logic from Level 1.
It works if…
the game plays normally for 30 s, then freezes and shows "Game Over! Score: N"
Show one possible solution
# Add these globals at the top: time_left = 30.0 game_over = False # Replace update() with update(dt): def update(dt): global time_left, game_over if game_over: return time_left -= dt if time_left <= 0: time_left = 0 game_over = True return for bug in bugs: bug.x += bug.dx bug.y += bug.dy off = bug.x < -60 or bug.x > WIDTH + 60 off = off or bug.y < -60 or bug.y > HEIGHT + 60 if off: respawn(bug) # Add to draw() after existing lines: def draw(): screen.fill("lawngreen") for bug in bugs: bug.draw() screen.draw.text(f"Score: {score}", topleft=(10, 10), fontsize=30, color="darkgreen") screen.draw.text(f"Time: {int(time_left)}s", topleft=(500, 10), fontsize=25, color="darkgreen") if game_over: screen.draw.text(f"Game Over! Score: {score}", center=(WIDTH // 2, HEIGHT // 2), fontsize=48, color="red")
update(dt) replaces plain update() — Pygame Zero detects the parameter and passes elapsed seconds automatically.
Recap
3 minactor.collidepoint(pos) detects a click on an actor's bounding box — no maths needed. Random respawning at a screen edge uses random.choice and random.randint. A list of actors plus a loop makes it easy to scale from one bug to ten without repeating code.
Vocabulary Card
- collidepoint(pos)
- Returns
Trueif the given(x, y)point falls inside the actor's bounding rectangle. - respawn
- Moving an actor to a new start position so it re-enters the game — commonly from a random screen edge.
- random.choice(list)
- Picks one random item from a list — used here to select a random edge: top, bottom, left or right.
- update(dt)
- Version of the update hook that receives elapsed seconds per frame — enables frame-rate-independent timers.
Homework
4 minAdd a high score to Bug Squasher. After the game ends, if the player's score beats the stored high score, update it and display "New High Score!". Add a "Play again?" message — press R to restart (reset score, time_left and game_over). Save as bug_squasher_v2.py.
Sample · bug_squasher_v2.py (key additions only)
# Key additions — paste into your full game file high_score = 0 def on_key_down(key): global score, time_left, game_over, high_score if key == keys.R and game_over: score = 0 time_left = 30.0 game_over = False for bug in bugs: respawn(bug) # At the moment game_over is set to True: # if score > high_score: # high_score = score # In draw(), after Game Over text: # screen.draw.text(f"High Score: {high_score}", ...) # screen.draw.text("Press R to play again", ...)
The full file combines this with the previous countdown code. Only the additions are shown here — your colours and font sizes may differ.