On February 26, 2025, I completed a functional Pac-Man clone in Python using Pygame during a three-hour coding session, relying entirely on Cursor and Claude Sonnet 3.7 for code generation. As someone with only basic Python and Pygame skills—enough to build simple programs—this project would have taken me days without AI assistance. Instead, the vibe coding approach, where intuition guides the process and AI handles the heavy lifting, delivered a working game with a maze, player movement, ghost AI, and core mechanics in record time. This article details the practical steps, challenges, and solutions from that session, aligning with the tech and AI focus of alanoliver.co.
This blog explores advancements in technology, AI, freedom tech, and productivity. This project showcases how AI tools can transform a novice’s idea into a complex program, offering insights for readers interested in AI-assisted development. Below, I outline the process, managed entirely by Cursor and Claude, with my role limited to guiding the AI through prompts.
Tools and Initial Setup
The setup included Python 3.11, Pygame 2.6.1, Cursor (an AI-powered code editor), and Claude Sonnet 3.7 (a conversational AI model integrated into Cursor). The goal was a Pac-Man game featuring a grid-based maze, a controllable Pac-Man, four ghosts with unique behaviors, and mechanics like power pellets and scoring. With my limited coding skills, I leaned on the AI to write all the code.
Once Cursor was installed I configured it to use Caude Sonnet 3.7 – the latest AI from Anthropic which has excellent code creation abilities
Hey AI, Write Me A Pacman Game!
A simple prompt to write a pacman game started the process. The AI decided it was going to use Python with Pygame and within minutes the first iteration was ready to test. However when I ran the code, nothing…
Pygame failed to run on my Windows 10 system—no window, no errors. A test script to draw a red rectangle also produced nothing. Claude identified multiple Python installations via where python
, and Cursor adjusted the command to C:\Users\Alan\AppData\Local\Programs\Python\Python311\python.exe pacman.py
. After installing Pygame with pip install pygame
, the environment was ready, all handled by the AI within 30 minutes of starting.
Constructing the Maze
By the 60-minute mark, the focus shifted to the maze. I supplied a JSON file defining the classic 28×31 Pac-Man layout: walls (#), pellets (.), power pellets (O), Pac-Man’s start (P), ghost spawns (1-4), and the ghost door (-). Claude’s initial code parsed this into a Pygame grid, but Pac-Man spawned in a wall, and the ghost house lacked a proper door. Without my ability to edit code, I prompted Cursor to refine the create_maze
method, mapping JSON symbols to game objects: blue wall rectangles, white pellet dots, and a white door line offset above the grid. After 90 minutes, the maze aligned with the original (well almost, good enough for me to test at least), featuring tunnels at rows 9-10 and power pellets in the corners.
Implementing Pac-Man Movement
Pac-Man’s movement took 45 minutes of iteration (90–135 minutes elapsed). I requested smooth motion, grid-aligned turns, and no wall penetration—beyond my skill to code manually. Claude’s first attempt used pixel-based movement with arrow keys, but Pac-Man got stuck in walls, especially moving right or down. Through repeated prompts, Cursor adjusted the logic to calculate grid positions (current_grid_x = int(self.pixel_x // CELL_SIZE)
), check collisions ahead, and move only if clear. Turns required near-grid alignment within the 30-pixel cell size. By 135 minutes, Pac-Man moved at 2 pixels per frame, stopping at walls and turning at intersections, all crafted by the AI.
Ghost Logic and Behavior
Ghost implementation spanned 45 minutes (135–180 minutes elapsed). I specified four ghosts—Blinky, Pinky, Inky, and Clyde—with unique spawn points (1-4) and behaviors: Blinky chasing Pac-Man, Pinky ambushing, Inky collaborating, and Clyde toggling chase and scatter. Claude’s initial code placed them at spawn points but failed to move them—they either stayed put or got stuck in walls. Cursor decided to use distance targeting with 20% randomness to break loops, adding staggered exit delays through the door (row 13, columns 13-14). Power pellet logic made ghosts flash blue and white (0.3s intervals), slow to 1.5 pixels per frame, and flee, with eaten ghosts’ eyes returning to spawn at 4 pixels per frame. By 180 minutes, ghost movement was functional, though minor wall issues lingered.
Final Mechanics and Polish
The last 30 minutes (180–210 minutes) added essential features. I requested lives tracking, an attract mode, a game-over screen, and a high-score table—all beyond my coding expertise. Cursor implemented lives decrementing on ghost collisions, resetting positions cleanly, and added a “Press Space to Start” attract mode transitioning to game-over at zero lives, then back to attract mode. High scores tracked in memory, updating on game end. Graphics included Pac-Man’s arc-based mouth (8-cycle animation) and ghosts’ wavy bottoms (6 waves, 16-cycle frequency), all generated by Claude. Testing showed slight corner hops for Pac-Man and occasional ghost wall sticks, but the session ended at 210 minutes with a solid prototype.
Technical Takeaways
This three-hour session highlighted AI’s power to bridge skill gaps. Cursor and Claude wrote every line of pacman.py
, from Pygame setup to maze parsing, while I provided direction through prompts. Challenges like Pygame initialization and movement bugs were resolved by the AI, cutting development time from days to hours for someone with my limited skills. The vibe coding method—relying on intuition and AI iteration—enabled rapid prototyping, producing a 90% faithful Pac-Man clone in under 200 lines of core logic. Refinements like ghost AI precision or audio could follow, but the foundation showcases AI’s productivity boost.
For alanoliver.co readers, this demonstrates how AI tools democratize tech creation. With limited coding knowledge and the right tools you can execute a complex project. The process proves that AI can amplify intent, making ambitious ideas achievable without deep expertise.
The Pacman Code
The unfinished pacman clone as it was written by Cursor with the help of Claude Sonnet 3.7
pacman.py
import pygame
import random
from enum import Enum
import math
import time
# Initialize Pygame
pygame.init()
# Constants
CELL_SIZE = 30
PACMAN_SPEED = 2 # Pixels per frame
GRID_WIDTH = 28
GRID_HEIGHT = 31
SCREEN_WIDTH = GRID_WIDTH * CELL_SIZE
SCREEN_HEIGHT = GRID_HEIGHT * CELL_SIZE
# Colors
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
WHITE = (255, 255, 255)
YELLOW = (255, 255, 0)
# Remove ghost-related constants
PACMAN_LIVES = 3
# Additional constants
POWER_PELLET_DURATION = 10 # seconds
GHOST_VULNERABLE_COLOR = (33, 33, 255) # Blue color for vulnerable ghosts
GHOST_FLASH_START = 7 # Seconds remaining when ghosts start flashing
GHOST_POINTS = [200, 400, 800, 1600] # Points for consecutive ghost kills
PACMAN_LIVES = 3
GHOST_HOUSE_POS = (10, 10) # Center of ghost house
# Add to existing colors
PINK = (255, 192, 203)
CYAN = (0, 255, 255)
ORANGE = (255, 165, 0)
# Add to constants section
RED = (255, 0, 0)
GHOST_SPEED = 2.25
GHOST_SCARED_SPEED = 1.5
GHOST_BLUE = (33, 33, 255)
GHOST_WHITE = (222, 222, 255)
POWER_PILL_DURATION = 8 # seconds
GHOST_FLASH_TIME = 2 # seconds before power pill ends
GHOST_FLASH_INTERVAL = 0.25 # seconds between flashes
GHOST_EYES_SPEED = 8.0 # Double the previous speed (was 4.0)
# Direction enum
class Direction(Enum):
UP = 1
DOWN = 2
LEFT = 3
RIGHT = 4
# Add to constants
class GameState(Enum):
ATTRACT = 1
PLAYING = 2
GAME_OVER = 3
HIGH_SCORE = 4
# Add high score constants
HIGH_SCORES_FILE = "pacman_scores.txt"
MAX_HIGH_SCORES = 5
class Pacman:
def __init__(self, x, y):
self.pixel_x = x * CELL_SIZE
self.pixel_y = y * CELL_SIZE
self.direction = Direction.LEFT # Start facing left like original
self.next_direction = Direction.LEFT
self.speed = PACMAN_SPEED
self.score = 0
self.mouth_angle = 0
self.mouth_direction = 1
self.mouth_speed = 10
self.max_mouth_angle = 45
self.lives = PACMAN_LIVES
self.power_pill_time = 0
self.ghost_score_multiplier = 0
self.game = None
@property
def grid_x(self):
return int(self.pixel_x // CELL_SIZE)
@property
def grid_y(self):
return int(self.pixel_y // CELL_SIZE)
def can_move_in_direction(self, direction, walls):
# Check if we can move in a given direction from current position
test_x = self.grid_x
test_y = self.grid_y
if direction == Direction.UP:
test_y -= 1
elif direction == Direction.DOWN:
test_y += 1
elif direction == Direction.LEFT:
test_x -= 1
elif direction == Direction.RIGHT:
test_x += 1
return (test_x, test_y) not in walls
def move(self, walls):
# Get current grid position
current_grid_x = self.grid_x
current_grid_y = self.grid_y
# First handle direction change request
if self.next_direction != self.direction:
# Calculate test position for requested direction
test_x = current_grid_x
test_y = current_grid_y
if self.next_direction == Direction.UP:
test_y -= 1
elif self.next_direction == Direction.DOWN:
test_y += 1
elif self.next_direction == Direction.LEFT:
test_x -= 1
elif self.next_direction == Direction.RIGHT:
test_x += 1
# If the way is clear and we're close to aligned, allow turn
if (test_x, test_y) not in walls:
# Check if we're close enough to grid to turn
offset_x = self.pixel_x % CELL_SIZE
offset_y = self.pixel_y % CELL_SIZE
# Allow turn if close to grid alignment
if offset_x <= self.speed or offset_x >= CELL_SIZE - self.speed:
if offset_y <= self.speed or offset_y >= CELL_SIZE - self.speed:
# Snap to grid when turning
self.pixel_x = current_grid_x * CELL_SIZE
self.pixel_y = current_grid_y * CELL_SIZE
self.direction = self.next_direction
# Calculate next position
next_x = self.pixel_x
next_y = self.pixel_y
# Move in current direction
if self.direction == Direction.UP:
next_y -= self.speed
elif self.direction == Direction.DOWN:
next_y += self.speed
elif self.direction == Direction.LEFT:
next_x -= self.speed
elif self.direction == Direction.RIGHT:
next_x += self.speed
# Handle tunnel wrapping
if next_x < 0:
next_x = (GRID_WIDTH - 1) * CELL_SIZE
elif next_x >= GRID_WIDTH * CELL_SIZE:
next_x = 0
# Check if next position would hit a wall
next_grid_x = int((next_x + CELL_SIZE // 2) // CELL_SIZE)
next_grid_y = int((next_y + CELL_SIZE // 2) // CELL_SIZE)
# Check for wall collision
if (next_grid_x, next_grid_y) not in walls:
self.pixel_x = next_x
self.pixel_y = next_y
else:
# If hitting a wall, stop movement and allow direction change
if self.direction in [Direction.LEFT, Direction.RIGHT]:
self.pixel_y = current_grid_y * CELL_SIZE # Align vertically
else: # UP/DOWN
self.pixel_x = current_grid_x * CELL_SIZE # Align horizontally
# Allow direction change immediately after hitting a wall
if (self.grid_x, self.grid_y) in walls:
self.direction = self.next_direction # Allow changing direction
def draw(self, screen):
# Update mouth animation
self.mouth_angle += self.mouth_direction * self.mouth_speed
if self.mouth_direction == -1 and self.mouth_angle <= 0:
self.mouth_angle = 0
self.mouth_direction = 1
elif self.mouth_direction == 1 and self.mouth_angle >= self.max_mouth_angle:
self.mouth_direction = -1
# Calculate center position and rotation
center = (self.pixel_x + CELL_SIZE // 2, self.pixel_y + CELL_SIZE // 2)
rotation = {
Direction.RIGHT: 0,
Direction.LEFT: 180,
Direction.UP: 90,
Direction.DOWN: 270
}[self.direction]
# Draw Pacman
pygame.draw.circle(screen, YELLOW, center, CELL_SIZE // 2)
# Draw mouth
rect = (self.pixel_x, self.pixel_y, CELL_SIZE, CELL_SIZE)
start_angle = math.radians(rotation - self.mouth_angle)
end_angle = math.radians(rotation + self.mouth_angle)
# Draw mouth components
pygame.draw.arc(screen, BLACK, rect, start_angle, end_angle, CELL_SIZE // 2)
mouth_line1 = (
center[0] + math.cos(start_angle) * CELL_SIZE // 2,
center[1] - math.sin(start_angle) * CELL_SIZE // 2
)
mouth_line2 = (
center[0] + math.cos(end_angle) * CELL_SIZE // 2,
center[1] - math.sin(end_angle) * CELL_SIZE // 2
)
pygame.draw.line(screen, BLACK, center, mouth_line1, 2)
pygame.draw.line(screen, BLACK, center, mouth_line2, 2)
pygame.draw.polygon(screen, BLACK, [center, mouth_line1, mouth_line2])
def eat_ghost(self, ghost):
# Calculate score for eating ghost
score = GHOST_POINTS[self.ghost_score_multiplier]
self.score += score
self.ghost_score_multiplier = min(self.ghost_score_multiplier + 1, 3)
class Ghost:
def __init__(self, x, y, color, scatter_target, exit_delay, index):
self.pixel_x = x * CELL_SIZE
self.pixel_y = y * CELL_SIZE
self.color = color
self.direction = Direction.LEFT
self.speed = GHOST_SPEED
self.scatter_target = scatter_target
self.is_vulnerable = False
self.is_in_house = True
self.mode = "scatter"
self.mode_timer = time.time()
self.mode_durations = {
"scatter": 5, # Reduced scatter time
"chase": 20 # Increased chase time
}
self.game = None
self.start_time = time.time()
self.exit_delay = exit_delay # Time to wait before exiting ghost house
self.original_color = color
self.is_scared = False
self.is_eaten = False
self.flash_time = 0
self.eaten_speed = GHOST_EYES_SPEED # Update to faster speed
self.flash_start = 0
self.should_flash = False # New flag to control flashing state
self.wave_offset = 0 # Add wave animation offset
self.wave_speed = 0.2 # Speed of wave animation
self.bounce_offset = 0
self.bounce_speed = 16 # Faster bounce
self.bounce_height = 6
self.bounce_time_offset = index * 0.5 # Staggered bounce offset based on index
@property
def grid_x(self):
return self.pixel_x // CELL_SIZE
@property
def grid_y(self):
return self.pixel_y // CELL_SIZE
def is_in_ghost_house(self, x, y):
house = self.game.ghost_house
return (house['left'] <= x <= house['right'] and
house['top'] <= y <= house['bottom'])
def move(self, pacman, walls):
current_time = time.time()
# Get current position
current_grid_x = int(self.pixel_x // CELL_SIZE)
current_grid_y = int(self.pixel_y // CELL_SIZE)
# Handle ghost house behavior
if self.is_in_house:
# Update bounce animation while waiting
if current_time - self.start_time < self.exit_delay:
# Fast, energetic bounce with offset
self.bounce_offset = math.sin((current_time + self.bounce_time_offset) * self.bounce_speed) * self.bounce_height
return
# Move up to exit through door
if current_grid_y == self.game.ghost_house['top'] + 1: # If at spawn row
# Move up towards door
self.pixel_y -= self.speed
next_grid_y = int(self.pixel_y // CELL_SIZE)
# If we've reached the door position
if next_grid_y <= self.game.ghost_house['top']:
self.pixel_y = self.game.ghost_house['top'] * CELL_SIZE
self.is_in_house = False
self.direction = Direction.LEFT
return
# Switch between scatter and chase modes
if current_time - self.mode_timer > self.mode_durations[self.mode]:
self.mode = "chase" if self.mode == "scatter" else "scatter"
self.mode_timer = current_time
# Check if we're at a grid intersection
at_intersection = (
abs(self.pixel_x % CELL_SIZE) < self.speed and
abs(self.pixel_y % CELL_SIZE) < self.speed
)
if at_intersection:
# Align to grid
self.pixel_x = current_grid_x * CELL_SIZE
self.pixel_y = current_grid_y * CELL_SIZE
# Get available directions (excluding opposite direction)
available_directions = []
opposite = self.get_opposite_direction()
for direction in Direction:
if direction == opposite:
continue
# Check next position
test_x = current_grid_x
test_y = current_grid_y
if direction == Direction.UP:
test_y -= 1
elif direction == Direction.DOWN:
test_y += 1
elif direction == Direction.LEFT:
test_x -= 1
elif direction == Direction.RIGHT:
test_x += 1
# Only add direction if not hitting a wall
if (test_x, test_y) not in walls:
available_directions.append(direction)
# Choose new direction if at intersection with multiple options
if len(available_directions) > 1:
target = self.get_target_position(pacman)
self.direction = min(
available_directions,
key=lambda d: self.get_distance_to_target(
current_grid_x, current_grid_y, d, target
)
)
elif available_directions: # Only one direction available
self.direction = available_directions[0]
else: # No valid directions (shouldn't happen)
self.direction = opposite
# Calculate next position
next_x = self.pixel_x
next_y = self.pixel_y
# Check if in tunnel (update y position to match maze)
in_tunnel = (current_grid_x <= 1 or current_grid_x >= GRID_WIDTH - 2) and current_grid_y == 10
current_speed = self.speed * (0.4 if in_tunnel else 1.0) # Slow down to 40% speed in tunnel
if self.direction == Direction.UP:
next_y -= current_speed
elif self.direction == Direction.DOWN:
next_y += current_speed
elif self.direction == Direction.LEFT:
next_x -= current_speed
elif self.direction == Direction.RIGHT:
next_x += current_speed
# Handle tunnel wrapping
if next_x < 0:
next_x = (GRID_WIDTH - 1) * CELL_SIZE
elif next_x >= GRID_WIDTH * CELL_SIZE:
next_x = 0
# Check if next position is valid
next_grid_x = int(next_x // CELL_SIZE)
next_grid_y = int(next_y // CELL_SIZE)
# Only allow movement if not hitting a wall
if (next_grid_x, next_grid_y) not in walls:
self.pixel_x = next_x
self.pixel_y = next_y
# Update speed based on state
if self.is_eaten:
self.speed = self.eaten_speed
elif self.is_scared:
self.speed = GHOST_SCARED_SPEED
else:
self.speed = GHOST_SPEED
def has_multiple_choices(self, x, y, walls):
"""Check if current position has more than one possible direction."""
available_count = 0
for direction in Direction:
next_x = x
next_y = y
if direction == Direction.UP:
next_y -= 1
elif direction == Direction.DOWN:
next_y += 1
elif direction == Direction.LEFT:
next_x -= 1
elif direction == Direction.RIGHT:
next_x += 1
if (next_x, next_y) not in walls:
available_count += 1
return available_count > 2 # More than 2 means it's an intersection
def get_next_position(self, x, y, direction):
if direction == Direction.UP:
return (x, y - 1)
elif direction == Direction.DOWN:
return (x, y + 1)
elif direction == Direction.LEFT:
return (x - 1, y)
else: # RIGHT
return (x + 1, y)
def get_opposite_direction(self):
return {
Direction.UP: Direction.DOWN,
Direction.DOWN: Direction.UP,
Direction.LEFT: Direction.RIGHT,
Direction.RIGHT: Direction.LEFT
}[self.direction]
def get_distance_to_target(self, x, y, direction, target):
# Calculate position after moving in this direction
if direction == Direction.UP:
y -= 1
elif direction == Direction.DOWN:
y += 1
elif direction == Direction.LEFT:
x -= 1
elif direction == Direction.RIGHT:
x += 1
# Return Manhattan distance to target
return abs(x - target[0]) + abs(y - target[1])
def get_target_position(self, pacman):
if self.is_eaten:
# When eaten, always target closest ghost spawn point
closest_spawn = min(self.game.ghost_spawns,
key=lambda spawn: abs(spawn[0] - self.grid_x) +
abs(spawn[1] - self.grid_y))
# If we've reached a spawn point, transform back to normal ghost
if (self.grid_x, self.grid_y) == closest_spawn:
self.is_eaten = False
self.is_in_house = True
self.is_scared = False
self.should_flash = False
self.speed = GHOST_SPEED
self.start_time = time.time() # Reset exit delay
self.mode = "scatter" # Reset to scatter mode
self.mode_timer = time.time()
self.exit_delay = 2 # Set a 2-second delay before exiting
# Reset position exactly to spawn point
self.pixel_x = closest_spawn[0] * CELL_SIZE
self.pixel_y = closest_spawn[1] * CELL_SIZE
# Reset bounce animation
self.bounce_offset = 0
return closest_spawn
elif self.is_scared:
# Run away from Pacman when scared
away_x = self.grid_x + (self.grid_x - pacman.grid_x)
away_y = self.grid_y + (self.grid_y - pacman.grid_y)
return (away_x, away_y)
elif self.mode == "scatter":
return self.scatter_target
else:
if self.color == RED: # Blinky - directly targets Pacman
return (pacman.grid_x, pacman.grid_y)
elif self.color == PINK: # Pinky - targets 4 tiles ahead of Pacman
target_x = pacman.grid_x
target_y = pacman.grid_y
# Calculate target based on Pacman's direction
if pacman.direction == Direction.UP:
target_y -= 4
elif pacman.direction == Direction.DOWN:
target_y += 4
elif pacman.direction == Direction.LEFT:
target_x -= 4
elif pacman.direction == Direction.RIGHT:
target_x += 4
return (target_x, target_y)
elif self.color == CYAN: # Inky - targets based on Blinky's position
# Find Blinky
blinky = next(ghost for ghost in self.game.ghosts if ghost.color == RED)
# Get position 2 tiles ahead of Pacman
target_x = pacman.grid_x
target_y = pacman.grid_y
if pacman.direction == Direction.UP:
target_y -= 2
elif pacman.direction == Direction.DOWN:
target_y += 2
elif pacman.direction == Direction.LEFT:
target_x -= 2
elif pacman.direction == Direction.RIGHT:
target_x += 2
# Double the vector from Blinky to this position
vector_x = target_x - blinky.grid_x
vector_y = target_y - blinky.grid_y
return (blinky.grid_x + vector_x * 2,
blinky.grid_y + vector_y * 2)
else: # ORANGE (Clyde) - alternates between chase and scatter
distance_to_pacman = abs(self.grid_x - pacman.grid_x) + abs(self.grid_y - pacman.grid_y)
if distance_to_pacman > 8: # Chase when far from Pacman
return (pacman.grid_x, pacman.grid_y)
else: # Scatter when close to Pacman
return self.scatter_target
def draw(self, screen):
# Apply bounce offset to y position when in house
draw_y = self.pixel_y
if self.is_in_house:
draw_y += self.bounce_offset
# Use draw_y instead of self.pixel_y for all drawing operations
x = self.pixel_x
y = draw_y
# Determine ghost color
current_color = self.original_color
if self.is_scared:
if self.should_flash:
# Flash between blue and white based on time
time_since_flash = time.time() - self.flash_start
if (time_since_flash // GHOST_FLASH_INTERVAL) % 2 == 0:
current_color = GHOST_BLUE
else:
current_color = GHOST_WHITE
else:
current_color = GHOST_BLUE
elif self.is_eaten:
# Draw eyes when eaten
self.draw_eyes(screen)
return
# Update wave animation
self.wave_offset = (self.wave_offset + self.wave_speed) % (2 * math.pi)
# Main body (semi-circle top with rectangle bottom)
radius = CELL_SIZE // 2
pygame.draw.circle(screen, current_color,
(x + radius, y + radius),
radius) # Top semi-circle
pygame.draw.rect(screen, current_color,
(x, y + radius, CELL_SIZE, radius)) # Bottom rectangle
# Animated wavy bottom with higher frequency
wave_points = [(x, y + CELL_SIZE)]
wave_height = 3 # Slightly smaller height for smoother look
num_waves = 6 # More waves for higher frequency
for i in range(num_waves * 2 + 1):
wave_x = x + (i * CELL_SIZE // (num_waves * 2))
# Multiply i by 2 for higher frequency waves
wave_y = y + CELL_SIZE + math.sin(self.wave_offset + i * 2) * wave_height
wave_points.append((wave_x, wave_y))
wave_points.append((x + CELL_SIZE, y + CELL_SIZE))
pygame.draw.polygon(screen, current_color, wave_points)
# Eyes (white part)
eye_spacing = 6
eye_radius = 4
left_eye_pos = (x + CELL_SIZE//3, y + CELL_SIZE//3)
right_eye_pos = (x + 2*CELL_SIZE//3, y + CELL_SIZE//3)
pygame.draw.circle(screen, WHITE, left_eye_pos, eye_radius)
pygame.draw.circle(screen, WHITE, right_eye_pos, eye_radius)
# Pupils (based on direction)
pupil_offset = 2
if self.direction == Direction.LEFT:
pupil_x_offset = -pupil_offset
pupil_y_offset = 0
elif self.direction == Direction.RIGHT:
pupil_x_offset = pupil_offset
pupil_y_offset = 0
elif self.direction == Direction.UP:
pupil_x_offset = 0
pupil_y_offset = -pupil_offset
else: # DOWN
pupil_x_offset = 0
pupil_y_offset = pupil_offset
pygame.draw.circle(screen, BLUE,
(left_eye_pos[0] + pupil_x_offset,
left_eye_pos[1] + pupil_y_offset), 2)
pygame.draw.circle(screen, BLUE,
(right_eye_pos[0] + pupil_x_offset,
right_eye_pos[1] + pupil_y_offset), 2)
def draw_eyes(self, screen):
# Draw only the eyes when ghost is eaten
x = self.pixel_x
y = self.pixel_y
# Eyes (white part)
eye_radius = 4
left_eye_pos = (x + CELL_SIZE // 3, y + CELL_SIZE // 3)
right_eye_pos = (x + 2 * CELL_SIZE // 3, y + CELL_SIZE // 3)
pygame.draw.circle(screen, WHITE, left_eye_pos, eye_radius)
pygame.draw.circle(screen, WHITE, right_eye_pos, eye_radius)
# Pupils (based on direction)
pupil_offset = 2
if self.direction == Direction.LEFT:
pupil_x_offset = -pupil_offset
pupil_y_offset = 0
elif self.direction == Direction.RIGHT:
pupil_x_offset = pupil_offset
pupil_y_offset = 0
elif self.direction == Direction.UP:
pupil_x_offset = 0
pupil_y_offset = -pupil_offset
else: # DOWN
pupil_x_offset = 0
pupil_y_offset = pupil_offset
pygame.draw.circle(screen, BLUE,
(left_eye_pos[0] + pupil_x_offset,
left_eye_pos[1] + pupil_y_offset), 2)
pygame.draw.circle(screen, BLUE,
(right_eye_pos[0] + pupil_x_offset,
right_eye_pos[1] + pupil_y_offset), 2)
class Game:
def __init__(self):
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Pacman")
self.clock = pygame.time.Clock()
self.state = GameState.ATTRACT
self.running = True
self.high_scores = self.load_high_scores()
self.current_score = 0
self.setup_new_game()
def setup_new_game(self):
self.walls = []
self.dots = []
self.power_pellets = []
self.ghost_door = []
self.ghost_spawns = [] # Add list to track spawn points
self.game_over = False
self.level = 1
# Create Pacman
self.pacman = Pacman(10, 19)
self.pacman.game = self
# Create maze first to get ghost spawn positions
self.create_maze()
# Create ghosts at their specific numbered spawn points
ghost_colors = [RED, PINK, CYAN, ORANGE] # Ghost 1, 2, 3, 4 respectively
scatter_targets = [
(GRID_WIDTH-2, 0), # Blinky (1) - top right
(2, 0), # Pinky (2) - top left
(GRID_WIDTH-2, GRID_HEIGHT-2), # Inky (3) - bottom right
(2, GRID_HEIGHT-2) # Clyde (4) - bottom left
]
exit_delays = [0, 2, 4, 6] # Staggered exit times
self.ghosts = []
for i, (color, target, delay) in enumerate(zip(ghost_colors, scatter_targets, exit_delays)):
# Find spawn point for this ghost (i+1 because ghosts are numbered 1-4)
spawn_pos = next(pos for pos in self.ghost_spawns if self.maze_layout[pos[1]][pos[0]] == str(i+1))
ghost = Ghost(spawn_pos[0], spawn_pos[1], color, target, delay, i)
ghost.game = self
ghost.is_in_house = True
self.ghosts.append(ghost)
# Set ghost door position
self.ghost_door_y = 11
self.ghost_door_x_start = 13
self.ghost_door_x_end = 15
def load_sounds(self):
# Create dummy sound object that does nothing
class DummySound:
def play(self): pass
self.sounds = {
'chomp': DummySound(),
'power_pellet': DummySound(),
'eat_ghost': DummySound(),
'death': DummySound(),
'game_start': DummySound()
}
def create_maze(self):
# Define the maze layout
self.maze_layout = [ # Define maze layout as an instance variable
"############################",
"#............##............#",
"#.####.#####.##.#####.####.#",
"#o####.#####.##.#####.####o#",
"#.####.#####.##.#####.####.#",
"#..........................#",
"#.####.##.########.##.####.#",
"#......##....##....##......#",
"######.#####.##.#####.######",
" #.#####.##.#####.# ",
" #.##..........##.# ",
" #.##.##----##.##.# ",
"######.##.# 1234 #.##.######",
"..........########..........",
"######................######",
"#.####.#####.##.#####.####.#",
"#o..##................##..o#",
"###.##.##.########.##.##.###",
"#......##....##....##......#",
"#.##########.##.##########.#",
"#..........................#",
"#.####.#####.##.#####.####.#",
"#.####.#####.##.#####.####.#",
"#...##........P.......##...#",
"###.##.##.########.##.##.###",
"#......##....##....##......#",
"#.##########.##.##########.#",
"#..........................#",
"#.####.#####.##.#####.####.#",
"############################"
]
# Process the maze layout
for y, row in enumerate(self.maze_layout):
for x, cell in enumerate(row):
if cell == '#':
self.walls.append((x, y))
elif cell == '.':
self.dots.append((x, y))
elif cell == 'o':
self.power_pellets.append((x, y))
elif cell == 'P':
self.pacman.pixel_x = x * CELL_SIZE
self.pacman.pixel_y = y * CELL_SIZE
elif cell in ['1', '2', '3', '4']: # Ghost spawn points
self.ghost_spawns.append((x, y))
elif cell == '-': # Door above ghost house
self.ghost_door.append((x, y))
# Define ghost house explicitly
self.ghost_house = {
'left': 11, # Leftmost x coordinate
'right': 16, # Rightmost x coordinate
'top': 11, # Top y coordinate
'bottom': 13, # Bottom y coordinate
'door_y': 11 # Door y coordinate
}
# Remove dots from ghost house area
ghost_house_area = [(x, y)
for x in range(self.ghost_house['left'], self.ghost_house['right'] + 1)
for y in range(self.ghost_house['top'], self.ghost_house['bottom'] + 1)]
self.dots = [dot for dot in self.dots if dot not in ghost_house_area]
# Define tunnel positions (y=9 in original game)
self.tunnels = [(0, 9), (GRID_WIDTH-1, 9)]
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: # Check for Escape key
self.running = False # Exit the game
if self.state == GameState.ATTRACT:
if event.key == pygame.K_SPACE:
self.setup_new_game()
self.state = GameState.PLAYING
elif self.state == GameState.PLAYING:
if event.key == pygame.K_UP:
self.pacman.next_direction = Direction.UP
elif event.key == pygame.K_DOWN:
self.pacman.next_direction = Direction.DOWN
elif event.key == pygame.K_LEFT:
self.pacman.next_direction = Direction.LEFT
elif event.key == pygame.K_RIGHT:
self.pacman.next_direction = Direction.RIGHT
elif self.state == GameState.GAME_OVER:
if event.key == pygame.K_SPACE:
self.state = GameState.ATTRACT
elif self.state == GameState.HIGH_SCORE:
if event.key == pygame.K_SPACE:
self.state = GameState.ATTRACT
def update(self):
# Only update game logic if in PLAYING state
if self.state != GameState.PLAYING:
return
current_time = time.time()
# Update power pill status
if self.pacman.power_pill_time > 0:
time_left = POWER_PILL_DURATION - (current_time - self.pacman.power_pill_time)
if time_left <= 0:
# Power pill expired
self.pacman.power_pill_time = 0
self.pacman.ghost_score_multiplier = 0
for ghost in self.ghosts:
ghost.is_scared = False
ghost.should_flash = False
elif time_left <= GHOST_FLASH_TIME:
# Start flashing ghosts near end of power pill
for ghost in self.ghosts:
if ghost.is_scared:
ghost.should_flash = True
if not ghost.flash_start: # Only set start time once
ghost.flash_start = current_time
self.pacman.move(self.walls)
# Update ghosts
for ghost in self.ghosts:
ghost.move(self.pacman, self.walls)
# Check for collision with Pacman
if self.check_collision(ghost, self.pacman):
if ghost.is_scared:
# Eat the ghost
ghost.is_eaten = True
ghost.is_scared = False
self.pacman.eat_ghost(ghost)
elif not ghost.is_eaten:
# Pacman dies
self.handle_collision()
return
# Check for power pill collection
pacman_grid_pos = (self.pacman.grid_x, self.pacman.grid_y)
if pacman_grid_pos in self.power_pellets:
self.power_pellets.remove(pacman_grid_pos)
self.pacman.score += 50
self.pacman.power_pill_time = current_time
self.pacman.ghost_score_multiplier = 0
# Make all ghosts scared
for ghost in self.ghosts:
if not ghost.is_eaten:
ghost.is_scared = True
ghost.should_flash = False
ghost.flash_start = 0 # Reset flash timing
ghost.speed = GHOST_SCARED_SPEED
# Check for dot collection
pacman_grid_pos = (self.pacman.grid_x, self.pacman.grid_y)
if pacman_grid_pos in self.dots:
self.dots.remove(pacman_grid_pos)
self.pacman.score += 10
# Check win condition
if not self.dots and not self.power_pellets:
self.running = False
def check_collision(self, ghost, pacman):
# Use a smaller collision radius for more precise detection
collision_distance = CELL_SIZE * 0.75
distance = math.sqrt(
(ghost.pixel_x - pacman.pixel_x) ** 2 +
(ghost.pixel_y - pacman.pixel_y) ** 2
)
return distance < collision_distance
def handle_collision(self):
self.pacman.lives -= 1
if self.pacman.lives <= 0:
# Check for high score
if self.check_high_score(self.pacman.score):
self.state = GameState.HIGH_SCORE
else:
self.state = GameState.GAME_OVER
return
# Reset positions if still has lives
self.reset_positions()
# Brief pause
pygame.time.wait(1000)
def reset_positions(self):
# Reset Pacman to the correct spawn position
self.pacman.pixel_x = 14 * CELL_SIZE # Center horizontally
self.pacman.pixel_y = 23 * CELL_SIZE # Lower part of maze, standard position
self.pacman.direction = Direction.LEFT # Start facing left like original
self.pacman.next_direction = Direction.LEFT
# Reset ghosts to starting positions - using the actual ghost spawn points from row 12
starting_positions = [
(12, 12, 0), # Blinky
(13, 12, 2), # Pinky
(14, 12, 4), # Inky
(15, 12, 6) # Clyde
]
for ghost, (x, y, delay) in zip(self.ghosts, starting_positions):
ghost.pixel_x = x * CELL_SIZE
ghost.pixel_y = y * CELL_SIZE
ghost.is_in_house = True
ghost.direction = Direction.LEFT
ghost.start_time = time.time() # Reset exit delay
ghost.mode = "scatter"
ghost.mode_timer = time.time()
def draw(self):
self.screen.fill(BLACK)
if self.state == GameState.ATTRACT:
self.draw_attract_screen()
elif self.state == GameState.PLAYING:
self.draw_game()
elif self.state == GameState.GAME_OVER:
self.draw_game()
self.draw_game_over_screen()
elif self.state == GameState.HIGH_SCORE:
self.draw_high_scores()
pygame.display.flip()
def draw_attract_screen(self):
# Title
font = pygame.font.Font(None, 74)
title = font.render('PACMAN', True, YELLOW)
self.screen.blit(title, (SCREEN_WIDTH//2 - title.get_width()//2, 100))
# Instructions
font = pygame.font.Font(None, 36)
press_start = font.render('PRESS SPACE TO START', True, WHITE)
self.screen.blit(press_start,
(SCREEN_WIDTH//2 - press_start.get_width()//2, 300))
# High Scores
self.draw_high_scores(y_offset=400)
def draw_game_over_screen(self):
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
overlay.fill(BLACK)
overlay.set_alpha(128)
self.screen.blit(overlay, (0, 0))
font = pygame.font.Font(None, 74)
game_over = font.render('GAME OVER', True, RED)
self.screen.blit(game_over,
(SCREEN_WIDTH//2 - game_over.get_width()//2, 200))
font = pygame.font.Font(None, 36)
final_score = font.render(f'FINAL SCORE: {self.pacman.score}', True, WHITE)
self.screen.blit(final_score,
(SCREEN_WIDTH//2 - final_score.get_width()//2, 300))
press_space = font.render('PRESS SPACE TO CONTINUE', True, WHITE)
self.screen.blit(press_space,
(SCREEN_WIDTH//2 - press_space.get_width()//2, 400))
def draw_high_scores(self, y_offset=100):
font = pygame.font.Font(None, 36)
title = font.render('HIGH SCORES', True, WHITE)
self.screen.blit(title,
(SCREEN_WIDTH//2 - title.get_width()//2, y_offset))
for i, score in enumerate(self.high_scores):
text = font.render(f"{i+1}. {score}", True, WHITE)
self.screen.blit(text,
(SCREEN_WIDTH//2 - text.get_width()//2,
y_offset + 50 + i * 30))
def draw_game(self):
# Draw walls
for wall in self.walls:
x, y = wall[0] * CELL_SIZE, wall[1] * CELL_SIZE
pygame.draw.rect(self.screen, BLUE, (x, y, CELL_SIZE, CELL_SIZE))
# Draw dots
for dot in self.dots:
pygame.draw.circle(self.screen, WHITE,
(dot[0] * CELL_SIZE + CELL_SIZE // 2,
dot[1] * CELL_SIZE + CELL_SIZE // 2), 3)
# Draw power pellets
for pellet in self.power_pellets:
pygame.draw.circle(self.screen, WHITE,
(pellet[0] * CELL_SIZE + CELL_SIZE // 2,
pellet[1] * CELL_SIZE + CELL_SIZE // 2), 8)
# Draw ghost door - draw white line for each '-' in the maze
for door_x, door_y in self.ghost_door:
pygame.draw.line(self.screen, WHITE,
(door_x * CELL_SIZE, door_y * CELL_SIZE + CELL_SIZE * 0.8),
(door_x * CELL_SIZE + CELL_SIZE, door_y * CELL_SIZE + CELL_SIZE * 0.8),
2)
# Draw Pacman
self.pacman.draw(self.screen)
# Draw ghosts
for ghost in self.ghosts:
ghost.draw(self.screen)
# Draw score
font = pygame.font.Font(None, 36)
score_text = font.render(f'Score: {self.pacman.score}', True, WHITE)
self.screen.blit(score_text, (10, 10))
# Draw lives
for i in range(self.pacman.lives):
pygame.draw.circle(self.screen, YELLOW,
(CELL_SIZE * (i + 1), SCREEN_HEIGHT - CELL_SIZE//2),
CELL_SIZE//3)
def run(self):
FRAME_RATE = 60
dt = 1.0 / FRAME_RATE
accumulator = 0
current_time = time.time()
while self.running:
new_time = time.time()
frame_time = new_time - current_time
current_time = new_time
accumulator += frame_time
self.handle_events()
while accumulator >= dt:
self.update()
accumulator -= dt
self.draw()
self.clock.tick(FRAME_RATE)
def load_high_scores(self):
try:
with open(HIGH_SCORES_FILE, 'r') as f:
scores = [int(line.strip()) for line in f.readlines()]
return sorted(scores, reverse=True)[:MAX_HIGH_SCORES]
except:
return [10000, 8000, 6000, 4000, 2000] # Default scores
def save_high_scores(self):
with open(HIGH_SCORES_FILE, 'w') as f:
for score in self.high_scores:
f.write(f"{score}\n")
def check_high_score(self, score):
if not self.high_scores or score > min(self.high_scores):
self.high_scores.append(score)
self.high_scores.sort(reverse=True)
self.high_scores = self.high_scores[:MAX_HIGH_SCORES]
self.save_high_scores()
return True
return False
if __name__ == "__main__":
try:
game = Game()
game.run()
finally:
pygame.quit()
References and Resources
- Pygame Documentation – Core library for game mechanics.
- Cursor AI Editor – AI tool that wrote the code.
- Claude by Anthropic – Details on the AI model used.
- Pac-Man Maze Reference – Basis for the JSON layout.