Code of the Day
BeginnerCollision and Polish

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.

Lab · optionalGame DevBeginner25 min
Recommended first
By the end of this lesson you will be able to:
  • 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 won flag.
  • 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
            break

In 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  = 0

The 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.

Finished reading? Mark it complete to track your progress.

On this page