Created
February 19, 2025 16:26
-
-
Save CypherpunkSamurai/1c3c8e6e1d120f51d05a608a0366f991 to your computer and use it in GitHub Desktop.
Pygame Breaker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pygame | |
import sys | |
import random | |
import abc | |
import math | |
from typing import List, Tuple | |
# Initialize Pygame | |
pygame.init() | |
# Constants | |
WINDOW_WIDTH = 800 | |
WINDOW_HEIGHT = 600 | |
PADDLE_WIDTH = 100 | |
PADDLE_HEIGHT = 20 | |
BALL_SIZE = 8 | |
BRICK_WIDTH = 80 | |
BRICK_HEIGHT = 30 | |
BRICK_ROWS = 5 | |
BRICK_COLS = 10 | |
PADDLE_SPEED = 8 | |
FPS = 60 | |
# Colors | |
WHITE = (255, 255, 255) | |
BLACK = (0, 0, 0) | |
RED = (255, 0, 0) | |
BLUE = (0, 0, 255) | |
GREEN = (0, 255, 0) | |
YELLOW = (255, 255, 0) | |
ORANGE = (255, 165, 0) | |
COLORS = [RED, ORANGE, YELLOW, GREEN, BLUE] | |
class Particle: | |
def __init__(self, x: float, y: float, color: Tuple[int, int, int]): | |
self.x = x | |
self.y = y | |
self.color = color | |
self.size = random.randint(2, 4) | |
angle = random.uniform(0, 2 * math.pi) | |
speed = random.uniform(2, 5) | |
self.dx = math.cos(angle) * speed | |
self.dy = math.sin(angle) * speed | |
self.life = 1.0 # Life from 1 to 0 | |
def update(self): | |
self.x += self.dx | |
self.y += self.dy | |
self.dy += 0.1 # Gravity | |
self.life -= 0.02 | |
self.size = max(0, self.size - 0.1) | |
return self.life > 0 | |
def draw(self, screen): | |
alpha = int(self.life * 255) | |
particle_surface = pygame.Surface((self.size * 2, self.size * 2), pygame.SRCALPHA) | |
pygame.draw.circle(particle_surface, (*self.color, alpha), | |
(self.size, self.size), self.size) | |
screen.blit(particle_surface, (int(self.x - self.size), int(self.y - self.size))) | |
class Brick: | |
def __init__(self, x: int, y: int, color: Tuple[int, int, int], strength: int = 1): | |
self.rect = pygame.Rect(x, y, BRICK_WIDTH, BRICK_HEIGHT) | |
self.color = color | |
self.strength = strength | |
self.hit_animation = 0 | |
def draw(self, screen): | |
# Draw brick with hit animation | |
if self.hit_animation > 0: | |
color = tuple(min(255, c + 100) for c in self.color) | |
self.hit_animation -= 1 | |
else: | |
color = self.color | |
pygame.draw.rect(screen, color, self.rect) | |
pygame.draw.rect(screen, tuple(max(0, c - 50) for c in self.color), | |
self.rect, 2) # Border | |
class Player(abc.ABC): | |
def __init__(self, paddle_x: float, paddle_y: float): | |
self.paddle_x = paddle_x | |
self.paddle_y = paddle_y | |
self.target_x = paddle_x # For smooth movement | |
self.paddle_rect = pygame.Rect(paddle_x, paddle_y, PADDLE_WIDTH, PADDLE_HEIGHT) | |
self.trail: List[Tuple[float, float]] = [] | |
@abc.abstractmethod | |
def update(self, ball_x: float, ball_y: float): | |
"""Update paddle position based on implementation""" | |
pass | |
def draw(self, screen): | |
# Draw trail effect | |
for i, (x, y) in enumerate(self.trail): | |
alpha = int(255 * (i / len(self.trail))) | |
trail_surface = pygame.Surface((PADDLE_WIDTH, PADDLE_HEIGHT), pygame.SRCALPHA) | |
pygame.draw.rect(trail_surface, (*BLUE[:3], alpha), | |
(0, 0, PADDLE_WIDTH, PADDLE_HEIGHT)) | |
screen.blit(trail_surface, (int(x), int(y))) | |
# Draw main paddle | |
self.paddle_rect = pygame.Rect(self.paddle_x, self.paddle_y, PADDLE_WIDTH, PADDLE_HEIGHT) | |
pygame.draw.rect(screen, BLUE, self.paddle_rect) | |
# Add a gradient effect | |
for i in range(3): | |
pygame.draw.rect(screen, tuple(max(0, c - 20 * i) for c in BLUE), | |
self.paddle_rect, 2) | |
# Update trail | |
self.trail.append((self.paddle_x, self.paddle_y)) | |
if len(self.trail) > 3: | |
self.trail.pop(0) | |
class PlayerHuman(Player): | |
def __init__(self, paddle_x: float, paddle_y: float): | |
super().__init__(paddle_x, paddle_y) | |
def update(self, ball_x: float, ball_y: float): | |
keys = pygame.key.get_pressed() | |
if keys[pygame.K_LEFT]: | |
self.target_x = max(0, self.paddle_x - PADDLE_SPEED) | |
if keys[pygame.K_RIGHT]: | |
self.target_x = min(WINDOW_WIDTH - PADDLE_WIDTH, self.paddle_x + PADDLE_SPEED) | |
# Smooth movement | |
self.paddle_x += (self.target_x - self.paddle_x) * 0.3 | |
class PlayerCPU(Player): | |
def __init__(self, paddle_x: float, paddle_y: float, difficulty: int = 3): | |
super().__init__(paddle_x, paddle_y) | |
self.difficulty = max(1, min(5, difficulty)) | |
self.speed = PADDLE_SPEED * (self.difficulty / 3) | |
self.prediction_error = (6 - self.difficulty) * 20 | |
self.reaction_delay = max(1, (6 - self.difficulty) * 5) | |
self.frame_counter = 0 | |
def update(self, ball_x: float, ball_y: float): | |
self.frame_counter += 1 | |
if self.frame_counter % self.reaction_delay == 0: | |
# Predict ball position with error based on difficulty | |
target_x = ball_x + random.randint(-self.prediction_error, self.prediction_error) | |
target_x = max(PADDLE_WIDTH/2, min(WINDOW_WIDTH - PADDLE_WIDTH/2, target_x)) | |
self.target_x = target_x - PADDLE_WIDTH/2 | |
# Smooth movement | |
self.paddle_x += (self.target_x - self.paddle_x) * (self.difficulty * 0.1) | |
class Game: | |
def __init__(self, player_type: str = "human", difficulty: int = 3): | |
self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) | |
pygame.display.set_caption("Brick Breaker") | |
self.clock = pygame.time.Clock() | |
# Player initialization | |
paddle_x = WINDOW_WIDTH // 2 - PADDLE_WIDTH // 2 | |
paddle_y = WINDOW_HEIGHT - 40 | |
if player_type.lower() == "human": | |
self.player = PlayerHuman(paddle_x, paddle_y) | |
else: | |
self.player = PlayerCPU(paddle_x, paddle_y, difficulty) | |
# Ball initialization | |
self.ball_x = WINDOW_WIDTH // 2 | |
self.ball_y = WINDOW_HEIGHT - 60 | |
self.ball_dx = 5 | |
self.ball_dy = -5 | |
self.ball_trail: List[Tuple[float, float]] = [] | |
# Game state | |
self.particles: List[Particle] = [] | |
self.bricks: List[Brick] = [] | |
self.score = 0 | |
self.combo = 1 | |
self.combo_timer = 0 | |
self.game_paused = False | |
# UI | |
self.font = pygame.font.Font(None, 36) | |
self.small_font = pygame.font.Font(None, 24) | |
self.init_bricks() | |
def init_bricks(self): | |
for row in range(BRICK_ROWS): | |
for col in range(BRICK_COLS): | |
brick = Brick( | |
col * (BRICK_WIDTH + 2) + 1, | |
row * (BRICK_HEIGHT + 2) + 1, | |
COLORS[row % len(COLORS)], | |
strength=BRICK_ROWS - row | |
) | |
self.bricks.append(brick) | |
def create_particles(self, x: float, y: float, color: Tuple[int, int, int], count: int = 10): | |
for _ in range(count): | |
self.particles.append(Particle(x, y, color)) | |
def handle_events(self): | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
return False | |
elif event.type == pygame.KEYDOWN: | |
if event.key == pygame.K_r: | |
self.reset_game() | |
elif event.key == pygame.K_p: | |
self.game_paused = not self.game_paused | |
return True | |
def update(self): | |
if self.game_paused: | |
return | |
# Update player | |
self.player.update(self.ball_x, self.ball_y) | |
# Update ball position | |
self.ball_x += self.ball_dx | |
self.ball_y += self.ball_dy | |
# Update ball trail | |
self.ball_trail.append((self.ball_x, self.ball_y)) | |
if len(self.ball_trail) > 5: | |
self.ball_trail.pop(0) | |
# Ball collision with walls | |
if self.ball_x <= 0 or self.ball_x >= WINDOW_WIDTH - BALL_SIZE: | |
self.ball_dx *= -1 | |
self.create_particles(self.ball_x, self.ball_y, WHITE) | |
if self.ball_y <= 0: | |
self.ball_dy *= -1 | |
self.create_particles(self.ball_x, self.ball_y, WHITE) | |
# Ball collision with paddle | |
ball_rect = pygame.Rect(self.ball_x - BALL_SIZE/2, | |
self.ball_y - BALL_SIZE/2, | |
BALL_SIZE, BALL_SIZE) | |
if ball_rect.colliderect(self.player.paddle_rect): | |
self.ball_dy = -abs(self.ball_dy) | |
hit_pos = (self.ball_x - self.player.paddle_x) / PADDLE_WIDTH | |
self.ball_dx = 7 * (hit_pos - 0.5) * 2 | |
self.create_particles(self.ball_x, self.ball_y, BLUE) | |
# Ball collision with bricks | |
for brick in self.bricks[:]: | |
if ball_rect.colliderect(brick.rect): | |
brick.strength -= 1 | |
if brick.strength <= 0: | |
self.bricks.remove(brick) | |
self.score += 10 * self.combo | |
self.combo += 1 | |
self.combo_timer = 2 * FPS # 2 seconds | |
self.create_particles(self.ball_x, self.ball_y, brick.color, 15) | |
else: | |
brick.hit_animation = 10 | |
self.create_particles(self.ball_x, self.ball_y, brick.color, 5) | |
# Determine bounce direction based on collision side | |
brick_center_x = brick.rect.centerx | |
brick_center_y = brick.rect.centery | |
angle = math.atan2(self.ball_y - brick_center_y, | |
self.ball_x - brick_center_x) | |
if abs(math.sin(angle)) > 0.7: # Top/bottom collision | |
self.ball_dy *= -1 | |
else: # Side collision | |
self.ball_dx *= -1 | |
# Update particles | |
self.particles = [p for p in self.particles if p.update()] | |
# Update combo timer | |
if self.combo_timer > 0: | |
self.combo_timer -= 1 | |
else: | |
self.combo = 1 | |
# Check if ball is below paddle (game over) | |
if self.ball_y >= WINDOW_HEIGHT: | |
self.reset_game() | |
def reset_game(self): | |
self.ball_x = WINDOW_WIDTH // 2 | |
self.ball_y = WINDOW_HEIGHT - 60 | |
self.ball_dx = random.choice([-5, 5]) | |
self.ball_dy = -5 | |
self.score = 0 | |
self.combo = 1 | |
self.combo_timer = 0 | |
self.bricks.clear() | |
self.particles.clear() | |
self.init_bricks() | |
self.player.paddle_x = WINDOW_WIDTH // 2 - PADDLE_WIDTH // 2 | |
self.game_paused = False | |
def draw(self): | |
self.screen.fill(BLACK) | |
# Draw ball trail | |
for i, (x, y) in enumerate(self.ball_trail): | |
alpha = int(255 * (i / len(self.ball_trail))) | |
trail_surface = pygame.Surface((BALL_SIZE, BALL_SIZE), pygame.SRCALPHA) | |
pygame.draw.circle(trail_surface, (WHITE[0], WHITE[1], WHITE[2], alpha), | |
(BALL_SIZE//2, BALL_SIZE//2), BALL_SIZE//2) | |
self.screen.blit(trail_surface, (int(x - BALL_SIZE/2), int(y - BALL_SIZE/2))) | |
# Draw player | |
self.player.draw(self.screen) | |
# Draw particles | |
for particle in self.particles: | |
particle.draw(self.screen) | |
# Draw ball | |
pygame.draw.circle(self.screen, WHITE, | |
(int(self.ball_x), int(self.ball_y)), BALL_SIZE) | |
# Draw bricks | |
for brick in self.bricks: | |
brick.draw(self.screen) | |
# Draw score and combo | |
score_text = self.font.render(f"Score: {self.score}", True, WHITE) | |
self.screen.blit(score_text, (10, 10)) | |
if self.combo > 1: | |
combo_text = self.font.render(f"Combo: x{self.combo}", True, | |
(255, 255, 0)) | |
self.screen.blit(combo_text, (10, 50)) | |
# Draw player type and difficulty if CPU | |
player_info = "Human Player" if isinstance(self.player, PlayerHuman) else f"CPU Player (Difficulty: {self.player.difficulty})" | |
info_text = self.font.render(player_info, True, GREEN) | |
self.screen.blit(info_text, (WINDOW_WIDTH - 250, 10)) | |
# Draw controls info | |
controls_text = self.small_font.render("P: Pause | R: Reset", True, WHITE) | |
self.screen.blit(controls_text, (WINDOW_WIDTH - 150, WINDOW_HEIGHT - 30)) | |
# Draw pause screen | |
if self.game_paused: | |
pause_surface = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA) | |
pause_surface.fill((0, 0, 0, 128)) | |
self.screen.blit(pause_surface, (0, 0)) | |
pause_text = self.font.render("PAUSED", True, WHITE) | |
text_rect = pause_text.get_rect(center=(WINDOW_WIDTH/2, WINDOW_HEIGHT/2)) | |
self.screen.blit(pause_text, text_rect) | |
pygame.display.flip() | |
def run(self): | |
running = True | |
while running: | |
running = self.handle_events() | |
self.update() | |
self.draw() | |
self.clock.tick(FPS) | |
if __name__ == "__main__": | |
# Create game with either human or CPU player | |
# game = Game(player_type="human") # For human player | |
game = Game(player_type="cpu", difficulty=3) # For CPU player, difficulty 1-5 | |
game.run() | |
pygame.quit() | |
sys.exit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment