๐Ÿ My First Python Game ยท Episode 6 of 7 ยท See All Episodes
โšก Episode 6 ยท Improver+ ยท Upgrades Episode 5

Power Up!
Breakout Part 2

Open your Episode 5 code, we are upgrading it. Falling power-ups (wide paddle, multi-ball, slow-mo), tough two-hit bricks, multiple levels and screen shake. This is how a demo becomes a game.

๐Ÿ‘ถ Ages 10+ โฑ๏ธ ~1.5 Hours ๐Ÿ Python โœ“ Free
๐ŸŽ Power-up drops โ†”๏ธ Wide paddle โšช Multi-ball ๐Ÿงฑ Tough bricks ๐Ÿ—บ๏ธ Level patterns ๐Ÿ“ณ Screen shake
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
๐Ÿงฉ
Need the game so far? This episode continues your Breakout from Part 1, so it assumes you already have that pygame window, paddle, ball and loop. Open your Part 1 file to keep building, or revisit Part 1 and the base template here: Breakout Part 1 โ†’ ยท Python Cheatsheet โ†’
1
๐ŸŽ
Power-Up Drops
20% of broken bricks drop a falling capsule
Active
๐ŸŽฏ
Goal for this step

Make destroyed bricks sometimes drop a capsule that falls toward the paddle.

  • 1Open your finished Episode 5 file, everything here extends it.
  • 2New list: powerups = []. When a brick dies, 20% chance to append one at the brickโ€™s position.
  • 3Each is a dict: rect + kind, picked randomly from "wide", "multi", "slow".
  • 4They fall 3 px/frame; delete off-screen ones. Draw as coloured capsules with a letter.
game.py
powerups = []
KINDS = ["wide", "multi", "slow"]

# when a brick is destroyed:
if random.random() < 0.2:
    powerups.append({
        "rect": pygame.Rect(b["rect"].centerx - 12, b["rect"].y, 24, 24),
        "kind": random.choice(KINDS)
    })

for p in powerups[:]:
    p["rect"].y += 3
    if p["rect"].top > 520:
        powerups.remove(p)powerups = []
KINDS = ["wide", "multi", "slow"]

# when a brick is destroyed:
if random.random() < 0.2:
    powerups.append({
        "rect": pygame.Rect(b["rect"].centerx - 12, b["rect"].y, 24, 24),
        "kind": random.choice(KINDS)
    })

for p in powerups[:]:
    p["rect"].y += 3
    if p["rect"].top > 520:
        powerups.remove(p)
๐Ÿ“ฆ Starting point , your finished Episode 5 Breakout

This episode extends Episode 5. If you don't have your Part 1 file handy, here is the complete finished Breakout to start from. Every step below adds power-ups, multi-ball and juice to it.

game.py
import pygame
pygame.init()
screen = pygame.display.set_mode((700, 520))
pygame.display.set_caption("Brick Breaker")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 64)

colours = [(248, 113, 113), (251, 146, 60), (250, 204, 21), (74, 222, 128), (96, 165, 250)]

def build_bricks():
    b = []
    for row in range(5):
        for col in range(10):
            b.append({"rect": pygame.Rect(col*68 + 12, row*26 + 50, 64, 22), "row": row})
    return b

paddle = pygame.Rect(295, 480, 110, 14)
ball = pygame.Rect(340, 300, 14, 14)
bvx, bvy = 4, -4
bricks = build_bricks()
score = 0
lives = 3
state = "PLAY"

