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.
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.
Make destroyed bricks sometimes drop a capsule that falls toward the paddle.
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)
Implement catching and your first timed effect: a wider paddle.
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
if wide_timer > 0:
wide_timer -= if wide_timer > 0:
wide_timer -=
if wide_timer == 0:
paddle.width =
if wide_timer == 0:
paddle.width =
The big refactor: many balls at once, and losing a life only when the LAST drops.
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})
Add bricks with hit points and implement the "slow" power-up.
# 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)
b["hp"] -= b["hp"] -=
if b["hp"] ==
if b["hp"] == :
bricks.:
bricks.(b)(b)
Define multiple levels as text patterns and progress through them.
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])
Add game-feel effects that make every hit feel chunky.
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()
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)
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.
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()