Ce que vous allez apprendre
Dans ce chapitre, nous ajoutons de la physique au jeu : des briques speciales (rouge = acceleration, bleu = deceleration, vert = gravite), et des power-ups qui tombent des briques detruites (raquette elargie, vie supplementaire, bonus multi-balle).
Le concept
Les briques speciales ajoutent de la variete au gameplay. Quand la balle touche une brique rouge, elle accelere. Une brique bleue la ralentit. Une brique verte active un effet de gravite sur la balle pendant quelques secondes (elle est tiree vers le bas).
Les power-ups sont des objets qui tombent avec la gravite et que le joueur attrape avec la raquette.
Etape par etape
1. Types de briques
Chaque brique a un attribut brick_type : NORMAL, RED, BLUE ou GREEN. Les briques speciales sont marquees d'un cercle colore au centre.
BRICK_NORMAL = 0
BRICK_RED = 1 # accelere la balle
BRICK_BLUE = 2 # ralentit la balle
BRICK_GREEN = 3 # balle lourde (gravite)
2. Effets de la balle
La balle a de nouvelles methodes : accelerate() augmente la vitesse, decelerate() la reduit (minimum 150), et make_heavy() active la gravite.
def accelerate(self, amount=60):
self.speed += amount
angle = math.atan2(self.vy, self.vx)
self.vx = self.speed * math.cos(angle)
self.vy = self.speed * math.sin(angle)
3. La classe PowerUp
Un power-up tombe du ciel avec la gravite et une oscillation. Le joueur le capture en le touchant avec la raquette. Trois types : W (raquette large), + (vie supplementaire), M (multi-balle).
4. Raquette elargie
Le power-up W elargit la raquette de 50 pourcent pendant 8 secondes. La raquette se reconstruit avec la nouvelle largeur et revient a la taille normale apres le delai.
Points cles a retenir
Astuce : pour modifier la direction de la balle sans changer sa vitesse, utilisez atan2 pour obtenir l'angle actuel.
Piege courant : oublier de limiter la vitesse minimale de la balle apres deceleration peut la rendre trop lente ou immobile.
Code complet
"""Brick Breaker - Chapter 18: Physics"""
import pygame
import sys
import math
import array
import random
import json
import os
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=1, buffer=512)
GAME_WIDTH = 800
GAME_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_ROWS = 6
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_DIFFICULTY = 5
STATE_HIGH_SCORES = 6
# Difficulty
DIFFICULTY_EASY = 0
DIFFICULTY_NORMAL = 1
DIFFICULTY_HARD = 2
DIFFICULTY_NAMES = ["Easy", "Normal", "Hard"]
DIFFICULTY_SPEEDS = [200, 300, 420]
# Brick types
BRICK_NORMAL = 0
BRICK_RED = 1 # accelerates ball
BRICK_BLUE = 2 # decelerates ball
BRICK_GREEN = 3 # heavy ball (gravity)
# Power-up types
POWERUP_WIDER = 0 # wider paddle
POWERUP_LIFE = 1 # extra life
POWERUP_MULTI = 2 # multi-ball hint (visual cue)
POWERUP_COLORS = {
POWERUP_WIDER: (255, 200, 0),
POWERUP_LIFE: (255, 50, 50),
POWERUP_MULTI: (50, 150, 255),
}
POWERUP_LABELS = {
POWERUP_WIDER: "W",
POWERUP_LIFE: "+",
POWERUP_MULTI: "M",
}
POWERUP_SIZE = 20
POWERUP_FALL_SPEED = 120
POWERUP_DURATION = 8.0 # seconds for timed power-ups
# Animation constants
TRAIL_LENGTH = 8
TRAIL_INTERVAL = 0.016
BRICK_SHATTER_DURATION = 0.3
PADDLE_GLOW_DURATION = 0.2
HIGH_SCORE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"highscores.json")
# -- 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_click = generate_beep(frequency=600, duration_ms=40, volume=0.2)
snd_powerup = generate_beep(frequency=900, duration_ms=120, volume=0.3)
snd_speed_up = generate_beep(frequency=1000, duration_ms=60, volume=0.25)
snd_speed_down = generate_beep(frequency=200, duration_ms=100, volume=0.25)
# -- Helper functions -------------------------------------------------------
def rainbow_color(row, total_rows):
hue = int(360 * row / total_rows)
color = pygame.Color(0)
color.hsva = (hue, 100, 100, 100)
return (color.r, color.g, color.b)
# -- High Scores -----------------------------------------------------------
def load_high_scores():
if os.path.exists(HIGH_SCORE_FILE):
try:
with open(HIGH_SCORE_FILE, "r") as f:
return json.load(f)[:10]
except (json.JSONDecodeError, IOError):
pass
return []
def save_high_scores(scores):
scores = sorted(scores, key=lambda s: s["score"], reverse=True)[:10]
try:
with open(HIGH_SCORE_FILE, "w") as f:
json.dump(scores, f, indent=2)
except IOError:
pass
def add_high_score(new_score, difficulty):
scores = load_high_scores()
scores.append({"score": new_score,
"difficulty": DIFFICULTY_NAMES[difficulty]})
scores = sorted(scores, key=lambda s: s["score"], reverse=True)[:10]
save_high_scores(scores)
return scores
# -- Screen manager ---------------------------------------------------------
class ScreenManager:
def __init__(self):
self.game_surface = pygame.Surface((GAME_WIDTH, GAME_HEIGHT))
self.fullscreen = False
self.window_width = GAME_WIDTH
self.window_height = GAME_HEIGHT
self.screen = pygame.display.set_mode(
(self.window_width, self.window_height), pygame.RESIZABLE)
pygame.display.set_caption("Brick Breaker - Chapter 18")
self._update_scaling()
def _update_scaling(self):
win_w, win_h = self.screen.get_size()
game_aspect = GAME_WIDTH / GAME_HEIGHT
win_aspect = win_w / win_h
if win_aspect > game_aspect:
scaled_h = win_h
scaled_w = int(win_h * game_aspect)
else:
scaled_w = win_w
scaled_h = int(win_w / game_aspect)
self.scaled_rect = pygame.Rect(
(win_w - scaled_w) // 2, (win_h - scaled_h) // 2,
scaled_w, scaled_h)
def toggle_fullscreen(self):
self.fullscreen = not self.fullscreen
if self.fullscreen:
self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
else:
self.screen = pygame.display.set_mode(
(self.window_width, self.window_height), pygame.RESIZABLE)
self._update_scaling()
def handle_resize(self, event):
if not self.fullscreen:
self.window_width = event.w
self.window_height = event.h
self.screen = pygame.display.set_mode(
(event.w, event.h), pygame.RESIZABLE)
self._update_scaling()
def present(self):
self.screen.fill((0, 0, 0))
scaled = pygame.transform.smoothscale(
self.game_surface,
(self.scaled_rect.width, self.scaled_rect.height))
self.screen.blit(scaled, self.scaled_rect.topleft)
pygame.display.flip()
def window_to_game(self, wx, wy):
gx = (wx - self.scaled_rect.x) * GAME_WIDTH / self.scaled_rect.width
gy = (wy - self.scaled_rect.y) * GAME_HEIGHT / self.scaled_rect.height
return (int(gx), int(gy))
# -- Button class -----------------------------------------------------------
class Button:
def __init__(self, x, y, width, height, label, font,
color=(80, 80, 120), hover_color=(120, 120, 180),
text_color=(255, 255, 255)):
self.rect = pygame.Rect(x, y, width, height)
self.label = label
self.font = font
self.color = color
self.hover_color = hover_color
self.text_color = text_color
self.hovered = False
self.clicked = False
def update(self, mouse_pos, mouse_click):
self.hovered = self.rect.collidepoint(mouse_pos)
self.clicked = self.hovered and mouse_click
return self.clicked
def draw(self, surface):
current_color = self.hover_color if self.hovered else self.color
shadow_rect = self.rect.move(3, 3)
pygame.draw.rect(surface, (0, 0, 0, 100), shadow_rect,
border_radius=6)
pygame.draw.rect(surface, current_color, self.rect, border_radius=6)
border_color = (200, 200, 255) if self.hovered else (100, 100, 150)
pygame.draw.rect(surface, border_color, self.rect, width=2,
border_radius=6)
text_surf = self.font.render(self.label, True, self.text_color)
text_rect = text_surf.get_rect(center=self.rect.center)
surface.blit(text_surf, text_rect)
# -- Particle System --------------------------------------------------------
class Particle:
def __init__(self, x, y, vx, vy, lifetime, color, size):
self.x = float(x)
self.y = float(y)
self.vx = vx
self.vy = vy
self.lifetime = lifetime
self.max_lifetime = lifetime
self.color = color
self.size = size
self.alive = True
self.gravity = 120.0
def update(self, dt):
self.x += self.vx * dt
self.y += self.vy * dt
self.vy += self.gravity * dt
self.lifetime -= dt
if self.lifetime <= 0:
self.alive = False
def draw(self, surface):
if not self.alive:
return
progress = 1.0 - (self.lifetime / self.max_lifetime)
alpha = int(255 * (1 - progress))
current_size = max(1, int(self.size * (1 - progress * 0.5)))
if alpha <= 0:
return
r, g, b = self.color
surf = pygame.Surface((current_size * 2, current_size * 2),
pygame.SRCALPHA)
pygame.draw.circle(surf, (r, g, b, alpha),
(current_size, current_size), current_size)
surface.blit(surf,
(int(self.x) - current_size, int(self.y) - current_size))
class ParticleSystem:
def __init__(self):
self.particles = []
def update(self, dt):
for p in self.particles:
p.update(dt)
self.particles = [p for p in self.particles if p.alive]
def draw(self, surface):
for p in self.particles:
p.draw(surface)
def clear(self):
self.particles.clear()
def emit_brick_explosion(self, rect, color):
cx, cy = rect.centerx, rect.centery
for _ in range(15):
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(60, 200)
vx = speed * math.cos(angle)
vy = speed * math.sin(angle) - 50
lifetime = random.uniform(0.3, 0.8)
r = min(255, max(0, color[0] + random.randint(-30, 30)))
g = min(255, max(0, color[1] + random.randint(-30, 30)))
b = min(255, max(0, color[2] + random.randint(-30, 30)))
size = random.randint(2, 5)
self.particles.append(
Particle(cx, cy, vx, vy, lifetime, (r, g, b), size))
def emit_paddle_sparks(self, paddle_rect, ball_x):
cx, cy = ball_x, paddle_rect.top
for _ in range(10):
angle = random.uniform(-math.pi, 0)
speed = random.uniform(40, 150)
vx = speed * math.cos(angle)
vy = speed * math.sin(angle)
lifetime = random.uniform(0.2, 0.5)
r = random.randint(220, 255)
g = random.randint(200, 255)
b = random.randint(100, 200)
size = random.randint(1, 3)
self.particles.append(
Particle(cx, cy, vx, vy, lifetime, (r, g, b), size))
def emit_wall_dust(self, x, y, direction):
for _ in range(5):
if direction == "left":
vx = random.uniform(20, 80)
vy = random.uniform(-50, 50)
elif direction == "right":
vx = random.uniform(-80, -20)
vy = random.uniform(-50, 50)
else:
vx = random.uniform(-50, 50)
vy = random.uniform(20, 80)
lifetime = random.uniform(0.2, 0.4)
gray = random.randint(150, 220)
size = random.randint(1, 3)
self.particles.append(
Particle(x, y, vx, vy, lifetime, (gray, gray, gray), size))
# -- Animation classes ------------------------------------------------------
class BrickShatter:
def __init__(self, rect, color):
self.rect = pygame.Rect(rect)
self.color = color
self.timer = 0.0
self.duration = BRICK_SHATTER_DURATION
self.alive = True
def update(self, dt):
self.timer += dt
if self.timer >= self.duration:
self.alive = False
def draw(self, surface):
progress = self.timer / self.duration
if progress < 0.3:
flash = int(255 * (1 - progress / 0.3))
r = min(255, self.color[0] + flash)
g = min(255, self.color[1] + flash)
b = min(255, self.color[2] + flash)
alpha = 255
else:
fade = (progress - 0.3) / 0.7
r, g, b = self.color
alpha = int(255 * (1 - fade))
if alpha <= 0:
return
shrink = int(progress * 10)
draw_rect = self.rect.inflate(-shrink * 2, -shrink * 2)
if draw_rect.width <= 0 or draw_rect.height <= 0:
return
surf = pygame.Surface((draw_rect.width, draw_rect.height),
pygame.SRCALPHA)
surf.fill((r, g, b, alpha))
surface.blit(surf, draw_rect.topleft)
class BallTrail:
def __init__(self):
self.positions = []
self.timer = 0.0
def update(self, dt, ball_x, ball_y):
self.timer += dt
if self.timer >= TRAIL_INTERVAL:
self.timer -= TRAIL_INTERVAL
self.positions.append((ball_x, ball_y))
if len(self.positions) > TRAIL_LENGTH:
self.positions.pop(0)
def draw(self, surface):
count = len(self.positions)
for i, (x, y) in enumerate(self.positions):
ratio = (i + 1) / count if count > 0 else 0
alpha = int(80 * ratio)
radius = max(1, int(BALL_RADIUS * ratio * 0.8))
trail_surf = pygame.Surface((radius * 2, radius * 2),
pygame.SRCALPHA)
pygame.draw.circle(trail_surf, (100, 100, 255, alpha),
(radius, radius), radius)
surface.blit(trail_surf, (int(x) - radius, int(y) - radius))
def reset(self):
self.positions.clear()
self.timer = 0.0
class PaddleGlow:
def __init__(self):
self.timer = 0.0
self.active = False
self.intensity = 0.0
def trigger(self):
self.active = True
self.timer = 0.0
self.intensity = 1.0
def update(self, dt):
if self.active:
self.timer += dt
self.intensity = max(0.0,
1.0 - self.timer / PADDLE_GLOW_DURATION)
if self.timer >= PADDLE_GLOW_DURATION:
self.active = False
def draw(self, surface, paddle_rect):
if not self.active or self.intensity <= 0:
return
glow_alpha = int(150 * self.intensity)
glow_expand = int(6 * self.intensity)
glow_rect = paddle_rect.inflate(glow_expand * 2, glow_expand * 2)
glow_surf = pygame.Surface(
(glow_rect.width, glow_rect.height), pygame.SRCALPHA)
glow_surf.fill((200, 200, 255, glow_alpha))
surface.blit(glow_surf, glow_rect.topleft)
# -- PowerUp class ----------------------------------------------------------
class PowerUp:
"""A falling power-up item released from a special brick."""
def __init__(self, x, y, ptype):
self.x = float(x)
self.y = float(y)
self.ptype = ptype
self.color = POWERUP_COLORS[ptype]
self.label = POWERUP_LABELS[ptype]
self.alive = True
self.vy = POWERUP_FALL_SPEED
self.gravity = 60.0
self.bob_timer = random.uniform(0, math.pi * 2)
self.size = POWERUP_SIZE
def update(self, dt):
self.vy += self.gravity * dt
self.y += self.vy * dt
self.bob_timer += dt * 4
if self.y > GAME_HEIGHT + self.size:
self.alive = False
def get_rect(self):
return pygame.Rect(int(self.x) - self.size // 2,
int(self.y) - self.size // 2,
self.size, self.size)
def draw(self, surface):
if not self.alive:
return
bob_offset = math.sin(self.bob_timer) * 3
cx = int(self.x)
cy = int(self.y + bob_offset)
# Glow
glow_surf = pygame.Surface((self.size * 2, self.size * 2),
pygame.SRCALPHA)
pygame.draw.circle(glow_surf,
(self.color[0], self.color[1], self.color[2], 60),
(self.size, self.size), self.size)
surface.blit(glow_surf, (cx - self.size, cy - self.size))
# Body
pygame.draw.circle(surface, self.color, (cx, cy), self.size // 2)
pygame.draw.circle(surface, (255, 255, 255), (cx, cy),
self.size // 2, 2)
# Label
font = pygame.font.SysFont("monospace", 14, bold=True)
lbl = font.render(self.label, True, (255, 255, 255))
surface.blit(lbl, (cx - lbl.get_width() // 2,
cy - lbl.get_height() // 2))
# -- Sprite classes ---------------------------------------------------------
class Paddle(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.base_width = PADDLE_WIDTH
self.current_width = PADDLE_WIDTH
self.wide_timer = 0.0
self._build_image()
self.rect.centerx = GAME_WIDTH // 2
self.rect.y = GAME_HEIGHT - 40
self.use_mouse = False
def _build_image(self):
w = self.current_width
self.image = pygame.Surface((w, 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), (w, y))
pygame.draw.rect(self.image, (0, 0, 0, 0), (0, 0, 3, 3))
pygame.draw.rect(self.image, (0, 0, 0, 0), (w - 3, 0, 3, 3))
# Tint if wide
if self.current_width > self.base_width:
tint = pygame.Surface((w, PADDLE_HEIGHT), pygame.SRCALPHA)
tint.fill((255, 200, 0, 40))
self.image.blit(tint, (0, 0))
self.image = self.image.convert_alpha()
old_center = self.rect.center if hasattr(self, 'rect') else (
GAME_WIDTH // 2, GAME_HEIGHT - 40)
self.rect = self.image.get_rect(center=old_center)
def make_wider(self, duration=POWERUP_DURATION):
self.current_width = int(self.base_width * 1.5)
self.wide_timer = duration
self._build_image()
def update(self, dt=0, mouse_game_x=None):
# Update power-up timer
if self.wide_timer > 0:
self.wide_timer -= dt
if self.wide_timer <= 0:
self.wide_timer = 0
self.current_width = self.base_width
self._build_image()
if self.use_mouse and mouse_game_x is not None:
self.rect.centerx = mouse_game_x
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(GAME_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(GAME_WIDTH // 2)
self.float_y = float(GAME_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)
self.heavy = False
self.heavy_timer = 0.0
self.heavy_gravity = 40.0 # pixels/s^2 downward pull
def reset(self):
self.float_x = float(GAME_WIDTH // 2)
self.float_y = float(GAME_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)
self.heavy = False
self.heavy_timer = 0.0
def make_heavy(self, duration=5.0):
self.heavy = True
self.heavy_timer = duration
def update(self, dt):
# Heavy ball: gravity pulls downward
if self.heavy:
self.vy += self.heavy_gravity * dt
self.heavy_timer -= dt
if self.heavy_timer <= 0:
self.heavy = False
self.heavy_timer = 0.0
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
wall_direction = None
if self.float_x - BALL_RADIUS <= 0:
self.float_x = BALL_RADIUS
self.vx = abs(self.vx)
self.speed += BALL_SPEED_INCREMENT
bounced = True
wall_direction = "left"
if self.float_x + BALL_RADIUS >= GAME_WIDTH:
self.float_x = GAME_WIDTH - BALL_RADIUS
self.vx = -abs(self.vx)
self.speed += BALL_SPEED_INCREMENT
bounced = True
wall_direction = "right"
if self.float_y - BALL_RADIUS <= 0:
self.float_y = BALL_RADIUS
self.vy = abs(self.vy)
self.speed += BALL_SPEED_INCREMENT
bounced = True
wall_direction = "top"
if bounced:
self.rect.center = (int(self.float_x), int(self.float_y))
return bounced, wall_direction
def fell_off_bottom(self):
return self.float_y - BALL_RADIUS > GAME_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.current_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
def accelerate(self, amount=60):
"""Increase speed (from red brick)."""
self.speed += amount
current_angle = math.atan2(self.vy, self.vx)
self.vx = self.speed * math.cos(current_angle)
self.vy = self.speed * math.sin(current_angle)
def decelerate(self, amount=40):
"""Decrease speed (from blue brick), minimum 150."""
self.speed = max(150, self.speed - amount)
current_angle = math.atan2(self.vy, self.vx)
self.vx = self.speed * math.cos(current_angle)
self.vy = self.speed * math.sin(current_angle)
class Brick(pygame.sprite.Sprite):
def __init__(self, x, y, color, row, brick_type=BRICK_NORMAL):
super().__init__()
self.brick_type = brick_type
self.color = color
self.row = row
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))
# Special brick marker
if brick_type != BRICK_NORMAL:
marker_colors = {
BRICK_RED: (255, 60, 60),
BRICK_BLUE: (60, 60, 255),
BRICK_GREEN: (60, 255, 60),
}
mc = marker_colors.get(brick_type, (255, 255, 255))
pygame.draw.circle(self.image, mc,
(BRICK_WIDTH // 2, BRICK_HEIGHT // 2), 4)
pygame.draw.circle(self.image, (255, 255, 255),
(BRICK_WIDTH // 2, BRICK_HEIGHT // 2), 4, 1)
self.image = self.image.convert_alpha()
self.rect = self.image.get_rect(topleft=(x, y))
def score_value(self):
base = (BRICK_ROWS - self.row) * 10
if self.brick_type != BRICK_NORMAL:
base += 20 # bonus for special bricks
return base
def create_brick_group():
group = pygame.sprite.Group()
for row in range(BRICK_ROWS):
color = rainbow_color(row, BRICK_ROWS)
for col in range(BRICK_COLS):
x = BRICK_OFFSET_X + col * (BRICK_WIDTH + BRICK_PADDING)
y = BRICK_OFFSET_Y + row * (BRICK_HEIGHT + BRICK_PADDING)
# 20 pct chance of special brick
roll = random.random()
if roll < 0.07:
btype = BRICK_RED
bcolor = (220, 60, 60)
elif roll < 0.14:
btype = BRICK_BLUE
bcolor = (60, 60, 220)
elif roll < 0.20:
btype = BRICK_GREEN
bcolor = (60, 200, 60)
else:
btype = BRICK_NORMAL
bcolor = color
group.add(Brick(x, y, bcolor, row, btype))
return group
# -- Game setup -------------------------------------------------------------
scr_mgr = ScreenManager()
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)
button_font = pygame.font.SysFont("monospace", 22, bold=True)
paddle = Paddle()
ball = Ball()
bricks = create_brick_group()
paddle_group = pygame.sprite.GroupSingle(paddle)
ball_group = pygame.sprite.GroupSingle(ball)
ball_trail = BallTrail()
paddle_glow = PaddleGlow()
shatter_animations = []
particles = ParticleSystem()
powerups = []
lives = MAX_LIVES
score = 0
level = 1
game_state = STATE_MENU
current_difficulty = DIFFICULTY_NORMAL
master_volume = 0.5
sound_muted = False
menu_blink_timer = 0.0
# Buttons
btn_start = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 10,
200, 45, "Start", button_font,
color=(40, 100, 60), hover_color=(60, 150, 90))
btn_difficulty = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 65,
200, 45, "Difficulty", button_font)
btn_high_scores = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 120,
200, 45, "High Scores", button_font)
btn_quit = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 175,
200, 45, "Quit", button_font,
color=(120, 40, 40), hover_color=(180, 60, 60))
menu_buttons = [btn_start, btn_difficulty, btn_high_scores, btn_quit]
btn_easy = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 - 30,
200, 45, "Easy", button_font,
color=(40, 120, 40), hover_color=(60, 180, 60))
btn_normal = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 30,
200, 45, "Normal", button_font,
color=(120, 120, 40), hover_color=(180, 180, 60))
btn_hard = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 90,
200, 45, "Hard", button_font,
color=(120, 40, 40), hover_color=(180, 60, 60))
btn_diff_back = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 160,
200, 45, "Back", button_font)
difficulty_buttons = [btn_easy, btn_normal, btn_hard, btn_diff_back]
btn_resume = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 - 20,
200, 45, "Resume", button_font,
color=(40, 100, 60), hover_color=(60, 150, 90))
btn_restart = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 40,
200, 45, "Restart", button_font,
color=(120, 120, 40), hover_color=(180, 180, 60))
btn_pause_quit = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT // 2 + 100,
200, 45, "Quit to Menu", button_font,
color=(120, 40, 40), hover_color=(180, 60, 60))
pause_buttons = [btn_resume, btn_restart, btn_pause_quit]
btn_hs_back = Button(GAME_WIDTH // 2 - 100, GAME_HEIGHT - 80,
200, 45, "Back", button_font)
def play_sound(sound):
if not sound_muted:
sound.set_volume(master_volume)
sound.play()
def spawn_powerup(brick):
"""Spawn a power-up from a special brick."""
if brick.brick_type == BRICK_NORMAL:
return
# 50 pct chance to drop a power-up from a special brick
if random.random() > 0.5:
return
ptype = random.choice([POWERUP_WIDER, POWERUP_LIFE, POWERUP_MULTI])
powerups.append(PowerUp(brick.rect.centerx, brick.rect.centery, ptype))
def apply_powerup(pu):
"""Apply a caught power-up."""
global lives
if pu.ptype == POWERUP_WIDER:
paddle.make_wider()
elif pu.ptype == POWERUP_LIFE:
lives = min(lives + 1, MAX_LIVES + 2)
elif pu.ptype == POWERUP_MULTI:
# Multi-ball hint: just add score bonus (full multi-ball is complex)
pass
def start_game():
global lives, score, level, bricks, game_state, shatter_animations
global powerups
lives = MAX_LIVES
score = 0
level = 1
bricks = create_brick_group()
ball.reset()
ball.speed = DIFFICULTY_SPEEDS[current_difficulty]
angle = math.radians(-60)
ball.vx = ball.speed * math.cos(angle)
ball.vy = ball.speed * math.sin(angle)
ball_trail.reset()
shatter_animations = []
particles.clear()
powerups = []
paddle.current_width = paddle.base_width
paddle.wide_timer = 0.0
paddle._build_image()
game_state = STATE_PLAYING
# -- Draw helpers -----------------------------------------------------------
def draw_menu(surface):
global menu_blink_timer
menu_blink_timer += clock.get_time() / 1000.0
surface.fill(BG_COLOR)
title = big_font.render("BRICK BREAKER", True, (255, 200, 50))
surface.blit(title, (GAME_WIDTH // 2 - title.get_width() // 2,
GAME_HEIGHT // 3 - 60))
subtitle = medium_font.render("Physics Edition", True, (200, 200, 255))
surface.blit(subtitle, (GAME_WIDTH // 2 - subtitle.get_width() // 2,
GAME_HEIGHT // 3 - 10))
def draw_difficulty_screen(surface):
surface.fill(BG_COLOR)
title = big_font.render("DIFFICULTY", True, (255, 200, 50))
surface.blit(title, (GAME_WIDTH // 2 - title.get_width() // 2,
GAME_HEIGHT // 4 - 30))
def draw_high_scores_screen(surface):
surface.fill(BG_COLOR)
title = big_font.render("HIGH SCORES", True, (255, 200, 50))
surface.blit(title, (GAME_WIDTH // 2 - title.get_width() // 2, 40))
scores = load_high_scores()
if not scores:
ns = medium_font.render("No scores yet!", True, (150, 150, 150))
surface.blit(ns, (GAME_WIDTH // 2 - ns.get_width() // 2,
GAME_HEIGHT // 2 - 20))
else:
for i, entry in enumerate(scores[:10]):
color = (255, 215, 0) if i == 0 else (
(200, 200, 200) if i < 3 else (150, 150, 150))
line = "{:>3}. {:>6} {}".format(
i + 1, entry["score"], entry.get("difficulty", "---"))
ts = hud_font.render(line, True, color)
surface.blit(ts, (GAME_WIDTH // 2 - 140, 110 + i * 35))
def draw_hud(surface):
score_surf = hud_font.render("Score: {}".format(score),
True, (255, 255, 100))
surface.blit(score_surf, (10, 10))
lives_surf = hud_font.render("Lives: {}".format(lives),
True, (255, 100, 100))
surface.blit(lives_surf,
(GAME_WIDTH // 2 - lives_surf.get_width() // 2, 10))
level_surf = hud_font.render("Level: {}".format(level),
True, (100, 200, 255))
surface.blit(level_surf,
(GAME_WIDTH - level_surf.get_width() - 10, 10))
# Status indicators
status_parts = []
if ball.heavy:
status_parts.append("HEAVY BALL {:.1f}s".format(ball.heavy_timer))
if paddle.wide_timer > 0:
status_parts.append("WIDE PADDLE {:.1f}s".format(paddle.wide_timer))
if status_parts:
status_text = " | ".join(status_parts)
st = small_font.render(status_text, True, (255, 200, 100))
surface.blit(st, (GAME_WIDTH // 2 - st.get_width() // 2, 30))
# Legend
legend = small_font.render(
"Red=Accel Blue=Decel Green=Heavy Drops=PowerUps",
True, (80, 80, 80))
surface.blit(legend, (10, 30))
def draw_overlay(surface, title_text, title_color):
overlay = pygame.Surface((GAME_WIDTH, GAME_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
surface.blit(overlay, (0, 0))
t = big_font.render(title_text, True, title_color)
surface.blit(t, (GAME_WIDTH // 2 - t.get_width() // 2,
GAME_HEIGHT // 2 - 100))
fs = medium_font.render("Final score: {}".format(score),
True, (255, 255, 255))
surface.blit(fs, (GAME_WIDTH // 2 - fs.get_width() // 2,
GAME_HEIGHT // 2 - 50))
r = medium_font.render("Press SPACE for menu", True, (200, 200, 200))
surface.blit(r, (GAME_WIDTH // 2 - r.get_width() // 2,
GAME_HEIGHT // 2 - 10))
def draw_game_scene(surface):
surface.fill(BG_COLOR)
bricks.draw(surface)
for anim in shatter_animations:
anim.draw(surface)
ball_trail.draw(surface)
paddle_glow.draw(surface, paddle.rect)
paddle_group.draw(surface)
ball_group.draw(surface)
# Draw power-ups
for pu in powerups:
pu.draw(surface)
particles.draw(surface)
draw_hud(surface)
# -- Main loop --------------------------------------------------------------
running = True
while running:
dt = clock.tick(FPS) / 1000.0
raw_mx, raw_my = pygame.mouse.get_pos()
mouse_pos = scr_mgr.window_to_game(raw_mx, raw_my)
mouse_click = False
gs = scr_mgr.game_surface
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.VIDEORESIZE:
scr_mgr.handle_resize(event)
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mouse_click = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_F11:
scr_mgr.toggle_fullscreen()
if event.key == pygame.K_ESCAPE:
if game_state == STATE_PLAYING:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
game_state = STATE_PLAYING
else:
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
# -- Button updates --
if game_state == STATE_MENU:
for btn in menu_buttons:
btn.update(mouse_pos, mouse_click)
if btn_start.clicked:
play_sound(snd_click)
start_game()
elif btn_difficulty.clicked:
play_sound(snd_click)
game_state = STATE_DIFFICULTY
elif btn_high_scores.clicked:
play_sound(snd_click)
game_state = STATE_HIGH_SCORES
elif btn_quit.clicked:
running = False
elif game_state == STATE_DIFFICULTY:
for btn in difficulty_buttons:
btn.update(mouse_pos, mouse_click)
if btn_easy.clicked:
play_sound(snd_click)
current_difficulty = DIFFICULTY_EASY
game_state = STATE_MENU
elif btn_normal.clicked:
play_sound(snd_click)
current_difficulty = DIFFICULTY_NORMAL
game_state = STATE_MENU
elif btn_hard.clicked:
play_sound(snd_click)
current_difficulty = DIFFICULTY_HARD
game_state = STATE_MENU
elif btn_diff_back.clicked:
play_sound(snd_click)
game_state = STATE_MENU
elif game_state == STATE_HIGH_SCORES:
btn_hs_back.update(mouse_pos, mouse_click)
if btn_hs_back.clicked:
play_sound(snd_click)
game_state = STATE_MENU
elif game_state == STATE_PAUSED:
for btn in pause_buttons:
btn.update(mouse_pos, mouse_click)
if btn_resume.clicked:
play_sound(snd_click)
game_state = STATE_PLAYING
elif btn_restart.clicked:
play_sound(snd_click)
start_game()
elif btn_pause_quit.clicked:
play_sound(snd_click)
game_state = STATE_MENU
# -- Update --
if game_state == STATE_PLAYING:
paddle.update(dt=dt, mouse_game_x=mouse_pos[0])
ball.update(dt)
ball_trail.update(dt, ball.float_x, ball.float_y)
paddle_glow.update(dt)
particles.update(dt)
for anim in shatter_animations:
anim.update(dt)
shatter_animations = [a for a in shatter_animations if a.alive]
# Update power-ups
for pu in powerups:
pu.update(dt)
powerups = [pu for pu in powerups if pu.alive]
# Power-up catch (paddle collision)
for pu in powerups[:]:
if pu.alive and pu.get_rect().colliderect(paddle.rect):
pu.alive = False
apply_powerup(pu)
play_sound(snd_powerup)
# Wall bounces
bounced, wall_dir = ball.bounce_wall()
if bounced:
play_sound(snd_wall)
if wall_dir == "left":
particles.emit_wall_dust(BALL_RADIUS, ball.float_y, "left")
elif wall_dir == "right":
particles.emit_wall_dust(
GAME_WIDTH - BALL_RADIUS, ball.float_y, "right")
elif wall_dir == "top":
particles.emit_wall_dust(ball.float_x, BALL_RADIUS, "top")
if ball.fell_off_bottom():
lives -= 1
if lives <= 0:
game_state = STATE_GAME_OVER
play_sound(snd_game_over)
add_high_score(score, current_difficulty)
else:
play_sound(snd_lose_life)
ball.reset()
ball.speed = DIFFICULTY_SPEEDS[current_difficulty]
angle = math.radians(-60)
ball.vx = ball.speed * math.cos(angle)
ball.vy = ball.speed * math.sin(angle)
ball_trail.reset()
if ball.bounce_off_paddle(paddle):
play_sound(snd_paddle)
paddle_glow.trigger()
particles.emit_paddle_sparks(paddle.rect, ball.float_x)
hit_bricks = pygame.sprite.spritecollide(ball, bricks, True)
if hit_bricks:
for brick in hit_bricks:
score += brick.score_value()
shatter_animations.append(
BrickShatter(brick.rect, brick.color))
particles.emit_brick_explosion(brick.rect, brick.color)
# Special brick effects
if brick.brick_type == BRICK_RED:
ball.accelerate(60)
play_sound(snd_speed_up)
elif brick.brick_type == BRICK_BLUE:
ball.decelerate(40)
play_sound(snd_speed_down)
elif brick.brick_type == BRICK_GREEN:
ball.make_heavy(5.0)
# Possibly spawn power-up
spawn_powerup(brick)
ball.vy = -ball.vy
ball.speed += BALL_SPEED_INCREMENT
play_sound(snd_brick)
if len(bricks) == 0:
game_state = STATE_YOU_WIN
play_sound(snd_win)
add_high_score(score, current_difficulty)
# -- Draw --
if game_state == STATE_MENU:
draw_menu(gs)
for btn in menu_buttons:
btn.draw(gs)
elif game_state == STATE_DIFFICULTY:
draw_difficulty_screen(gs)
for btn in difficulty_buttons:
btn.draw(gs)
elif game_state == STATE_HIGH_SCORES:
draw_high_scores_screen(gs)
btn_hs_back.draw(gs)
elif game_state == STATE_PLAYING:
draw_game_scene(gs)
info = small_font.render(
"[M] Mouse [P] Pause [F1] Mute [F11] Fullscreen",
True, (150, 150, 150))
gs.blit(info, (10, GAME_HEIGHT - 25))
elif game_state == STATE_PAUSED:
draw_game_scene(gs)
overlay = pygame.Surface((GAME_WIDTH, GAME_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 150))
gs.blit(overlay, (0, 0))
t = big_font.render("PAUSED", True, (200, 200, 255))
gs.blit(t, (GAME_WIDTH // 2 - t.get_width() // 2,
GAME_HEIGHT // 2 - 120))
for btn in pause_buttons:
btn.draw(gs)
elif game_state == STATE_GAME_OVER:
draw_game_scene(gs)
draw_overlay(gs, "GAME OVER", (255, 60, 60))
elif game_state == STATE_YOU_WIN:
draw_game_scene(gs)
draw_overlay(gs, "YOU WIN!", (100, 255, 100))
scr_mgr.present()
pygame.quit()
sys.exit()
Telechargement
Telecharger le code source de ce chapitre : brick-breaker-ch18.zip
