Lab: Build a basic game
Apply the game loop, player input, and state management to build a minimal but complete game — a player that moves, a target to reach, and a win condition.
- Build a minimal complete game from scratch using a game loop
- Place a player and a goal in the same game world
- Detect when the player reaches the goal and display a win state
- Restart the game by pressing a key
You have the pieces. This lab connects them into a game — small, but complete. "Complete" means it has a player, a goal, a win condition, and a way to play again. That is the minimum viable game.
The target: a player square controlled by arrow keys must reach a goal square. When they overlap, the player wins. Pressing R resets for another round.
Pyodide (the in-browser Python runner) does not support pygame's display
system. Work through this lab locally: pip install pygame then create
game.py and run python game.py after each checkpoint.
Checkpoint 1 — Scaffold and state
Start with the skeleton. Before writing any game logic, get the window open and define all the state you will need.
import pygame
import sys
import random
pygame.init()
WIDTH, HEIGHT = 640, 480
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Reach the target")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 36)
# Colours
BG = (20, 20, 30)
PLAYER_C = (100, 210, 120)
GOAL_C = (255, 180, 50)
WIN_C = (255, 255, 255)
SIZE = 40 # side length of both squares
SPEED = 4
def new_goal():
"""Return a random goal position that does not overlap the player start."""
gx = random.randint(0, (WIDTH - SIZE) // SIZE) * SIZE
gy = random.randint(0, (HEIGHT - SIZE) // SIZE) * SIZE
return gx, gy
# --- Game state ---
px, py = WIDTH // 2, HEIGHT // 2 # player position
gx, gy = new_goal() # goal position
won = FalseRun this. It should open a blank dark window (no loop yet, so it will close
immediately — add pygame.time.wait(1000) at the end temporarily if you want
to see it).
Checkpoint 2 — The game loop
Add the game loop and the three phases. Use everything from the player-input lesson: event queue for QUIT and R-to-restart, polled keys for movement.
running = True
while running:
# Phase 1 — input
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r:
# Reset state
px, py = WIDTH // 2, HEIGHT // 2
gx, gy = new_goal()
won = False
if not won:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]: px -= SPEED
if keys[pygame.K_RIGHT]: px += SPEED
if keys[pygame.K_UP]: py -= SPEED
if keys[pygame.K_DOWN]: py += SPEED
# Clamp inside window
px = max(0, min(WIDTH - SIZE, px))
py = max(0, min(HEIGHT - SIZE, py))
# Phase 2 — update (win check comes next)
# Phase 3 — render
screen.fill(BG)
pygame.draw.rect(screen, GOAL_C, (gx, gy, SIZE, SIZE))
pygame.draw.rect(screen, PLAYER_C, (px, py, SIZE, SIZE))
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()Run this. The player should move. The goal appears but nothing happens on overlap yet.
Checkpoint 3 — Win detection
Add the overlap check in the update phase. Two rectangles overlap when their
ranges on both the x and y axes intersect — that is AABB collision, which the
next module covers in detail. For now, pygame.Rect handles it for us.
Replace the # Phase 2 — update comment with:
# Phase 2 — update
if not won:
player_rect = pygame.Rect(px, py, SIZE, SIZE)
goal_rect = pygame.Rect(gx, gy, SIZE, SIZE)
if player_rect.colliderect(goal_rect):
won = TrueAnd add a win message to the render phase, after the two draw.rect calls:
if won:
msg = font.render("You reached it! R to play again", True, WIN_C)
screen.blit(msg, (WIDTH // 2 - msg.get_width() // 2, HEIGHT // 2 - 18))Run it. Move the player onto the goal. The message appears and the player stops moving. Press R to reset.
The complete game
Here is the final program assembled. Use it to check your work or as a reference if any checkpoint gave you trouble.
import pygame
import sys
import random
pygame.init()
WIDTH, HEIGHT = 640, 480
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Reach the target")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 36)
BG = (20, 20, 30)
PLAYER_C = (100, 210, 120)
GOAL_C = (255, 180, 50)
WIN_C = (255, 255, 255)
SIZE = 40
SPEED = 4
def new_goal():
gx = random.randint(0, (WIDTH - SIZE) // SIZE) * SIZE
gy = random.randint(0, (HEIGHT - SIZE) // SIZE) * SIZE
return gx, gy
px, py = WIDTH // 2, HEIGHT // 2
gx, gy = new_goal()
won = False
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r:
px, py = WIDTH // 2, HEIGHT // 2
gx, gy = new_goal()
won = False
if not won:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]: px -= SPEED
if keys[pygame.K_RIGHT]: px += SPEED
if keys[pygame.K_UP]: py -= SPEED
if keys[pygame.K_DOWN]: py += SPEED
px = max(0, min(WIDTH - SIZE, px))
py = max(0, min(HEIGHT - SIZE, py))
player_rect = pygame.Rect(px, py, SIZE, SIZE)
goal_rect = pygame.Rect(gx, gy, SIZE, SIZE)
if player_rect.colliderect(goal_rect):
won = True
screen.fill(BG)
pygame.draw.rect(screen, GOAL_C, (gx, gy, SIZE, SIZE))
pygame.draw.rect(screen, PLAYER_C, (px, py, SIZE, SIZE))
if won:
msg = font.render("You reached it! R to play again", True, WIN_C)
screen.blit(msg, (WIDTH // 2 - msg.get_width() // 2, HEIGHT // 2 - 18))
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()This game is small, but it has every structural element a larger game needs: a loop, separated input/update/render phases, mutable state, a win condition, and a restart mechanism. Everything from here is adding to this scaffold, not replacing it.
Where to go next
Next module: collision and polish — formal AABB collision detection, obstacles that block the player, a score counter, and a restart key. Your basic game is about to become a real challenge.