Tower Defense — Milestone 2

The Path & Walking Enemies

# TOWER DEFENSE — MILESTONE 2: The Path & Walking Enemies
# =============================================================================
# TARGET (show me when this works):
#   A winding blue path runs from the green ENTRY to the red BASE in the middle.
#   Yellow enemies spawn every 2 seconds and walk the whole path.
#   When an enemy touches the BASE it disappears and you lose a life.
#   At 0 lives the game stops spawning and prints GAME OVER.
#
# NEW IN THIS FILE (compared to Milestone 1):
#   - The BASE moved to the MIDDLE of the map.
#     WHY: later, enemies will attack from all four sides at once.
#   - PATH: a list of "waypoints" (the corners the enemies turn at)
#   - path_cells(): turns those corners into every cell along the route
#   - class Enemy: an object that knows how to move itself and draw itself
#   - A HUD bar at the bottom of the window (the window got 60px taller)
#
# THE ONE IDEA TO UNDERSTAND — WAYPOINT FOLLOWING:
#   The enemy always aims at the NEXT corner in the list.
#   Each frame it takes one small step toward that corner.
#   When it arrives, it moves on to the corner after that.
#   To step toward a target we use:
#       dx, dy   = how far away the target is
#       dist     = math.hypot(dx, dy)      <- Pythagorean theorem
#       dx/dist  = a "unit vector": direction only, length exactly 1
#       then multiply by speed to take a step of the right size
#
# NEXT (Milestone 3): towers that shoot the enemies.
# =============================================================================

import pygame
import math

pygame.init()

COLS, ROWS = 20, 15
CELL = 40
HUD_H = 60
WIDTH = COLS * CELL
HEIGHT = ROWS * CELL + HUD_H

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tower Defense — Milestone 2")
clock = pygame.time.Clock()
font = pygame.font.SysFont("Courier", 18, bold=True)
big_font = pygame.font.SysFont("Courier", 40, bold=True)

# Colors
BG_COLOR = (20, 28, 40)
GRID_COLOR = (30, 40, 55)
OPEN_COLOR = (35, 50, 70)
PATH_COLOR = (52, 84, 126)
ENTRY_COLOR = (50, 180, 80)
BASE_COLOR = (200, 60, 60)
ENEMY_COLOR = (220, 180, 30)
HUD_COLOR = (185, 220, 255)

# Settings
ENEMY_SPEED = 1.5      # pixels per frame
SPAWN_MS = 2000        # a new enemy every 2 seconds
START_LIVES = 25

# Grid Setup — the BASE is now in the CENTER of the map
BASE = (10, 7)

# PATH: just the corners. The enemy walks in straight lines between them.
PATH = [(0, 5), (6, 5), (6, 11), (14, 11), (14, 3), (10, 3), BASE]

grid = [['open'] * ROWS for _ in range(COLS)]


def path_cells(waypoints):
    """Turn a list of corners into EVERY grid cell along the route."""
    cells = []
    for i in range(len(waypoints) - 1):
        col0, row0 = waypoints[i]
        col1, row1 = waypoints[i + 1]
        # Which direction do we step? -1, 0 or +1 on each axis.
        step_col = 0 if col0 == col1 else (1 if col1 > col0 else -1)
        step_row = 0 if row0 == row1 else (1 if row1 > row0 else -1)
        col, row = col0, row0
        while (col, row) != (col1, row1):
            cells.append((col, row))
            col += step_col
            row += step_row
    cells.append(waypoints[-1])
    return cells


# Write the path into the grid so draw_grid() can color it
for cell in path_cells(PATH):
    grid[cell[0]][cell[1]] = 'path'
grid[PATH[0][0]][PATH[0][1]] = 'entry'
grid[BASE[0]][BASE[1]] = 'base'


class Enemy:
    RADIUS = 11

    def __init__(self, waypoints, speed):
        self.waypoints = waypoints
        self.index = 0                    # which corner are we walking toward
        self.speed = speed
        col, row = waypoints[0]
        self.x = float(col * CELL + CELL / 2)
        self.y = float(row * CELL + CELL / 2)
        self.alive = True
        self.reached_base = False

    def update(self):
        # Have we run out of corners? Then we are standing on the base.
        if self.index >= len(self.waypoints) - 1:
            self.alive = False
            self.reached_base = True
            return

        col, row = self.waypoints[self.index + 1]
        target_x = col * CELL + CELL / 2
        target_y = row * CELL + CELL / 2

        dx = target_x - self.x
        dy = target_y - self.y
        dist = math.hypot(dx, dy)

        if dist < self.speed:
            # Close enough — snap onto the corner and aim at the next one
            self.x = target_x
            self.y = target_y
            self.index += 1
        else:
            self.x += dx / dist * self.speed
            self.y += dy / dist * self.speed

    def draw(self, surface):
        center = (int(self.x), int(self.y))
        pygame.draw.circle(surface, ENEMY_COLOR, center, self.RADIUS)
        pygame.draw.circle(surface, (0, 0, 0), center, self.RADIUS, 2)


def draw_grid():
    for row in range(ROWS):
        for col in range(COLS):
            x = col * CELL
            y = row * CELL

            cell_type = grid[col][row]
            if cell_type == 'base':
                color = BASE_COLOR
            elif cell_type == 'entry':
                color = ENTRY_COLOR
            elif cell_type == 'path':
                color = PATH_COLOR
            else:
                color = OPEN_COLOR

            pygame.draw.rect(screen, color, (x, y, CELL, CELL))
            pygame.draw.rect(screen, GRID_COLOR, (x, y, CELL, CELL), 1)

    base_x = BASE[0] * CELL + CELL // 2
    base_y = BASE[1] * CELL + CELL // 2
    label = font.render("BASE", True, (255, 200, 200))
    screen.blit(label, label.get_rect(center=(base_x, base_y)))


def draw_hud(lives):
    top = ROWS * CELL
    pygame.draw.rect(screen, (12, 16, 26), (0, top, WIDTH, HUD_H))
    text = font.render(f"LIVES {lives}", True, HUD_COLOR)
    screen.blit(text, (10, top + 12))
    hint = font.render("enemies that reach the BASE cost a life", True, (110, 130, 155))
    screen.blit(hint, (10, top + 34))


# Game state
enemies = []
lives = START_LIVES

SPAWN_EVENT = pygame.USEREVENT + 1
pygame.time.set_timer(SPAWN_EVENT, SPAWN_MS)

running = True

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        if event.type == SPAWN_EVENT and lives > 0:
            enemies.append(Enemy(PATH, ENEMY_SPEED))

    # Update every enemy
    for enemy in enemies[:]:
        enemy.update()
        if not enemy.alive:
            enemies.remove(enemy)
            if enemy.reached_base:
                lives -= 1
                if lives < 0:
                    lives = 0

    # Draw
    screen.fill(BG_COLOR)
    draw_grid()
    for enemy in enemies:
        enemy.draw(screen)
    draw_hud(lives)

    if lives <= 0:
        shade = pygame.Surface((WIDTH, ROWS * CELL), pygame.SRCALPHA)
        shade.fill((0, 0, 0, 170))
        screen.blit(shade, (0, 0))
        msg = big_font.render("GAME OVER", True, (225, 70, 70))
        screen.blit(msg, msg.get_rect(center=(WIDTH // 2, ROWS * CELL // 2)))

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

pygame.quit()