pyodide: loading…

[challenge]Game Development with Pygame

Brick Breakaway · Deep Dive & Demo Prep

# theory

how everything works

This is a deep breakdown of the Brick Breakaway game you built. Everything here maps to your actual code. Know this inside and out for the Zoom demo with Gerber.


the big picture

The game has 5 core systems working together:

  1. Platform: keyboard-controlled paddle at the bottom
  2. Ball: bounces around, destroys bricks, uses delta time
  3. Bricks: grid of targets with variable health + power-ups
  4. Power-Ups: special drops that modify gameplay (wider paddle, extra life, slow ball, multi-ball)
  5. Levels: 3 different brick layouts with increasing difficulty

Everything runs inside one game loop doing events → update → draw at 60fps.


delta time: why it matters

dt = clock.tick(60) / 1000.0  # seconds since last frame

Every movement in the game multiplies by dt. This is frame-rate independence. If your computer runs at 30fps instead of 60fps, dt is bigger, so things move farther per frame, keeping the actual speed the same.

Gerber will probably ask: "Why do you use delta time?" Your answer: "So the game runs the same speed on every machine. Without dt, faster computers would make the ball move faster. Multiplying by dt converts from pixels-per-frame to pixels-per-second."

# BAD; frame dependent
self.x += 5  # 5 pixels per frame = different speeds at different framerates

# GOOD; frame independent
self.x += self.speed * dt  # 500 pixels per second = same speed everywhere

ball physics: launch and bounce

Random Launch

angle_deg = random.uniform(-60, 60)        # random angle range
angle_rad = math.radians(angle_deg - 90)   # offset so 0 = straight up
self.dx = math.cos(angle_rad) * self.speed # horizontal component
self.dy = math.sin(angle_rad) * self.speed # vertical component (negative = up)

Why subtract 90? Pygame's angle system starts at 3 o'clock (right). We want 0 degrees to mean straight up, so we rotate the whole thing by -90 degrees.

Gerber might ask: "How does the random launch work?" Your answer: "I pick a random angle between -60 and 60 degrees from vertical, convert to radians, then use cos and sin to get the x and y velocity components. The ball always goes up but at a random horizontal angle."

Platform Bounce With Angle

hit_pos = (self.x - platform.x) / platform.width  # 0.0 to 1.0
self.dx = (hit_pos - 0.5) * self.speed * 2

This makes the ball bounce in different directions depending on WHERE it hits the paddle:

  • Hit the left edge → ball goes left (hit_pos near 0, so 0 - 0.5 = -0.5)
  • Hit the center → ball goes straight up (hit_pos = 0.5, so 0.5 - 0.5 = 0)
  • Hit the right edge → ball goes right (hit_pos near 1.0, so 1.0 - 0.5 = 0.5)

Gerber might ask: "How do you control the bounce angle?" Your answer: "I calculate where on the paddle the ball hit as a 0-to-1 ratio. Subtract 0.5 to center it, then multiply by speed to get the new horizontal velocity. Left side sends it left, right side sends it right."

Wall Bounces

if self.x - self.radius <= 0:       # left wall
    self.dx = abs(self.dx)           # force positive (rightward)
if self.x + self.radius >= SCREEN_WIDTH:  # right wall
    self.dx = -abs(self.dx)          # force negative (leftward)
if self.y - self.radius <= 0:       # top wall
    self.dy = abs(self.dy)           # force positive (downward)

Using abs() instead of *= -1 prevents the "stuck in wall" bug where the ball flips direction twice if it's deep inside the wall.


Brick collision detection

if ball_rect.colliderect(brick.get_rect()):
    # figure out which side was hit
    overlap_left = ball_rect.right - b.left
    overlap_right = b.right - ball_rect.left
    overlap_top = ball_rect.bottom - b.top
    overlap_bottom = b.bottom - ball_rect.top

    min_overlap = min(overlap_left, overlap_right, overlap_top, overlap_bottom)

    if min_overlap == overlap_top or min_overlap == overlap_bottom:
        ball.dy *= -1  # vertical bounce
    else:
        ball.dx *= -1  # horizontal bounce

How it works: When two rectangles overlap, the smallest overlap tells you which side the ball entered from. Top/bottom overlap → flip vertical velocity. Left/right overlap → flip horizontal velocity.

Why break after one collision? If the ball hits the corner where two bricks meet, processing both in the same frame would flip the velocity twice (canceling out). One brick per frame avoids this.

Gerber might ask: "How does your collision detection work?" Your answer: "I use colliderect() to check if the ball overlaps a brick. Then I calculate the overlap on all four sides. The smallest overlap tells me which side the ball came from, and I flip the appropriate velocity component."


power-up system

Brick Types

Power-up bricks look different (colored with a letter symbol) and drop a collectible when destroyed:

TypeColorSymbolEffect
Wide PaddleCyanWPlatform +40px wider (max 220px)
Extra LifePink++1 life immediately
Slow BallPurpleSBall speed -80 px/sec (min 250)
Multi BallGreenMSpawns second ball going opposite direction

How Drops Work

class PowerUpDrop:
    def update(self, dt):
        self.y += self.speed * dt  # falls down at 150 px/sec
        if self.y > SCREEN_HEIGHT:
            self.alive = False      # missed it, gone

When a power-up brick dies, a PowerUpDrop spawns at the brick's position and falls. If the platform catches it (colliderect), the effect applies. If it falls off screen, it's gone.

Gerber might ask: "How do power-ups work in your game?" Your answer: "Certain bricks are randomly tagged as power-up bricks when the level loads. When you break one, it spawns a falling drop object. If you catch it with the paddle, the power-up activates. There are four types: wider paddle, extra life, slower ball, and multi-ball."

Multi-Ball Implementation

extra = Ball()
extra.x = ball.x
extra.y = ball.y
extra.dx = -ball.dx  # opposite horizontal direction
extra.dy = ball.dy
extra.launched = True
game_state["extra_balls"].append(extra)

Spawns a second ball at the same position going the opposite horizontal direction. Extra balls are tracked separately and get cleaned up when they fall off screen. Only the main ball costs lives when lost.


level system

Three level functions return different brick layouts:

Level 1; Standard Grid: 10×5 bricks, top rows have more HP (row 0 = 5HP, row 4 = 1HP)

Level 2; Diamond Pattern: Bricks arranged in a diamond shape. Center bricks have higher health. Uses a rows_config = [2, 4, 6, 8, 10, 8, 6, 4, 2] array to define width per row.

Level 3; Alternating Gaps: 8 rows, odd rows skip every other brick creating a gap pattern. Higher health in lower rows. Harder because the gaps make precision bounces more important.

Each level randomly places 4-6 power-up bricks. Ball speed increases by 30 px/sec per level. Clearing all bricks gives a 100-point level bonus.

Gerber might ask: "How do your levels work?" Your answer: "Each level is a function that returns a list of Brick objects with different positions and health values. When you clear all bricks, the game loads the next level function. Ball speed increases slightly per level. After all three levels, you win."


background: procedural generation

Instead of loading an image file, the background is generated with code:

def create_background():
    bg = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
    for y in range(SCREEN_HEIGHT):
        ratio = y / SCREEN_HEIGHT
        r = int(10 + ratio * 15)
        g = int(15 + ratio * 10)
        b = int(40 + ratio * 20)
        pygame.draw.line(bg, (r, g, b), (0, y), (SCREEN_WIDTH, y))
    # add stars
    for _ in range(80):
        sx = random.randint(0, SCREEN_WIDTH)
        sy = random.randint(0, SCREEN_HEIGHT - 100)
        pygame.draw.circle(bg, (brightness, brightness, brightness + 30), (sx, sy), size)
    return bg