def reset():
    global paddle, ball, bvx, bvy, bricks, score, lives, state
    paddle = pygame.Rect(295, 480, 110, 14)
    ball = pygame.Rect(340, 300, 14, 14)
    bvx, bvy = 4, -4
    bricks = build_bricks()
    score = 0
    lives = 3
    state = "PLAY"

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_r and state != "PLAY":
            reset()

    if state == "PLAY":
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            paddle.x -= 8
        if keys[pygame.K_RIGHT]:
            paddle.x += 8
        paddle.x = max(0, min(700 - paddle.width, paddle.x))

        ball.x += bvx
        ball.y += bvy
        if ball.left <= 0 or ball.right >= 700:
            bvx = -bvx
        if ball.top <= 0:
            bvy = -bvy

        if ball.colliderect(paddle) and bvy > 0:
            bvy = -bvy
            offset = ball.centerx - paddle.centerx
            bvx = offset // 12
            if bvx == 0:
                bvx = 1

        for b in bricks[:]:
            if ball.colliderect(b["rect"]):
                bricks.remove(b)
                score += (5 - b["row"]) * 10
                bvy = -bvy
                break

        if ball.top > 520:
            lives -= 1
            ball.center = (350, 300)
            bvx, bvy = 4, -4
            if lives == 0:
                state = "LOSE"

        if len(bricks) == 0:
            state = "WIN"

    # draw
    screen.fill((25, 30, 60))
    for b in bricks:
        pygame.draw.rect(screen, colours[b["row"]], b["rect"])
    pygame.draw.rect(screen, (255, 119, 0), paddle)
    pygame.draw.ellipse(screen, (255, 255, 255), ball)
    screen.blit(font.render("Score: " + str(score), True, (255, 255, 255)), (12, 8))
    screen.blit(font.render("Lives: " + str(max(0, lives)), True, (255, 255, 255)), (560, 8))
    if state != "PLAY":
        text = "YOU WIN!" if state == "WIN" else "GAME OVER"
        msg = big_font.render(text, True, (255, 209, 102))
        screen.blit(msg, msg.get_rect(center=(350, 235)))
        sub = font.render("Press R to play again", True, (255, 255, 255))
        screen.blit(sub, sub.get_rect(center=(350, 290)))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()import pygame
pygame.init()
screen = pygame.display.set_mode((700, 520))
pygame.display.set_caption("Brick Breaker")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 64)

colours = [(248, 113, 113), (251, 146, 60), (250, 204, 21), (74, 222, 128), (96, 165, 250)]

def build_bricks():
    b = []
    for row in range(5):
        for col in range(10):
            b.append({"rect": pygame.Rect(col*68 + 12, row*26 + 50, 64, 22), "row": row})
    return b

paddle = pygame.Rect(295, 480, 110, 14)
ball = pygame.Rect(340, 300, 14, 14)
bvx, bvy = 4, -4
bricks = build_bricks()
score = 0
lives = 3
state = "PLAY"

def reset():
    global paddle, ball, bvx, bvy, bricks, score, lives, state
    paddle = pygame.Rect(295, 480, 110, 14)
    ball = pygame.Rect(340, 300, 14, 14)
    bvx, bvy = 4, -4
    bricks = build_bricks()
    score = 0
    lives = 3
    state = "PLAY"

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_r and state != "PLAY":
            reset()

    if state == "PLAY":
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            paddle.x -= 8
        if keys[pygame.K_RIGHT]:
            paddle.x += 8
        paddle.x = max(0, min(700 - paddle.width, paddle.x))

        ball.x += bvx
        ball.y += bvy
        if ball.left <= 0 or ball.right >= 700:
            bvx = -bvx
        if ball.top <= 0:
            bvy = -bvy

        if ball.colliderect(paddle) and bvy > 0:
            bvy = -bvy
            offset = ball.centerx - paddle.centerx
            bvx = offset // 12
            if bvx == 0:
                bvx = 1

        for b in bricks[:]:
            if ball.colliderect(b["rect"]):
                bricks.remove(b)
                score += (5 - b["row"]) * 10
                bvy = -bvy
                break

        if ball.top > 520:
            lives -= 1
            ball.center = (350, 300)
            bvx, bvy = 4, -4
            if lives == 0:
                state = "LOSE"

        if len(bricks) == 0:
            state = "WIN"

    # draw
    screen.fill((25, 30, 60))
    for b in bricks:
        pygame.draw.rect(screen, colours[b["row"]], b["rect"])
    pygame.draw.rect(screen, (255, 119, 0), paddle)
    pygame.draw.ellipse(screen, (255, 255, 255), ball)
    screen.blit(font.render("Score: " + str(score), True, (255, 255, 255)), (12, 8))
    screen.blit(font.render("Lives: " + str(max(0, lives)), True, (255, 255, 255)), (560, 8))
    if state != "PLAY":
        text = "YOU WIN!" if state == "WIN" else "GAME OVER"
        msg = big_font.render(text, True, (255, 209, 102))
        screen.blit(msg, msg.get_rect(center=(350, 235)))
        sub = font.render("Press R to play again", True, (255, 255, 255))
        screen.blit(sub, sub.get_rect(center=(350, 290)))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()
