Python for Humans · Draft v0.1 · Oct 29, 2026 · Draft position: chapter 19 · First short project draft.

Dodge the Blocks

You should already know
Loops, functions, dictionaries and the local package workflow.
You'll build
A game window: move left and right, dodge falling blocks and restart after a collision.
You'll be able to
Handle events, update positions over time, draw a frame and detect collisions.
You need
Your local setup, any plain-text editor, uv and internet access for installing packages. Python 3.14 is selected for these examples.

Your card game waits for a command before anything changes. A graphical game keeps moving while you decide what to do.

We'll draw a player and falling blocks using pygame-ce, a community edition of Pygame. No artwork or sound files are needed. Your score increases for every block that passes you. A collision ends the round.

An example game frame with a green player, three falling orange blocks and a score.

Open a Window

From your course folder, make a separate project:

mkdir dodge-game
cd dodge-game
uv init --bare --vcs none --no-workspace --python 3.14
uv python pin 3.14
uv add pygame-ce

The package is called pygame-ce; its import is pygame. Save this first experiment as dodge.py:

import pygame

pygame.init()
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    screen.fill('navy')
    pygame.display.flip()
    clock.tick(60)
pygame.quit()

Run uv run dodge.py. A navy window should open. Close it with its close button and confirm that the terminal prompt returns.

The loop handles queued events, paints a frame and displays it. clock.tick(60) limits how often it repeats. Without checking events, the window couldn't respond properly to a close request.

Add Movement and Obstacles

The complete game below separates three jobs: new_game() creates state, update() changes it, and draw() displays it. Read those functions before running the file. Predict which values a restart must reset.

Compare the complete dodge.py
import random
import pygame

WIDTH = 640
HEIGHT = 480


def new_game():
    return {
        'player': pygame.FRect(300, 420, 40, 30),
        'blocks': [],
        'spawn': 0.0,
        'score': 0,
        'over': False,
    }


def update(game, direction, dt):
    if game['over']:
        return
    player = game['player']
    player.x = max(0, min(WIDTH - player.width, player.x + direction * 360 * dt))
    game['spawn'] += dt
    if game['spawn'] >= 0.55:
        game['spawn'] = 0.0
        game['blocks'].append(pygame.FRect(random.randint(0, WIDTH - 40), -40, 40, 40))

    remaining = []
    for block in game['blocks']:
        block.y += 220 * dt
        if block.top >= HEIGHT:
            game['score'] += 1
        else:
            remaining.append(block)
    game['blocks'] = remaining
    for block in game['blocks']:
        if player.colliderect(block):
            game['over'] = True


def draw(screen, font, game):
    screen.fill('#14213d')
    pygame.draw.rect(screen, '#46d9bf', game['player'])
    for block in game['blocks']:
        pygame.draw.rect(screen, '#ffb454', block)
    label = font.render(f"Dodged: {game['score']}", True, 'white')
    screen.blit(label, (16, 16))
    if game['over']:
        message = font.render('Game over! R to restart; Esc to quit.', True, 'white')
        screen.blit(message, (50, 220))


def main():
    pygame.init()
    try:
        screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption('Dodge the Blocks')
        font = pygame.font.Font(None, 30)
        clock = pygame.time.Clock()
        game = new_game()
        running = True
        while running:
            dt = min(clock.tick(60) / 1000, 0.05)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        running = False
                    elif event.key == pygame.K_r:
                        game = new_game()
            keys = pygame.key.get_pressed()
            direction = int(keys[pygame.K_RIGHT]) - int(keys[pygame.K_LEFT])
            update(game, direction, dt)
            draw(screen, font, game)
            pygame.display.flip()
    finally:
        pygame.quit()


if __name__ == '__main__':
    main()

You can also download dodge.py. Save the complete version in your project and run it. Use the left/right arrow keys, R to restart and Esc to quit. Click the game window if it isn't receiving keyboard input.

Follow One Frame

pygame.FRect holds a rectangle with floating-point coordinates. Its x, y, width and height describe a position and size. colliderect() asks whether two rectangles overlap. We use rectangles for both drawing and collision rules.

pygame.key.get_pressed() reports keys held down now. Events such as pressing R happen once; held arrow keys move the player across many frames. Converting each Boolean to int gives a direction of -1, 0 or 1.

dt is the elapsed time in seconds, calculated from the clock's millisecond result. Movement is speed multiplied by elapsed time. The small cap avoids a huge jump after a pause; this simple game slows during a long stall rather than trying to catch up instantly.

The player is clamped between the left and right edges. A timer creates blocks above the window. Blocks move downward, and each one that leaves the bottom adds a point. remaining collects the blocks still on screen; we don't remove items from the list we're iterating over.

A collision sets over to True. Later updates return immediately, freezing the round. Drawing still happens so we can show the restart message. R replaces the state dictionary with a fresh round.

Check a Rule Without Playing

Save as check_game.py beside the full game:

import pygame
from dodge import new_game, update

first = new_game()
second = new_game()
first['blocks'].append(first['player'].copy())
update(first, 0, 0)
assert first['over'] is True
assert second['over'] is False
assert second['blocks'] == []

fresh = new_game()
fresh['blocks'].append(pygame.FRect(0, 480, 40, 40))
update(fresh, 0, 0)
assert fresh['score'] == 1
assert fresh['blocks'] == []
print('Game checks passed.')

Run uv run check_game.py. The fixed rectangles make collision and scoring predictable. The main guard lets this check import the game without opening its window.

Make It Yours

Change the fall speed, spawn interval or player width one at a time. Before each change, predict whether it makes survival easier. Then play long enough to compare.

Add a best score that lasts between rounds. Decide which value must live outside the dictionary replaced by new_game(). Saving it between program runs can reuse the file techniques from Part II.

A larger optional extension is a maze: draw a grid, mark walls and find a route. That adds a search algorithm, so write a tiny maze with a known answer before attempting animation.

What I've Learned

Explain why a graphical game needs a loop even when the player isn't typing, and which work belongs in update versus draw.

Continue with the pygame-ce documentation. This first draft uses simple rectangle collisions; it doesn't attempt a physics simulation.