Lesson 8: Enemy Bullets, Lives, and Game Over

This final lesson makes the game complete. You’ll add:

Start from your Lesson 7 code.

Step 1 — Enemy bullets

# === ENEMY BULLETS ===
enemy_bullets = []
E_BULLET_SPEED = 3

def enemies_fire():
    import random
    shooters = [e for e in enemies if e["alive"]]
    if shooters and random.random() < 0.02:  # ~2% chance per frame
        e = random.choice(shooters)
        enemy_bullets.append({
            "x": e["x"] + e["w"]//2 - 3,
            "y": e["y"] + e["h"],
            "w": 6, "h": 10,
            "vy": E_BULLET_SPEED,
            "active": True
        })

def update_enemy_bullets(screen_h):
    global enemy_bullets
    for b in enemy_bullets:
        if not b["active"]: continue
        b["y"] += b["vy"]
        if b["y"] > screen_h: b["active"] = False
    enemy_bullets = [b for b in enemy_bullets if b["active"]]

def draw_enemy_bullets(screen):
    import pygame
    for b in enemy_bullets:
        pygame.draw.rect(screen, (255,107,107), (b["x"], b["y"], b["w"], b["h"]))

Step 2 — Lives, player hits, lose/win checks

lives = 3

def handle_player_hits():
    global lives, enemy_bullets
    player_rect = {"x": player_x, "y": player_y, "w": player_w, "h": player_h}
    for b in enemy_bullets:
        if not b["active"]: continue
        if (b["x"] < player_rect["x"] + player_rect["w"] and
            b["x"] + b["w"] > player_rect["x"] and
            b["y"] < player_rect["y"] + player_rect["h"] and
            b["y"] + b["h"] > player_rect["y"]):
            b["active"] = False
            lives -= 1
            if lives <= 0:
                return "out_of_lives"
    enemy_bullets = [b for b in enemy_bullets if b["active"]]
    return None

def enemies_reached_player():
    return any(e["alive"] and e["y"] + e["h"] >= player_y for e in enemies)

Full example (save as lesson8.py)

import sys, random, pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Space Invader — Lesson 8 (Final)")
clock = pygame.time.Clock()

SPACE = (10,15,30)
PLAYER_COLOR = (154,180,255)
BULLET_COLOR = (255,211,105)
ENEMY_COLOR = (123,211,255)
EYE = (11,18,38)
E_BULLET_COLOR = (255,107,107)
INK = (209,231,255)
GOOD = (134,239,172)
BAD = (255,107,107)

# Player
player_w, player_h = 40, 20
player_x = 800//2 - player_w//2
player_y = 600 - 60
player_speed = 0

# Player bullets
bullets = []
BULLET_W, BULLET_H = 4, 10
BULLET_SPEED = -6
FIRE_COOLDOWN_MS = 250
last_shot_at = 0

# Enemies
enemies = []
ENEMY_COLS, ENEMY_ROWS = 8, 3
ENEMY_SPACING_X, ENEMY_SPACING_Y = 70, 50
ENEMY_START_X, ENEMY_START_Y = 80, 60
ENEMY_W, ENEMY_H = 36, 20
enemy_dx = 1.2
ENEMY_DROP_Y = 18

# Enemy bullets
enemy_bullets = []
E_BULLET_SPEED = 3

score = 0
lives = 3

# -------- helpers --------
def fire_bullet(now_ms):
    global last_shot_at, bullets
    if now_ms - last_shot_at < FIRE_COOLDOWN_MS: return
    bx = player_x + player_w//2 - BULLET_W//2
    by = player_y - BULLET_H
    bullets.append({"x": bx, "y": by, "vy": BULLET_SPEED, "active": True})
    last_shot_at = now_ms

def update_bullets():
    global bullets
    for b in bullets:
        if not b["active"]: continue
        b["y"] += b["vy"]
        if b["y"] + BULLET_H < 0: b["active"] = False
    bullets = [b for b in bullets if b["active"]]

def draw_bullets():
    for b in bullets:
        pygame.draw.rect(screen, BULLET_COLOR, (b["x"], b["y"], BULLET_W, BULLET_H))

def create_enemies():
    global enemies
    enemies = []
    for r in range(ENEMY_ROWS):
        for c in range(ENEMY_COLS):
            enemies.append({
                "x": ENEMY_START_X + c*ENEMY_SPACING_X,
                "y": ENEMY_START_Y + r*ENEMY_SPACING_Y,
                "w": ENEMY_W, "h": ENEMY_H,
                "alive": True,
                "points": 30 if r==0 else (20 if r==1 else 10)
            })

def update_enemies():
    global enemy_dx
    hit_left, hit_right = False, False
    for e in enemies:
        if not e["alive"]: continue
        nx = e["x"] + enemy_dx
        if nx < 10: hit_left = True
        if nx + e["w"] > 800 - 10: hit_right = True
    if hit_left or hit_right:
        for e in enemies:
            if e["alive"]: e["y"] += ENEMY_DROP_Y
        enemy_dx = -enemy_dx
    for e in enemies:
        if e["alive"]: e["x"] += enemy_dx