โœ๏ธ
Fill in the Blanks
+15 XP
A destroyed brick drops a capsule with probability 0.2, i.e. %. The capsule kind is picked with random.(KINDS).
๐Ÿง 
Knowledge Check
+15 XP
Why do power-ups FALL instead of activating instantly?
AFalling is easier to code
BCatching them is a choice and a risk, chase the capsule or stay safe for the ball?
CInstant effects crash Pygame
2
โ†”๏ธ
Catching: Wide Paddle
Catch a capsule, grow the paddle, temporarily
Locked
๐ŸŽฏ
Goal for this step

Implement catching and your first timed effect: a wider paddle.

  • 1Paddle-capsule collision: apply the effect, remove the capsule.
  • 2"wide": paddle width becomes 170 and a frame timer starts: wide_timer = 600 (10 seconds).
  • 3Each frame tick it down; at 0, width back to 110. Keep the paddle centred when resizing (adjust x by half the change).
  • 4Draw the timer as a shrinking bar so players see it running out.
game.py
wide_timer = 0

for p in powerups[:]:
    if p["rect"].colliderect(paddle):
        if p["kind"] == "wide":
            if wide_timer == 0:
                paddle.x -= 30        # keep centred
                paddle.width = 170
            wide_timer = 600          # 10 seconds
        powerups.remove(p)

if wide_timer > 0:
    wide_timer -= 1
    if wide_timer == 0:
        paddle.x += 30
        paddle.width = 110wide_timer = 0

for p in powerups[:]:
    if p["rect"].colliderect(paddle):
        if p["kind"] == "wide":
            if wide_timer == 0:
                paddle.x -= 30        # keep centred
                paddle.width = 170
            wide_timer = 600          # 10 seconds
        powerups.remove(p)

if wide_timer > 0:
    wide_timer -= 1
    if wide_timer == 0:
        paddle.x += 30
        paddle.width = 110
โŒจ๏ธ
Code Challenge
+20 XP
Tick the effect down and end it cleanly:
game.py
if wide_timer > 0:
    wide_timer -= if wide_timer > 0:
    wide_timer -= 
    if wide_timer == 0:
        paddle.width = 
    if wide_timer == 0:
        paddle.width = 
๐Ÿ’ก Hint: Count down one per frame; when it reaches zero restore the original width.
๐Ÿง 
Knowledge Check
+15 XP
Catching a second "wide" while one is active just sets wide_timer = 600 again. What does this prevent?
ANothing, it is decoration
BThe paddle growing twice (width 230!), the guard means refresh, not stack
CSlower frame rates
3
โšช
Multi-Ball!
Refactor to a list of balls, then split into three
Locked
๐ŸŽฏ
Goal for this step

The big refactor: many balls at once, and losing a life only when the LAST drops.

  • 1Change the single ball into a list of dicts: balls = [{"rect": ..., "vx": 4, "vy": -4}].
  • 2Wrap ALL ball logic (move, walls, paddle, bricks) in for bl in balls[:]:, mostly re-indenting.
  • 3"multi" effect: for each current ball add two clones with vx -3 and +3.
  • 4A dropped ball is just removed, you lose a life only when balls is empty, then serve a fresh one.
game.py
balls = [{"rect": pygame.Rect(340, 300, 14, 14), "vx": 4, "vy": -4}]

# "multi" effect:
for bl in balls[:]:
    for new_vx in (-3, 3):
        balls.append({"rect": bl["rect"].copy(),
                      "vx": new_vx, "vy": bl["vy"]})

# dropped ball:
for bl in balls[:]:
    if bl["rect"].top > 520:
        balls.remove(bl)
if len(balls) == 0:
    lives -= 1
    balls.append({"rect": pygame.Rect(340, 300, 14, 14), "vx": 4, "vy": -4})balls = [{"rect": pygame.Rect(340, 300, 14, 14), "vx": 4, "vy": -4}]

