Editor

Breakout in Python

Pygame Online · 175 lines · Left and Right, Up or Space to launch

Clear every brick with the ball, and keep it off the floor.

How it works

Sixty bricks in a list

Six rows of ten pygame.Rects. Each row has its own colour, and higher rows score more.

def new_game():
    global bricks, score, lives, state
    bricks = []
    for row in range(len(ROW_COLORS)):
        for col in range(10):
            bricks.append(pygame.Rect(6 + col * 47, BRICK_TOP + row * 18, 44, 14))
    score = 0
    lives = 3
    state = "serve"  # "serve", "playing", "over" or "won"

One axis at a time

The ball moves sideways first and checks for walls and bricks, then up or down and checks again. Splitting the move tells the game which side of a brick was hit, so the ball bounces the right way.

# Move sideways first: bounce off the side walls and bricks.
ball_x += ball_dx
ball.x = round(ball_x)
if ball.left < 0 or ball.right > WIDTH:
    ball_dx = -ball_dx
    ball_x = max(0, min(ball_x, WIDTH - ball.width))
    ball.x = round(ball_x)
brick = break_brick()
if brick:
    if ball_dx > 0:
        ball.right = brick.left
    else:
        ball.left = brick.right
    ball_x = ball.x
    ball_dx = -ball_dx

Aiming with the paddle

Where the ball lands on the paddle decides its new angle: up to 60 degrees to the left or right of straight up. launch turns that angle into a speed across and a speed up.

def launch(offset):
    """Send the ball up. offset -1 is far left, 0 straight up, 1 far right."""
    global ball_dx, ball_dy
    angle = math.radians(offset * 60)
    ball_dx = ball_speed() * math.sin(angle)
    ball_dy = -ball_speed() * math.cos(angle)

Make it yours

Change the code above and press Run again. Some ideas:

  • Add a row of strong bricks that need two hits.
  • Make the paddle wider for the first ten seconds of each life.
  • Speed the ball up faster: change 0.05 in ball_speed.
  • Add a second level with a different brick pattern once the first is cleared.

The whole program

Plain pygame: it runs the same on a computer with Python and pygame installed.

import math
import random
import pygame

pygame.init()
WIDTH, HEIGHT = 480, 360
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Breakout")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 28)
big_font = pygame.font.Font(None, 60)

BG = (16, 18, 22)
YELLOW = (255, 212, 59)
BLUE = (48, 105, 152)
TEXT = (230, 232, 235)
# One colour per row of bricks, top to bottom.
ROW_COLORS = [(235, 95, 85), (240, 150, 60), (255, 212, 59), (110, 200, 120), (90, 162, 224), (48, 105, 152)]
BRICK_TOP = 50
PADDLE_SPEED = 7
START_KEYS = (pygame.K_UP, pygame.K_SPACE, pygame.K_RETURN)

paddle = pygame.Rect(WIDTH // 2 - 35, HEIGHT - 30, 70, 10)
ball = pygame.Rect(0, 0, 10, 10)


def new_game():
    global bricks, score, lives, state
    bricks = []
    for row in range(len(ROW_COLORS)):
        for col in range(10):
            bricks.append(pygame.Rect(6 + col * 47, BRICK_TOP + row * 18, 44, 14))
    score = 0
    lives = 3
    state = "serve"  # "serve", "playing", "over" or "won"


def ball_speed():
    """The ball gets faster as bricks disappear."""
    return min(4 + (60 - len(bricks)) * 0.05, 7)


def launch(offset):
    """Send the ball up. offset -1 is far left, 0 straight up, 1 far right."""
    global ball_dx, ball_dy
    angle = math.radians(offset * 60)
    ball_dx = ball_speed() * math.sin(angle)
    ball_dy = -ball_speed() * math.cos(angle)


def break_brick():
    """Remove the brick the ball touches, if any, and score it."""
    global score
    i = ball.collidelist(bricks)
    if i == -1:
        return None
    brick = bricks.pop(i)
    row = (brick.y - BRICK_TOP) // 18
    score += len(ROW_COLORS) - row  # higher rows are worth more
    return brick


def draw_text(text, text_font, color, center):
    image = text_font.render(text, True, color)
    screen.blit(image, image.get_rect(center=center))


new_game()
ball_x = ball_y = ball_dx = ball_dy = 0
wait = 0  # frames until a new game may start
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN and event.key in START_KEYS:
            if state == "serve":
                launch(random.uniform(-0.5, 0.5))
                state = "playing"
            elif state in ("over", "won") and wait == 0:
                new_game()

    keys = pygame.key.get_pressed()
    if state in ("serve", "playing"):
        if keys[pygame.K_LEFT]:
            paddle.x -= PADDLE_SPEED
        if keys[pygame.K_RIGHT]:
            paddle.x += PADDLE_SPEED
        paddle.clamp_ip(screen.get_rect())

    if wait > 0:
        wait -= 1

    if state == "serve":
        # The ball rides on the paddle until it is launched.
        ball.midbottom = paddle.midtop
        ball_x, ball_y = ball.x, ball.y

    elif state == "playing":
        # Move sideways first: bounce off the side walls and bricks.
        ball_x += ball_dx
        ball.x = round(ball_x)
        if ball.left < 0 or ball.right > WIDTH:
            ball_dx = -ball_dx
            ball_x = max(0, min(ball_x, WIDTH - ball.width))
            ball.x = round(ball_x)
        brick = break_brick()
        if brick:
            if ball_dx > 0:
                ball.right = brick.left
            else:
                ball.left = brick.right
            ball_x = ball.x
            ball_dx = -ball_dx

        # Then up or down: the ceiling, bricks and the paddle.
        ball_y += ball_dy
        ball.y = round(ball_y)
        if ball.top < 0:
            ball_dy = -ball_dy
            ball_y = ball.y = 0
        brick = break_brick()
        if brick:
            if ball_dy > 0:
                ball.bottom = brick.top
            else:
                ball.top = brick.bottom
            ball_y = ball.y
            ball_dy = -ball_dy
        if ball_dy > 0 and ball.colliderect(paddle):
            # Where it lands on the paddle decides the angle it leaves at.
            offset = (ball.centerx - paddle.centerx) / (paddle.width / 2)
            launch(max(-1, min(1, offset)))
            ball.bottom = paddle.top
            ball_y = ball.y

        if not bricks:
            state = "won"
            wait = 30
        elif ball.top > HEIGHT:
            lives -= 1
            if lives > 0:
                state = "serve"
            else:
                state = "over"
                wait = 30

    screen.fill(BG)
    for brick in bricks:
        row = (brick.y - BRICK_TOP) // 18
        pygame.draw.rect(screen, ROW_COLORS[row], brick, border_radius=3)
    pygame.draw.rect(screen, YELLOW, paddle, border_radius=5)
    if state in ("serve", "playing"):
        pygame.draw.circle(screen, TEXT, ball.center, 5)

    screen.blit(font.render(f"Score {score}", True, TEXT), (12, 14))
    for i in range(lives):
        pygame.draw.circle(screen, YELLOW, (WIDTH - 20 - i * 20, 22), 6)

    if state == "serve":
        draw_text("Left and Right move the paddle", font, TEXT, (WIDTH // 2, 230))
        draw_text("Press Up or Space to launch", font, YELLOW, (WIDTH // 2, 260))
    elif state in ("over", "won"):
        panel = pygame.Rect(0, 0, 360, 130)
        panel.center = (WIDTH // 2, 220)
        pygame.draw.rect(screen, BG, panel, border_radius=12)
        pygame.draw.rect(screen, BLUE, panel, 2, border_radius=12)
        title = "You win!" if state == "won" else "Game over"
        draw_text(title, big_font, YELLOW, (panel.centerx, panel.y + 45))
        draw_text("Press Up or Space to play again", font, TEXT, (panel.centerx, panel.y + 98))

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

pygame.quit()