Learning Goals
3 minBy the end of this lesson you can:
- Build a list of
Rectobjects with a list comprehension to form a brick wall. - Detect ball-to-brick collisions with
colliderect, remove the brick, and add to the score. - Combine the paddle, ball, brick wall, and score into a complete single-file game.
Warm-Up · Grid of Rects
5 minThe brick wall is a list of Rect objects. Predict how many rects this comprehension produces and where the first and last ones are:
BW, BH = 60, 20 GAP = 4 bricks = [ Rect((col * (BW + GAP) + 10, row * (BH + GAP) + 50), (BW, BH)) for row in range(4) for col in range(8) ] print(len(bricks)) print(bricks[0]) print(bricks[-1])
Show the answer
Output
32 Rect(10, 50, 60, 20) Rect(514, 122, 60, 20)
4 rows × 8 columns = 32 bricks. Top-left brick starts at (10, 50); bottom-right is at (514, 122).
One line creates all 32 rects. Adding a row later is as easy as changing range(4) to range(5).
New Concept · Ball-Brick Collision
12 minA brick wall is like a list of soap bubbles. When the ball touches any bubble, it pops (gets removed) and the ball bounces off. The collision check is one for loop with a colliderect inside it.
Checking all bricks each frame
ball_rect = Rect((int(bx) - BR, int(by) - BR), (BR * 2, BR * 2)) for brick in bricks[:]: # copy so remove is safe if ball_rect.colliderect(brick): bricks.remove(brick) # destroy score += 10 # score vy = -vy # bounce vertically break # only one brick per frame
Why break?
Without break the ball could destroy several adjacent bricks in one frame and flip vy multiple times, making the bounce direction unpredictable. One brick per frame keeps the physics honest.
Ball-paddle collision
paddle_rect = Rect((paddle_x - PAD_W // 2, HEIGHT - 30), (PAD_W, 12)) if ball_rect.colliderect(paddle_rect): vy = -abs(vy) # always move upward after paddle hit
Worked Example · Brick Breaker (full build)
12 minStage 1 · Paddle + ball (no bricks yet)
# brick_breaker.py — stage 1: paddle + ball import pgzrun WIDTH = 600 HEIGHT = 450 TITLE = "Brick Breaker" PAD_W, PAD_H = 90, 12 BR = 9 bx, by = WIDTH // 2, HEIGHT // 2 vx, vy = 3, -4 paddle_x = WIDTH // 2 score = 0 lives = 3
def draw(): screen.fill("black") screen.draw.filled_rect( Rect((paddle_x - PAD_W // 2, HEIGHT - 30), (PAD_W, PAD_H)), "deepskyblue") screen.draw.filled_circle((int(bx), int(by)), BR, "white") screen.draw.text(f"Score: {score} Lives: {lives}", topleft=(8, 8), fontsize=26, color="white") def update(): global bx, by, vx, vy, paddle_x, lives if keyboard.left and paddle_x > PAD_W // 2: paddle_x -= 6 if keyboard.right and paddle_x < WIDTH - PAD_W // 2: paddle_x += 6 bx += vx by += vy if bx <= BR or bx >= WIDTH - BR: vx = -vx if by <= BR: vy = abs(vy) pad_r = Rect((paddle_x - PAD_W // 2, HEIGHT - 30), (PAD_W, PAD_H)) ball_r = Rect((int(bx) - BR, int(by) - BR), (BR * 2, BR * 2)) if ball_r.colliderect(pad_r): vy = -abs(vy) if by > HEIGHT: lives -= 1 bx, by = WIDTH // 2, HEIGHT // 2 vx, vy = 3, -4 pgzrun.go()
Stage 2 · Add the brick wall
Add these lines near the top (after the lives variable), then extend update() with the brick loop, and add the brick drawing to draw().
BW, BH = 58, 18 GAP = 4 BRICK_COLOURS = ["tomato", "orange", "gold", "limegreen"] bricks = [ Rect((col * (BW + GAP) + 10, row * (BH + GAP) + 50), (BW, BH)) for row in range(4) for col in range(8) ]
# inside draw(), after drawing the paddle: for i, brick in enumerate(bricks): colour = BRICK_COLOURS[i % len(BRICK_COLOURS)] screen.draw.filled_rect(brick, colour) # inside update(), after paddle collision: ball_r = Rect((int(bx) - BR, int(by) - BR), (BR * 2, BR * 2)) for brick in bricks[:]: if ball_r.colliderect(brick): bricks.remove(brick) score += 10 vy = -vy break
What you'll see
Try It Yourself
13 minWhen lives reaches 0, stop moving the ball and display "Game Over" in the centre. Use a game_over boolean flag.
# inside draw(): if game_over: screen.draw.text("Game Over", center=(WIDTH // 2, HEIGHT // 2), fontsize=56, color="red") # inside update(), at the very start: if game_over: return
When bricks is empty, display "Hebat! You Win!" ("Hebat" = great in Malay) in gold and stop the ball. Also reload a new brick wall so the player can keep going.
# inside update(), after the brick loop: if not bricks and not game_over: # repopulate and nudge speed up slightly bricks.extend([ Rect((col * (BW + GAP) + 10, row * (BH + GAP) + 50), (BW, BH)) for row in range(4) for col in range(8) ])
Mini-Challenge · Power-Up Brick
8 minWei Jie wants a special "power-up brick" that widens the paddle when hit. His code crashes because he used an index that is out of range. Find and fix the bug, then make the power-up work.
# wei_jie_powerup.py — buggy
import pgzrun
import random
WIDTH = 600
HEIGHT = 450
BW, BH, GAP = 58, 18, 4
bricks = [
Rect((col * (BW + GAP) + 10, row * (BH + GAP) + 50), (BW, BH))
for row in range(4) for col in range(8)
]
power_idx = random.randint(0, len(bricks)) # BUG!
PAD_W = 90
bx, by = 300, 300
vx, vy = 3, -4
BR = 9
score = 0
paddle_x = 300
def draw():
screen.fill("black")
for i, brick in enumerate(bricks):
col = "gold" if i == power_idx else "tomato"
screen.draw.filled_rect(brick, col)
screen.draw.filled_circle((int(bx), int(by)), BR, "white")
screen.draw.filled_rect(
Rect((paddle_x - PAD_W // 2, HEIGHT - 30), (PAD_W, 12)), "deepskyblue")
def update():
global bx, by, vx, vy, PAD_W, score
bx += vx
by += vy
if bx <= BR or bx >= WIDTH - BR:
vx = -vx
if by <= BR:
vy = abs(vy)
ball_r = Rect((int(bx) - BR, int(by) - BR), (BR * 2, BR * 2))
for i, brick in enumerate(bricks[:]):
if ball_r.colliderect(brick):
if i == power_idx:
PAD_W = min(PAD_W + 20, 200)
bricks.remove(brick)
score += 10
vy = -vy
break
pgzrun.go()It works if…
the game launches without error; the gold brick widens the paddle when hit
Show the fix
# fix: randint upper bound must be len-1 power_idx = random.randint(0, len(bricks) - 1)
random.randint(a, b) is inclusive on both ends. With len(bricks) = 32, the original code could produce index 32, which does not exist (valid indices are 0–31).
Recap
3 minBrick Breaker combines everything learnt so far: a player-controlled paddle, a bouncing ball with vx/vy, and a list of Rect bricks that are destroyed and scored on collision. Iterating a copy of the list ([:]) and using break after the first hit keeps the physics clean.
Vocabulary Card
- list comprehension (nested)
- Two
forclauses in one[...]expression — used here to build a grid of rects in a single readable line. - break
- Stops the
forloop immediately. Used after a ball-brick hit so only one brick is destroyed per frame. - lives
- A counter decremented each time the ball passes the paddle; when it reaches 0 the game ends.
- random.randint(a, b)
- Returns a random integer from a to b inclusive. Correct upper bound for a list of length n is
n - 1.
Homework
4 minExtend Brick Breaker with a "hard row": add a fifth row of bricks at the top that takes two hits to destroy (store a separate tough_bricks list and a matching hits dictionary). Each tough brick starts grey; it turns orange after the first hit and disappears on the second. Save as brick_hard.py.
Sample · brick_hard.py
# brick_hard.py — two-hit bricks in row 0 import pgzrun WIDTH = 600 HEIGHT = 450 TITLE = "Brick Breaker — Hard Row" PAD_W, PAD_H = 90, 12 BR = 9 bx, by = WIDTH // 2, HEIGHT // 2 vx, vy = 3, -4 paddle_x = WIDTH // 2 score = 0 lives = 3 BW, BH, GAP = 58, 18, 4 # normal bricks (rows 1-4) bricks = [ Rect((col * (BW + GAP) + 10, row * (BH + GAP) + 70), (BW, BH)) for row in range(4) for col in range(8) ] # tough bricks (row 0) tough_bricks = [ Rect((col * (BW + GAP) + 10, 50), (BW, BH)) for col in range(8) ] hits = {id(b): 0 for b in tough_bricks} def draw(): screen.fill("black") for brick in bricks: screen.draw.filled_rect(brick, "tomato") for brick in tough_bricks: colour = "orange" if hits[id(brick)] == 1 else "grey" screen.draw.filled_rect(brick, colour) screen.draw.filled_rect( Rect((paddle_x - PAD_W // 2, HEIGHT - 30), (PAD_W, PAD_H)), "deepskyblue") screen.draw.filled_circle((int(bx), int(by)), BR, "white") screen.draw.text(f"Score: {score} Lives: {lives}", topleft=(8, 8), fontsize=26, color="white") def update(): global bx, by, vx, vy, paddle_x, score, lives if keyboard.left and paddle_x > PAD_W // 2: paddle_x -= 6 if keyboard.right and paddle_x < WIDTH - PAD_W // 2: paddle_x += 6 bx += vx by += vy if bx <= BR or bx >= WIDTH - BR: vx = -vx if by <= BR: vy = abs(vy) ball_r = Rect((int(bx) - BR, int(by) - BR), (BR * 2, BR * 2)) pad_r = Rect((paddle_x - PAD_W // 2, HEIGHT - 30), (PAD_W, PAD_H)) if ball_r.colliderect(pad_r): vy = -abs(vy) for brick in bricks[:]: if ball_r.colliderect(brick): bricks.remove(brick) score += 10 vy = -vy break for brick in tough_bricks[:]: if ball_r.colliderect(brick): hits[id(brick)] += 1 if hits[id(brick)] >= 2: tough_bricks.remove(brick) score += 20 vy = -vy break if by > HEIGHT: lives -= 1 bx, by = WIDTH // 2, HEIGHT // 2 vx, vy = 3, -4 pgzrun.go()
Using id(brick) as a dictionary key maps each Rect object to its hit count without needing a wrapper class. Your colours and layout can vary.