Learning Goals
3 minBy the end of this lesson you can:
- Move a player actor with WASD keys and keep it inside the window boundaries.
- Make a chaser actor home in on the player using simple x/y stepping logic.
- Track survival time with
update(dt)and trigger a game-over state on collision.
Warm-Up · Stepping Towards a Target
5 minIn PZ-17 bugs moved in straight lines. Today the cat needs to follow the mouse wherever it goes. The simplest chasing algorithm moves a tiny step toward the target each frame. Predict the cat's x after two frames:
cat_x = 100 mouse_x = 160 CAT_SPEED = 2 # Frame 1 if cat_x < mouse_x: cat_x += CAT_SPEED print("frame 1:", cat_x) # Frame 2 if cat_x < mouse_x: cat_x += CAT_SPEED print("frame 2:", cat_x)
Show the answer
Output
frame 1: 102 frame 2: 104
Each frame the cat moves 2 pixels closer. Over many frames it closes the gap. Add a y version and the cat moves diagonally toward the player.
New Concept · Simple Chase & Survival Timer
12 minThink of the cat like a heat-seeking snack detector. Every frame it asks "where is the mouse?" and takes one small step in that direction on both the x axis and the y axis independently.
x/y step chasing
CAT_SPEED = 1.5 def chase(chaser, target): if chaser.x < target.x: chaser.x += CAT_SPEED elif chaser.x > target.x: chaser.x -= CAT_SPEED if chaser.y < target.y: chaser.y += CAT_SPEED elif chaser.y > target.y: chaser.y -= CAT_SPEED
This produces diagonal movement because both axes update independently each frame — exactly like the multi-key trick from PZ-14.
Survival timer with update(dt)
Pass dt (elapsed seconds) to update and add it to a running total:
survival_time = 0.0 def update(dt): global survival_time survival_time += dt score = int(survival_time)
Collision = game over
Use cat.colliderect(mouse_actor) to detect the catch. When it fires, set game_over = True and stop updating.
def update(dt): global game_over, survival_time if game_over: return survival_time += dt # move player, chase, then: if cat.colliderect(mouse_actor): game_over = True
Why it matters
Simple x/y stepping is used in thousands of games. It is not perfectly smooth (the cat moves faster diagonally), but it is easy to read and tweak. Smooth normalised chasing comes later when you learn vectors.
Worked Example · Cat & Mouse Chase
12 minThe story
Wei Jie wants to build a fast survival game. The mouse (player) dodges a chasing cat. Every second alive adds to the score. Save as cat_mouse.py:
# cat_mouse.py — part 1: setup import pgzrun WIDTH = 600 HEIGHT = 400 TITLE = "Cat and Mouse" PLAYER_SPEED = 3 CAT_SPEED = 1.4 mouse_actor = Actor("mouse") # images/mouse.png or circle fallback mouse_actor.pos = (300, 200) cat = Actor("cat") # images/cat.png or circle fallback cat.pos = (50, 50) survival_time = 0.0 game_over = False
# cat_mouse.py — part 2: draw and update def draw(): screen.fill("lightgreen") mouse_actor.draw() cat.draw() screen.draw.text( f"Survival: {int(survival_time)}s", topleft=(10, 10), fontsize=28, color="darkgreen", ) if game_over: screen.draw.text( "Caught! Press R to retry", center=(WIDTH // 2, HEIGHT // 2), fontsize=40, color="red", )
# cat_mouse.py — part 3: update logic def update(dt): global survival_time, game_over if game_over: return survival_time += dt if keyboard.w: mouse_actor.y -= PLAYER_SPEED if keyboard.s: mouse_actor.y += PLAYER_SPEED if keyboard.a: mouse_actor.x -= PLAYER_SPEED if keyboard.d: mouse_actor.x += PLAYER_SPEED mouse_actor.x = max(20, min(WIDTH - 20, mouse_actor.x)) mouse_actor.y = max(20, min(HEIGHT - 20, mouse_actor.y)) if cat.x < mouse_actor.x: cat.x += CAT_SPEED elif cat.x > mouse_actor.x: cat.x -= CAT_SPEED if cat.y < mouse_actor.y: cat.y += CAT_SPEED elif cat.y > mouse_actor.y: cat.y -= CAT_SPEED if cat.colliderect(mouse_actor): game_over = True def on_key_down(key): global survival_time, game_over if key == keys.R and game_over: survival_time = 0.0 game_over = False mouse_actor.pos = (300, 200) cat.pos = (50, 50) pgzrun.go()
What you'll see
In draw() replace each actor.draw() with screen.draw.filled_circle((int(actor.x), int(actor.y)), 18, colour). The actors still provide colliderect because Actor objects always have a bounding box even without an image on screen.
Try It Yourself
13 minEvery 5 seconds of survival, increase CAT_SPEED by 0.3. Display the current cat speed on screen so the player can see the difficulty creeping up.
Hint
cat_speed = 1.4 def update(dt): global cat_speed, survival_time survival_time += dt cat_speed = 1.4 + int(survival_time / 5) * 0.3
Create a second Actor("cat") starting at the bottom-right corner (580, 380). Give it a slightly higher speed than the first cat. Both cats should chase the mouse and trigger game over on contact.
Hint
cat2 = Actor("cat") cat2.pos = (580, 380) CAT2_SPEED = 1.8 # In update: chase cat2 the same way, then check: if cat2.colliderect(mouse_actor): game_over = True
Mini-Challenge 🔥 · Cheese Boost
8 minAdd a cheese power-up to the game. A yellow circle appears at a random position. If the mouse runs over it (mouse_actor.collidepoint or distance check), the mouse gets a speed boost for 3 seconds and the cheese respawns. Combine today's chase mechanic with the random and timer skills from PZ-17.
It works if…
touching the yellow circle makes the mouse noticeably faster for a few seconds, then the speed returns to normal
Show one possible solution
import random cheese_x = random.randint(50, 550) cheese_y = random.randint(50, 350) boost_timer = 0.0 BASE_SPEED = 3 def update(dt): global survival_time, game_over, cheese_x, cheese_y, boost_timer if game_over: return survival_time += dt boost_timer = max(0.0, boost_timer - dt) speed = BASE_SPEED + (3 if boost_timer > 0 else 0) if keyboard.w: mouse_actor.y -= speed if keyboard.s: mouse_actor.y += speed if keyboard.a: mouse_actor.x -= speed if keyboard.d: mouse_actor.x += speed mouse_actor.x = max(20, min(WIDTH - 20, mouse_actor.x)) mouse_actor.y = max(20, min(HEIGHT - 20, mouse_actor.y)) cdx = mouse_actor.x - cheese_x cdy = mouse_actor.y - cheese_y if cdx * cdx + cdy * cdy < 30 * 30: boost_timer = 3.0 cheese_x = random.randint(50, 550) cheese_y = random.randint(50, 350) # cat chase as before...
Draw the cheese as a screen.draw.filled_circle((cheese_x, cheese_y), 14, "yellow") in draw(). The boost timer counts down each frame via dt.
Recap
3 minCat and Mouse combines WASD movement, simple x/y step chasing, a survival timer using update(dt), and colliderect to end the game. Each system is small on its own — chaining them together produces a complete, playable game.
Vocabulary Card
- x/y step chasing
- Moving toward a target one step per frame on each axis independently — simple and effective for beginner-friendly AI.
- update(dt)
- Version of the update hook with a delta-time parameter (seconds since last frame) — used to build timers that run at real-world speed regardless of frame rate.
- colliderect(other)
- Returns
Truewhen two actors' bounding rectangles overlap — used here to detect the cat catching the mouse. - game_over flag
- A boolean that freezes all movement and shows the end screen when set to
True.
Homework
4 minAdd a high score screen to Cat and Mouse. After game over, if int(survival_time) beats the stored high score, update it and display "New Record!" on screen. The high score should persist across restarts within the same session (just a global variable — no file-saving needed yet). Save as cat_mouse_v2.py.
Sample · cat_mouse_v2.py (key additions)
# Key additions to your cat_mouse.py: high_score = 0 new_record = False # When game_over is set to True (inside update): # global high_score, new_record # if int(survival_time) > high_score: # high_score = int(survival_time) # new_record = True # else: # new_record = False # game_over = True # In draw(), after the "Caught!" text: # screen.draw.text(f"Best: {high_score}s", # topleft=(10, 50), fontsize=24, color="darkgreen") # if new_record: # screen.draw.text("New Record!", # center=(WIDTH // 2, HEIGHT // 2 + 60), # fontsize=32, color="gold") # In on_key_down R restart: # new_record = False # reset flag on retry
The high score lives in a global variable — it resets when you close the programme. File-saving comes in a later lesson. Your display positions may differ.