34. Moving things — Homework solutions

Problem 1 — Clamped mover

Problem. A square that moves with arrow keys and stays inside the window.

How to think about it. Apply the get_pressed movement, then clamp all four edges using rect.left, rect.right, rect.top, rect.bottom.

Worked solution.

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Clamped Mover")
clock = pygame.time.Clock()

rect  = pygame.Rect(370, 270, 60, 60)
speed = 5

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        rect.x -= speed
    if keys[pygame.K_RIGHT]:
        rect.x += speed
    if keys[pygame.K_UP]:
        rect.y -= speed
    if keys[pygame.K_DOWN]:
        rect.y += speed

    if rect.left < 0:
        rect.left = 0
    if rect.right > 800:
        rect.right = 800
    if rect.top < 0:
        rect.top = 0
    if rect.bottom > 600:
        rect.bottom = 600

    screen.fill((20, 20, 40))
    pygame.draw.rect(screen, (100, 200, 255), rect)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

Common mistakes.

  • Clamping rect.x < 0 but not accounting for width. If you check x < 0 and clamp to x = 0, the left edge is correct. But for the right edge you must check rect.right > 800 (which is rect.x + rect.width > 800). Using rect.right and rect.left is simpler than doing the arithmetic manually.

Problem 2 — Two-ball bounce

Problem. Two balls bouncing off all four walls independently.

How to think about it. Use two sets of position and velocity variables. The bounce logic is the same for both; just write it twice (or make a function).

Worked solution.

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Two Balls")
clock = pygame.time.Clock()

ax, ay, adx, ady = 200, 150, 4, 3
bx, by, bdx, bdy = 600, 400, -3, 5
radius = 20
W, H = 800, 600

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    ax += adx
    ay += ady
    if ax - radius < 0 or ax + radius > W:
        adx = -adx
    if ay - radius < 0 or ay + radius > H:
        ady = -ady

    bx += bdx
    by += bdy
    if bx - radius < 0 or bx + radius > W:
        bdx = -bdx
    if by - radius < 0 or by + radius > H:
        bdy = -bdy

    screen.fill((10, 10, 10))
    pygame.draw.circle(screen, (255, 80, 80),  (ax, ay), radius)
    pygame.draw.circle(screen, (80, 180, 255), (bx, by), radius)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

The boundary check uses ax - radius < 0 because the position is the centre of the circle, and the circle extends radius pixels in each direction.

Common mistakes.

  • Checking ax < 0 instead of ax - radius < 0. The centre can be at (5, ...) while the left edge of the ball is already off screen.

Problem 3 — Follow the mouse

Problem. A square moves toward the mouse position each frame, a fixed step at a time.

How to think about it. Compute dx = mx - rect.centerx and dy = my - rect.centery. These are the distances to travel. Each frame, move a fraction of that distance (a step smaller than the full gap) or stop when close enough.

Worked solution.

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Follow Mouse")
clock = pygame.time.Clock()

rect  = pygame.Rect(370, 270, 50, 50)
speed = 5

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    mx, my = pygame.mouse.get_pos()
    dx = mx - rect.centerx
    dy = my - rect.centery

    if abs(dx) > speed:
        rect.x += speed if dx > 0 else -speed
    else:
        rect.centerx = mx

    if abs(dy) > speed:
        rect.y += speed if dy > 0 else -speed
    else:
        rect.centery = my

    screen.fill((20, 20, 20))
    pygame.draw.rect(screen, (255, 200, 50), rect)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

speed if dx > 0 else -speed moves one step in the direction of the target. The else branch snaps to the target when the remaining gap is smaller than one step, preventing jittering.

Challenge — Collect a target

Problem. Player moves with arrow keys. Touching the target moves it to a random position.

How to think about it. Use pygame.Rect.colliderect for the collision. import random to pick the new position. The target's rect must stay fully inside the window — subtract target.width and target.height from the upper bound.

Worked solution.

import pygame
import random

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Collect Target")
clock = pygame.time.Clock()
W, H = 800, 600

player = pygame.Rect(370, 270, 50, 50)
target = pygame.Rect(random.randint(0, W - 40), random.randint(0, H - 40), 40, 40)
speed  = 5

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        player.x -= speed
    if keys[pygame.K_RIGHT]:
        player.x += speed
    if keys[pygame.K_UP]:
        player.y -= speed
    if keys[pygame.K_DOWN]:
        player.y += speed

    # clamp
    player.clamp_ip(pygame.Rect(0, 0, W, H))

    if player.colliderect(target):
        print("collected!")
        target.x = random.randint(0, W - target.width)
        target.y = random.randint(0, H - target.height)

    screen.fill((20, 20, 30))
    pygame.draw.circle(screen, (255, 220, 0),
                       (target.centerx, target.centery), target.width // 2)
    pygame.draw.rect(screen, (100, 200, 255), player)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

pygame.Rect.clamp_ip(boundary_rect) moves the rect so it fits inside the boundary — a built-in alternative to four separate edge checks.

Common mistakes.

  • Placing the target randomly at any x up to W without subtracting target.width, allowing part of the target to spawn off-screen.

Done?

Chapter 35 adds text rendering and score tracking to complete a basic game loop.