Game Dev Lab — Day 3
Classes: Clean Architecture
Space Shooter with Classes
# INSTRUCTOR — Game Dev Lab Day 3: Classes — Clean Architecture
# -------------------------------------------------------
# TEACH: The game from Day 2 works, but everything is in one big pile of globals.
# Today we refactor using classes. Same game — cleaner code.
#
# CORE IDEA — a class bundles data + behavior:
# class Enemy:
# def __init__(self, x, y): — set up the object
# def update(self): — move it
# def draw(self, screen): — draw it
# rect — its Rect for collision
#
# TEACH: the pattern update() + draw() used here is the standard game-object
# pattern. Every object knows how to move and draw itself. The main loop just
# calls obj.update() and obj.draw() for every object — the loop doesn't care
# what type of object it is.
#
# MILESTONE (show me when done):
# Game plays the same as Day 2, but code uses Player, Enemy, Bullet classes.
# Game has a title screen (press Enter to start) and a game-over screen (press R).
#
# EXTENSION challenges:
# - Add an Explosion class: spawns colored circles that fade out on enemy death
# - Add a Level system: every 10 kills → new level, enemies faster
# - Add sound: pygame.mixer.Sound("hit.wav").play() on collision
# (pygame can generate a simple beep: see pygame.sndarray or use a tiny .wav)
import pygame
import sys
import random
pygame.init()
WIDTH, HEIGHT = 600, 700
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Space Shooter — Day 3")
clock = pygame.time.Clock()
font = pygame.font.SysFont("courier", 22, bold=True)
big = pygame.font.SysFont("courier", 36, bold=True)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
CYAN = (0, 220, 220)
YELLOW = (255, 255, 0)
RED = (220, 50, 50)
GRAY = (120, 120, 120)
# ── Classes ──────────────────────────────────────────────────────────────────
class Player:
SPEED = 5
def __init__(self):
self.rect = pygame.Rect(WIDTH // 2 - 20, HEIGHT - 70, 40, 30)
self.lives = 3
self.score = 0
def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] and self.rect.left > 0:
self.rect.x -= self.SPEED
if keys[pygame.K_RIGHT] and self.rect.right < WIDTH:
self.rect.x += self.SPEED
def draw(self, surface):
cx = self.rect.centerx
top = (cx, self.rect.top)
left = (self.rect.left, self.rect.bottom)
right = (self.rect.right, self.rect.bottom)
pygame.draw.polygon(surface, CYAN, [top, left, right])
def fire(self):
return Bullet(self.rect.centerx, self.rect.top)
class Bullet:
SPEED = 9
def __init__(self, x, y):
self.rect = pygame.Rect(x - 4, y - 18, 8, 18)
def update(self):
self.rect.y -= self.SPEED
def draw(self, surface):
pygame.draw.ellipse(surface, YELLOW, self.rect)
@property
def off_screen(self):
return self.rect.bottom < 0
class Enemy:
SIZE = 40
def __init__(self, speed=2):
x = random.randint(20, WIDTH - self.SIZE - 20)
self.rect = pygame.Rect(x, -self.SIZE, self.SIZE, self.SIZE)
self.speed = speed
def update(self):
self.rect.y += self.speed
def draw(self, surface):
pygame.draw.rect(surface, RED, self.rect, border_radius=6)
@property
def off_screen(self):
return self.rect.top > HEIGHT
# ── Game states ──────────────────────────────────────────────────────────────
STATE_TITLE = 'title'
STATE_PLAYING = 'playing'
STATE_GAMEOVER = 'gameover'
# ── Helpers ───────────────────────────────────────────────────────────────────
def draw_centered(surface, text, fnt, color, y):
rendered = fnt.render(text, True, color)
rect = rendered.get_rect(center=(WIDTH // 2, y))
surface.blit(rendered, rect)
def new_game():
player = Player()
bullets = []
enemies = [Enemy() for _ in range(5)]
return player, bullets, enemies
# ── Main ─────────────────────────────────────────────────────────────────────
player, bullets, enemies = new_game()
state = STATE_TITLE
SPAWN_EVENT = pygame.USEREVENT + 1
pygame.time.set_timer(SPAWN_EVENT, 1400)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit(); sys.exit()
if event.type == pygame.KEYDOWN:
if state == STATE_TITLE and event.key == pygame.K_RETURN:
player, bullets, enemies = new_game()
state = STATE_PLAYING
elif state == STATE_PLAYING and event.key == pygame.K_SPACE:
bullets.append(player.fire())
elif state == STATE_GAMEOVER and event.key == pygame.K_r:
player, bullets, enemies = new_game()
state = STATE_PLAYING
if event.type == SPAWN_EVENT and state == STATE_PLAYING:
enemies.append(Enemy(speed=2 + player.score // 10))
# ── Update ────────────────────────────────────────────────────────────────
if state == STATE_PLAYING:
player.update()
for b in bullets[:]:
b.update()
if b.off_screen:
bullets.remove(b)
for e in enemies[:]:
e.update()
if e.off_screen:
enemies.remove(e)
player.lives -= 1
if player.lives <= 0:
state = STATE_GAMEOVER
# Collision
for b in bullets[:]:
for e in enemies[:]:
if b.rect.colliderect(e.rect):
bullets.remove(b)
enemies.remove(e)
player.score += 1
break
# ── Draw ──────────────────────────────────────────────────────────────────
screen.fill(BLACK)
if state == STATE_TITLE:
draw_centered(screen, "SPACE SHOOTER", big, CYAN, HEIGHT // 2 - 60)
draw_centered(screen, "Press ENTER to start", font, GRAY, HEIGHT // 2)
draw_centered(screen, "Arrow keys: move Space: fire", font, WHITE, HEIGHT // 2 + 50)
elif state in (STATE_PLAYING, STATE_GAMEOVER):
player.draw(screen)
for b in bullets:
b.draw(screen)
for e in enemies:
e.draw(screen)
hud = font.render(f"Score: {player.score} Lives: {player.lives}", True, WHITE)
screen.blit(hud, (10, 10))
if state == STATE_GAMEOVER:
draw_centered(screen, "GAME OVER", big, RED, HEIGHT // 2 - 40)
draw_centered(screen, f"Final score: {player.score}", font, WHITE, HEIGHT // 2 + 10)
draw_centered(screen, "Press R to restart", font, GRAY, HEIGHT // 2 + 55)
pygame.display.flip()
clock.tick(60)