Code of the Day
BeginnerGame Concepts

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.

Lab · optionalGame DevBeginner30 min
Recommended first
By the end of this lesson you will be able to:
  • 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      = False

Run 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 = True

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

Finished reading? Mark it complete to track your progress.

On this page