"""Fish tank screensaver. Burn-in safe for the CRT: the background is pure black and every drawn element is always in motion. Fish images are loaded from the fish folder and are assumed to face right; a fish swimming left gets flipped horizontally. Bubbles rise from each fish's mouth. """ import math import random import pygame FISH_MIN_H, FISH_MAX_H = 60, 140 FISH_MIN_SPEED, FISH_MAX_SPEED = 40, 130 # px/sec BUBBLE_COLOR = (150, 200, 255) class Bubble: def __init__(self, x, y): self.x = x self.y = y self.radius = random.uniform(2, 6) self.speed = random.uniform(30, 70) self.wobble_phase = random.uniform(0, math.tau) self.wobble_amp = random.uniform(2, 8) def update(self, dt): self.y -= self.speed * dt self.wobble_phase += dt * 3 def draw(self, screen): x = self.x + math.sin(self.wobble_phase) * self.wobble_amp pygame.draw.circle(screen, BUBBLE_COLOR, (int(x), int(self.y)), int(self.radius), width=1) class Fish: def __init__(self, image, screen_size): self.screen_w, self.screen_h = screen_size height = random.randint(FISH_MIN_H, FISH_MAX_H) width = int(image.get_width() * height / image.get_height()) self.image_right = pygame.transform.smoothscale(image, (width, height)) self.image_left = pygame.transform.flip(self.image_right, True, False) self.w, self.h = width, height self.speed = random.uniform(FISH_MIN_SPEED, FISH_MAX_SPEED) self.direction = random.choice([-1, 1]) self.x = random.uniform(0, self.screen_w - width) self.base_y = random.uniform(0, self.screen_h - height) self.bob_phase = random.uniform(0, math.tau) self.bob_amp = random.uniform(5, 20) self.bubble_timer = random.uniform(1, 4) @property def y(self): return self.base_y + math.sin(self.bob_phase) * self.bob_amp def mouth_pos(self): mouth_x = self.x + self.w if self.direction > 0 else self.x return mouth_x, self.y + self.h * 0.45 def update(self, dt, bubbles): self.x += self.speed * self.direction * dt self.bob_phase += dt * random.uniform(0.8, 1.2) # Wrap around: swim fully off one edge, re-enter from the other # at a fresh depth so fish never sit still or trace fixed lines. if self.direction > 0 and self.x > self.screen_w: self.x = -self.w self.base_y = random.uniform(0, self.screen_h - self.h) elif self.direction < 0 and self.x < -self.w: self.x = self.screen_w self.base_y = random.uniform(0, self.screen_h - self.h) self.bubble_timer -= dt if self.bubble_timer <= 0: self.bubble_timer = random.uniform(1.5, 5) mx, my = self.mouth_pos() for _ in range(random.randint(1, 3)): bubbles.append(Bubble(mx + random.uniform(-4, 4), my + random.uniform(-4, 4))) def draw(self, screen): image = self.image_right if self.direction > 0 else self.image_left screen.blit(image, (int(self.x), int(self.y))) class FishTank: def __init__(self, screen_size, fish_images, num_fish): self.fish = [Fish(random.choice(fish_images), screen_size) for _ in range(num_fish)] self.bubbles = [] def update(self, dt): for fish in self.fish: fish.update(dt, self.bubbles) for bubble in self.bubbles: bubble.update(dt) self.bubbles = [b for b in self.bubbles if b.y > -10] def draw(self, screen): screen.fill((0, 0, 0)) for bubble in self.bubbles: bubble.draw(screen) for fish in self.fish: fish.draw(screen)