Learning Goals
3 minBy the end of this lesson you can:
- Complete a one-page game design: genre, controls, win/lose condition, and scoring plan.
- Build a playable prototype skeleton — a window, a player that moves, and one core mechanic running inside a four-state machine.
- Identify which polish features (particles, shake, sound) you will add in Part 2.
Warm-Up · Pick Your Genre
5 minYou have 48 lessons of Pygame Zero skills. Today you pick one idea and build it. Here are five genres that work well with the tools you have:
| Genre | Core mechanic | Win condition |
|---|---|---|
| Catcher | Move paddle left/right; catch falling objects | Score X before 3 misses |
| Dodger | Move player; avoid falling enemies | Survive for 60 seconds |
| Shooter | Move player; fire bullets; hit targets | Clear all targets |
| Platformer lite | Gravity + jump; reach the end | Reach the flag |
| Clicker | Click moving targets before they escape | Hit 20 before 5 escape |
Choose one, or invent your own. Write it down — you will refer to it during the build.
A finished small game beats an unfinished big one. Pick the simplest version of your idea. You can always extend it.
New Concept · The One-Page Game Design
12 minProfessional game developers write a Game Design Document (GDD). For a 30-minute game jam it becomes a one-page design. Fill in these five fields before writing a single line of code:
Your design template
Title: ___________
Genre: (catcher / dodger / shooter / platformer / clicker / other)
Controls: e.g. “LEFT/RIGHT arrows move the paddle”
Win condition: e.g. “Score 20 points before losing 3 lives”
Scoring: e.g. “+1 per catch; −1 life per miss; bonus +5 every 10 catches”
Polish plan (Part 2): e.g. “particles on catch, shake on miss, hit flash, title screen”
What your prototype must have
- A window with
WIDTH,HEIGHT, andTITLEset. - A player that responds to input and moves on screen.
- One mechanic working: the thing that makes it a game (falling objects, bullets, enemies).
- A four-state machine: title, play, pause, game-over — even if the title screen is just text.
- Score and lives visible on screen during play.
No particles, no sound, no shake yet — those are Part 2. Today is about the game being playable.
Worked Example · Prototype Scaffold End-to-End
12 minThe story
Aiman decides to build “Roti Canai Rain” — a catcher where roti canai falls from the sky and the player moves a tray left and right to catch it. Here is the complete prototype scaffold.
# roti_rain.py — capstone prototype (Part 1) import pgzrun import random WIDTH = 480 HEIGHT = 320 TITLE = "Roti Canai Rain" # --- game globals --- state = "title" score = 0 lives = 3 tray_x = 240 TRAY_Y = 290 TRAY_W = 70 roti_x = 240 roti_y = -30 ROTI_SPEED = 4
def reset_game(): global score, lives, tray_x, roti_x, roti_y score = 0 lives = 3 tray_x = 240 roti_x = random.randint(30, WIDTH - 30) roti_y = -30
def update_play(): global roti_y, roti_x, lives, state # move tray if keyboard.left and tray_x > TRAY_W // 2 + 5: tray_x -= 5 if keyboard.right and tray_x < WIDTH - TRAY_W // 2 - 5: tray_x += 5 # move roti roti_y += ROTI_SPEED # catch check if (abs(roti_x - tray_x) < TRAY_W // 2 + 14 and abs(roti_y - TRAY_Y) < 20): global score score += 1 roti_x = random.randint(30, WIDTH - 30) roti_y = -30 # miss check elif roti_y > HEIGHT + 30: lives -= 1 roti_x = random.randint(30, WIDTH - 30) roti_y = -30 if lives <= 0: state = "game_over"
def on_key_down(key): global state if state == "title" and key == keys.SPACE: reset_game() state = "play" elif state == "play" and key == keys.P: state = "pause" elif state == "pause" and key == keys.P: state = "play" elif state == "game_over" and key == keys.R: state = "title"
def draw_title(): screen.draw.text("ROTI CANAI RAIN", center=(240, 120), fontsize=44, color="gold") screen.draw.text("LEFT / RIGHT to move tray", center=(240, 175), fontsize=20, color="white") screen.draw.text("Press SPACE to start", center=(240, 205), fontsize=20, color="grey") def draw_play(): # tray tray_r = Rect((tray_x - TRAY_W//2, TRAY_Y - 8), (TRAY_W, 16)) screen.draw.filled_rect(tray_r, (180, 120, 60)) # roti screen.draw.filled_circle((roti_x, int(roti_y)), 20, "wheat") screen.draw.text(f"Score:{score} Lives:{lives}", topleft=(10, 10), fontsize=22, color="white") def draw_pause_overlay(): screen.draw.text("PAUSED — press P to resume", center=(240, 160), fontsize=28, color="white") def draw_game_over(): screen.draw.text("GAME OVER", center=(240, 110), fontsize=48, color="crimson") screen.draw.text(f"Score: {score}", center=(240, 175), fontsize=30, color="white") screen.draw.text("Press R to return to title", center=(240, 220), fontsize=20, color="grey")
def update(): if state == "play": update_play() def draw(): screen.fill("black") if state == "title": draw_title() elif state == "play": draw_play() elif state == "pause": draw_play() draw_pause_overlay() elif state == "game_over": draw_game_over() pgzrun.go()
Aiman's list for Part 2: particles on catch, screen shake on miss, hit flash on the tray when it catches, title and game-over screens dressed up with colour, and a catch sound. The prototype proves the game is fun first.
Try It Yourself
13 minBefore writing any code, fill in the five-field design template from the Concept section. Write it as a comment block at the top of a new file called capstone.py.
Hint
# capstone.py # Title: Kuih Raya Catcher # Genre: Catcher # Controls: LEFT / RIGHT arrows move the basket # Win: Score 20 before losing 3 lives # Scoring: +1 per catch, -1 life per miss # Polish (Part 2): particles, shake on miss, sound import pgzrun # ...
Start from the four-state skeleton in PZ-45's homework answer. Replace the placeholder content with your own player, one mechanic, score, and lives. The game should be playable — not polished — by the end of the lesson.
Hint — dodger genre starter
import pgzrun import random WIDTH = 480 HEIGHT = 320 state = "title" score = 0 lives = 3 player_x = 240 player_y = 270 enemies = [] SPAWN_INTERVAL = 60 spawn_timer = 0
Mini-Challenge · Difficulty Ramp
8 minMost good games get harder over time. Add a difficulty ramp to your prototype: every 10 points the falling speed (or enemy spawn rate) increases by a small fixed amount. Use integer division (score // 10) to compute the ramp level.
Combine today's prototype with the arithmetic skills from Level 1.
It works if…
at score 0 the speed is 4 px/frame; at score 10 it is 5; at score 20 it is 6 — noticeably faster
Show one possible solution
BASE_SPEED = 4 def current_speed(): return BASE_SPEED + score // 10 # in update_play(), replace the fixed ROTI_SPEED with: roti_y += current_speed()
One helper function. score // 10 gives 0 for scores 0–9, 1 for 10–19, and so on — a clean step-function ramp.
Recap
3 minA good prototype does the minimum needed to prove the game is fun: a moving player, one mechanic, score and lives, and a four-state machine so you can start and restart cleanly. Write the design template first — it keeps you focused. Polish (particles, shake, sound) comes in Part 2.
Vocabulary Card
- game design document (GDD)
- A written description of a game's mechanics, controls, win/lose conditions, and art direction. Even a five-line comment block counts.
- prototype
- The simplest version of a game that lets you test whether the core mechanic is fun. No art, no sound, no polish.
- difficulty ramp
- A formula that increases speed or challenge as the player's score grows, keeping the game engaging over time.
- core mechanic
- The one repeating action that makes the game what it is — catching, dodging, shooting, jumping.
Homework
4 minFinish your prototype if you did not complete it in class. By the start of PZ-48 your capstone.py must:
- Open a window and show a title screen.
- Let the player move and interact with at least one mechanic.
- Track score and lives and show them on screen.
- Transition to a game-over screen when lives reach zero.
- Support restarting from the game-over screen.
Bring it to PZ-48 — that lesson adds all the polish.
Sample · capstone.py (dodger prototype)
# capstone.py — Dodger prototype (Part 1 complete) import pgzrun import random WIDTH = 480 HEIGHT = 320 TITLE = "KL Dodger" state = "title" score = 0 lives = 3 player_x = 240 enemies = [] spawn_timer = 0 BASE_SPEED = 3 def reset_game(): global score, lives, player_x, enemies, spawn_timer score = 0 lives = 3 player_x = 240 enemies.clear() spawn_timer = 0 def current_speed(): return BASE_SPEED + score // 10 def update_play(): global player_x, spawn_timer, score, lives, state if keyboard.left and player_x > 20: player_x -= 5 if keyboard.right and player_x < WIDTH - 20: player_x += 5 spawn_timer += 1 if spawn_timer >= 50: spawn_timer = 0 enemies.append({"x": random.randint(20, WIDTH-20), "y": -20}) for e in enemies: e["y"] += current_speed() for e in enemies[:]: if abs(e["x"] - player_x) < 22 and abs(e["y"] - 290) < 22: lives -= 1 enemies.remove(e) if lives <= 0: state = "game_over" enemies[:] = [e for e in enemies if e["y"] < HEIGHT + 30] score += 1 def on_key_down(key): global state if state == "title" and key == keys.SPACE: reset_game() state = "play" elif state == "play" and key == keys.P: state = "pause" elif state == "pause" and key == keys.P: state = "play" elif state == "game_over" and key == keys.R: state = "title" def update(): if state == "play": update_play() def draw(): screen.fill("black") if state == "title": screen.draw.text("KL DODGER", center=(240, 120), fontsize=52, color="gold") screen.draw.text("SPACE to start", center=(240, 190), fontsize=24, color="white") elif state in ("play", "pause"): screen.draw.filled_circle((player_x, 290), 18, "deepskyblue") for e in enemies: screen.draw.filled_circle((e["x"], int(e["y"])), 16, "crimson") screen.draw.text(f"Score:{score} Lives:{lives}", topleft=(10,10), fontsize=22, color="white") if state == "pause": screen.draw.text("PAUSED", center=(240, 160), fontsize=40, color="white") elif state == "game_over": screen.draw.text("GAME OVER", center=(240,110), fontsize=48, color="crimson") screen.draw.text(f"Score: {score}", center=(240,175), fontsize=28, color="white") screen.draw.text("R to restart", center=(240,215), fontsize=20, color="grey") pgzrun.go()
This is one valid prototype — yours will differ based on your chosen genre. The non-negotiables are the five checklist items above.