# "multi" effect:
for bl in balls[:]:
    for new_vx in (-3, 3):
        balls.append({"rect": bl["rect"].copy(),
                      "vx": new_vx, "vy": bl["vy"]})

# dropped ball:
for bl in balls[:]:
    if bl["rect"].top > 520:
        balls.remove(bl)
if len(balls) == 0:
    lives -= 1
    balls.append({"rect": pygame.Rect(340, 300, 14, 14), "vx": 4, "vy": -4})
โœ๏ธ
Fill in the Blanks
+15 XP
After the refactor every ball is a in the balls list. A life is only lost when len(balls) == .
๐Ÿง 
Knowledge Check
+15 XP
This refactor (one thing โ†’ list of things) is the same one you did in which earlier game?
AThe maze ghost
BFalling gems, one object becomes a list, logic wraps in a loop
CThe fruit basket
4
๐Ÿงฑ
Tough Bricks & Slow-Mo
Two-hit silver bricks and the slow-motion catch
Locked
๐ŸŽฏ
Goal for this step

Add bricks with hit points and implement the "slow" power-up.

  • 1Give each brick "hp": 1, but the top row spawns with hp 2 (silver).
  • 2A hit now does hp -= 1; only remove at 0. Silver bricks darken when damaged, visible feedback.
  • 3"slow" effect: a slow_timer (400 frames), while active, balls move HALF speed each frame (move every other frame using a frame counter, keeping velocities intact).
  • 4Draw silver bricks grey, damaged ones darker grey.
game.py
# building the wall:
hp = 2 if row == 0 else 1
bricks.append({"rect": ..., "row": row, "hp": hp})

# on hit:
b["hp"] -= 1
if b["hp"] == 0:
    bricks.remove(b)
    ...

# slow effect (move balls every other frame):
frame += 1
move_balls = (slow_timer == 0) or (frame % 2 == 0)# building the wall:
hp = 2 if row == 0 else 1
bricks.append({"rect": ..., "row": row, "hp": hp})

# on hit:
b["hp"] -= 1
if b["hp"] == 0:
    bricks.remove(b)
    ...

# slow effect (move balls every other frame):
frame += 1
move_balls = (slow_timer == 0) or (frame % 2 == 0)
โŒจ๏ธ
Code Challenge
+20 XP
Damage a brick and only destroy it at zero HP:
game.py
b["hp"] -= b["hp"] -= 
if b["hp"] == 
if b["hp"] == :
    bricks.:
    bricks.(b)(b)
๐Ÿ’ก Hint: Reduce the hit points by one; removal only happens when none remain.
๐Ÿง 
Knowledge Check
+15 XP
Why slow the balls by skipping every other frame instead of halving vx/vy?
AIt looks cooler
BInteger velocities like 3 halve badly (1.5 โ†’ rounding bugs); frame-skipping keeps exact velocities
CPygame cannot halve numbers
5
๐Ÿ—บ๏ธ
Level Patterns
Design walls with ASCII art strings
Locked
๐ŸŽฏ
Goal for this step

Define multiple levels as text patterns and progress through them.

  • 1Levels as strings: each line a row, # = brick, 2 = silver, . = gap.
  • 2A build_wall(pattern) function converts text to the bricks list.
  • 3This is data-driven design: adding a level means typing a picture, not code.
  • 4Clearing a wall loads the next pattern, +1 ball speed. After the last: the grand victory.
game.py
LEVELS = [
    [ "##########",
      "##########",
      ".########.",
      "..######..",],
    [ "2222222222",
      "#.#.#.#.#.",
      ".#.#.#.#.#",
      "##########",],
]

def build_wall(pattern):
    wall = []
    for r, line in enumerate(pattern):
        for c, ch in enumerate(line):
            if ch != ".":
                wall.append({"rect": pygame.Rect(c*68+12, r*26+50, 64, 22),
                             "row": r, "hp": 2 if ch == "2" else 1})
    return wall

bricks = build_wall(LEVELS[0])LEVELS = [
    [ "##########",
      "##########",
      ".########.",
      "..######..",],
    [ "2222222222",
      "#.#.#.#.#.",
      ".#.#.#.#.#",
      "##########",],
]