def draw_enemies():
    for e in enemies:
        if not e["alive"]: continue
        pygame.draw.rect(screen, ENEMY_COLOR, (e["x"], e["y"], e["w"], e["h"]))
        pygame.draw.rect(screen, EYE, (e["x"]+6, e["y"]+6, 6, 6))
        pygame.draw.rect(screen, EYE, (e["x"]+e["w"]-12, e["y"]+6, 6, 6))

def rects_overlap(a, b):
    return (a["x"] < b["x"] + b["w"] and a["x"] + a["w"] > b["x"] and
            a["y"] < b["y"] + b["h"] and a["y"] + a["h"] > b["y"])

def handle_bullet_enemy_collisions():
    global bullets, score
    for e in enemies:
        if not e["alive"]: continue
        for b in bullets:
            if not b["active"]: continue
            bullet_rect = {"x": b["x"], "y": b["y"], "w": BULLET_W, "h": BULLET_H}
            if rects_overlap(bullet_rect, e):
                e["alive"] = False
                b["active"] = False
                score += e.get("points", 10)
                break
    bullets = [b for b in bullets if b["active"]]

def enemies_fire():
    shooters = [e for e in enemies if e["alive"]]
    if shooters and random.random() < 0.02:
        e = random.choice(shooters)
        enemy_bullets.append({
            "x": e["x"] + e["w"]//2 - 3,
            "y": e["y"] + e["h"],
            "w": 6, "h": 10,
            "vy": E_BULLET_SPEED,
            "active": True
        })

def update_enemy_bullets():
    global enemy_bullets
    for b in enemy_bullets:
        if not b["active"]: continue
        b["y"] += b["vy"]
        if b["y"] > 600: b["active"] = False
    enemy_bullets = [b for b in enemy_bullets if b["active"]]

def draw_enemy_bullets():
    for b in enemy_bullets:
        pygame.draw.rect(screen, E_BULLET_COLOR, (b["x"], b["y"], b["w"], b["h"]))

def handle_player_hits():
    global lives, enemy_bullets
    player_rect = {"x": player_x, "y": player_y, "w": player_w, "h": player_h}
    for b in enemy_bullets:
        if not b["active"]: continue
        if rects_overlap(b, player_rect):
            b["active"] = False
            lives -= 1
            if lives <= 0: return "out_of_lives"
    enemy_bullets = [b for b in enemy_bullets if b["active"]]
    return None

def enemies_reached_player():
    return any(e["alive"] and e["y"] + e["h"] >= player_y for e in enemies)

def draw_hud(surface):
    font = pygame.font.SysFont(None, 28)
    surface.blit(font.render(f"Score: {score}", True, INK), (10,10))
    surface.blit(font.render(f"Lives: {lives}", True, INK), (680,10))

# -------- main --------
create_enemies()
running = True

while running:
    now = pygame.time.get_ticks()
    for event in pygame.event.get():
        if event.type == pygame.QUIT: running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:  player_speed = -4
            if event.key == pygame.K_RIGHT: player_speed = 4
            if event.key == pygame.K_SPACE: fire_bullet(now)
        if event.type == pygame.KEYUP:
            if event.key in (pygame.K_LEFT, pygame.K_RIGHT): player_speed = 0

    # move player + clamp
    player_x = max(10, min(player_x + player_speed, 800 - 10 - player_w))

    # update world
    update_bullets()
    update_enemies()
    enemies_fire()
    update_enemy_bullets()
    handle_bullet_enemy_collisions()

    # lose checks
    lose_reason = handle_player_hits()
    if lose_reason == "out_of_lives" or enemies_reached_player():
        screen.fill(SPACE)
        draw_enemies(); draw_bullets(); draw_enemy_bullets(); draw_hud(screen)
        font = pygame.font.SysFont(None, 40)
        screen.blit(font.render("Game Over", True, BAD), (330, 280))
        pygame.display.flip()
        pygame.time.wait(1500)
        break

    # win check
    if not any(e["alive"] for e in enemies):
        screen.fill(SPACE)
        draw_enemies(); draw_bullets(); draw_enemy_bullets(); draw_hud(screen)
        font = pygame.font.SysFont(None, 40)
        screen.blit(font.render("You win!", True, GOOD), (350, 280))
        pygame.display.flip()
        pygame.time.wait(1500)
        break

    # draw
    screen.fill(SPACE)
    pygame.draw.rect(screen, PLAYER_COLOR, (player_x, player_y, player_w, player_h))
    pygame.draw.polygon(screen, (207,225,255), [
        (player_x + player_w//2, player_y - 6),
        (player_x + player_w//2 - 6, player_y),
        (player_x + player_w//2 + 6, player_y)
    ])
    draw_bullets(); draw_enemies(); draw_enemy_bullets(); draw_hud(screen)

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

pygame.quit()
sys.exit()