Game Dev Lab — Day 5
Polish & Demo
Polish Feature Reference
# 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)