Lab: Polish the basic game
Extend your basic game with obstacles that reset progress on collision, a score that increments on reaching the target, and an R-key restart.
- Add rectangular obstacles to an existing game
- Respond to obstacle collision by resetting the player position
- Increment a score counter when the player reaches the target
- Display the score on screen with pygame.font
- Implement an R-key full restart
The basic game from the previous lab has one problem: once you know where the goal is, nothing stops you from walking straight to it. This lab adds obstacles that reset the player's position on collision, a score that tracks how many goals you have reached, and a cleaner restart. The game will still be simple — but it will feel like a game.
Pyodide (the in-browser Python runner) does not support pygame's display
system. Work through this lab locally: pip install pygame then run
python polished.py after each checkpoint.
Checkpoint 1 — Start from the basic game
Copy your completed basic game (game.py) to polished.py. The starting state
you need:
- A 640x480 window.
- A player rect, a goal rect, and a
wonflag. - A game loop with input, update (including win check), and render phases.
If your version of the basic game is incomplete, use the finished listing at the end of the previous lab as your starting point.
Checkpoint 2 — Add obstacles
Add a list of obstacle rects and respond to collisions by sending the player back to the start position (not just undoing one frame of movement — a full reset makes obstacles feel punishing and meaningful).
Add this near your other state declarations:
START_X, START_Y = WIDTH // 2, HEIGHT // 2
obstacles = [
pygame.Rect(150, 80, 20, 300), # left vertical wall
pygame.Rect(350, 100, 20, 300), # right vertical wall
pygame.Rect(200, 220, 150, 20), # horizontal barrier
]In the update phase, after updating px and py and clamping them, add:
player_rect = pygame.Rect(px, py, SIZE, SIZE)
for obs in obstacles:
if player_rect.colliderect(obs):
# Reset to start — touching an obstacle costs progress
px, py = START_X, START_Y
breakIn the render phase, draw obstacles before the player so the player appears on top:
for obs in obstacles:
pygame.draw.rect(screen, (180, 80, 80), obs)Run it. You should now be unable to walk through the red walls.
Checkpoint 3 — Score counter
Add a score variable, increment it when the player wins, then generate a new
goal so the game continues immediately rather than stopping.
Change the win detection block:
# Was:
if player_rect.colliderect(goal_rect):
won = True
# Now:
if player_rect.colliderect(goal_rect):
score += 1
gx, gy = new_goal() # immediately generate a new target
# (no 'won' flag needed anymore — the game continues)Remove the won input guard (if not won:) and the won flag entirely — the
game no longer has a stopping win state, it just accumulates score. Update the
render phase to show the score:
score_surf = font.render(f"Score: {score}", True, (255, 255, 255))
screen.blit(score_surf, (10, 10))Run it. Each time you reach the goal, the score increments and a new goal appears.
Checkpoint 4 — R-key restart
Add a full restart when R is pressed. Reset all mutable state: player position, goal position, and score.
In the event-handling block:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r:
px, py = START_X, START_Y
gx, gy = new_goal()
score = 0The complete polished game
import pygame
import sys
import random
pygame.init()
WIDTH, HEIGHT = 640, 480
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Polished game")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 36)
BG = (20, 20, 30)
PLAYER_C = (100, 210, 120)
GOAL_C = (255, 180, 50)
OBS_C = (180, 80, 80)
SIZE = 40
SPEED = 4
START_X, START_Y = WIDTH // 2, HEIGHT // 2
obstacles = [
pygame.Rect(150, 80, 20, 300),
pygame.Rect(350, 100, 20, 300),
pygame.Rect(200, 220, 150, 20),
]
def new_goal():
while True:
gx = random.randint(0, (WIDTH - SIZE) // SIZE) * SIZE
gy = random.randint(0, (HEIGHT - SIZE) // SIZE) * SIZE
candidate = pygame.Rect(gx, gy, SIZE, SIZE)
# Retry if goal spawns inside an obstacle
if not any(candidate.colliderect(o) for o in obstacles):
return gx, gy
px, py = START_X, START_Y
gx, gy = new_goal()
score = 0
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 = START_X, START_Y
gx, gy = new_goal()
score = 0
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)
for obs in obstacles:
if player_rect.colliderect(obs):
px, py = START_X, START_Y
break
if player_rect.colliderect(goal_rect):
score += 1
gx, gy = new_goal()
screen.fill(BG)
for obs in obstacles:
pygame.draw.rect(screen, OBS_C, obs)
pygame.draw.rect(screen, GOAL_C, goal_rect)
pygame.draw.rect(screen, PLAYER_C, player_rect)
score_surf = font.render(f"Score: {score} R to restart", True, (255, 255, 255))
screen.blit(score_surf, (10, 10))
pygame.display.flip()
clock.tick(60)
pygame.quit()
sys.exit()Notice the new_goal function now retries if the goal would spawn inside an
obstacle. This small guard prevents an unwinnable state — a detail that
distinguishes a game that was actually playtested from one that wasn't.
Thinking through edge cases is a programming skill, not a game-design luxury.
What you have now
This game has every structural feature a larger game needs:
- Mutable state managed in one place.
- A game loop with separated input, update, and render phases.
- AABB collision detection for both obstacles and the goal.
- A score system that persists across rounds.
- A reset mechanic that clears all state cleanly.
From here, extensions are additive: add more obstacle patterns, add a timer,
add levels with increasing difficulty, add sound with pygame.mixer. The
scaffold supports all of it without structural changes.
Collision in code
Use pygame.Rect and its colliderect method to detect overlaps between the player and obstacles, then respond to them.
Game state machines
A state machine names each mode of play and enforces explicit transitions — turning a tangle of if/elif chains into a structure you can reason about.