def build_wall(pattern):
    wall = []
    for r, line in enumerate(pattern):
        for c, ch in enumerate(line):
            if ch != ".":
                wall.append({"rect": pygame.Rect(c*68+12, r*26+50, 64, 22),
                             "row": r, "hp": 2 if ch == "2" else 1})
    return wall

bricks = build_wall(LEVELS[0])
โœ๏ธ
Fill in the Blanks
+15 XP
In the pattern strings, # is a normal brick, 2 is a two-hit brick, and . is a . The converter function is called .
๐Ÿง 
Knowledge Check
+15 XP
The biggest advantage of levels-as-text isโ€ฆ
AStrings are fast
BAnyone (even non-coders) can design a level by drawing with characters, data-driven design
CIt removes all bugs
6
๐Ÿ“ณ
Juice: Shake & Flash
Screen shake on brick breaks, flash on power-ups
Locked
๐ŸŽฏ
Goal for this step

Add game-feel effects that make every hit feel chunky.

  • 1Screen shake: draw everything onto an offscreen Surface, then blit it at a small random offset while shake > 0.
  • 2Set shake = 6 on every brick break, tick it down per frame.
  • 3Flash: on power-up catch, set flash = 8 and overlay a translucent white rect while it ticks down.
  • 4Play it. Feel the difference. That is juice, and your Breakout is DONE. ๐ŸŽ‰
game.py
world = pygame.Surface((700, 520))
shake = 0

# draw everything onto world instead of screen, then:
offset = (0, 0)
if shake > 0:
    shake -= 1
    offset = (random.randint(-4, 4), random.randint(-4, 4))
screen.blit(world, offset)
pygame.display.flip()world = pygame.Surface((700, 520))
shake = 0

# draw everything onto world instead of screen, then:
offset = (0, 0)
if shake > 0:
    shake -= 1
    offset = (random.randint(-4, 4), random.randint(-4, 4))
screen.blit(world, offset)
pygame.display.flip()
โŒจ๏ธ
Code Challenge
+20 XP
Blit the world with a shake offset:
game.py
if shake > 0:
    shake -= 1
    offset = (random.randint(if shake > 0:
    shake -= 1
    offset = (random.randint(, 4), random.randint(-4, , 4), random.randint(-4, ))
screen.blit())
screen.blit(, offset), offset)
๐Ÿ’ก Hint: The offset wobbles between -4 and 4 in both axes; blit the offscreen surface, not the screen.
๐Ÿง 
Knowledge Check
+15 XP
Why draw to an offscreen surface for screen shake?
AScreens cannot be moved
BShifting ONE pre-drawn surface moves everything together cheaply, no need to offset every draw call
CSurfaces are prettier
โœ… See the complete finished Breakout Deluxe , all 6 steps assembled

Falling power-up capsules (wide paddle, multi-ball, slow-mo), a multi-ball refactor, two-hit silver bricks, ASCII-pattern levels, and screen-shake & flash juice , one file. Save it as game.pygame.py, run python game.pypython game.py, and play. Stuck on a step? Compare your file against this.

game.py
import pygame, random
pygame.init()
screen = pygame.display.set_mode((700, 520))
world = pygame.Surface((700, 520))
pygame.display.set_caption("Brick Breaker Deluxe")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 64)

colours = [(248, 113, 113), (251, 146, 60), (250, 204, 21), (74, 222, 128), (96, 165, 250)]
KINDS = ["wide", "multi", "slow"]
PU_COLOUR = {"wide": (96, 165, 250), "multi": (74, 222, 128), "slow": (250, 204, 21)}

LEVELS = [
    ["##########",
     "##########",
     ".########.",
     "..######.."],
    ["2222222222",
     "#.#.#.#.#.",
     ".#.#.#.#.#",
     "##########"],
]

def build_wall(pattern):
    wall = []
    for r, line in enumerate(pattern):
        for c, ch in enumerate(line):
            if ch != ".":
                wall.append({"rect": pygame.Rect(c*68 + 12, r*26 + 50, 64, 22),
                             "row": r, "hp": 2 if ch == "2" else 1, "silver": ch == "2"})
    return wall

def serve_ball():
    return {"rect": pygame.Rect(340, 300, 14, 14), "vx": serve_speed, "vy": -serve_speed}