Draws a vertical gradient line by line (dark blue getting darker toward the bottom), then scatters random "stars" on top. Generated once at startup, blitted every frame.

If Gerber asks why you didn't use an image file: "I wanted the background to be self-contained in the code so there's no external file dependency. The gradient generates in milliseconds at startup and gets cached as a surface."

If you want to swap in a real image: Replace background = create_background() with:

background = pygame.image.load("background.png")
background = pygame.transform.scale(background, (SCREEN_WIDTH, SCREEN_HEIGHT))

Brick sprites: cached rendering

BRICK_SPRITES = {}  # cache

def create_brick_sprite(width, height, color, health):
    surf = pygame.Surface((width, height), pygame.SRCALPHA)
    pygame.draw.rect(surf, color, (0, 0, width, height), border_radius=4)
    # highlight on top
    highlight = tuple(min(c + 50, 255) for c in color)
    pygame.draw.rect(surf, highlight, (2, 2, width - 4, 4), border_radius=2)
    # shadow on bottom
    shadow = tuple(max(c - 40, 0) for c in color)
    pygame.draw.rect(surf, shadow, (2, height - 5, width - 4, 3), border_radius=2)
    return surf

Each unique brick appearance (health level + power-up type) gets rendered once and cached in BRICK_SPRITES. After the first frame, bricks are just blitting pre-rendered surfaces instead of calling draw.rect() every frame.

Gerber might ask: "Why cache the sprites?" Your answer: "So I'm not re-drawing highlight and shadow effects on 50+ bricks every frame. I render each unique appearance once and reuse the surface. It's a performance optimization."


common Gerber questions: quick answers

"Walk me through the game loop." "Events first; check for quit, spacebar launch, spacebar restart. Then update; move platform, move ball, check collisions, check if ball fell off screen. Then draw; background, platform, ball, bricks, HUD. Then flip the display."

"What happens when the ball falls off screen?" "I check ball.is_off_screen() which returns true if the ball's y position is below the screen height. If so, decrement lives. If lives hit 0, game over. Otherwise reset the ball to follow the platform and reset paddle width."

"How do you handle multiple balls?" "Extra balls from the multi-ball power-up are stored in a list. They get the same update and collision logic as the main ball. When they fall off screen they just get removed from the list, no life lost."

"What's the hardest part of this project?" "Getting the collision direction right. The overlap method works but it can be finicky at corners. I used a break after the first collision to avoid double-bounces."

"How would you add more levels?" "Just write another function that returns a list of Brick objects and add it to the LEVELS array. The level progression code handles the rest automatically."

"How would you add sound?" "Import pygame.mixer, load .wav files at startup with pygame.mixer.Sound(), and call .play() on collision events. Background music with pygame.mixer.music.load() and .play(-1) for looping."

# examples [2]

# example 01 · delta time pattern

The foundational pattern that makes everything consistent. Know this cold.

1
2
3
4
5
6
7
8
9
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# example 02 · collision direction detection

Minimum overlap tells you which side the ball entered from.

1
2
3
4
5
6
7
8
9
10
11
12
13
🐍
Loading PythonSetting up pandas & numpy...

pygame needs a real window — copy this into a .py file and run it locally.

# challenges [4]

# challenge 01/04todo
Why do you use abs(self.dx) instead of self.dx *= -1 for wall bounces? What bug does it prevent?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
🐍
Loading PythonSetting up pandas & numpy...
# challenge 02/04todo
How does the platform bounce angle work? What does hit_pos represent and why subtract 0.5?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
🐍
Loading PythonSetting up pandas & numpy...
# challenge 03/04todo
Why does check_ball_brick_collisions() use 'break' after hitting one brick?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
🐍
Loading PythonSetting up pandas & numpy...
# challenge 04/04todo
How does the multi-ball power-up work? Why does losing an extra ball NOT cost a life?
pygame needs a real window. copy this into a .py file and run it locally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
🐍
Loading PythonSetting up pandas & numpy...