Lesson 27: Advanced Capstone: Build, Playtest, Ship
This is where the whole course comes together: you plan, build, playtest and ship a polished game of your own that runs on at least two of the engine-level systems you built in Game Dev III. By the end of the studio you will have a playable build, real feedback from real players, and a release checklist to finish it.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Plan a scoped game on one page, choosing at least two Advanced systems that do real work in it.
- Build on a studio skeleton with scenes, a fixed timestep, an event bus, safe saves and telemetry.
- Run a peer playtest with silent observation, think-aloud and telemetry, then triage what you learn.
- Prepare a release build with a checklist, credits and a short devlog.
- Evaluate your own and a peer's game against a shared rubric.
Project: your Advanced capstone game, built from studio_starter.py, with a sample finished capstone (Drone Salvage) as a reference.
In This Lesson
๐ฌ The Brief
Game studios run "game jams" and vertical-slice sprints for the same reason this capstone exists: a tight constraint forces good decisions. Your brief:
๐ Capstone brief
- Make a polished, complete game in Python with pygame-ce: a title screen, a clear goal, a way to lose or finish, and a way to play again.
- Use at least two Advanced systems from this course, and make each one do real work (remove it and the game changes).
- Keep one core loop that a new player understands within a minute; a full session should take roughly 3 to 10 minutes.
- Playtest it with at least two classmates using the methods from Playtesting & Telemetry, and make at least one change because of what you saw.
- Ship a build other people can run, with a
CREDITS.mdlisting every asset you did not make.
Choose systems that suit your idea, not the other way round. Some pairings that work well:
| Game idea | Advanced systems it uses |
|---|---|
| Roguelike dungeon crawler | Dungeons & Caves, Utility AI or Behavior Trees, Robust Saves |
| Atmospheric stealth game | 2D Lighting, Behavior Trees, Adaptive Audio & DSP |
| Swarm survival arena | Entity-Component-System, Steering & Flocking, Difficulty & DDA |
| Two-player online duel | Sockets, TCP & UDP, Client Prediction & Reconciliation, Lobby Server & Client |
| Physics puzzle | Build a Physics Engine (+ pymunk), Wave Function Collapse, Post-processing |
| Top-down racer or mini RTS | Racing Physics or Real-time Strategy, Advanced Pathfinding, Playtesting & Telemetry |
The sample capstone in the lab, Drone Salvage, is deliberately small: grab salvage, dodge drones. It combines Event Systems, Steering & Flocking, Difficulty & DDA and Playtesting & Telemetry, and saves the best score the way Robust Saves taught. Use it as a reference for structure, not as a template for your idea.
๐ Plan on One Page
Before any code, fill in the one-page design doc (design_doc_template.md in the lab folder). Its most important section is scope, split three ways:
- Must have: at most five things without which the game is not done.
- Nice to have: only after every must-have works.
- Not in this version: ideas you are deliberately parking. Writing them down is what lets you stop thinking about them.
For each Advanced system, the doc asks "how will you know it works?" For Drone Salvage the answers were concrete: "drones never overlap in a clump" (separation), "the level rises when I play well and falls when I get hit" (DDA), "the CSV has one row per pickup and hit" (telemetry). A system you can't check is a system you can't finish.
โ Growth Mindset: Cutting Scope Is a Skill, Not a Failure
Almost every first capstone plan is too big, and that's normal. Professional teams cut features all the time; the skill is cutting early and on purpose instead of late and in a panic. If your "not in this version" list is longer than your "must have" list, you're doing it right. A small game that is finished and polished teaches you more, and impresses players more, than a big one that is half-built.
๐๏ธ The Studio Skeleton
studio_starter.py gives you the plumbing every finished game needs, already working, so your time goes into your game. It is one file to start; split it into modules once it grows.
my_capstone/
main.py # starts the game (from studio_starter.py)
scenes.py # TitleScene, PlayScene, GameOverScene (split out when main.py grows)
systems/ # your Advanced systems, one module each
assets/ # images, sounds, fonts (loaded once, relative to __file__)
CREDITS.md # every asset you did not make: source, license, files
requirements.txt # pygame-ce, plus moderngl / numpy / pymunk if you use them
README.md # how to run it, controls, known issues
Four pieces of the skeleton are worth understanding before you build on them.
Scenes
Each scene has the same three methods, as in Game States & Scenes, and Game.change() swaps them:
class TitleScene:
def __init__(self, game):
self.game = game
def handle(self, event):
if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
self.game.change(PlayScene(self.game))
def update(self, dt):
pass
def draw(self, screen):
screen.fill((16, 20, 30))
A fixed timestep
The accumulator from Velocity, Acceleration & Timesteps runs the simulation in whole steps of 1/120 s, and caps a stall at 0.25 s so a dragged window can't trigger hundreds of catch-up steps:
def advance(self, dt):
"""Fixed timestep: run whole STEPs of simulation for the real time that passed."""
self.accumulator = min(self.accumulator + dt, MAX_FRAME)
steps = 0
while self.accumulator >= STEP:
self.telemetry.clock += STEP
self.scene.update(STEP)
self.accumulator -= STEP
steps += 1
return steps
Events that feed telemetry
Gameplay code publishes events; it never writes log files itself. The Game subscribes the telemetry logger to the event names your playtest needs, and the bus isolates a broken listener so a logging bug can't crash a playtest:
for name in ("run_started", "salvage", "hit", "game_over"):
self.bus.subscribe(name, lambda _n=name, **data: self.telemetry.log(_n, **data))
# ...and in gameplay code:
g.bus.publish("hit", shield=self.shield, level=round(self.director.level, 2))
The _n=name default argument captures each name when the lambda is created; without it, every lambda would see the loop's last value.
Saves that survive crashes and upgrades
The best score and run count are saved as versioned JSON. Writing to a temporary file and then calling os.replace() means a crash in the middle of saving leaves the old save intact, and migrate() upgrades older layouts instead of rejecting them (to-do 6 in the skeleton marks where your own upgrade code goes once your save layout changes):
def write_save(path, data):
"""Write to a temporary file, then atomically replace the old save."""
tmp = path.with_suffix(".tmp")
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f)
os.replace(tmp, path)
๐๏ธ Milestones and the Studio Session
The capstone is one project spread over the whole studio session and the time after it. Four milestones keep it moving; each ends with something you can show.
| Milestone | Done when |
|---|---|
| 1. Vertical slice | The core loop is playable start to finish, with placeholder art. Ugly is fine. |
| 2. Systems integrated | Both (or all) Advanced systems do real work, and you can show how you know each works. |
| 3. Content and polish | Feedback (sound, juice, clear HUD), tuning from the playtest, no known crash. |
| 4. Release | A build someone else ran successfully, plus README.md, CREDITS.md and a short devlog. |
A suggested plan for the three-hour studio session (your instructor may adjust it):
| Time | Activity |
|---|---|
| 0:00 โ 0:20 | Pitch round: one minute each, design doc on screen. Peers ask "how will you know it works?" |
| 0:20 โ 1:30 | Build toward Milestone 1 on the studio skeleton. Commit (or copy) a working version every 20 minutes. |
| 1:30 โ 2:15 | Peer playtest rounds (next section): each game gets at least two testers. |
| 2:15 โ 2:45 | Triage and fix the top issue from your playtest. |
| 2:45 โ 3:00 | Show and tell, and write the plan for Milestones 2โ4. |
๐งช The Peer Playtest
Run the protocol from Playtesting & Telemetry, compressed to fit a class:
- Before: write your one question and the one-sentence task you'll say to every tester. Make sure your telemetry logs the events that answer it.
- During (8 minutes per tester): say the task, then stay silent. Ask the tester to think aloud. Note the time and what they did whenever they hesitate, die or look confused.
- After (2 minutes): ask "what was the hardest part?" and "what would you change?", then optionally the ten SUS questions.
- Swap roles and repeat, so every game gets at least two testers.
Triage: what to fix first
You will collect more problems than you can fix. Sort them by how many testers hit each one and how badly it hurt:
| Most testers hit it | One tester hit it | |
|---|---|---|
| Blocks progress or crashes | Fix now | Fix soon (and try to reproduce it) |
| Annoying but playable | Fix before release | Note it; revisit after more tests |
Change one thing at a time, then test again. If you change five things and the game gets better, you don't know which change did it.
โ Growth Mindset: Feedback Is About the Game, Not About You
Hearing "I didn't get it" about something you spent hours on stings. Every designer feels that. Write the comment down exactly as it was said, thank the tester, and decide what to do with it later, calmly. The testers who confuse you most are giving you the most valuable information.
๐ Ship It
A game that only runs on your machine isn't shipped. Use what you learned in Game Dev II:
- Desktop build: Building Executables covers PyInstaller,
__file__-relative asset paths and a user folder for saves. It is the most direct route for games that use moderngl, numpy, pymunk or sockets; test the build on a machine that doesn't have your Python setup. - Web build: Share Your Game (pygbag + zip) from Game Dev I puts pure pygame-ce games in the browser. Check that every library your game uses works there before you promise a web version.
- Release: Launch & Live Ops covers the itch.io page, uploading builds with butler, version numbers and patch notes.
Release checklist
- Someone else ran the build on their own machine, start to finish.
- No debug keys, cheats or test prints left on by accident; the window title is the game's name.
- Saves and logs go to a sensible folder, and a missing or damaged save doesn't crash the game.
README.md: how to run it, controls, known issues, and the Advanced systems it uses.CREDITS.md: every asset you didn't make, with source, license and files. Kenney publishes many game assets under CC0 1.0, and thanking the creator is good practice even when the license doesn't require it.- A short devlog: what you set out to make, what you cut, what the playtest changed.
๐ The Rubric
You, a peer and your instructor score the game against the same rubric, 4 points per row, 20 in total. Read it before you start: it is also a checklist.
| Criterion | 4: Excellent | 3: Solid | 2: Developing | 1: Beginning |
|---|---|---|---|---|
| Core loop and polish | Clear goal, satisfying feedback, restart, no rough edges | Complete loop, a few rough edges | Playable but unclear or unfinished | Not yet playable start to finish |
| Advanced systems | Two or more, each essential and verified | Two, working, one lightly used | One working, one partial | None working yet |
| Code and architecture | Scenes, dt/fixed step, events; readable modules; no crash | Mostly organized; rare issues | Works but tangled or fragile | Hard to follow or unstable |
| Playtest and iteration | Question, telemetry, notes, triage; changes justified by evidence | Playtest run and one change made | Playtest run, no change yet | No playtest yet |
| Shipping and documentation | Build runs elsewhere; README, CREDITS, devlog | Build runs; docs mostly complete | Runs only from source | Not yet shareable |
๐๏ธ Practice Exercise: Your Capstone Studio
Objective: turn the studio skeleton into the vertical slice of your own game, playtest it with classmates, and leave the session with a fix made and a plan for the remaining milestones.
Time: the full studio session (about 3 hours), then the remaining milestones at home. Starter files: studio_starter.py and design_doc_template.md (your instructor has them). The skeleton already runs: a title screen, a placeholder play scene, game over, saves and telemetry. Its numbered to-do comments mark where your game goes.
- Fill in the one-page design doc, including two Advanced systems and how you'll verify each. (โ 20 min)
- Run the skeleton, then give it your game's name (to-do 1) and create your game objects in
PlayScene(to-do 2). (โ 15 min) - Build your core loop in
PlayScene.update(to-do 3) and draw it (to-do 4), with every timer in seconds. Replace the SPACE placeholder with your real win or lose rule. (โ 50 min) - Publish events for everything your playtest needs and add their names to the telemetry list (to-do 5). (โ 10 min)
- Run the peer playtest: at least two testers, silent observation, notes, the telemetry CSV. (โ 45 min)
- Triage, fix the top issue, and test the fix. (โ 30 min)
- Write your plan for Milestones 2โ4 in the design doc. (โ 10 min)
You are done (for the session) when:
- your core loop is playable from the title screen to game over and back;
- at least one Advanced system is working and you know how you will verify the second;
- two classmates have played it, and you have their notes and a telemetry CSV;
- you made one change because of the playtest, and can say why;
- your design doc lists dates for the remaining milestones.
๐ก Hint
If you are stuck on what to build first, build the thing the player does every few seconds, with rectangles and circles, and nothing else. Open the sample solution below and find Drone Salvage's PlayScene.update: notice that it is just "read input, move things, check collisions, publish events". Your first version can be that simple.
โ Example Solution
Your capstone is your own game, so there is no single answer. This is the lab's sample capstone, Drone Salvage, built on the same skeleton with four Advanced systems. If your instructor hands you the lab file, you will see a few extra lines marked lab runtime near the top and and frame_budget() in the loop; they let the instructor's checker run it automatically and do nothing when you run it yourself.
"""Drone Salvage: Advanced Lesson 27 sample capstone (solution).
A small but complete game built the way the capstone brief asks: one core
loop (grab salvage, dodge drones), scenes, a fixed-timestep update, and four
Advanced systems working together:
- an event bus that isolates failing listeners (Event Systems),
- steering drones that seek the player and keep apart (Steering & Flocking),
- dynamic difficulty in levels per second (Difficulty & DDA),
- telemetry saved as CSV for playtests (Playtesting & Telemetry),
plus a versioned save file written atomically (Robust Saves).
Keys: ENTER starts, WASD or arrows fly, ESC returns to the title.
Close the window to quit.
"""
import csv
import json
import os
import random
import time
from collections import defaultdict, deque
from pathlib import Path
import pygame
WIDTH, HEIGHT = 960, 600
STEP = 1 / 120 # fixed simulation step, seconds
MAX_FRAME = 0.25 # never simulate more than this per frame (after a stall)
SAVE_PATH = Path(__file__).parent / "drone_salvage_save.json"
LOG_DIR = Path(__file__).parent / "playtest_logs"
SAVE_VERSION = 2
PLAYER_SPEED = 260 # px/s
INVULNERABLE = 1.0 # seconds after a hit
TEXT = (235, 235, 235)
ACCENT = (120, 210, 255)
def clamp(v, lo, hi):
return max(lo, min(hi, v))
# --- events (Event Systems) -----------------------------------------------------------------
class EventBus:
def __init__(self):
self.handlers = defaultdict(list)
self.errors = 0
def subscribe(self, name, handler):
self.handlers[name].append(handler)
def publish(self, name, **data):
"""Call every listener; one broken listener must not stop the others (or the game)."""
for handler in list(self.handlers[name]):
try:
handler(**data)
except Exception as exc: # isolate listener bugs, but report them
self.errors += 1
print(f"listener error on {name!r}: {exc!r}")
# --- saves (Robust Saves) ----------------------------------------------------------------------
DEFAULT_SAVE = {"version": SAVE_VERSION, "best_score": 0, "runs": 0}
def migrate(data):
"""Upgrade old save layouts one version at a time."""
if data.get("version", 1) == 1: # version 1 was {"best": n}
data = {"version": 2, "best_score": int(data.get("best", 0)), "runs": 0}
return data
def load_save(path):
"""Missing or damaged saves fall back to defaults instead of crashing the game."""
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict): # valid JSON, but not a save (a list, a number...)
return dict(DEFAULT_SAVE)
data = migrate(data)
except (OSError, ValueError, TypeError):
return dict(DEFAULT_SAVE)
if not (isinstance(data.get("best_score"), int) and isinstance(data.get("runs"), int)):
return dict(DEFAULT_SAVE)
return data
def write_save(path, data):
"""Write to a temporary file, then atomically replace the old save."""
tmp = path.with_suffix(".tmp")
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f)
os.replace(tmp, path)
# --- telemetry (Playtesting & Telemetry) --------------------------------------------------------
class Telemetry:
def __init__(self, tester="T01"):
self.session = time.strftime("%Y%m%d-%H%M%S")
self.tester = tester
self.clock = 0.0
self.rows = []
def log(self, kind, **data):
self.rows.append({"t": round(self.clock, 3), "session": self.session, "tester": self.tester,
"kind": kind, "data": json.dumps(data, sort_keys=True)})
def save(self, folder):
folder.mkdir(parents=True, exist_ok=True)
path = folder / f"session_{self.session}_{self.tester}.csv"
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["t", "session", "tester", "kind", "data"])
writer.writeheader()
writer.writerows(self.rows)
return path
# --- difficulty (Difficulty & DDA) -------------------------------------------------------------------
class Director:
def __init__(self, level=3.0, target=0.6, gain=1.5, max_rate=0.4):
self.level, self.target, self.gain, self.max_rate = level, target, gain, max_rate
self.recent = deque() # (time, +1 salvage / -1 hit)
self.clock = 0.0
def record(self, value):
self.recent.append((self.clock, value))
def performance(self):
while self.recent and self.clock - self.recent[0][0] > 20.0:
self.recent.popleft()
good = sum(1 for _, v in self.recent if v > 0)
bad = sum(1 for _, v in self.recent if v < 0)
return clamp(0.5 + 0.05 * good - 0.2 * bad, 0.0, 1.0)
def update(self, dt):
self.clock += dt
error = self.performance() - self.target
rate = clamp(self.gain * error, -self.max_rate, self.max_rate) if abs(error) > 0.05 else 0.0
self.level = clamp(self.level + rate * dt, 1.0, 10.0)
def params(self):
mult = self.level / 5
return {"drone_speed": 120 * mult ** 0.8, "drone_count": 2 + round(self.level),
"salvage_value": max(1, round(10 * mult ** -0.3))}
# --- steering (Steering & Flocking) --------------------------------------------------------------------
def seek(pos, vel, target, max_speed, max_force):
"""Steering force toward target, limited to max_force (px/s^2)."""
desired = target - pos
if desired.length_squared() == 0:
return pygame.Vector2()
force = desired.normalize() * max_speed - vel
if force.length() > max_force:
force.scale_to_length(max_force)
return force
def separation(pos, others, radius):
"""Push away from neighbors closer than radius; stronger when closer."""
push = pygame.Vector2()
for other in others:
offset = pos - other
d = offset.length()
if 0 < d < radius:
push += offset / d * (radius - d) / radius
return push
class Drone:
def __init__(self, pos):
self.pos = pygame.Vector2(pos)
self.vel = pygame.Vector2()
def update(self, target, neighbors, speed, dt):
force = seek(self.pos, self.vel, target, speed, 400) + separation(self.pos, neighbors, 60) * 600
self.vel += force * dt
if self.vel.length() > speed:
self.vel.scale_to_length(speed)
self.pos += self.vel * dt
# --- scenes (Game States & Scenes) --------------------------------------------------------------------------
class TitleScene:
def __init__(self, game):
self.game = game
def handle(self, event):
if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
self.game.change(PlayScene(self.game))
def update(self, dt):
pass
def draw(self, screen):
screen.fill((16, 20, 30))
g = self.game
title = g.big.render("DRONE SALVAGE", True, ACCENT)
screen.blit(title, title.get_rect(center=(WIDTH / 2, HEIGHT / 2 - 40)))
info = g.font.render(f"ENTER to start best {g.save['best_score']} runs {g.save['runs']}", True, TEXT)
screen.blit(info, info.get_rect(center=(WIDTH / 2, HEIGHT / 2 + 20)))
class PlayScene:
def __init__(self, game, seed=None):
self.game = game
self.rng = random.Random(seed)
self.player = pygame.Vector2(WIDTH / 2, HEIGHT / 2)
self.move = pygame.Vector2()
self.shield = 3
self.invulnerable = 0.0
self.score = 0
self.drones = []
self.salvage = [self.random_point() for _ in range(4)]
self.director = Director()
game.bus.publish("run_started")
def random_point(self):
return pygame.Vector2(self.rng.uniform(40, WIDTH - 40), self.rng.uniform(60, HEIGHT - 40))
def handle(self, event):
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
self.game.change(TitleScene(self.game))
def update(self, dt):
g = self.game
self.director.update(dt)
p = self.director.params()
held = g.held
self.move.update((pygame.K_RIGHT in held or pygame.K_d in held) - (pygame.K_LEFT in held or pygame.K_a in held),
(pygame.K_DOWN in held or pygame.K_s in held) - (pygame.K_UP in held or pygame.K_w in held))
if self.move.length_squared() > 0:
self.player += self.move.normalize() * PLAYER_SPEED * dt
self.player.x = clamp(self.player.x, 12, WIDTH - 12)
self.player.y = clamp(self.player.y, 12, HEIGHT - 12)
while len(self.drones) < p["drone_count"]:
edge = self.rng.choice([(0, self.rng.uniform(0, HEIGHT)), (WIDTH, self.rng.uniform(0, HEIGHT))])
self.drones.append(Drone(edge))
positions = [d.pos for d in self.drones]
for d in self.drones:
d.update(self.player, positions, p["drone_speed"], dt)
for s in list(self.salvage):
if s.distance_to(self.player) < 22:
self.salvage.remove(s)
self.salvage.append(self.random_point())
self.score += p["salvage_value"]
self.director.record(+1)
g.bus.publish("salvage", value=p["salvage_value"], x=round(s.x), y=round(s.y))
self.invulnerable = max(0.0, self.invulnerable - dt)
if self.invulnerable == 0 and any(d.pos.distance_to(self.player) < 24 for d in self.drones):
self.shield -= 1
self.invulnerable = INVULNERABLE
self.director.record(-1)
g.bus.publish("hit", shield=self.shield, level=round(self.director.level, 2))
if self.shield <= 0:
g.bus.publish("game_over", score=self.score)
g.change(GameOverScene(g, self.score))
def draw(self, screen):
g = self.game
screen.fill((12, 16, 26))
for s in self.salvage:
pygame.draw.rect(screen, (240, 200, 80), (s.x - 7, s.y - 7, 14, 14), border_radius=3)
for d in self.drones:
pygame.draw.circle(screen, (230, 80, 90), d.pos, 11)
blink = self.invulnerable > 0 and int(self.invulnerable * 10) % 2 == 0
if not blink:
pygame.draw.circle(screen, ACCENT, self.player, 12)
hud = g.font.render(f"score {self.score} shield {self.shield} level {self.director.level:.1f}", True, TEXT)
screen.blit(hud, (12, 10))
class GameOverScene:
def __init__(self, game, score):
self.game = game
self.score = score
game.save["runs"] += 1
self.new_best = score > game.save["best_score"]
if self.new_best:
game.save["best_score"] = score
write_save(game.save_path, game.save)
def handle(self, event):
if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
self.game.change(PlayScene(self.game))
def update(self, dt):
pass
def draw(self, screen):
g = self.game
screen.fill((26, 14, 20))
lines = ["GAME OVER", f"score {self.score}" + (" NEW BEST!" if self.new_best else ""), "ENTER to fly again"]
for i, text in enumerate(lines):
img = (g.big if i == 0 else g.font).render(text, True, TEXT)
screen.blit(img, img.get_rect(center=(WIDTH / 2, HEIGHT / 2 - 50 + i * 50)))
class Game:
def __init__(self, save_path=SAVE_PATH):
self.bus = EventBus()
self.telemetry = Telemetry()
self.save_path = save_path
self.save = load_save(save_path)
self.held = set()
self.font = pygame.font.Font(None, 28) # fonts created once, shared by every scene
self.big = pygame.font.Font(None, 72)
self.accumulator = 0.0
for name in ("run_started", "salvage", "hit", "game_over"):
self.bus.subscribe(name, lambda _n=name, **data: self.telemetry.log(_n, **data))
self.scene = TitleScene(self)
def change(self, scene):
self.scene = scene
def advance(self, dt):
"""Fixed timestep: run whole STEPs of simulation for the real time that passed."""
self.accumulator = min(self.accumulator + dt, MAX_FRAME)
steps = 0
while self.accumulator >= STEP:
self.telemetry.clock += STEP
self.scene.update(STEP)
self.accumulator -= STEP
steps += 1
return steps
def main():
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Drone Salvage")
clock = pygame.time.Clock()
game = Game()
running = True
while running:
dt = clock.tick(60) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
game.held.add(event.key)
elif event.type == pygame.KEYUP:
game.held.discard(event.key)
game.scene.handle(event)
game.advance(dt)
game.scene.draw(screen)
pygame.display.flip()
pygame.quit()
log = game.telemetry.save(LOG_DIR)
print(f"Telemetry: {len(game.telemetry.rows)} events in {log.name}")
print(f"Best score: {game.save['best_score']} runs: {game.save['runs']}")
print("Studio closed cleanly.")
if __name__ == "__main__":
main()
๐ Learning Journal
Take five minutes to write in your learning journal (a notebook or a plain text file works). Jot down:
- Key concepts you learned today
- Techniques that clicked (and the ones that haven't, yet)
- Questions or confusion to bring to the next session
- Ideas to try in your own game
- Progress and feelings: how did this lesson go for you?
โ๏ธ This lesson's prompts:
- What did your testers do that surprised you most? What did you change because of it?
- Which item did you move to "not in this version", and how did that feel?
- Look back at the first lesson of Game Dev I. What can you build now that you couldn't imagine then?
๐ Summary
The capstone asks for one thing above all: a finished, polished game. You planned it on one page with a ruthless scope list and a way to verify each Advanced system. You built on a skeleton that already handles scenes, a fixed timestep, events, telemetry and crash-safe saves, so your effort went into your core loop. You ran a peer playtest the way a studio does, triaged what you learned, changed one thing at a time, and you now have a milestone plan and a checklist for shipping.
๐ Key Takeaways
- Choose Advanced systems that serve your idea, and decide up front how you'll know each works.
- Scope in three lists: must have, nice to have, not in this version.
- Build the vertical slice first: the core loop, start to finish, with placeholder art.
- Gameplay publishes events; telemetry, audio and UI subscribe to them.
- Playtest silently, triage by frequency and severity, change one thing at a time.
- Shipped means someone else ran it: build, README, CREDITS, devlog.
๐ญ Looking Ahead
This is the final lesson of the three-course series. Finish your remaining milestones, publish your game, and share the link: then pick a system from this course and take it further in your next project.
โ Common Questions
Can I reuse one of my labs as my capstone?
You can start from one, as long as the capstone becomes a complete game with a title screen, a goal, an ending and a restart, and it uses at least two Advanced systems doing real work. Tiny RTS or Lap Racer grown into a full game would qualify.
Can I work in a team?
If your instructor allows it, yes, with a bigger scope and a clear split of who owns which system. Each team member should be able to explain every system in the game.
Do I have to use the studio skeleton?
No, but your game needs the same things it provides: scenes, frame-rate independent updates, safe saves and a way to log playtest events. The skeleton just saves you the time.
What if my Advanced system is too hard to finish?
Cut it down, not out: a simpler version that works (flocking with only separation and seek, lighting with one light) beats an ambitious one that doesn't. If it truly can't work in time, swap to another system and note the change in your devlog.
Can I use art and sound I found online?
Only with a license that allows it, and every item goes in CREDITS.md. CC0 assets (such as many packs on kenney.nl) are the simplest choice. Code-drawn shapes are always fine.
๐ฏ Quick Quiz
Question 1: What does the capstone brief ask for about Advanced systems?
Question 2: Why does the design doc have a "not in this version" list?
Question 3: During the peer playtest, your tester can't find the exit for two minutes. What should you do?
Question 4: Why does the skeleton write the save to a temporary file and then call os.replace()?
Question 5: After your playtest you have a list of issues. Which should you fix first?
๐ Going Further
- A second playtest round: after your fix, test with two new players and compare the telemetry from both rounds using the tools from Playtesting & Telemetry.
- Profile before you polish: run your game under
cProfileas in Profiling & Performance and fix the slowest function you find, if any. - Accessibility pass: add at least one assist (Difficulty & DDA) and remappable keys.
- Game jam: enter a public game jam on itch.io with a new idea built on your skeleton; a deadline is a great teacher.
- Assets: browse Kenney's CC0 asset packs for art and sound that you can use freely (list them in
CREDITS.mdanyway, with thanks).