Learning Goals
3 minBy the end of this lesson you can:
- Explain what
actor.collidepoint(pos)tests and when to call it. - Use
on_mouse_down(pos, button)together withcollidepointto detect a click on a specific sprite. - Write a short clicking game where a score increases each time the player clicks the correct sprite.
Warm-Up · Rectangles and Points
5 minLast lesson you used colliderect to test whether two rectangles overlap. Now the second "shape" is just a point — the mouse cursor. Predict what this prints:
from pgzero.rect import Rect box = Rect((100, 100), (80, 80)) print(box.collidepoint((120, 130))) print(box.collidepoint((200, 130)))
Show the answer
Output
True False
Point (120, 130) is inside the box (x 100–180, y 100–180). Point (200, 130) is outside because 200 > 180.
A mouse click delivers a (x, y) tuple. collidepoint checks whether that point sits inside a rect — or an actor's rect. Same test, different caller.
New Concept · actor.collidepoint(pos)
12 minThink of every Actor as a bounding box drawn around its image. collidepoint is the question "is this point inside that box?" — like poking your finger at a stamp collection and asking "did I poke that stamp?"
Syntax
# actor.collidepoint(pos) returns True or False if my_actor.collidepoint(pos): print("clicked!")
pos is a tuple (x, y) — exactly what Pygame Zero passes to on_mouse_down.
Where to call it
Use it inside on_mouse_down(pos, button). That hook fires every time the player clicks anywhere in the window, and it hands you pos for free.
def on_mouse_down(pos, button): if coin.collidepoint(pos): print("You clicked the coin!")
Why it matters
Almost every clicking game — tap the enemy, catch the fruit, pop the balloon — is this one check. Learn it once and you can build all of them.
Worked Example · Catch the Durian
12 minThe story
Faiz wants a quick clicking game: a durian sits at a random spot; click it to score a point and it jumps to a new spot. Save as catch_durian.py. (No image? Use a coloured circle as a stand-in — see the note below.)
# catch_durian.py — click the actor to score import pgzrun import random WIDTH = 600 HEIGHT = 400 TITLE = "Catch the Durian" durian = Actor("durian") durian.pos = (300, 200) score = 0 def draw(): screen.fill("forestgreen") durian.draw() screen.draw.text( f"Score: {score}", topleft=(10, 10), fontsize=32, color="white", ) def on_mouse_down(pos, button): global score if durian.collidepoint(pos): score += 1 durian.x = random.randint(40, WIDTH - 40) durian.y = random.randint(40, HEIGHT - 40) pgzrun.go()
Replace Actor("durian") with a position variable and draw a circle instead:
# stand-in when no image file exists import pgzrun import random WIDTH = 600 HEIGHT = 400 TITLE = "Catch the Circle" cx, cy = 300, 200 RADIUS = 30 score = 0 def draw(): screen.fill("forestgreen") screen.draw.filled_circle((cx, cy), RADIUS, "yellow") screen.draw.text(f"Score: {score}", topleft=(10, 10), fontsize=32, color="white") def on_mouse_down(pos, button): global score, cx, cy mx, my = pos dist = ((mx - cx) ** 2 + (my - cy) ** 2) ** 0.5 if dist <= RADIUS: score += 1 cx = random.randint(40, WIDTH - 40) cy = random.randint(40, HEIGHT - 40) pgzrun.go()
What you'll see
Try It Yourself
13 minEvery click that does not hit the durian/circle adds one to a misses variable. Display it on screen next to the score.
misses = 0 def on_mouse_down(pos, button): global score, misses # your code here
Add a second circle in a different colour (the "wrong" target). Clicking the wrong one decreases the score by 1 (but not below 0).
cx2, cy2 = 100, 100 RADIUS2 = 25 def draw(): # draw both circles pass def on_mouse_down(pos, button): # check each circle separately pass
Mini-Challenge · Whack-a-Mole Timer
8 minKavitha's game below should show a 10-second countdown while the player clicks the target. When time_left reaches 0 it should stop accepting clicks and display "Habis!". But it crashes on the first click. Can you spot the problem?
# kavitha_whack.py — buggy
import pgzrun
import random
WIDTH = 500
HEIGHT = 400
cx, cy = 250, 200
RADIUS = 30
score = 0
time_left = 10
def draw():
screen.fill("navy")
screen.draw.filled_circle((cx, cy), RADIUS, "orange")
screen.draw.text(f"Score: {score} Time: {time_left}s",
topleft=(10, 10), fontsize=28, color="white")
if time_left <= 0:
screen.draw.text("Habis!", center=(250, 200),
fontsize=60, color="red")
def update(dt):
if time_left > 0:
time_left -= dt # BUG is here
def on_mouse_down(pos, button):
global score, cx, cy
if time_left > 0:
mx, my = pos
dist = ((mx - cx) ** 2 + (my - cy) ** 2) ** 0.5
if dist <= RADIUS:
score += 1
cx = random.randint(40, 460)
cy = random.randint(40, 360)
pgzrun.go()It works if…
the countdown runs smoothly, clicks are counted, and "Habis!" appears when time runs out
Show the fix
# fix: time_left must be global before we change it def update(dt): global time_left if time_left > 0: time_left -= dt if time_left < 0: time_left = 0
Any variable you assign inside a function must be declared global first — reading it is fine, but writing without global creates a local copy and raises an UnboundLocalError.
Recap
3 minactor.collidepoint(pos) tests whether a point sits inside a sprite's bounding rectangle. Pair it with on_mouse_down(pos, button) to detect clicks on any specific sprite. This single check is the foundation of every tap-and-click game.
Vocabulary Card
- collidepoint(pos)
- Actor method that returns
Trueif the given(x, y)point is inside the actor's bounding rectangle. - on_mouse_down(pos, button)
- Pygame Zero hook called once on each mouse click;
posis the click location as(x, y). - bounding rectangle
- The smallest axis-aligned rectangle that fully encloses an actor's image, including transparent pixels.
- global
- Keyword required inside a function when you want to assign a module-level variable rather than just read it.
Homework
4 minBuild a "Click the Correct Colour" game. Draw three filled circles of different colours. Only one colour is the "target" (store its name in a variable). Clicking the target adds a point; clicking a wrong circle shows a message. Save as colour_click.py and bring a screenshot to the next class.
Sample · colour_click.py
# colour_click.py — click the right colour import pgzrun WIDTH = 500 HEIGHT = 300 TITLE = "Click the Correct Colour" CIRCLES = [ {"pos": (100, 150), "r": 40, "colour": "red"}, {"pos": (250, 150), "r": 40, "colour": "blue"}, {"pos": (400, 150), "r": 40, "colour": "green"}, ] TARGET = "green" score = 0 message = f"Click the {TARGET} circle!" def draw(): screen.fill("white") for c in CIRCLES: screen.draw.filled_circle(c["pos"], c["r"], c["colour"]) screen.draw.text(message, center=(250, 40), fontsize=30, color="black") screen.draw.text(f"Score: {score}", topleft=(10, 10), fontsize=24, color="black") def on_mouse_down(pos, button): global score, message for c in CIRCLES: cx, cy = c["pos"] mx, my = pos if ((mx - cx) ** 2 + (my - cy) ** 2) ** 0.5 <= c["r"]: if c["colour"] == TARGET: score += 1 message = "Correct! +1" else: message = "Wrong colour!" pgzrun.go()
Your colours and layout will differ. The key idea: loop through your circles, check the distance, then compare the colour to the target string.