current = 0
serve_speed = 4
paddle = pygame.Rect(295, 480, 110, 14)
balls = [serve_ball()]
powerups = []
bricks = build_wall(LEVELS[0])
score = 0
lives = 3
wide_timer = 0
slow_timer = 0
frame = 0
shake = 0
flash = 0
state = "PLAY"

def reset():
    global current, serve_speed, paddle, balls, powerups, bricks
    global score, lives, wide_timer, slow_timer, shake, flash, state
    current = 0
    serve_speed = 4
    paddle = pygame.Rect(295, 480, 110, 14)
    balls = [serve_ball()]
    powerups = []
    bricks = build_wall(LEVELS[0])
    score = 0
    lives = 3
    wide_timer = 0
    slow_timer = 0
    shake = 0
    flash = 0
    state = "PLAY"

running = True
while running:
    frame += 1
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_r and state != "PLAY":
            reset()

    if state == "PLAY":
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            paddle.x -= 8
        if keys[pygame.K_RIGHT]:
            paddle.x += 8
        paddle.x = max(0, min(700 - paddle.width, paddle.x))

        # timed effects
        if wide_timer > 0:
            wide_timer -= 1
            if wide_timer == 0:
                paddle.x += 30
                paddle.width = 110
        if slow_timer > 0:
            slow_timer -= 1

        move_balls = (slow_timer == 0) or (frame % 2 == 0)

        if move_balls:
            for bl in balls[:]:
                r = bl["rect"]
                r.x += bl["vx"]
                r.y += bl["vy"]
                if r.left <= 0 or r.right >= 700:
                    bl["vx"] = -bl["vx"]
                if r.top <= 0:
                    bl["vy"] = -bl["vy"]

                if r.colliderect(paddle) and bl["vy"] > 0:
                    bl["vy"] = -bl["vy"]
                    offset = r.centerx - paddle.centerx
                    bl["vx"] = offset // 12
                    if bl["vx"] == 0:
                        bl["vx"] = 1

                for b in bricks[:]:
                    if r.colliderect(b["rect"]):
                        b["hp"] -= 1
                        bl["vy"] = -bl["vy"]
                        if b["hp"] == 0:
                            bricks.remove(b)
                            score += (5 - b["row"]) * 10
                            shake = 6
                            if random.random() < 0.2:
                                powerups.append({"rect": pygame.Rect(b["rect"].centerx - 12, b["rect"].y, 24, 24),
                                                 "kind": random.choice(KINDS)})
                        break

                if r.top > 520:
                    balls.remove(bl)

        if len(balls) == 0:
            lives -= 1
            balls.append(serve_ball())
            if lives == 0:
                state = "LOSE"

        # power-ups fall + catch
        for p in powerups[:]:
            p["rect"].y += 3
            if p["rect"].colliderect(paddle):
                flash = 8
                if p["kind"] == "wide":
                    if wide_timer == 0:
                        paddle.x -= 30
                        paddle.width = 170
                    wide_timer = 600
                elif p["kind"] == "multi":
                    for bl in balls[:]:
                        for new_vx in (-3, 3):
                            balls.append({"rect": bl["rect"].copy(), "vx": new_vx, "vy": bl["vy"]})
                elif p["kind"] == "slow":
                    slow_timer = 400
                powerups.remove(p)
            elif p["rect"].top > 520:
                powerups.remove(p)

        # level clear
        if len(bricks) == 0:
            current += 1
            if current < len(LEVELS):
                serve_speed += 1
                bricks = build_wall(LEVELS[current])
                balls = [serve_ball()]
                powerups = []
            else:
                state = "WIN"

    # ---- draw onto the offscreen world ----
    world.fill((25, 30, 60))
    for b in bricks:
        if b["silver"]:
            colour = (200, 200, 210) if b["hp"] == 2 else (120, 120, 130)
        else:
            colour = colours[b["row"] % len(colours)]
        pygame.draw.rect(world, colour, b["rect"])
    pygame.draw.rect(world, (255, 119, 0), paddle)
    for bl in balls:
        pygame.draw.ellipse(world, (255, 255, 255), bl["rect"])
    for p in powerups:
        pygame.draw.rect(world, PU_COLOUR.get(p["kind"], (255, 255, 255)), p["rect"], border_radius=6)
        letter = font.render(p["kind"][0].upper(), True, (10, 12, 20))
        world.blit(letter, letter.get_rect(center=p["rect"].center))
    world.blit(font.render("Score: " + str(score), True, (255, 255, 255)), (12, 8))
    world.blit(font.render("Lives: " + str(max(0, lives)), True, (255, 255, 255)), (560, 8))
    if wide_timer > 0:
        pygame.draw.rect(world, (96, 165, 250), (12, 500, int(120 * wide_timer / 600), 8))
    if flash > 0:
        flash -= 1
        glow = pygame.Surface((700, 520), pygame.SRCALPHA)
        glow.fill((255, 255, 255, flash * 12))
        world.blit(glow, (0, 0))
    if state != "PLAY":
        text = "YOU WIN!" if state == "WIN" else "GAME OVER"
        msg = big_font.render(text, True, (255, 209, 102))
        world.blit(msg, msg.get_rect(center=(350, 235)))
        sub = font.render("Press R to play again", True, (255, 255, 255))
        world.blit(sub, sub.get_rect(center=(350, 290)))

    # ---- screen shake blit ----
    offset = (0, 0)
    if shake > 0:
        shake -= 1
        offset = (random.randint(-4, 4), random.randint(-4, 4))
    screen.fill((0, 0, 0))
    screen.blit(world, offset)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()import pygame, random
