Chapter 2.5: Building Space Invaders

Classic Arcade Action with Sprites and Matrices

๐ŸŽฎ PROJECT 2.5 | Difficulty: Intermediate-Advanced | Time: 30 minutes

๐Ÿ’ป Interactive Options:

Note

Special Note: This project is designed for students who have completed the first chapter of CS Quests and are familiar with Python basics. If youโ€™re new to programming, consider starting with the earlier lessons before diving into this project.

Another Special Note: This project requires a local Python environment for full functionality. While we provide examples and code snippets here, some features may not run directly in the browser. For the best experience, consider setting up the project on your own computer.

๐Ÿ‘พ Game Design: Space Invaders

Goal: Defend Earth by shooting alien invaders before they reach the bottom

Game Mechanics:

  • Player ship moves left/right at bottom
  • Shoot bullets upward
  • Aliens move side-to-side and down
  • Aliens shoot back!
  • Game over if aliens reach bottom or player is hit

๐ŸŽจ Sprites Using Matrices

Weโ€™ll represent sprites as 2D matrices (like pixel art):

import pygame
import numpy as np

# Define alien as a matrix (1 = filled, 0 = empty)
ALIEN_SPRITE = [
    [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 1, 0, 1, 1, 0, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 1, 1, 1, 1, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1, 0, 1],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
]

PLAYER_SPRITE = [
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]

def draw_sprite(surface, sprite, x, y, color, pixel_size=3):
    """Draw a sprite matrix on the screen"""
    for row_idx, row in enumerate(sprite):
        for col_idx, pixel in enumerate(row):
            if pixel:
                rect = pygame.Rect(
                    x + col_idx * pixel_size,
                    y + row_idx * pixel_size,
                    pixel_size,
                    pixel_size
                )
                pygame.draw.rect(surface, color, rect)

print("โœ… Sprite system defined!")
print(f"   Alien: {len(ALIEN_SPRITE)}x{len(ALIEN_SPRITE[0])} matrix")
print(f"   Player: {len(PLAYER_SPRITE)}x{len(PLAYER_SPRITE[0])} matrix")

๐Ÿš€ Complete Space Invaders Game

# space_invaders.py
import pygame
import random
import sys

pygame.init()

# Constants
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FPS = 60

# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
YELLOW = (255, 255, 0)

# Sprites
ALIEN_SPRITE = [
    [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 1, 0, 1, 1, 0, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 1, 1, 1, 1, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1, 0, 1],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
]

