Game Dev Lab — Day 5

Polish & Demo

# INSTRUCTOR — Game Dev Lab Day 5: Polish & Demo
# -------------------------------------------------------
# TEACH: Day 5 is not a new game — it's about taking what the student has
# built and making it feel like a real, finished game.
#
# The student picks their best project from Days 1–4 and adds at least
# THREE "polish" features from the list below. Then they demo it.
#
# MILESTONE (show me when done):
#   Game runs without crashes.
#   At least 3 polish features added (from the list below).
#   Student can explain one design decision they made.
#
# ── POLISH FEATURE MENU ──────────────────────────────────────────────────────
# Pick any 3 (or more). Each one is a self-contained code snippet.
# Reference this file while the student builds.
# ─────────────────────────────────────────────────────────────────────────────

import pygame
import sys
import random
import json
import os

pygame.init()
pygame.mixer.init()


# ═══════════════════════════════════════════════════════════════════════════════
# FEATURE A — High Score (saved to a file)
# ═══════════════════════════════════════════════════════════════════════════════

HS_FILE = "highscore.json"

def load_highscore():
    if os.path.exists(HS_FILE):
        with open(HS_FILE) as f:
            return json.load(f).get("high", 0)
    return 0

def save_highscore(score):
    current = load_highscore()
    if score > current:
        with open(HS_FILE, "w") as f:
            json.dump({"high": score}, f)

# Usage in game-over logic:
#   save_highscore(player.score)
#   high = load_highscore()
#   screen.blit(font.render(f"Best: {high}", True, WHITE), (10, 40))


# ═══════════════════════════════════════════════════════════════════════════════
# FEATURE B — Particle Explosion
# ═══════════════════════════════════════════════════════════════════════════════

class Particle:
    def __init__(self, x, y, color):
        self.x   = float(x)
        self.y   = float(y)
        angle    = random.uniform(0, 360)
        speed    = random.uniform(1.5, 5)
        rad      = angle * 3.14159 / 180
        self.vx  = speed * (0 + 1 * (angle < 180) - 0.5)   # rough direction
        self.vx  = random.uniform(-speed, speed)
        self.vy  = random.uniform(-speed, 0.5)
        self.life = random.randint(15, 35)
        self.color = color

    def update(self):
        self.x  += self.vx
        self.y  += self.vy
        self.vy += 0.2   # gravity
        self.life -= 1

    def draw(self, surface):
        alpha = max(0, self.life * 7)   # fade out
        r = max(2, self.life // 6)
        pygame.draw.circle(surface, self.color, (int(self.x), int(self.y)), r)

    @property
    def dead(self):
        return self.life <= 0

# Usage: add to a particles list when an enemy is destroyed
#   particles = []
#   # on enemy death:
#   for _ in range(18):
#       particles.append(Particle(e.x, e.y, (255, 180, 30)))
#   # each frame:
#   for p in particles[:]:
#       p.update(); p.draw(screen)
#       if p.dead: particles.remove(p)


# ═══════════════════════════════════════════════════════════════════════════════
# FEATURE C — Beep Sound (no .wav file needed)
# ═══════════════════════════════════════════════════════════════════════════════

import numpy as np   # numpy is needed; install with: pip install numpy

def make_beep(frequency=440, duration_ms=80, volume=0.3):
    """Generate a simple sine-wave beep using numpy + pygame."""
    sample_rate = 44100
    n_samples   = int(sample_rate * duration_ms / 1000)
    t = np.linspace(0, duration_ms / 1000, n_samples, endpoint=False)
    wave = (np.sin(2 * np.pi * frequency * t) * volume * 32767).astype(np.int16)
    stereo = np.column_stack([wave, wave])
    sound  = pygame.sndarray.make_sound(stereo)
    return sound

# TEACH: call this once at startup, then .play() it on events
# shoot_sound = make_beep(600, 60)   # high beep for shooting
# hit_sound   = make_beep(200, 120)  # low thud for hit
# shoot_sound.play()


# ═══════════════════════════════════════════════════════════════════════════════
# FEATURE D — Screen Flash on Hit
# ═══════════════════════════════════════════════════════════════════════════════

flash_timer = 0   # set to e.g. 8 when hit
FLASH_COLOR = (200, 40, 40)   # red tint

def maybe_flash(surface):
    global flash_timer
    if flash_timer > 0:
        overlay = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
        alpha   = min(160, flash_timer * 20)
        overlay.fill((*FLASH_COLOR, alpha))
        surface.blit(overlay, (0, 0))
        flash_timer -= 1

# Usage: flash_timer = 8  (on player getting hit)
#        maybe_flash(screen)  (each frame, after drawing everything)


# ═══════════════════════════════════════════════════════════════════════════════
# FEATURE E — Scrolling Star Background
# ═══════════════════════════════════════════════════════════════════════════════

WIDTH, HEIGHT = 600, 700

stars = [
    {'x': random.randint(0, WIDTH),
     'y': random.randint(0, HEIGHT),
     'speed': random.uniform(0.5, 2.5),
     'size':  random.randint(1, 3)}
    for _ in range(80)
]

def update_stars():
    for s in stars:
        s['y'] += s['speed']
        if s['y'] > HEIGHT:
            s['y'] = 0
            s['x'] = random.randint(0, WIDTH)

def draw_stars(surface):
    for s in stars:
        brightness = int(100 + s['size'] * 50)
        color = (brightness, brightness, brightness)
        pygame.draw.circle(surface, color, (int(s['x']), int(s['y'])), s['size'])

# Usage: call update_stars() and draw_stars(screen) each frame, before other drawing


# ═══════════════════════════════════════════════════════════════════════════════
# FEATURE F — Pause Screen
# ═══════════════════════════════════════════════════════════════════════════════

# In your event loop:
#   if event.key == pygame.K_p:
#       paused = not paused
#
# In your game loop, wrap update logic:
#   if not paused:
#       <all update calls>
#
# Draw pause overlay:
#   if paused:
#       overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
#       overlay.fill((0, 0, 0, 140))
#       screen.blit(overlay, (0, 0))
#       msg = font.render("PAUSED — press P to continue", True, (200, 200, 200))
#       screen.blit(msg, msg.get_rect(center=(WIDTH // 2, HEIGHT // 2)))


# ═══════════════════════════════════════════════════════════════════════════════
# FEATURE G — Level Progression
# ═══════════════════════════════════════════════════════════════════════════════

# In Player class, track level:
#   self.level = 1
#   LEVEL_THRESHOLDS = [10, 25, 45, 70, 100]
#
# In game loop after score increases:
#   if player.score >= LEVEL_THRESHOLDS[player.level - 1]:
#       player.level += 1
#       enemy_speed += 0.5
#       spawn_interval = max(600, spawn_interval - 100)
#       pygame.time.set_timer(SPAWN_EVENT, spawn_interval)
#       # Optionally flash "LEVEL UP!" text for 2 seconds


# ═══════════════════════════════════════════════════════════════════════════════
# DUMMY MAIN — just proves the snippets load without error
# ═══════════════════════════════════════════════════════════════════════════════

if __name__ == "__main__":
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("Polish Reference — Day 5")
    clock  = pygame.time.Clock()
    font   = pygame.font.SysFont("courier", 20, bold=True)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit(); sys.exit()

        screen.fill((10, 10, 20))
        update_stars()
        draw_stars(screen)
        msg = font.render("Day 5 Polish Reference — pick 3 features!", True, (180, 220, 255))
        screen.blit(msg, msg.get_rect(center=(WIDTH // 2, HEIGHT // 2)))

        pygame.display.flip()
        clock.tick(60)