pygame.init()
screen = pygame.display.set_mode((700, 520))
world = pygame.Surface((700, 520))
pygame.display.set_caption("Brick Breaker Deluxe")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 64)

colours = [(248, 113, 113), (251, 146, 60), (250, 204, 21), (74, 222, 128), (96, 165, 250)]
KINDS = ["wide", "multi", "slow"]
PU_COLOUR = {"wide": (96, 165, 250), "multi": (74, 222, 128), "slow": (250, 204, 21)}

LEVELS = [
    ["##########",
     "##########",
     ".########.",
     "..######.."],
    ["2222222222",
     "#.#.#.#.#.",
     ".#.#.#.#.#",
     "##########"],
]

def build_wall(pattern):
    wall = []
    for r, line in enumerate(pattern):
        for c, ch in enumerate(line):
            if ch != ".":
                wall.append({"rect": pygame.Rect(c*68 + 12, r*26 + 50, 64, 22),
                             "row": r, "hp": 2 if ch == "2" else 1, "silver": ch == "2"})
    return wall

def serve_ball():
    return {"rect": pygame.Rect(340, 300, 14, 14), "vx": serve_speed, "vy": -serve_speed}

current = 0
serve_speed = 4
paddle = pygame.Rect(295, 480, 110, 14)
balls = [serve_ball()]
powerups = []
bricks = build_wall(LEVELS[0])
score = 0
lives = 3
wide_timer = 0
slow_timer = 0
frame = 0
shake = 0
flash = 0
state = "PLAY"

def reset():
    global current, serve_speed, paddle, balls, powerups, bricks
    global score, lives, wide_timer, slow_timer, shake, flash, state
    current = 0
    serve_speed = 4
    paddle = pygame.Rect(295, 480, 110, 14)
    balls = [serve_ball()]
    powerups = []
    bricks = build_wall(LEVELS[0])
    score = 0
    lives = 3
    wide_timer = 0
    slow_timer = 0
    shake = 0
    flash = 0
    state = "PLAY"