PLAYER_SPRITE = [
    [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0],
    [0, 0, 1, 1, 1, 1, 1, 1, 0, 0],
    [0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Space Invaders")
clock = pygame.time.Clock()

def draw_sprite(surface, sprite, x, y, color, pixel_size=3):
    for row_idx, row in enumerate(sprite):
        for col_idx, pixel in enumerate(row):
            if pixel:
                rect = pygame.Rect(
                    x + col_idx * pixel_size,
                    y + row_idx * pixel_size,
                    pixel_size,
                    pixel_size
                )
                pygame.draw.rect(surface, color, rect)

class Player:
    def __init__(self):
        self.x = SCREEN_WIDTH // 2
        self.y = SCREEN_HEIGHT - 80
        self.speed = 5
        self.width = 30
        self.height = 18
    
    def move(self, dx):
        self.x += dx * self.speed
        self.x = max(0, min(self.x, SCREEN_WIDTH - self.width))
    
    def draw(self, surface):
        draw_sprite(surface, PLAYER_SPRITE, self.x, self.y, GREEN)
    
    def shoot(self):
        return Bullet(self.x + self.width // 2, self.y, -1)

class Alien:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.width = 30
        self.height = 24
    
    def draw(self, surface):
        draw_sprite(surface, ALIEN_SPRITE, self.x, self.y, RED)
    
    def move(self, dx, dy):
        self.x += dx
        self.y += dy

class Bullet:
    def __init__(self, x, y, direction):
        self.x = x
        self.y = y
        self.direction = direction  # -1 for up, 1 for down
        self.speed = 7
        self.width = 3
        self.height = 10
        self.active = True
    
    def update(self):
        self.y += self.direction * self.speed
        if self.y < 0 or self.y > SCREEN_HEIGHT:
            self.active = False
    
    def draw(self, surface):
        color = YELLOW if self.direction == -1 else RED
        pygame.draw.rect(surface, color, (self.x, self.y, self.width, self.height))
    
    def get_rect(self):
        return pygame.Rect(self.x, self.y, self.width, self.height)

class Game:
    def __init__(self):
        self.player = Player()
        self.aliens = []
        self.player_bullets = []
        self.alien_bullets = []
        self.score = 0
        self.game_over = False
        self.alien_direction = 1
        self.alien_speed = 1
        self.shoot_cooldown = 0
        self.create_aliens()
    
    def create_aliens(self):
        rows = 4
        cols = 10
        spacing_x = 60
        spacing_y = 50
        start_x = 100
        start_y = 50
        
        for row in range(rows):
            for col in range(cols):
                x = start_x + col * spacing_x
                y = start_y + row * spacing_y
                self.aliens.append(Alien(x, y))
    
    def handle_input(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and self.shoot_cooldown == 0:
                    self.player_bullets.append(self.player.shoot())
                    self.shoot_cooldown = 20
                elif event.key == pygame.K_r and self.game_over:
                    self.__init__()
        
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.player.move(-1)
        if keys[pygame.K_RIGHT]:
            self.player.move(1)
        
        return True
    
    def update(self):
        if self.game_over:
            return
        
        if self.shoot_cooldown > 0:
            self.shoot_cooldown -= 1
        
        # Move aliens
        move_down = False
        for alien in self.aliens:
            alien.move(self.alien_direction * self.alien_speed, 0)
            if alien.x <= 0 or alien.x >= SCREEN_WIDTH - alien.width:
                move_down = True
        
        if move_down:
            self.alien_direction *= -1
            for alien in self.aliens:
                alien.y += 20
                if alien.y >= SCREEN_HEIGHT - 100:
                    self.game_over = True
        
        # Update bullets
        for bullet in self.player_bullets:
            bullet.update()
        self.player_bullets = [b for b in self.player_bullets if b.active]
        
        for bullet in self.alien_bullets:
            bullet.update()
        self.alien_bullets = [b for b in self.alien_bullets if b.active]
        
        # Alien shooting
        if random.random() < 0.01 and self.aliens:
            shooter = random.choice(self.aliens)
            self.alien_bullets.append(
                Bullet(shooter.x + shooter.width // 2, shooter.y + shooter.height, 1)
            )
        
        # Check collisions
        for bullet in self.player_bullets[:]:
            bullet_rect = bullet.get_rect()
            for alien in self.aliens[:]:
                alien_rect = pygame.Rect(alien.x, alien.y, alien.width, alien.height)
                if bullet_rect.colliderect(alien_rect):
                    self.aliens.remove(alien)
                    bullet.active = False
                    self.score += 10
                    break
        
        # Check player hit
        player_rect = pygame.Rect(self.player.x, self.player.y, 
                                  self.player.width, self.player.height)
        for bullet in self.alien_bullets:
            if bullet.get_rect().colliderect(player_rect):
                self.game_over = True
        
        # Win condition
        if not self.aliens:
            self.game_over = True
    
    def draw(self):
        screen.fill(BLACK)
        
        self.player.draw(screen)
        
        for alien in self.aliens:
            alien.draw(screen)
        
        for bullet in self.player_bullets:
            bullet.draw(screen)
        
        for bullet in self.alien_bullets:
            bullet.draw(screen)
        
        # UI
        font = pygame.font.Font(None, 36)
        score_text = font.render(f'Score: {self.score}', True, WHITE)
        screen.blit(score_text, (10, 10))
        
        if self.game_over:
            overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
            overlay.set_alpha(200)
            overlay.fill(BLACK)
            screen.blit(overlay, (0, 0))
            
            game_over_font = pygame.font.Font(None, 72)
            if not self.aliens:
                text = game_over_font.render('YOU WIN!', True, GREEN)
            else:
                text = game_over_font.render('GAME OVER', True, RED)
            
            screen.blit(text, (SCREEN_WIDTH // 2 - text.get_width() // 2, 250))
            
            restart_font = pygame.font.Font(None, 36)
            restart_text = restart_font.render('Press R to Restart', True, WHITE)
            screen.blit(restart_text, (SCREEN_WIDTH // 2 - restart_text.get_width() // 2, 350))
        
        pygame.display.flip()

# Main loop
game = Game()
running = True

while running:
    running = game.handle_input()
    game.update()
    game.draw()
    clock.tick(FPS)

pygame.quit()
sys.exit()

๐ŸŽฏ Key Concepts: Collision Detection

# Rectangle collision detection
def check_collision(rect1, rect2):
    """
    Check if two rectangles overlap
    rect format: (x, y, width, height)
    """
    x1, y1, w1, h1 = rect1
    x2, y2, w2, h2 = rect2
    
    # Check if rectangles DON'T overlap, then negate
    return not (x1 + w1 < x2 or  # rect1 left of rect2
                x2 + w2 < x1 or  # rect2 left of rect1
                y1 + h1 < y2 or  # rect1 above rect2
                y2 + h2 < y1)    # rect2 above rect1

# Examples
bullet = (100, 200, 3, 10)
alien = (95, 190, 30, 24)
miss = (200, 200, 3, 10)

print("๐ŸŽฏ Collision Examples:")
print(f"  Bullet hits alien: {check_collision(bullet, alien)}")
print(f"  Bullet misses: {check_collision(miss, alien)}")

๐Ÿ† Enhancement Challenges

Challenge 1: Shields

Add destructible shields between player and aliens:

  • Create 3-4 shields
  • Bullets destroy shield pixels
  • Shields protect player

Challenge 2: Wave System

Make aliens come in waves:

  • After defeating all aliens, spawn new wave
  • Each wave is faster/harder
  • Track wave number

Challenge 3: Power-Ups

Add power-ups that fall from destroyed aliens:

  • Rapid fire
  • Shield
  • Multi-shot

Challenge 4: Boss Battle

Add a boss alien that:

  • Has more health
  • Moves differently
  • Shoots multiple bullets
  • Appears every 5 waves

๐Ÿš€ Whatโ€™s Next?

Youโ€™ve now built two classic games! In the final slide, weโ€™ll explore:

  • Advanced game patterns
  • Particle effects
  • Game juice (polish)
  • Where to go next in game development