What you will learn
In this chapter we load levels from CSV files. Instead of a code-generated brick field, each level is designed in a text file. The player progresses through multiple levels, and victory comes when all levels are completed.
The concept
A tile map is a grid where each cell represents a world element. In our case, each cell is either empty (0) or a colored brick (1 to 6). The CSV file is easy to edit with a text editor or spreadsheet.
Here is an example level1.csv:
1,1,1,1,1,1,1,1,1,1
2,2,2,2,2,2,2,2,2,2
3,3,3,3,3,3,3,3,3,3
4,4,4,4,4,4,4,4,4,4
5,5,5,5,5,5,5,5,5,5
6,6,6,6,6,6,6,6,6,6
The color codes are:
- 1 = Red
- 2 = Orange
- 3 = Yellow
- 4 = Green
- 5 = Blue
- 6 = Purple
Step by step
1. The color dictionary
We define a dictionary that maps each number to an RGB color:
BRICK_COLORS = {
1: (255, 50, 50), # Red
2: (255, 165, 0), # Orange
3: (255, 255, 50), # Yellow
4: (50, 255, 50), # Green
5: (50, 150, 255), # Blue
6: (180, 50, 255), # Purple
}
2. Loading a CSV file
The load_level_from_csv() function reads the file line by line. Each non-empty, non-zero cell creates a brick at the right position in the grid.
def load_level_from_csv(filepath):
group = pygame.sprite.Group()
with open(filepath, newline="") as f:
reader = csv.reader(f)
for row_idx, row_data in enumerate(reader):
for col_idx, cell in enumerate(row_data):
cell = cell.strip()
if not cell or cell == "0":
continue
brick_type = int(cell)
color = BRICK_COLORS[brick_type]
x = BRICK_OFFSET_X + col_idx * (...)
y = BRICK_OFFSET_Y + row_idx * (...)
group.add(Brick(x, y, color, brick_type))
return group
3. Level progression
When all bricks are destroyed, the advance_level() function loads the next level. If all levels are done, we transition to the YOU_WIN state.
def advance_level():
global current_level, bricks, game_state
current_level += 1
if current_level < len(LEVEL_FILES):
bricks = load_level(current_level)
ball.reset()
game_state = STATE_LEVEL_CLEAR
else:
game_state = STATE_YOU_WIN
4. Creating your own levels
To create a new level, just create a CSV file and add it to the LEVEL_FILES list. Each line in the CSV represents a row of bricks, and each number is a brick (or 0 for empty space).
Key takeaways
Tip: Python's csv module handles all edge cases (quotes, commas in values). Use it instead of a simple split(',').
Common pitfall: forgetting os.path.abspath when building file paths. If the script is run from a different directory, relative paths will not work.
Possible evolution: replace CSV files with the TMX format (Tiled Map Editor) for more complex levels with multiple layers.
Complete code
"""Brick Breaker - Chapter 13: Tile Maps"""
import pygame
import sys
import math
import array
import csv
import os
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60
BG_COLOR = (20, 20, 40)
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 15
PADDLE_SPEED = 8
BALL_RADIUS = 8
BALL_BASE_SPEED = 300
BALL_SPEED_INCREMENT = 10
BRICK_COLS = 10
BRICK_WIDTH = 70
BRICK_HEIGHT = 20
BRICK_PADDING = 5
BRICK_OFFSET_X = 30
BRICK_OFFSET_Y = 50
MAX_LIVES = 3
# Game states
STATE_MENU = 0
STATE_PLAYING = 1
STATE_PAUSED = 2
STATE_GAME_OVER = 3
STATE_YOU_WIN = 4
STATE_LEVEL_CLEAR = 5
# Brick color map: number -> (R, G, B)
BRICK_COLORS = {
1: (255, 50, 50), # Red
2: (255, 165, 0), # Orange
3: (255, 255, 50), # Yellow
4: (50, 255, 50), # Green
5: (50, 150, 255), # Blue
6: (180, 50, 255), # Purple
}
# Level files (relative to this script)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LEVEL_FILES = [
os.path.join(SCRIPT_DIR, "level1.csv"),
os.path.join(SCRIPT_DIR, "level2.csv"),
os.path.join(SCRIPT_DIR, "level3.csv"),
]
# ── Sound generation ────────────────────────────────────────────────
def generate_beep(frequency=440, duration_ms=100, volume=0.3):
sample_rate = 44100
n_samples = int(sample_rate * duration_ms / 1000)
buf = array.array("h", [0] * n_samples)
max_amp = int(32767 * volume)
for i in range(n_samples):
t = i / sample_rate
value = int(max_amp * math.sin(2 * math.pi * frequency * t))
fade_samples = min(500, n_samples // 4)
if i >= n_samples - fade_samples:
value = int(value * (n_samples - i) / fade_samples)
buf[i] = value
return pygame.mixer.Sound(buffer=buf)
snd_paddle = generate_beep(frequency=500, duration_ms=60, volume=0.3)
snd_wall = generate_beep(frequency=300, duration_ms=50, volume=0.2)
snd_brick = generate_beep(frequency=700, duration_ms=80, volume=0.3)
snd_lose_life = generate_beep(frequency=150, duration_ms=300, volume=0.4)
snd_game_over = generate_beep(frequency=100, duration_ms=500, volume=0.5)
snd_win = generate_beep(frequency=880, duration_ms=400, volume=0.4)
snd_level_clear = generate_beep(frequency=660, duration_ms=300, volume=0.4)
# ── Sprite classes ──────────────────────────────────────────────────
class Paddle(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((PADDLE_WIDTH, PADDLE_HEIGHT), pygame.SRCALPHA)
for y in range(PADDLE_HEIGHT):
brightness = 200 + int(55 * (1 - y / PADDLE_HEIGHT))
pygame.draw.line(self.image, (brightness, brightness, brightness),
(0, y), (PADDLE_WIDTH, y))
pygame.draw.rect(self.image, (0, 0, 0, 0), (0, 0, 3, 3))
pygame.draw.rect(self.image, (0, 0, 0, 0), (PADDLE_WIDTH - 3, 0, 3, 3))
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect()
self.rect.centerx = SCREEN_WIDTH // 2
self.rect.y = SCREEN_HEIGHT - 40
self.use_mouse = False
def update(self):
if self.use_mouse:
mx, _ = pygame.mouse.get_pos()
self.rect.centerx = mx
else:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.rect.x -= PADDLE_SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.rect.x += PADDLE_SPEED
self.rect.left = max(0, self.rect.left)
self.rect.right = min(SCREEN_WIDTH, self.rect.right)
class Ball(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
size = BALL_RADIUS * 4
self.image = pygame.Surface((size, size), pygame.SRCALPHA)
center = size // 2
for r in range(BALL_RADIUS * 2, BALL_RADIUS, -1):
alpha = int(100 * (1 - (r - BALL_RADIUS) / BALL_RADIUS))
pygame.draw.circle(self.image, (100, 100, 255, alpha), (center, center), r)
pygame.draw.circle(self.image, (255, 255, 255), (center, center), BALL_RADIUS)
pygame.draw.circle(self.image, (255, 255, 255, 200),
(center - 2, center - 2), BALL_RADIUS // 3)
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect()
self.float_x = float(SCREEN_WIDTH // 2)
self.float_y = float(SCREEN_HEIGHT // 2)
self.rect.center = (int(self.float_x), int(self.float_y))
self.speed = BALL_BASE_SPEED
angle = math.radians(-60)
self.vx = self.speed * math.cos(angle)
self.vy = self.speed * math.sin(angle)
def reset(self):
self.float_x = float(SCREEN_WIDTH // 2)
self.float_y = float(SCREEN_HEIGHT // 2)
self.rect.center = (int(self.float_x), int(self.float_y))
self.speed = BALL_BASE_SPEED
angle = math.radians(-60)
self.vx = self.speed * math.cos(angle)
self.vy = self.speed * math.sin(angle)
def update(self, dt):
self.float_x += self.vx * dt
self.float_y += self.vy * dt
self.rect.center = (int(self.float_x), int(self.float_y))
def bounce_wall(self):
bounced = False
if self.float_x - BALL_RADIUS <= 0:
self.float_x = BALL_RADIUS
self.vx = abs(self.vx)
self.speed += BALL_SPEED_INCREMENT
bounced = True
if self.float_x + BALL_RADIUS >= SCREEN_WIDTH:
self.float_x = SCREEN_WIDTH - BALL_RADIUS
self.vx = -abs(self.vx)
self.speed += BALL_SPEED_INCREMENT
bounced = True
if self.float_y - BALL_RADIUS <= 0:
self.float_y = BALL_RADIUS
self.vy = abs(self.vy)
self.speed += BALL_SPEED_INCREMENT
bounced = True
if bounced:
self.rect.center = (int(self.float_x), int(self.float_y))
return bounced
def fell_off_bottom(self):
return self.float_y - BALL_RADIUS > SCREEN_HEIGHT
def bounce_off_paddle(self, paddle):
if self.rect.colliderect(paddle.rect) and self.vy > 0:
self.float_y = paddle.rect.top - BALL_RADIUS
hit_pos = (self.float_x - paddle.rect.left) / PADDLE_WIDTH
angle = 150 - hit_pos * 120
rad = math.radians(angle)
self.speed += BALL_SPEED_INCREMENT
self.vx = self.speed * math.cos(rad)
self.vy = -abs(self.speed * math.sin(rad))
self.rect.center = (int(self.float_x), int(self.float_y))
return True
return False
class Brick(pygame.sprite.Sprite):
def __init__(self, x, y, color, row):
super().__init__()
self.image = pygame.Surface((BRICK_WIDTH, BRICK_HEIGHT), pygame.SRCALPHA)
pygame.draw.rect(self.image, color, (0, 0, BRICK_WIDTH, BRICK_HEIGHT))
highlight = tuple(min(255, c + 50) for c in color)
pygame.draw.line(self.image, highlight, (1, 1), (BRICK_WIDTH - 2, 1))
pygame.draw.line(self.image, highlight, (1, 1), (1, BRICK_HEIGHT - 2))
shadow = tuple(max(0, c - 50) for c in color)
pygame.draw.line(self.image, shadow, (1, BRICK_HEIGHT - 1),
(BRICK_WIDTH - 1, BRICK_HEIGHT - 1))
pygame.draw.line(self.image, shadow, (BRICK_WIDTH - 1, 1),
(BRICK_WIDTH - 1, BRICK_HEIGHT - 1))
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect(topleft=(x, y))
self.row = row
self.color = color
def score_value(self):
return (7 - min(self.row, 6)) * 10
# ── Level loading from CSV ──────────────────────────────────────────
def load_level_from_csv(filepath):
"""Load a brick layout from a CSV file.
Each cell contains a number:
0 = empty
1-6 = brick color
Returns a pygame.sprite.Group.
"""
group = pygame.sprite.Group()
if not os.path.exists(filepath):
return group
with open(filepath, newline="") as f:
reader = csv.reader(f)
for row_idx, row_data in enumerate(reader):
for col_idx, cell in enumerate(row_data):
cell = cell.strip()
if not cell or cell == "0":
continue
brick_type = int(cell)
if brick_type not in BRICK_COLORS:
continue
color = BRICK_COLORS[brick_type]
x = BRICK_OFFSET_X + col_idx * (BRICK_WIDTH + BRICK_PADDING)
y = BRICK_OFFSET_Y + row_idx * (BRICK_HEIGHT + BRICK_PADDING)
group.add(Brick(x, y, color, brick_type))
return group
# ── Game setup ──────────────────────────────────────────────────────
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Brick Breaker - Chapter 13")
clock = pygame.time.Clock()
hud_font = pygame.font.SysFont("monospace", 18)
big_font = pygame.font.SysFont("monospace", 48, bold=True)
medium_font = pygame.font.SysFont("monospace", 24)
small_font = pygame.font.SysFont("monospace", 14)
paddle = Paddle()
ball = Ball()
bricks = pygame.sprite.Group()
paddle_group = pygame.sprite.GroupSingle(paddle)
ball_group = pygame.sprite.GroupSingle(ball)
lives = MAX_LIVES
score = 0
current_level = 0
game_state = STATE_MENU
master_volume = 0.5
sound_muted = False
menu_blink_timer = 0.0
level_clear_timer = 0.0
LEVEL_CLEAR_DELAY = 2.0
def play_sound(sound):
if not sound_muted:
sound.set_volume(master_volume)
sound.play()
def load_level(level_index):
"""Load a level by index. Returns the brick group."""
if level_index < len(LEVEL_FILES):
return load_level_from_csv(LEVEL_FILES[level_index])
else:
return pygame.sprite.Group()
def start_game():
global lives, score, current_level, bricks, game_state
lives = MAX_LIVES
score = 0
current_level = 0
bricks = load_level(current_level)
ball.reset()
game_state = STATE_PLAYING
def advance_level():
"""Move to next level or win the game."""
global current_level, bricks, game_state, level_clear_timer
current_level += 1
if current_level < len(LEVEL_FILES):
bricks = load_level(current_level)
ball.reset()
game_state = STATE_LEVEL_CLEAR
level_clear_timer = 0.0
play_sound(snd_level_clear)
else:
game_state = STATE_YOU_WIN
play_sound(snd_win)
# ── Draw helpers ────────────────────────────────────────────────────
def draw_menu():
global menu_blink_timer
menu_blink_timer += clock.get_time() / 1000.0
screen.fill(BG_COLOR)
title = big_font.render("BRICK BREAKER", True, (255, 200, 50))
screen.blit(title, (SCREEN_WIDTH // 2 - title.get_width() // 2,
SCREEN_HEIGHT // 3 - 30))
subtitle = medium_font.render("Tile Map Edition", True, (200, 200, 255))
screen.blit(subtitle, (SCREEN_WIDTH // 2 - subtitle.get_width() // 2,
SCREEN_HEIGHT // 3 + 30))
if int(menu_blink_timer * 2) % 2 == 0:
start_text = medium_font.render("Press SPACE to start", True, (255, 255, 255))
screen.blit(start_text, (SCREEN_WIDTH // 2 - start_text.get_width() // 2,
SCREEN_HEIGHT // 2 + 20))
levels_info = small_font.render(
"{} levels loaded from CSV files".format(len(LEVEL_FILES)),
True, (150, 200, 150))
screen.blit(levels_info, (SCREEN_WIDTH // 2 - levels_info.get_width() // 2,
SCREEN_HEIGHT // 2 + 60))
def draw_hud():
score_surf = hud_font.render("Score: {}".format(score), True, (255, 255, 100))
screen.blit(score_surf, (10, 10))
lives_surf = hud_font.render("Lives: {}".format(lives), True, (255, 100, 100))
screen.blit(lives_surf, (SCREEN_WIDTH // 2 - lives_surf.get_width() // 2, 10))
level_surf = hud_font.render(
"Level: {}/{}".format(current_level + 1, len(LEVEL_FILES)),
True, (100, 200, 255))
screen.blit(level_surf, (SCREEN_WIDTH - level_surf.get_width() - 10, 10))
vol_label = "MUTED" if sound_muted else "Vol: {}".format(int(master_volume * 100))
vol_color = (255, 80, 80) if sound_muted else (150, 150, 150)
vol_surf = small_font.render(vol_label, True, vol_color)
screen.blit(vol_surf, (SCREEN_WIDTH - vol_surf.get_width() - 10, 30))
def draw_overlay(title_text, title_color, sub_text="Press SPACE for menu"):
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
screen.blit(overlay, (0, 0))
t = big_font.render(title_text, True, title_color)
screen.blit(t, (SCREEN_WIDTH // 2 - t.get_width() // 2, SCREEN_HEIGHT // 2 - 40))
fs = medium_font.render("Score: {}".format(score), True, (255, 255, 255))
screen.blit(fs, (SCREEN_WIDTH // 2 - fs.get_width() // 2, SCREEN_HEIGHT // 2 + 20))
r = medium_font.render(sub_text, True, (200, 200, 200))
screen.blit(r, (SCREEN_WIDTH // 2 - r.get_width() // 2, SCREEN_HEIGHT // 2 + 60))
# ── Main loop ───────────────────────────────────────────────────────
running = True
while running:
dt = clock.tick(FPS) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
if event.key == pygame.K_F1:
sound_muted = not sound_muted
if event.key in (pygame.K_PLUS, pygame.K_KP_PLUS, pygame.K_EQUALS):
master_volume = min(1.0, master_volume + 0.1)
if event.key in (pygame.K_MINUS, pygame.K_KP_MINUS):
master_volume = max(0.0, master_volume - 0.1)
if game_state == STATE_MENU:
if event.key == pygame.K_SPACE:
start_game()
elif game_state == STATE_PLAYING:
if event.key == pygame.K_m:
paddle.use_mouse = not paddle.use_mouse
if event.key == pygame.K_p:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
if event.key == pygame.K_p:
game_state = STATE_PLAYING
elif game_state in (STATE_GAME_OVER, STATE_YOU_WIN):
if event.key == pygame.K_SPACE:
game_state = STATE_MENU
# ── Update ──────────────────────────────────────────────────
if game_state == STATE_PLAYING:
paddle_group.update()
ball.update(dt)
if ball.bounce_wall():
play_sound(snd_wall)
if ball.fell_off_bottom():
lives -= 1
if lives <= 0:
game_state = STATE_GAME_OVER
play_sound(snd_game_over)
else:
play_sound(snd_lose_life)
ball.reset()
if ball.bounce_off_paddle(paddle):
play_sound(snd_paddle)
hit_bricks = pygame.sprite.spritecollide(ball, bricks, True)
if hit_bricks:
for brick in hit_bricks:
score += brick.score_value()
ball.vy = -ball.vy
ball.speed += BALL_SPEED_INCREMENT
play_sound(snd_brick)
if len(bricks) == 0:
advance_level()
elif game_state == STATE_LEVEL_CLEAR:
level_clear_timer += dt
if level_clear_timer >= LEVEL_CLEAR_DELAY:
game_state = STATE_PLAYING
# ── Draw ────────────────────────────────────────────────────
if game_state == STATE_MENU:
draw_menu()
elif game_state == STATE_PLAYING:
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
info = small_font.render(
"[M] Mouse [P] Pause [F1] Mute [+/-] Volume [ESC] Quit",
True, (150, 150, 150))
screen.blit(info, (10, SCREEN_HEIGHT - 25))
elif game_state == STATE_PAUSED:
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_overlay("PAUSED", (200, 200, 255), "Press P to resume")
elif game_state == STATE_LEVEL_CLEAR:
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_overlay(
"LEVEL {} CLEAR!".format(current_level),
(255, 255, 100),
"Next level starting..."
)
elif game_state == STATE_GAME_OVER:
screen.fill(BG_COLOR)
bricks.draw(screen)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_overlay("GAME OVER", (255, 60, 60))
elif game_state == STATE_YOU_WIN:
screen.fill(BG_COLOR)
paddle_group.draw(screen)
ball_group.draw(screen)
draw_hud()
draw_overlay("ALL LEVELS CLEAR!", (100, 255, 100))
pygame.display.flip()
pygame.quit()
sys.exit()
Download
Download the source code for this chapter (includes 3 CSV level files): brick-breaker-ch13.zip