running = True
while running:
    frame += 1
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_r and state != "PLAY":
            reset()

    if state == "PLAY":
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            paddle.x -= 8
        if keys[pygame.K_RIGHT]:
            paddle.x += 8
        paddle.x = max(0, min(700 - paddle.width, paddle.x))

        # timed effects
        if wide_timer > 0:
            wide_timer -= 1
            if wide_timer == 0:
                paddle.x += 30
                paddle.width = 110
        if slow_timer > 0:
            slow_timer -= 1

        move_balls = (slow_timer == 0) or (frame % 2 == 0)

        if move_balls:
            for bl in balls[:]:
                r = bl["rect"]
                r.x += bl["vx"]
                r.y += bl["vy"]
                if r.left <= 0 or r.right >= 700:
                    bl["vx"] = -bl["vx"]
                if r.top <= 0:
                    bl["vy"] = -bl["vy"]

                if r.colliderect(paddle) and bl["vy"] > 0:
                    bl["vy"] = -bl["vy"]
                    offset = r.centerx - paddle.centerx
                    bl["vx"] = offset // 12
                    if bl["vx"] == 0:
                        bl["vx"] = 1

                for b in bricks[:]:
                    if r.colliderect(b["rect"]):
                        b["hp"] -= 1
                        bl["vy"] = -bl["vy"]
                        if b["hp"] == 0:
                            bricks.remove(b)
                            score += (5 - b["row"]) * 10
                            shake = 6
                            if random.random() < 0.2:
                                powerups.append({"rect": pygame.Rect(b["rect"].centerx - 12, b["rect"].y, 24, 24),
                                                 "kind": random.choice(KINDS)})
                        break

                if r.top > 520:
                    balls.remove(bl)

        if len(balls) == 0:
            lives -= 1
            balls.append(serve_ball())
            if lives == 0:
                state = "LOSE"

        # power-ups fall + catch
        for p in powerups[:]:
            p["rect"].y += 3
            if p["rect"].colliderect(paddle):
                flash = 8
                if p["kind"] == "wide":
                    if wide_timer == 0:
                        paddle.x -= 30
                        paddle.width = 170
                    wide_timer = 600
                elif p["kind"] == "multi":
                    for bl in balls[:]:
                        for new_vx in (-3, 3):
                            balls.append({"rect": bl["rect"].copy(), "vx": new_vx, "vy": bl["vy"]})
                elif p["kind"] == "slow":
                    slow_timer = 400
                powerups.remove(p)
            elif p["rect"].top > 520:
                powerups.remove(p)

        # level clear
        if len(bricks) == 0:
            current += 1
            if current < len(LEVELS):
                serve_speed += 1
                bricks = build_wall(LEVELS[current])
                balls = [serve_ball()]
                powerups = []
            else:
                state = "WIN"

    # ---- draw onto the offscreen world ----
    world.fill((25, 30, 60))
    for b in bricks:
        if b["silver"]:
            colour = (200, 200, 210) if b["hp"] == 2 else (120, 120, 130)
        else:
            colour = colours[b["row"] % len(colours)]
        pygame.draw.rect(world, colour, b["rect"])
    pygame.draw.rect(world, (255, 119, 0), paddle)
    for bl in balls:
        pygame.draw.ellipse(world, (255, 255, 255), bl["rect"])
    for p in powerups:
        pygame.draw.rect(world, PU_COLOUR.get(p["kind"], (255, 255, 255)), p["rect"], border_radius=6)
        letter = font.render(p["kind"][0].upper(), True, (10, 12, 20))
        world.blit(letter, letter.get_rect(center=p["rect"].center))
    world.blit(font.render("Score: " + str(score), True, (255, 255, 255)), (12, 8))
    world.blit(font.render("Lives: " + str(max(0, lives)), True, (255, 255, 255)), (560, 8))
    if wide_timer > 0:
        pygame.draw.rect(world, (96, 165, 250), (12, 500, int(120 * wide_timer / 600), 8))
    if flash > 0:
        flash -= 1
        glow = pygame.Surface((700, 520), pygame.SRCALPHA)
        glow.fill((255, 255, 255, flash * 12))
        world.blit(glow, (0, 0))
    if state != "PLAY":
        text = "YOU WIN!" if state == "WIN" else "GAME OVER"
        msg = big_font.render(text, True, (255, 209, 102))
        world.blit(msg, msg.get_rect(center=(350, 235)))
        sub = font.render("Press R to play again", True, (255, 255, 255))
        world.blit(sub, sub.get_rect(center=(350, 290)))

    # ---- screen shake blit ----
    offset = (0, 0)
    if shake > 0:
        shake -= 1
        offset = (random.randint(-4, 4), random.randint(-4, 4))
    screen.fill((0, 0, 0))
    screen.blit(world, offset)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
Power-ups, multi-ball, levels and juice, your Breakout is now a complete arcade game. One more episode: the platformer supercharge!
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โ–ถ Episode 7: Platformer Part 2 โ†’
โญ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi