import json import math import os import random from dataclasses import dataclass from pathlib import Path import pygame pygame.init() WIDTH, HEIGHT = 960, 760 FPS = 120 LEFT, RIGHT, TOP = 22, WIDTH - 22, 70 PADDLE_Y = HEIGHT - 67 BALL_RADIUS = 8 BASE_SPEED, MAX_SPEED = 350.0, 840.0 SCORE_FILE = Path.cwd() / "high_scores.json" ARCADE_FILE = Path("/tmp/alpaca_score.json") screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("NEON VAUS — Breakout / DOH") clock = pygame.time.Clock() Vec = pygame.Vector2 BG = (8, 12, 29) WHITE = (232, 245, 255) CYAN = (73, 227, 255) PINK = (255, 85, 161) YELLOW = (255, 219, 102) GRAY = (157, 165, 185) fonts = { "small": pygame.font.Font(None, 22), "normal": pygame.font.Font(None, 28), "large": pygame.font.Font(None, 49), "title": pygame.font.Font(None, 78), } POWER_COLORS = { "E": (77, 238, 154), "L": (255, 95, 117), "M": (95, 182, 255), "C": (249, 191, 75), "P": (177, 182, 195), } POWER_NAMES = {"E": "ENLARGE", "L": "LASER", "M": "MULTI", "C": "CATCH"} def text(message, x, y, color=WHITE, size="normal", center=False): image = fonts[size].render(str(message), True, color) rect = image.get_rect() if center: rect.center = (int(x), int(y)) else: rect.topleft = (int(x), int(y)) screen.blit(image, rect) return rect def load_scores(): try: with SCORE_FILE.open("r", encoding="utf-8") as file: data = json.load(file) entries = [] if isinstance(data, list): for row in data: if not isinstance(row, dict): continue name = row.get("initials", row.get("name", "AAA")) score = row.get("score") if isinstance(name, str) and isinstance(score, int) and score >= 0: entries.append({"initials": name[:10] or "AAA", "score": score}) return sorted(entries, key=lambda row: row["score"], reverse=True)[:5] except (OSError, ValueError, TypeError): return [] def atomic_json(path, data): path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_name(path.name + ".tmp") with temporary.open("w", encoding="utf-8") as file: json.dump(data, file, indent=2) os.replace(temporary, path) def circle_rect_overlap(position, radius, rect): closest = Vec( max(rect.left, min(position.x, rect.right)), max(rect.top, min(position.y, rect.bottom)), ) return (position - closest).length_squared() <= radius * radius def reflect_rect(ball, rect): closest = Vec( max(rect.left, min(ball.pos.x, rect.right)), max(rect.top, min(ball.pos.y, rect.bottom)), ) delta = ball.pos - closest distance_sq = delta.length_squared() if distance_sq > BALL_RADIUS * BALL_RADIUS: return False if distance_sq > 0.00001: distance = math.sqrt(distance_sq) normal = delta / distance ball.pos += normal * (BALL_RADIUS - distance + 0.15) else: sides = [ (abs(ball.pos.x - rect.left), Vec(-1, 0), Vec(rect.left - BALL_RADIUS - 0.1, ball.pos.y)), (abs(rect.right - ball.pos.x), Vec(1, 0), Vec(rect.right + BALL_RADIUS + 0.1, ball.pos.y)), (abs(ball.pos.y - rect.top), Vec(0, -1), Vec(ball.pos.x, rect.top - BALL_RADIUS - 0.1)), (abs(rect.bottom - ball.pos.y), Vec(0, 1), Vec(ball.pos.x, rect.bottom + BALL_RADIUS + 0.1)), ] _, normal, location = min(sides, key=lambda item: item[0]) ball.pos = location dot = ball.vel.dot(normal) if dot < 0: ball.vel -= 2 * dot * normal return True @dataclass class Ball: pos: Vec vel: Vec speed: float = BASE_SPEED held: bool = False offset: float = 0.0 trail: list = None def __post_init__(self): self.trail = [] @dataclass class Brick: rect: pygame.Rect kind: str hp: int = 1 max_hp: int = 1 @dataclass class Capsule: pos: Vec kind: str @dataclass class Alien: pos: Vec origin_x: float phase: float age: float = 0.0 class Game: def __init__(self): self.scores = load_scores() self.state = "title" self.paused = False self.time = 0.0 self.score = 0 self.lives = 3 self.level = 1 self.next_extra_life = 20000 self.paddle_x = WIDTH / 2 self.paddle_width = 112 self.aim = 0.0 self.balls = [] self.bricks = [] self.capsules = [] self.aliens = [] self.lasers = [] self.particles = [] self.effects = {} self.alien_timer = 6.0 self.laser_cooldown = 0.0 self.banner = "" self.banner_timer = 0.0 self.continue_timer = 10.0 self.name = "" self.result_message = "" self.run_recorded = True self.save_error = "" self.boss_hp = 0 self.boss_max_hp = 36 self.boss_phase = 0.0 self.boss_flash = 0.0 self.boss_rect = pygame.Rect(0, 0, 220, 154) star_rng = random.Random(8) self.stars = [ (star_rng.randrange(WIDTH), star_rng.randrange(TOP, HEIGHT), star_rng.choice([1, 1, 2]), star_rng.uniform(5, 24)) for _ in range(100) ] @property def paddle(self): return pygame.Rect( round(self.paddle_x - self.paddle_width / 2), PADDLE_Y, round(self.paddle_width), 18 ) def announce(self, message, duration=2.2): self.banner = message self.banner_timer = duration def add_score(self, amount): self.score += amount while self.score >= self.next_extra_life: self.lives += 1 self.next_extra_life += 60000 self.announce("SCORE BONUS: EXTRA PLAYER!", 3.0) def burst(self, position, color, count=14): for _ in range(count): angle = random.uniform(0, math.tau) velocity = Vec(math.cos(angle), math.sin(angle)) * random.uniform(40, 200) duration = random.uniform(0.25, 0.65) self.particles.append([Vec(position), velocity, duration, duration, color]) if len(self.particles) > 900: self.particles = self.particles[-900:] def clear_powers(self): self.effects.clear() self.paddle_width = 112 self.lasers.clear() self.capsules.clear() self.laser_cooldown = 0 def serve(self): self.paddle_x = WIDTH / 2 self.aim = 0 self.balls = [Ball( Vec(self.paddle_x, PADDLE_Y - BALL_RADIUS - 1), Vec(0, -BASE_SPEED), held=True )] def new_game(self): self.score = 0 self.lives = 3 self.next_extra_life = 20000 self.level = 1 self.name = "" self.run_recorded = False self.save_error = "" self.paused = False self.particles.clear() self.state = "playing" self.setup_level() def setup_level(self): self.clear_powers() self.bricks.clear() self.aliens.clear() self.boss_phase = 0.0 self.alien_timer = 5.0 if self.level <= 2: columns = 12 gap = 5 brick_width = 69 total = columns * brick_width + (columns - 1) * gap start_x = (WIDTH - total) // 2 for row in range(7): for col in range(columns): if row == 6 and col % 3 != 1: continue kind = "standard" hp = 1 if row in (2, 4) and col in (2, 9): kind = "wall" elif (row + col * 2 + self.level) % 9 == 0: kind = "explosive" elif (row + col + self.level) % 4 == 0 or ( self.level == 2 and row == 0 ): kind = "reinforced" hp = 2 if self.level == 1 else 3 rect = pygame.Rect(start_x + col * (brick_width + gap), 126 + row * 31, brick_width, 25) self.bricks.append(Brick(rect, kind, hp, hp)) self.boss_hp = 0 self.announce(f"SECTOR {self.level} — CLEAR ALL BREAKABLE BRICKS", 3.0) else: self.boss_hp = self.boss_max_hp self.position_boss() self.announce("FINAL SECTOR — DESTROY DOH", 3.5) self.serve() def position_boss(self): self.boss_rect.center = ( round(WIDTH / 2 + math.sin(self.boss_phase * 0.77) * 260), round(178 + math.sin(self.boss_phase * 1.31) * 35) ) def launch(self): if self.state != "playing" or self.paused: return for ball in self.balls: if ball.held: angle = math.radians(self.aim) ball.vel = Vec(math.sin(angle), -math.cos(angle)) * ball.speed ball.held = False ball.trail.clear() def drop_capsule(self, position): roll = random.random() if roll < 0.012: kind = "P" elif roll < 0.235: kind = random.choice(["E", "L", "M", "C"]) else: return self.capsules.append(Capsule(Vec(position), kind)) def damage_brick(self, brick): if brick not in self.bricks or brick.kind == "wall": return brick.hp -= 1 if brick.hp > 0: self.add_score(30) self.burst(brick.rect.center, CYAN, 5) return queue = [brick] while queue: current = queue.pop() if current not in self.bricks or current.kind == "wall": continue self.bricks.remove(current) value = {"standard": 150, "reinforced": 350, "explosive": 250}[current.kind] self.add_score(value) color = PINK if current.kind == "explosive" else CYAN self.burst(current.rect.center, color, 18) self.drop_capsule(current.rect.center) if current.kind == "explosive": blast = current.rect.inflate(160, 74) self.burst(current.rect.center, YELLOW, 35) for neighbor in list(self.bricks): if neighbor.kind != "wall" and blast.colliderect(neighbor.rect): queue.append(neighbor) def hit_boss(self): if self.boss_hp <= 0: return self.boss_hp -= 1 self.boss_flash = 0.13 self.add_score(250) self.burst(self.boss_rect.center, PINK, 12) if self.boss_hp == 0: self.add_score(10000) self.burst(self.boss_rect.center, YELLOW, 100) def apply_power(self, kind): if kind == "P": self.lives += 1 self.announce("PLAYER CAPSULE: EXTRA LIFE!") return self.effects[kind] = 18.0 if kind != "M" else 22.0 if kind == "E": self.paddle_width = 180 elif kind == "M": moving = [ball for ball in self.balls if not ball.held] if not moving: self.launch() moving = list(self.balls) additions = [] for original in moving: for angle in (-25, 25): if len(self.balls) + len(additions) >= 6: break direction = original.vel.rotate(angle) if abs(direction.y) < 100: direction.y = -140 direction.scale_to_length(original.speed) additions.append(Ball(Vec(original.pos), direction, original.speed)) self.balls.extend(additions) self.announce(POWER_NAMES[kind] + " ONLINE") def expire_power(self, kind): if kind == "E": self.paddle_width = 112 elif kind == "C": self.launch() elif kind == "M" and len(self.balls) > 1: moving = [ball for ball in self.balls if not ball.held] self.balls = [(moving or self.balls)[0]] def lose_life(self): self.lives -= 1 self.clear_powers() self.aliens.clear() if self.lives <= 0: self.state = "gameover" self.continue_timer = 10.0 self.announce("") else: self.serve() self.announce("PLAYER LOST — SPACE TO LAUNCH", 2.5) def continue_game(self): self.lives = 3 self.state = "playing" self.paused = False self.clear_powers() self.aliens.clear() self.serve() self.announce("COIN ACCEPTED — CONTINUING RUN", 2.5) def finish_run(self, victory=False): self.state = "name" self.paused = False self.name = "" self.result_message = "DOH DEFEATED — SECTOR SECURED!" if victory else "GAME OVER" pygame.key.start_text_input() def record_run(self): if self.run_recorded: return name = self.name.strip().upper()[:10] or "AAA" entry = {"initials": name, "score": int(self.score)} self.scores = sorted( self.scores + [entry], key=lambda row: row["score"], reverse=True )[:5] errors = [] try: atomic_json(SCORE_FILE, self.scores) except OSError as error: errors.append("high_scores.json: " + str(error)) try: atomic_json(ARCADE_FILE, entry) except OSError as error: errors.append("/tmp/alpaca_score.json: " + str(error)) self.save_error = " | ".join(errors) if self.save_error: print("Score save error:", self.save_error) self.run_recorded = True def handle_event(self, event): if event.type == pygame.TEXTINPUT and self.state == "name": for char in event.text.upper(): if char.isascii() and (char.isalnum() or char in " -_"): if len(self.name) < 10: self.name += char if event.type != pygame.KEYDOWN: return key = event.key if self.state in ("title", "results"): if key in (pygame.K_RETURN, pygame.K_SPACE): self.new_game() elif self.state == "playing": if key in (pygame.K_ESCAPE, pygame.K_p): self.paused = not self.paused elif key == pygame.K_SPACE: self.launch() elif self.state == "gameover": if key == pygame.K_c: self.continue_game() elif key == pygame.K_RETURN: self.finish_run() elif self.state == "name": if key == pygame.K_BACKSPACE: self.name = self.name[:-1] elif key == pygame.K_RETURN: self.record_run() pygame.key.stop_text_input() self.state = "results" def update_particles(self, dt): for particle in self.particles: particle[0] += particle[1] * dt particle[1] *= max(0, 1 - dt * 1.6) particle[2] -= dt self.particles = [particle for particle in self.particles if particle[2] > 0] def update(self, dt): self.time += dt if self.state == "gameover": self.update_particles(dt) self.continue_timer -= dt if self.continue_timer <= 0: self.finish_run() return if self.state != "playing": self.update_particles(dt) return if self.paused: return self.update_particles(dt) self.banner_timer = max(0, self.banner_timer - dt) self.boss_flash = max(0, self.boss_flash - dt) keys = pygame.key.get_pressed() movement = int(keys[pygame.K_RIGHT]) - int(keys[pygame.K_LEFT]) self.paddle_x += movement * 610 * dt self.paddle_x = max(LEFT + self.paddle_width / 2, min(RIGHT - self.paddle_width / 2, self.paddle_x)) self.aim += (int(keys[pygame.K_d]) - int(keys[pygame.K_a])) * 100 * dt self.aim = max(-72, min(72, self.aim)) for kind in list(self.effects): self.effects[kind] -= dt if self.effects[kind] <= 0: del self.effects[kind] self.expire_power(kind) if self.level == 3: self.boss_phase += dt self.position_boss() self.update_aliens(dt) self.update_capsules(dt) self.update_lasers(dt, keys) steps = max(1, math.ceil(dt / (1 / 240))) sub_dt = dt / steps for ball in list(self.balls): if ball not in self.balls: continue if ball.held: limit = self.paddle_width / 2 - BALL_RADIUS ball.offset = max(-limit, min(limit, ball.offset)) ball.pos = Vec(self.paddle_x + ball.offset, PADDLE_Y - BALL_RADIUS - 1) continue ball.speed = min(MAX_SPEED, ball.speed + 4.2 * dt) if ball.vel.length_squared() < 1: ball.vel = Vec(0, -ball.speed) ball.vel.scale_to_length(ball.speed) ball.trail.append(Vec(ball.pos)) ball.trail = ball.trail[-10:] for _ in range(steps): self.update_ball(ball, sub_dt) if ball.held or ball.pos.y > HEIGHT + 30: break if ball.pos.y > HEIGHT + 30 and ball in self.balls: self.balls.remove(ball) if self.level == 3 and self.boss_hp <= 0: self.finish_run(victory=True) return if self.level < 3 and not any(brick.kind != "wall" for brick in self.bricks): self.add_score(2000) self.level += 1 self.setup_level() return if not self.balls: self.lose_life() def update_capsules(self, dt): for capsule in list(self.capsules): capsule.pos.y += 130 * dt rect = pygame.Rect(round(capsule.pos.x - 15), round(capsule.pos.y - 10), 30, 20) if rect.colliderect(self.paddle): self.capsules.remove(capsule) self.add_score(100) self.apply_power(capsule.kind) elif capsule.pos.y > HEIGHT + 20: self.capsules.remove(capsule) def update_aliens(self, dt): self.alien_timer -= dt if self.alien_timer <= 0: self.alien_timer = random.uniform(4.0, 7.0) if self.level < 3 else 3.2 if len(self.aliens) < 5: x = random.uniform(100, WIDTH - 100) self.aliens.append(Alien(Vec(x, TOP + 28), x, random.uniform(0, math.tau))) for alien in list(self.aliens): alien.age += dt alien.pos.y += (44 if self.level < 3 else 65) * dt alien.pos.x = max(LEFT + 25, min( RIGHT - 25, alien.origin_x + math.sin(alien.phase + alien.age * 2.3) * 65 )) if alien.pos.y > PADDLE_Y - 25: self.aliens.remove(alien) def destroy_alien(self, alien): if alien in self.aliens: self.aliens.remove(alien) self.add_score(500) self.burst(alien.pos, (131, 255, 134), 22) def update_lasers(self, dt, keys): self.laser_cooldown -= dt if "L" in self.effects and keys[pygame.K_SPACE] and self.laser_cooldown <= 0: self.laser_cooldown = 0.19 for side in (-1, 1): self.lasers.append(Vec( self.paddle_x + side * (self.paddle_width / 2 - 10), PADDLE_Y - 10 )) for laser in list(self.lasers): old_y = laser.y laser.y -= 900 * dt sweep = pygame.Rect(round(laser.x - 2), round(laser.y - 10), 4, max(12, math.ceil(old_y - laser.y + 12))) hit = False for alien in list(self.aliens): if sweep.colliderect(pygame.Rect( round(alien.pos.x - 19), round(alien.pos.y - 14), 38, 28 )): self.destroy_alien(alien) hit = True break if not hit and self.level == 3 and self.boss_hp > 0: if sweep.colliderect(self.boss_rect): self.hit_boss() hit = True if not hit: candidates = [brick for brick in self.bricks if sweep.colliderect(brick.rect)] if candidates: brick = max(candidates, key=lambda item: item.rect.bottom) self.damage_brick(brick) hit = True if hit or laser.y < TOP: if laser in self.lasers: self.lasers.remove(laser) def update_ball(self, ball, dt): previous = Vec(ball.pos) ball.pos += ball.vel * dt if ball.pos.x - BALL_RADIUS < LEFT: ball.pos.x = LEFT + BALL_RADIUS ball.vel.x = abs(ball.vel.x) elif ball.pos.x + BALL_RADIUS > RIGHT: ball.pos.x = RIGHT - BALL_RADIUS ball.vel.x = -abs(ball.vel.x) if ball.pos.y - BALL_RADIUS < TOP: ball.pos.y = TOP + BALL_RADIUS ball.vel.y = abs(ball.vel.y) paddle = self.paddle if (ball.vel.y > 0 and previous.y + BALL_RADIUS <= paddle.top + 5 and circle_rect_overlap(ball.pos, BALL_RADIUS, paddle)): offset = max(-1.0, min(1.0, (ball.pos.x - self.paddle_x) / (self.paddle_width / 2))) ball.pos.y = PADDLE_Y - BALL_RADIUS - 0.2 ball.speed = min(MAX_SPEED, ball.speed + 7) if "C" in self.effects: ball.held = True ball.offset = offset * (self.paddle_width / 2 - BALL_RADIUS) ball.trail.clear() return angle = math.radians(offset * 74) ball.vel = Vec(math.sin(angle), -math.cos(angle)) * ball.speed self.burst(ball.pos, CYAN, 4) for alien in list(self.aliens): if (ball.pos - alien.pos).length_squared() < (BALL_RADIUS + 19) ** 2: angle = random.uniform(-math.pi, math.pi) direction = Vec(math.cos(angle), math.sin(angle)) if abs(direction.y) < 0.25: direction.y = -0.35 ball.vel = direction.normalize() * ball.speed self.destroy_alien(alien) break if self.level == 3 and self.boss_hp > 0: if reflect_rect(ball, self.boss_rect): self.hit_boss() for brick in list(self.bricks): if reflect_rect(ball, brick.rect): self.damage_brick(brick) break if abs(ball.vel.y) < 65: ball.vel.y = -65 if ball.vel.y <= 0 else 65 ball.vel.scale_to_length(ball.speed) def draw_background(self): screen.fill(BG) for x, y, radius, speed in self.stars: yy = TOP + (y - TOP + self.time * speed) % (HEIGHT - TOP) pygame.draw.circle(screen, (48, 72, 103), (int(x), int(yy)), radius) for x in range(LEFT, RIGHT, 48): pygame.draw.line(screen, (13, 25, 45), (x, TOP), (x, HEIGHT)) for y in range(TOP, HEIGHT, 48): pygame.draw.line(screen, (13, 25, 45), (LEFT, y), (RIGHT, y)) pygame.draw.line(screen, CYAN, (LEFT - 3, TOP), (LEFT - 3, HEIGHT - 37), 3) pygame.draw.line(screen, CYAN, (RIGHT + 3, TOP), (RIGHT + 3, HEIGHT - 37), 3) pygame.draw.line(screen, (62, 122, 159), (LEFT, TOP), (RIGHT, TOP), 2) pygame.draw.line(screen, (128, 37, 69), (LEFT, HEIGHT - 35), (RIGHT, HEIGHT - 35), 2) def draw_bricks(self): palette = [(49, 135, 212), (124, 81, 205), (40, 174, 189), (189, 71, 151)] for brick in self.bricks: rect = brick.rect if brick.kind == "wall": color = (77, 85, 109) elif brick.kind == "reinforced": color = (184, 155, 83) elif brick.kind == "explosive": color = (217, 72, 91) else: color = palette[(rect.y // 31) % len(palette)] pygame.draw.rect(screen, (4, 7, 17), rect.move(3, 3), border_radius=4) pygame.draw.rect(screen, color, rect, border_radius=4) lighter = tuple(min(255, channel + 50) for channel in color) pygame.draw.line(screen, lighter, (rect.left + 4, rect.top + 3), (rect.right - 5, rect.top + 3), 2) if brick.kind == "wall": pygame.draw.rect(screen, GRAY, rect.inflate(-7, -7), 1, border_radius=2) for x in (rect.left + 10, rect.right - 10): pygame.draw.circle(screen, (210, 220, 235), (x, rect.centery), 2) elif brick.kind == "reinforced": for i in range(brick.hp): cx = rect.centerx + (i - (brick.hp - 1) / 2) * 13 pygame.draw.rect(screen, YELLOW, (int(cx - 3), rect.centery - 3, 6, 7)) if brick.hp < brick.max_hp: pygame.draw.lines(screen, (60, 48, 33), False, [ (rect.left + 17, rect.top + 6), (rect.left + 25, rect.centery), (rect.left + 20, rect.bottom - 3) ], 2) elif brick.kind == "explosive": text("X", rect.centerx, rect.centery + 1, WHITE, "small", True) def draw_boss(self): if self.level != 3 or self.boss_hp <= 0: return rect = self.boss_rect color = (245, 182, 228) if self.boss_flash > 0 else (137, 108, 171) for side in (-1, 1): x = rect.centerx + side * (rect.width // 2 + 10) pygame.draw.rect(screen, (71, 63, 110), (x - 12, rect.top + 37, 24, 80), border_radius=8) pygame.draw.line(screen, CYAN, (x, rect.top + 49), (x, rect.top + 98), 3) pygame.draw.rect(screen, (54, 39, 86), rect.inflate(10, 10), border_radius=30) pygame.draw.rect(screen, color, rect, border_radius=26) pygame.draw.rect(screen, (174, 148, 204), (rect.x + 24, rect.y + 8, rect.w - 48, 25), border_radius=8) for side in (-1, 1): ex = rect.centerx + side * 53 pygame.draw.rect(screen, (26, 16, 43), (ex - 27, rect.y + 48, 54, 25), border_radius=5) pygame.draw.rect(screen, PINK, (ex - 19, rect.y + 55, 38, 10), border_radius=3) pygame.draw.circle(screen, WHITE, (ex, rect.y + 59), 4) pygame.draw.polygon(screen, (97, 75, 135), [ (rect.centerx, rect.y + 57), (rect.centerx - 19, rect.y + 106), (rect.centerx + 21, rect.y + 106) ]) pygame.draw.rect(screen, (28, 20, 43), (rect.centerx - 52, rect.bottom - 33, 104, 17), border_radius=4) for x in range(rect.centerx - 43, rect.centerx + 48, 18): pygame.draw.line(screen, (186, 167, 207), (x, rect.bottom - 31), (x, rect.bottom - 18), 3) text("D O H", rect.centerx, rect.y - 17, PINK, "normal", True) bar = pygame.Rect(WIDTH // 2 - 180, 84, 360, 12) pygame.draw.rect(screen, (60, 29, 57), bar, border_radius=6) pygame.draw.rect(screen, PINK, ( bar.x, bar.y, max(0, int(bar.width * self.boss_hp / self.boss_max_hp)), bar.height ), border_radius=6) pygame.draw.rect(screen, WHITE, bar, 1, border_radius=6) def draw_playfield(self): self.draw_bricks() self.draw_boss() for alien in self.aliens: x, y = round(alien.pos.x), round(alien.pos.y) pygame.draw.ellipse(screen, (45, 126, 105), (x - 23, y - 7, 46, 17)) pygame.draw.ellipse(screen, (121, 250, 157), (x - 12, y - 17, 24, 23)) pygame.draw.line(screen, (12, 38, 42), (x - 7, y - 7), (x + 7, y - 7), 5) for dx in (-15, 0, 15): pygame.draw.circle(screen, YELLOW, (x + dx, y + 5), 2) for capsule in self.capsules: rect = pygame.Rect(round(capsule.pos.x - 16), round(capsule.pos.y - 11), 32, 22) pygame.draw.rect(screen, POWER_COLORS[capsule.kind], rect, border_radius=10) pygame.draw.rect(screen, WHITE, rect, 1, border_radius=10) text(capsule.kind, rect.centerx, rect.centery + 1, BG, "small", True) for laser in self.lasers: pygame.draw.line(screen, PINK, (int(laser.x), int(laser.y)), (int(laser.x), int(laser.y - 15)), 5) pygame.draw.line(screen, WHITE, (int(laser.x), int(laser.y)), (int(laser.x), int(laser.y - 15)), 2) paddle = self.paddle if self.state == "playing" or self.lives > 0: pygame.draw.rect(screen, (26, 67, 103), paddle.inflate(0, 7), border_radius=8) pygame.draw.rect(screen, (116, 175, 209), paddle, border_radius=7) pygame.draw.rect(screen, CYAN if "C" not in self.effects else YELLOW, paddle.inflate(-30, -8), border_radius=4) for side in (-1, 1): x = self.paddle_x + side * (self.paddle_width / 2 - 9) pygame.draw.rect(screen, PINK, (round(x - 8), PADDLE_Y - 2, 16, 22), border_radius=4) if "L" in self.effects: pygame.draw.rect(screen, WHITE, (round(x - 3), PADDLE_Y - 13, 6, 15), border_radius=2) for ball in self.balls: for i, point in enumerate(ball.trail): radius = max(1, int(BALL_RADIUS * (i + 1) / 12)) pygame.draw.circle(screen, (29, 76 + i * 8, 107 + i * 9), (round(point.x), round(point.y)), radius) pygame.draw.circle(screen, CYAN, (round(ball.pos.x), round(ball.pos.y)), BALL_RADIUS + 2) pygame.draw.circle(screen, WHITE, (round(ball.pos.x), round(ball.pos.y)), BALL_RADIUS - 1) if ball.held and self.state == "playing": direction = Vec(math.sin(math.radians(self.aim)), -math.cos(math.radians(self.aim))) for distance in range(20, 100, 13): point = ball.pos + direction * distance pygame.draw.circle(screen, YELLOW, (round(point.x), round(point.y)), 2) for position, velocity, life, total, color in self.particles: faded = tuple(int(channel * life / total) for channel in color) pygame.draw.circle(screen, faded, (round(position.x), round(position.y)), 3) def draw_hud(self): pygame.draw.rect(screen, (10, 16, 34), (0, 0, WIDTH, TOP - 4)) pygame.draw.line(screen, (62, 122, 159), (0, TOP - 4), (WIDTH, TOP - 4), 2) text("SCORE", 30, 12, CYAN, "small") text(f"{self.score:07d}", 30, 32, WHITE, "large") text("HIGH", WIDTH // 2 - 60, 12, PINK, "small") best = self.scores[0]["score"] if self.scores else 0 text(f"{best:07d}", WIDTH // 2 - 60, 32, WHITE, "normal") text("LIVES", WIDTH - 200, 12, YELLOW, "small") for i in range(min(self.lives, 6)): x = WIDTH - 200 + i * 30 pygame.draw.circle(screen, CYAN, (x + 10, 42), 9) pygame.draw.circle(screen, WHITE, (x + 10, 42), 4) if self.lives > 6: text(f"+{self.lives - 6}", WIDTH - 20, 32, WHITE, "small") if self.level == 3: text("DOH", WIDTH - 90, 12, PINK, "small") else: text(f"SECTOR {self.level}", WIDTH - 110, 12, CYAN, "small") y = 60 for kind, remaining in self.effects.items(): label = POWER_NAMES.get(kind, kind) text(f"{label} {remaining:0.1f}", 30, y, POWER_COLORS.get(kind, WHITE), "small") y += 20 if self.banner_timer > 0 and self.banner: text(self.banner, WIDTH // 2, HEIGHT // 2 - 120, YELLOW, "normal", True) def draw_title(self): self.draw_background() text("NEON VAUS", WIDTH // 2, 190, CYAN, "title", True) text("BREAKOUT // DOH PROTOCOL", WIDTH // 2, 250, PINK, "normal", True) text("LEFT / RIGHT MOVE PADDLE", WIDTH // 2, 330, WHITE, "normal", True) text("SPACE LAUNCH / FIRE LASER", WIDTH // 2, 360, WHITE, "normal", True) text("A / D AIM HELD BALL", WIDTH // 2, 390, WHITE, "normal", True) text("P / ESC PAUSE", WIDTH // 2, 420, WHITE, "normal", True) text("PRESS ENTER OR SPACE TO START", WIDTH // 2, 500, YELLOW, "large", True) self.draw_scoreboard(WIDTH // 2, 580) def draw_scoreboard(self, cx, top): text("HIGH SCORES", cx, top, PINK, "normal", True) if not self.scores: text("NO RECORDS YET", cx, top + 34, GRAY, "small", True) return for i, row in enumerate(self.scores): y = top + 34 + i * 24 text(f"{i + 1}. {row['initials']}", cx - 90, y, WHITE, "small") text(f"{row['score']:07d}", cx + 90, y, CYAN, "small") def draw_gameover(self): self.draw_playfield() overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) overlay.fill((4, 6, 18, 205)) screen.blit(overlay, (0, 0)) text("GAME OVER", WIDTH // 2, 200, PINK, "title", True) text(f"FINAL SCORE {self.score:07d}", WIDTH // 2, 280, WHITE, "large", True) text(f"CONTINUE IN {max(0, self.continue_timer):0.1f}s", WIDTH // 2, 340, YELLOW, "normal", True) text("PRESS C TO INSERT COIN", WIDTH // 2, 380, CYAN, "normal", True) text("PRESS ENTER TO END RUN", WIDTH // 2, 410, GRAY, "small", True) self.draw_scoreboard(WIDTH // 2, 470) def draw_name(self): self.draw_playfield() overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) overlay.fill((4, 6, 18, 215)) screen.blit(overlay, (0, 0)) text(self.result_message, WIDTH // 2, 200, PINK, "large", True) text(f"FINAL SCORE {self.score:07d}", WIDTH // 2, 260, WHITE, "normal", True) text("ENTER YOUR INITIALS", WIDTH // 2, 330, CYAN, "normal", True) box = pygame.Rect(WIDTH // 2 - 160, 360, 320, 60) pygame.draw.rect(screen, (12, 22, 44), box, border_radius=8) pygame.draw.rect(screen, CYAN, box, 2, border_radius=8) text(self.name + "_", box.centerx, box.centery, WHITE, "large", True) text("PRESS ENTER TO SAVE", WIDTH // 2, 450, YELLOW, "small", True) def draw_results(self): self.draw_background() text("RUN RECORDED", WIDTH // 2, 150, CYAN, "large", True) text(f"{self.name or 'AAA'} — {self.score:07d}", WIDTH // 2, 210, WHITE, "normal", True) if self.save_error: text("SAVE WARNING: " + self.save_error[:70], WIDTH // 2, 250, PINK, "small", True) self.draw_scoreboard(WIDTH // 2, 300) text("PRESS ENTER OR SPACE FOR NEW GAME", WIDTH // 2, 560, YELLOW, "normal", True) def draw(self): if self.state == "title": self.draw_title() elif self.state == "playing": self.draw_background() self.draw_playfield() self.draw_hud() if self.paused: overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) overlay.fill((4, 6, 18, 170)) screen.blit(overlay, (0, 0)) text("PAUSED", WIDTH // 2, HEIGHT // 2, CYAN, "title", True) elif self.state == "gameover": self.draw_gameover() elif self.state == "name": self.draw_name() elif self.state == "results": self.draw_results() pygame.display.flip() def main(): game = Game() running = True while running: dt = min(clock.tick(FPS) / 1000.0, 0.05) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False else: game.handle_event(event) game.update(dt) game.draw() pygame.quit() if __name__ == "__main__": main()