Ce que vous allez apprendre
Dans ce chapitre, nous chargeons les niveaux depuis des fichiers CSV. Au lieu d'un champ de briques genere par code, chaque niveau est dessine dans un fichier texte. Le joueur progresse a travers plusieurs niveaux, et la victoire arrive quand tous les niveaux sont termines.
Le concept
Un tile map (carte de tuiles) est une grille ou chaque cellule represente un element du monde. Dans notre cas, chaque cellule est soit vide (0), soit une brique de couleur (1 a 6). Le fichier CSV est facile a editer avec un editeur de texte ou un tableur.
Voici un exemple de fichier 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
Les codes couleur sont :
- 1 = Rouge
- 2 = Orange
- 3 = Jaune
- 4 = Vert
- 5 = Bleu
- 6 = Violet
Etape par etape
1. Le dictionnaire de couleurs
On definit un dictionnaire qui associe chaque numero a une couleur RGB :
BRICK_COLORS = {
1: (255, 50, 50), # Rouge
2: (255, 165, 0), # Orange
3: (255, 255, 50), # Jaune
4: (50, 255, 50), # Vert
5: (50, 150, 255), # Bleu
6: (180, 50, 255), # Violet
}
2. Charger un fichier CSV
La fonction load_level_from_csv() lit le fichier ligne par ligne. Chaque cellule non vide et non nulle cree une brique a la bonne position dans la grille.
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. Progression de niveaux
Quand toutes les briques sont detruites, la fonction advance_level() charge le niveau suivant. Si tous les niveaux sont termines, on passe a l'etat YOU_WIN.
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. Creer ses propres niveaux
Pour creer un nouveau niveau, il suffit de creer un fichier CSV et de l'ajouter a la liste LEVEL_FILES. Chaque ligne du CSV represente une rangee de briques, et chaque nombre une brique (ou 0 pour un espace vide).
Points cles a retenir
Astuce : le module csv de Python gere tous les cas particuliers (guillemets, virgules dans les valeurs). Utilisez-le au lieu de faire un simple split(',').
Piege courant : oublier os.path.abspath pour construire le chemin vers les fichiers. Si le script est lance depuis un autre repertoire, les chemins relatifs ne fonctionneront pas.
Evolution possible : remplacer les fichiers CSV par le format TMX (Tiled Map Editor) pour des niveaux plus complexes avec plusieurs couches.
Code complet
"""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()
Telechargement
Telecharger le code source de ce chapitre (inclut les 3 fichiers de niveaux CSV) : brick-breaker-ch13.zip
