---
title: Reusable Development Prompt for a Fully Functional Python Curses-Based Pac-Man Terminal Game with Specific Gameplay Logic
created: 2026-03-30
updated: 2026-03-30
type: summary
tags: ["game", "notes", "pacman", "python", "telegram"]
external: http://go/ui/posts/2297/
---

Here's a reusable prompt that fully captures everything in the game:

---

## 🎮 Pac-Man CLI Game — Generation Prompt

```
Create a fully working Pac-Man game for the Mac/Linux terminal using Python.
Zero external dependencies — stdlib only (curses, time, random).
Run with: python3 pacman.py

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ENVIRONMENT & ARCHITECTURE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Use Python's `curses` library via `curses.wrapper(main)`.
- Two classes: Ghost and PacmanGame (holds stdscr).
- Game loop runs at ~50 fps (sleep 0.02s); movement is time-gated using
  last-move timestamps, NOT frame counting.
  - Pac-Man move interval: 0.14s
  - Ghost move interval:   0.22s
  - Blink/animation toggle: 0.35s

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MAZE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 31 rows × 28 cols, stored as a list of strings (MAZE_TEMPLATE), then
  copied to a list-of-lists at game start so cells can be mutated.
- Cell legend:
    '#'  wall
    ' '  empty corridor
    '.'  dot  (+10 pts, counts toward win condition)
    'o'  power pellet  (+50 pts, triggers frightened mode)
    'G'  ghost house door (passable by ghosts, wall for Pac-Man)
- Include a central ghost house (approx rows 12-17, cols 11-16) with a
  single-cell-wide door marked 'G'.
- Four power pellets at the four corner regions of the maze.
- Left/right edges of the tunnel row (row 14) are open — Pac-Man and
  ghosts can wrap horizontally using `col % COLS`.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PAC-MAN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Start position: row 23, col 14; initial direction: LEFT.
- Store both `pac_dir` (current) and `next_dir` (buffered input).
  On each move tick, try next_dir first; if blocked, try pac_dir.
  This gives smooth cornering.
- Horizontal tunnel wrap: nc = nc % COLS.
- Visual: directional Unicode arrows that alternate with 'O' for mouth
  animation:
    UP    → ᐱ  (mouth open) / O (closed)
    DOWN  → ᐯ
    LEFT  → ᐸ
    RIGHT → ᐳ
- Color: curses.COLOR_YELLOW | A_BOLD.
- 3 lives; on death reset Pac-Man and all ghosts, sleep 1s, continue.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GHOSTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Four ghosts: Blinky (red), Pinky (magenta), Inky (cyan), Clyde (yellow).
Each starts inside the ghost house with a staggered spawn_delay:
  idx 0 → 0s, idx 1 → 2s, idx 2 → 4s, idx 3 → 6s.

Movement algorithm (called every ghost tick, receives dt):
1. Decrement spawn_delay by dt; skip movement until it reaches 0.
2. Determine target cell:
   - eaten=True  → target = ghost's home cell (return to house).
   - frightened  → target = random cell (scatter).
   - normal      → chase target = (pac_r + dr, pac_c + dc) where
       offsets = [(0,0), (2,2), (-2,2), (2,-2)] indexed by ghost.idx.
3. Shuffle DIRS for random tie-breaking.
4. Pick the walkable non-reverse direction minimising Euclidean² distance
   to target. Ghost house door ('G') IS walkable for ghosts.
5. Move one cell in chosen direction; update self.dir.

States:
- frightened=True for 7.0s after any power pellet is eaten.
  During last 2s, ghost blinks (alternate white / blue on a 0.35s timer).
- eaten=True when Pac-Man collides with a frightened ghost (+200 pts).
  Eaten ghost displays as '◌' (white) and navigates home; resets there.
- Normal collision (non-frightened, non-eaten) → lose a life.

Visual:
- Normal:     'ᗣ' in ghost's color | A_BOLD.
- Frightened: 'ᗣ' blue | A_BOLD; blink to white when timer < 2s.
- Eaten:      '◌' white.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SCORING & WIN/LOSE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Dot:           +10
- Power pellet:  +50
- Eat ghost:     +200
- Win condition: dots_eaten >= total_dots (count '.' + 'o' at init).
- Lose condition: lives reach 0.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
RENDERING
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Center the maze in the terminal using:
  offset_r = max(0, (sh - ROWS - 3) // 2)
  offset_c = max(0, (sw - COLS)     // 2)

Cell glyphs:
  '#' → '█'  white | A_DIM
  '.' → '·'  white
  'o' → '●'  white | A_BOLD
  'G' → '─'  white | A_DIM
  ' ' → ' '

HUD one row below maze: "SCORE: 000000   LIVES: ● ● ●  LEVEL: 1"
in green | A_BOLD.

Wrap all stdscr.addstr calls in try/except curses.error to avoid crashes
on small terminals. Minimum recommended terminal: 80 × 34.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CURSES COLOR PAIRS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  1 → RED      Blinky
  2 → MAGENTA  Pinky
  3 → CYAN     Inky
  4 → YELLOW   Clyde
  5 → YELLOW   Pac-Man
  6 → BLUE     Frightened ghost
  7 → WHITE    Maze / dots / HUD
  8 → GREEN    Score text
Use curses.use_default_colors() and background -1 for transparency.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CONTROLS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Arrow keys / WASD  → direction (buffered as next_dir)
  P                  → toggle pause (skip move ticks, still draw)
  Q                  → quit immediately

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SCREENS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Splash screen (before first game):
  - Big block-letter "PAC-MAN" title in yellow using box-drawing chars.
  - One line listing all four ghost names with their ᗣ symbol.
  - Controls reminder line.
  - "Press any key to start!" — uses nodelay(False) to block.

Game-over screen (after each game):
  - "YOU WIN! 🎉" (green) or "GAME OVER" (red), centered.
  - Final score line.
  - "Play again? (Y / N)" — Y re-runs _init_game(); N exits.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CODE STRUCTURE SUMMARY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Ghost class methods:
  __init__, reset, move, _walkable (staticmethod)

PacmanGame class methods:
  __init__, _setup_colors, _init_game
  run → _show_splash → loop(_init_game → _play → _game_over_screen)
  _play (main loop)
  _handle_input, _move_pacman
  _check_collisions, _lose_life, _check_win
  _draw, _addch, _center_msg
  _show_splash, _game_over_screen

Entry point: curses.wrapper(main)
```

---

You can drop this prompt directly into any AI coding session and it will reproduce a game equivalent to this one. The key details that usually get lost without a spec like this are the **buffered input cornering logic**, the **time-gated movement** (vs frame counting), the **ghost blink countdown at 2s**, and the **staggered spawn delays**.

## Minimax-m2.7 ✅

```
#!/usr/bin/env python3
"""
Pac-Man game for terminal using curses.
Run with: python3 pacman.py
Zero external dependencies — stdlib only.
"""

import curses
import time
import random
from enum import Enum
from typing import List, Tuple, Optional

# ═══════════════════════════════════════════════════════════════
# CONSTANTS
# ═══════════════════════════════════════════════════════════════

ROWS = 31
COLS = 28

PAC_MOVE_INTERVAL = 0.14
GHOST_MOVE_INTERVAL = 0.22
BLINK_INTERVAL = 0.35

FRIGHTENED_DURATION = 7.0
FRIGHTENED_BLINK_START = 2.0
GHOST_SPAWN_DELAYS = [0, 2, 4, 6]

POINTS = {'dot': 10, 'pellet': 50, 'ghost': 200}

# Maze: 31 rows × 28 cols
# Legend: '#' wall, ' ' corridor, '.' dot, 'o' power pellet, 'G' ghost door
MAZE_TEMPLATE = [
    "##############################",
    "#............##............#",
    "#.####.#####.##.#####.####.#",
    "#.####.#####.##.#####.####.#",
    "#.####.#####.##.#####.####.#",
    "#..........................#",
    "#.####.##.##########.##.###.#",
    "#.####.##.##########.##.###.#",
    "#......##....##....##......#",
    "######.##### ## #####.######",
    "#     .##### ## #####.     #",
    "#     .##       G    .     #",
    "#     .##  ###--###  .     #",
    "######.##  #      #  .######",
    "          . #      # .      ",
    "######.##  #      #  .######",
    "#     .##  ########  .     #",
    "#     .##            .     #",
    "#     .##  ########  .     #",
    "######.##  ########  .######",
    "#............##............#",
    "#.####.#####.##.#####.####.#",
    "#.####.#####.##.#####.####.#",
    "#o..##.......  .......##..o#",
    "###.##.##.##########.##.###.#",
    "#......##....##....##......#",
    "#.##########.##.##########.#",
    "#..........................#",
    "##############################"
]


# Direction enum with row/col deltas
class Dir(Enum):
    UP = (-1, 0)
    DOWN = (1, 0)
    LEFT = (0, -1)
    RIGHT = (0, 1)


# Pac-Man mouth animation glyphs per direction
PAC_DIRS = {
    Dir.UP: ('ᐱ', 'O'),
    Dir.DOWN: ('ᐯ', 'O'),
    Dir.LEFT: ('ᐸ', 'O'),
    Dir.RIGHT: ('ᐳ', 'O'),
}

# Ghost color indices (1-4)
GHOST_COLORS = [1, 2, 3, 4]
GHOST_NAMES = ['Blinky', 'Pinky', 'Inky', 'Clyde']

# Chase offsets: Blinky=0, Pinky=1, Inky=2, Clyde=3
CHASE_OFFSETS = [(0, 0), (2, 2), (-2, 2), (2, -2)]


# ═══════════════════════════════════════════════════════════════
# GHOST CLASS
# ═══════════════════════════════════════════════════════════════

class Ghost:
    """Individual ghost entity with AI behavior."""

    def __init__(self, idx: int, row: int, col: int,
                 home_row: int, home_col: int, color_pair: int):
        self.idx = idx
        self.row = row
        self.col = col
        self.home_row = home_row
        self.home_col = home_col
        self.color_pair = color_pair
        self.dir = Dir.UP
        self.frightened = False
        self.eaten = False
        self.frightened_timer = 0.0
        self.blink_timer = 0.0
        self.spawn_delay = GHOST_SPAWN_DELAYS[idx]
        self.visible = True

    def reset(self):
        """Reset ghost to home position and initial state."""
        self.row = self.home_row
        self.col = self.home_col
        self.dir = Dir.UP
        self.frightened = False
        self.eaten = False
        self.frightened_timer = 0.0
        self.blink_timer = 0.0
        self.spawn_delay = GHOST_SPAWN_DELAYS[self.idx]
        self.visible = True

    @staticmethod
    def is_walkable(cell: str, is_ghost: bool = False) -> bool:
        """Check if a maze cell can be walked on."""
        return cell in (' ', '.', 'o') or (is_ghost and cell == 'G')

    def get_target(self, pac_r: int, pac_c: int,
                   pac_dir: Dir) -> Tuple[int, int]:
        """Return target cell based on ghost mode."""
        if self.eaten:
            return (self.home_row, self.home_col)
        elif self.frightened:
            # Random target when frightened
            return (random.randint(0, ROWS - 1),
                    random.randint(0, COLS - 1))
        else:
            # Chase: target = pac_pos + offset
            dr, dc = CHASE_OFFSETS[self.idx]
            return (pac_r + dr, pac_c + dc)

    def move(self, maze: List[List[str]], pac_r: int, pac_c: int,
             pac_dir: Dir, dt: float):
        """Update ghost position using time delta dt."""
        # Handle spawn delay
        if self.spawn_delay > 0:
            self.spawn_delay -= dt
            return

        # Decrement frightened timer
        if self.frightened_timer > 0:
            self.frightened_timer -= dt
            if self.frightened_timer <= 0:
                self.frightened = False

        # Handle blink animation when frightened
        if self.frightened:
            self.blink_timer += dt
            if self.blink_timer >= BLINK_INTERVAL:
                self.blink_timer = 0.0
                self.visible = not self.visible

        # Determine target based on state
        if self.eaten:
            target_r, target_c = self.home_row, self.home_col
        elif self.frightened:
            target_r, target_c = self.get_target(pac_r, pac_c, pac_dir)
        else:
            target_r, target_c = self.get_target(pac_r, pac_c, pac_dir)

        # Move toward target using Euclidean distance
        dirs = list(Dir)
        random.shuffle(dirs)

        best_dir = None
        best_dist = float('inf')

        for d in dirs:
            nr = (self.row + d.value[0]) % ROWS
            nc = (self.col + d.value[1]) % COLS

            if self.is_walkable(maze[nr][nc], is_ghost=True):
                # Euclidean distance squared
                dist = (nr - target_r) ** 2 + (nc - target_c) ** 2
                if dist < best_dist:
                    best_dist = dist
                    best_dir = d

        if best_dir:
            self.row = (self.row + best_dir.value[0]) % ROWS
            self.col = (self.col + best_dir.value[1]) % COLS
            self.dir = best_dir

        # Check if eaten ghost reached home
        if self.eaten and self.row == self.home_row and self.col == self.home_col:
            self.eaten = False


# ═══════════════════════════════════════════════════════════════
# PACMAN GAME CLASS
# ═══════════════════════════════════════════════════════════════

class PacmanGame:
    """Main game controller holding stdscr and game state."""

    def __init__(self, stdscr):
        self.stdscr = stdscr
        self.pac_color_pair = 5
        self._setup_colors()
        self._init_vars()

    def _setup_colors(self):
        """Initialize curses color pairs."""
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_RED, -1)      # Blinky
        curses.init_pair(2, curses.COLOR_MAGENTA, -1)  # Pinky
        curses.init_pair(3, curses.COLOR_CYAN, -1)     # Inky
        curses.init_pair(4, curses.COLOR_YELLOW, -1)   # Clyde
        curses.init_pair(5, curses.COLOR_YELLOW, -1)   # Pac-Man
        curses.init_pair(6, curses.COLOR_BLUE, -1)    # Frightened
        curses.init_pair(7, curses.COLOR_WHITE, -1)   # Maze/dots
        curses.init_pair(8, curses.COLOR_GREEN, -1)   # Score text

    def _init_vars(self):
        """Initialize/reset game variables for a new game."""
        self.maze: List[List[str]] = []
        self.ghosts: List[Ghost] = []
        self.score = 0
        self.lives = 3
        self.level = 1
        self.paused = False
        self.game_over = False
        self.won = False
        self.total_dots = 0
        self.dots_eaten = 0
        self.pac_row = 23
        self.pac_col = 14
        self.pac_dir = Dir.LEFT
        self.next_dir = Dir.LEFT
        self.mouth_open = True
        self.last_pac_move = 0.0
        self.last_ghost_move = 0.0
        self.last_blink = 0.0
        self.pause_msg = ""

    def _init_game(self):
        """Initialize maze and entities for a new game."""
        self._init_vars()
        # Copy template to mutable maze
        self.maze = [list(row) for row in MAZE_TEMPLATE]

        # Count total dots and pellets
        self.total_dots = sum(
            row.count('.') + row.count('o') for row in self.maze
        )
        self.dots_eaten = 0

        # Create ghosts with staggered spawn positions
        # All start at ghost house (row 14), staggered columns
        spawn_positions = [(14, 13), (14, 14), (14, 13), (14, 14)]
        for i, (r, c) in enumerate(spawn_positions):
            self.ghosts.append(Ghost(
                idx=i,
                row=r, col=c,
                home_row=r, home_col=c,
                color_pair=curses.color_pair(GHOST_COLORS[i])
            ))

    # ─────────────────────────────────────────────────────────────
    # Main loop and screens
    # ─────────────────────────────────────────────────────────────

    def run(self):
        """Entry point: show splash, then play loop."""
        self._show_splash()
        while True:
            self._init_game()
            if not self._play():
                break
            if not self._game_over_screen():
                break
        return

    def _show_splash(self):
        """Display splash screen, block for keypress."""
        self.stdscr.clear()
        sh, sw = self.stdscr.getmaxyx()

        # Big "PAC-MAN" title using box characters
        title = [
            " ██████╗  ██████╗ ██╗   ██╗███████╗███████╗ ██████╗ ██╗   ██╗ ",
            " ██╔══██╗██╔════╝ ██║   ██║██╔════╝██╔════╝██╔════╝ ██║   ██║ ",
            " ██████╔╝██║  ███╗██║   ██║█████╗  ███████╗██║  ███╗██║   ██║ ",
            " ██╔═══╝ ██║   ██║██║   ██║██╔══╝  ╚════██║██║   ██║██║   ██║ ",
            " ██║     ╚██████╔╝╚██████╔╝███████╗███████║╚██████╔╝╚██████╔╝ ",
            " ╚═╝      ╚═════╝  ╚═════╝ ╚══════╝╚══════╝ ╚═════╝  ╚═════╝  ",
        ]

        for i, line in enumerate(title):
            try:
                y = sh // 2 - 10 + i
                x = max(0, (sw - len(line)) // 2)
                self.stdscr.addstr(y, x, line,
                                   curses.color_pair(5) | curses.A_BOLD)
            except curses.error:
                pass

        # Ghost names with symbols
        ghost_line = "  ".join(
            f"{GHOST_NAMES[i]} ᗣ" for i in range(4)
        )
        try:
            self.stdscr.addstr(
                sh // 2, max(0, (sw - len(ghost_line)) // 2),
                ghost_line, curses.A_BOLD
            )
        except curses.error:
            pass

        # Controls reminder
        ctrl = "Arrow Keys / WASD to move | P to Pause | Q to Quit"
        try:
            self.stdscr.addstr(
                sh // 2 + 2, max(0, (sw - len(ctrl)) // 2),
                ctrl, curses.A_DIM
            )
        except curses.error:
            pass

        # Start prompt
        prompt = "Press any key to start!"
        try:
            self.stdscr.addstr(
                sh // 2 + 5, max(0, (sw - len(prompt)) // 2),
                prompt, curses.color_pair(8) | curses.A_BOLD
            )
        except curses.error:
            pass

        self.stdscr.nodelay(False)
        self.stdscr.refresh()
        self.stdscr.getch()

    def _play(self) -> bool:
        """Main game loop. Returns False to quit, True to replay."""
        self.stdscr.nodelay(True)
        self.stdscr.keypad(True)
        self.last_pac_move = time.time()
        self.last_ghost_move = time.time()
        self.last_blink = time.time()

        while not self.game_over:
            current_time = time.time()

            # Handle input (non-blocking)
            self._handle_input()
            if self.game_over:
                return not self.won

            if not self.paused:
                dt = current_time - self.last_pac_move

                # Move Pac-Man
                if dt >= PAC_MOVE_INTERVAL:
                    self._move_pacman()
                    self.last_pac_move = current_time

                # Move ghosts with proper dt
                ghost_dt = current_time - self.last_ghost_move
                if ghost_dt >= GHOST_MOVE_INTERVAL:
                    for ghost in self.ghosts:
                        ghost.move(self.maze, self.pac_row, self.pac_col,
                                   self.pac_dir, ghost_dt)
                    self.last_ghost_move = current_time

                # Check collisions
                self._check_collisions()

                # Check win
                if self._check_win():
                    self.won = True
                    self.game_over = True

            self._draw()
            curses.napms(20)  # ~50 fps

        return True

    def _game_over_screen(self) -> bool:
        """Show game over/win screen. Returns True to replay, False to quit."""
        self.stdscr.clear()
        sh, sw = self.stdscr.getmaxyx()

        # Title message
        if self.won:
            msg = "YOU WIN!"
            color = curses.color_pair(8) | curses.A_BOLD
        else:
            msg = "GAME OVER"
            color = curses.color_pair(1) | curses.A_BOLD

        try:
            self.stdscr.addstr(
                sh // 2 - 2, max(0, (sw - len(msg)) // 2),
                msg, color
            )
        except curses.error:
            pass

        # Score
        score_msg = f"Final Score: {self.score}"
        try:
            self.stdscr.addstr(
                sh // 2, max(0, (sw - len(score_msg)) // 2),
                score_msg, curses.A_BOLD
            )
        except curses.error:
            pass

        # Prompt
        prompt = "Play again? (Y / N)"
        try:
            self.stdscr.addstr(
                sh // 2 + 2, max(0, (sw - len(prompt)) // 2),
                prompt, curses.A_BOLD
            )
        except curses.error:
            pass

        self.stdscr.nodelay(False)
        self.stdscr.refresh()

        while True:
            key = self.stdscr.getch()
            if key in (ord('y'), ord('Y')):
                return True
            elif key in (ord('n'), ord('N')):
                return False

    # ─────────────────────────────────────────────────────────────
    # Input handling
    # ─────────────────────────────────────────────────────────────

    def _handle_input(self):
        """Process keyboard input."""
        try:
            key = self.stdscr.getch()
            if key == -1:
                return

            # Direction keys
            if key in (curses.KEY_UP, 119):      # UP, w
                self.next_dir = Dir.UP
            elif key in (curses.KEY_DOWN, 115):  # DOWN, s
                self.next_dir = Dir.DOWN
            elif key in (curses.KEY_LEFT, 97):   # LEFT, a
                self.next_dir = Dir.LEFT
            elif key in (curses.KEY_RIGHT, 100):  # RIGHT, d
                self.next_dir = Dir.RIGHT
            elif chr(key).upper() == 'P':
                self.paused = not self.paused
            elif chr(key).upper() == 'Q':
                self.game_over = True

        except curses.error:
            pass

    # ─────────────────────────────────────────────────────────────
    # Movement and collision
    # ─────────────────────────────────────────────────────────────

    def _move_pacman(self):
        """Move Pac-Man using buffered input with wrap-around."""
        # Try next_dir first, then pac_dir if blocked
        for direction in (self.next_dir, self.pac_dir):
            nr = (self.pac_row + direction.value[0]) % ROWS
            nc = (self.pac_col + direction.value[1]) % COLS

            cell = self.maze[nr][nc]

            # Walls and ghost door block Pac-Man
            if cell not in ('#', 'G'):
                self.pac_dir = direction
                self.pac_row = nr
                self.pac_col = nc

                # Collect dot
                if cell == '.':
                    self.score += POINTS['dot']
                    self.maze[nr][nc] = ' '
                    self.dots_eaten += 1

                # Collect power pellet
                elif cell == 'o':
                    self.score += POINTS['pellet']
                    self.maze[nr][nc] = ' '
                    self.dots_eaten += 1

                    # Trigger frightened mode for all ghosts
                    for ghost in self.ghosts:
                        ghost.frightened = True
                        ghost.frightened_timer = FRIGHTENED_DURATION

                break

        # Toggle mouth animation
        self.mouth_open = not self.mouth_open

    def _check_collisions(self):
        """Check Pac-Man vs ghost collisions."""
        for ghost in self.ghosts:
            # Skip if still spawning
            if ghost.spawn_delay > 0:
                continue

            if ghost.row == self.pac_row and ghost.col == self.pac_col:
                if ghost.frightened and not ghost.eaten:
                    # Eat ghost
                    ghost.eaten = True
                    self.score += POINTS['ghost']
                elif not ghost.eaten:
                    # Ghost catches Pac-Man
                    self._lose_life()
                    return

    def _lose_life(self):
        """Handle losing a life."""
        self.lives -= 1
        if self.lives <= 0:
            self.game_over = True
        else:
            # Reset positions
            time.sleep(1)
            self.pac_row = 23
            self.pac_col = 14
            self.pac_dir = Dir.LEFT
            self.next_dir = Dir.LEFT
            for ghost in self.ghosts:
                ghost.reset()

    def _check_win(self) -> bool:
        """Check if all dots have been eaten."""
        return self.dots_eaten >= self.total_dots

    # ─────────────────────────────────────────────────────────────
    # Rendering
    # ─────────────────────────────────────────────────────────────

    def _draw(self):
        """Render entire game state to terminal."""
        self.stdscr.clear()
        sh, sw = self.stdscr.getmaxyx()

        # Calculate centering offset
        offset_r = max(0, (sh - ROWS - 3) // 2)
        offset_c = max(0, (sw - COLS) // 2)

        # Draw maze cells
        for r, row in enumerate(self.maze):
            for c, cell in enumerate(row):
                if cell == '#':
                    self._addch(offset_r + r, offset_c + c, '█',
                                curses.color_pair(7) | curses.A_DIM)
                elif cell == '.':
                    self._addch(offset_r + r, offset_c + c, '·',
                                curses.color_pair(7))
                elif cell == 'o':
                    self._addch(offset_r + r, offset_c + c, '●',
                                curses.color_pair(7) | curses.A_BOLD)
                elif cell == 'G':
                    self._addch(offset_r + r, offset_c + c, '─',
                                curses.color_pair(7) | curses.A_DIM)

        # Draw Pac-Man
        glyph_idx = 0 if self.mouth_open else 1
        pac_char = PAC_DIRS[self.pac_dir][glyph_idx]
        self._addch(
            offset_r + self.pac_row, offset_c + self.pac_col,
            pac_char, curses.color_pair(5) | curses.A_BOLD
        )

        # Draw ghosts
        for ghost in self.ghosts:
            if ghost.spawn_delay > 0:
                continue

            gr = offset_r + ghost.row
            gc = offset_c + ghost.col

            if ghost.eaten:
                char = '◌'
                color = curses.color_pair(7) | curses.A_BOLD
            elif ghost.frightened:
                # Blink during last 2 seconds
                if ghost.frightened_timer < FRIGHTENED_BLINK_START:
                    char = 'ᗣ'
                    color = (curses.color_pair(7) if ghost.visible
                             else curses.color_pair(6) | curses.A_BOLD)
                else:
                    char = 'ᗣ'
                    color = curses.color_pair(6) | curses.A_BOLD
            else:
                char = 'ᗣ'
                color = ghost.color_pair | curses.A_BOLD

            self._addch(gr, gc, char, color)

        # Draw HUD
        lives_str = '● ' * self.lives
        hud = f"SCORE: {self.score:06d}   LIVES: {lives_str} LEVEL: {self.level}"
        try:
            self.stdscr.addstr(
                offset_r + ROWS + 1, offset_c,
                hud, curses.color_pair(8) | curses.A_BOLD
            )
        except curses.error:
            pass

        # Draw pause overlay
        if self.paused:
            self._center_msg("PAUSED - Press P to continue")

        self.stdscr.refresh()

    def _addch(self, r: int, c: int, ch: str, attr: int):
        """Safe wrapper for stdscr.addch with bounds checking."""
        try:
            self.stdscr.addch(r, c, ch, attr)
        except curses.error:
            pass

    def _center_msg(self, msg: str):
        """Display a centered message overlay."""
        sh, sw = self.stdscr.getmaxyx()
        try:
            self.stdscr.addstr(
                sh // 2, max(0, (sw - len(msg)) // 2),
                msg, curses.A_BOLD | curses.color_pair(8)
            )
        except curses.error:
            pass


# ═══════════════════════════════════════════════════════════════
# ENTRY POINT
# ═══════════════════════════════════════════════════════════════

def main(stdscr):
    """Entry point for curses.wrapper."""
    game = PacmanGame(stdscr)
    game.run()


if __name__ == '__main__':
    curses.wrapper(main)
```

## GLM 5 Turbo ❌

## KIMI 2 ✅

```
#!/usr/bin/env python3
"""
Pac-Man Terminal Game
Zero dependencies - stdlib only (curses, time, random)
Run with: python3 pacman.py
"""

import curses
import time
import random
import math

# ═══════════════════════════════════════════════════════════════
# CONSTANTS
# ═══════════════════════════════════════════════════════════════

ROWS = 31
COLS = 28

# Timing constants
PAC_MOVE_INTERVAL = 0.14
GHOST_MOVE_INTERVAL = 0.22
BLINK_INTERVAL = 0.35
FRIGHTENED_DURATION = 7.0
BLINK_START = 2.0

# Spawn delays for ghosts (staggered)
SPAWN_DELAYS = [0.0, 2.0, 4.0, 6.0]

# Ghost chase offsets (relative to Pac-Man)
CHASE_OFFSETS = [(0, 0), (2, 2), (-2, 2), (2, -2)]

# Directions: (dr, dc)
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]  # UP, DOWN, LEFT, RIGHT
DIR_NAMES = ['UP', 'DOWN', 'LEFT', 'RIGHT']

# Unicode characters
CHAR_WALL = '█'
CHAR_DOT = '·'
CHAR_PELLET = '●'
CHAR_DOOR = '─'
CHAR_GHOST = 'ᗣ'
CHAR_GHOST_EATEN = '◌'
CHAR_PACMAN = {'UP': 'ᐱ', 'DOWN': 'ᐯ', 'LEFT': 'ᐸ', 'RIGHT': 'ᐳ', 'CLOSED': 'O'}

# ═══════════════════════════════════════════════════════════════
# MAZE TEMPLATE
# ═══════════════════════════════════════════════════════════════

MAZE_TEMPLATE = [
    "############################",
    "#............##............#",
    "#.####.#####.##.#####.####.#",
    "#o####.#####.##.#####.####o#",
    "#.####.#####.##.#####.####.#",
    "#..........................#",
    "#.####.##.########.##.####.#",
    "#.####.##.########.##.####.#",
    "#......##....##....##......#",
    "######.##### ## #####.######",
    "     #.##### ## #####.#     ",
    "     #.##          ##.#     ",
    "     #.## ###  ### ##.#     ",
    "######.## #      # ##.######",
    "      .   #      #   .      ",
    "######.## #      # ##.######",
    "     #.## ######## ##.#     ",
    "     #.##          ##.#     ",
    "     #.## ######## ##.#     ",
    "######.## ######## ##.######",
    "#............##............#",
    "#.####.#####.##.#####.####.#",
    "#.####.#####.##.#####.####.#",
    "#o..##.......  .......##..o#",
    "###.##.##.########.##.##.###",
    "###.##.##.########.##.##.###",
    "#......##....##....##......#",
    "#.##########.##.##########.#",
    "#.##########.##.##########.#",
    "#..........................#",
    "############################",
]

# ═══════════════════════════════════════════════════════════════
# GHOST CLASS
# ═══════════════════════════════════════════════════════════════

class Ghost:
    def __init__(self, idx, start_r, start_c, home_r, home_c, color):
        self.idx = idx
        self.start_r = start_r
        self.start_c = start_c
        self.home_r = home_r
        self.home_c = home_c
        self.color = color
        self.reset()

    def reset(self):
        self.r = self.start_r
        self.c = self.start_c
        self.dir = (-1, 0)  # Start moving up
        self.spawn_delay = SPAWN_DELAYS[self.idx]
        self.frightened = False
        self.eaten = False
        self.in_house = True

    def move(self, maze, pac_r, pac_c, frightened_active, frightened_timer, dt):
        # Handle spawn delay
        if self.spawn_delay > 0:
            self.spawn_delay -= dt
            if self.spawn_delay <= 0:
                self.in_house = False
            return

        # Update states based on global frightened status
        if frightened_active and not self.eaten:
            self.frightened = True
        else:
            self.frightened = False

        # Determine target
        if self.eaten:
            target = (self.home_r, self.home_c)
        elif self.frightened:
            # Random scatter
            target = (random.randint(0, ROWS-1), random.randint(0, COLS-1))
        else:
            # Chase with offset
            offset = CHASE_OFFSETS[self.idx]
            target = (pac_r + offset[0], pac_c + offset[1])

        # Find best direction
        best_dir = None
        best_dist = float('inf')

        # Shuffle directions for random tie-breaking
        dirs_shuffled = DIRS[:]
        random.shuffle(dirs_shuffled)

        reverse_dir = (-self.dir[0], -self.dir[1])

        for d in dirs_shuffled:
            nr, nc = self.r + d[0], self.c + d[1]

            # Don't reverse direction unless eaten (returning home)
            if d == reverse_dir and not self.eaten:
                continue

            if self._walkable(maze, nr, nc):
                dist = (nr - target[0])**2 + (nc - target[1])**2
                if dist < best_dist:
                    best_dist = dist
                    best_dir = d

        if best_dir:
            self.r += best_dir[0]
            self.c += best_dir[1]
            self.dir = best_dir

            # Handle tunnel wrap
            self.c = self.c % COLS

            # Check if reached home when eaten
            if self.eaten and self.r == self.home_r and self.c == self.home_c:
                self.eaten = False
                self.frightened = False
                self.spawn_delay = 1.0  # Brief delay before re-emerging

    @staticmethod
    def _walkable(maze, r, c):
        if r < 0 or r >= ROWS or c < 0 or c >= COLS:
            return False
        cell = maze[r][c]
        # Ghosts can walk through doors ('G') but not walls ('#')
        return cell != '#'


# ═══════════════════════════════════════════════════════════════
# PAC-MAN GAME CLASS
# ═══════════════════════════════════════════════════════════════

class PacmanGame:
    def __init__(self, stdscr):
        self.stdscr = stdscr
        self._setup_colors()
        self.running = True
        self.paused = False

    def _setup_colors(self):
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_RED, -1)      # Blinky
        curses.init_pair(2, curses.COLOR_MAGENTA, -1)  # Pinky
        curses.init_pair(3, curses.COLOR_CYAN, -1)     # Inky
        curses.init_pair(4, curses.COLOR_YELLOW, -1)   # Clyde
        curses.init_pair(5, curses.COLOR_YELLOW, -1)   # Pac-Man
        curses.init_pair(6, curses.COLOR_BLUE, -1)     # Frightened
        curses.init_pair(7, curses.COLOR_WHITE, -1)    # Maze / dots
        curses.init_pair(8, curses.COLOR_GREEN, -1)    # HUD

    def _init_game(self):
        # Copy maze template
        self.maze = [list(row) for row in MAZE_TEMPLATE]

        # Count total dots
        self.total_dots = sum(row.count('.') + row.count('o') for row in self.maze)
        self.dots_eaten = 0

        # Score and lives
        self.score = 0
        self.lives = 3
        self.level = 1

        # Pac-Man state
        self.pac_r = 23
        self.pac_c = 14
        self.pac_dir = (0, -1)  # LEFT
        self.next_dir = (0, -1)
        self.pac_mouth_open = True

        # Ghosts
        # Ghost house center roughly at row 13-14, col 13-14
        self.ghosts = [
            Ghost(0, 13, 13, 13, 13, curses.color_pair(1)),  # Blinky - red
            Ghost(1, 13, 14, 13, 14, curses.color_pair(2)),  # Pinky - magenta
            Ghost(2, 14, 13, 14, 13, curses.color_pair(3)),  # Inky - cyan
            Ghost(3, 14, 14, 14, 14, curses.color_pair(4)),  # Clyde - yellow
        ]

        # Game state
        self.frightened = False
        self.frightened_timer = 0.0
        self.blink_state = False

        # Timestamps
        self.last_pac_move = 0
        self.last_ghost_move = 0
        self.last_blink = 0

        # Animation toggle
        self.anim_toggle = True

    def run(self):
        self._show_splash()

        while self.running:
            self._init_game()
            result = self._play()
            if not self.running:
                break
            if not self._game_over_screen(result):
                break

    def _show_splash(self):
        self.stdscr.nodelay(False)
        self.stdscr.clear()

        sh, sw = self.stdscr.getmaxyx()

        # Big PAC-MAN title using box drawing
        title = [
            "╔═══════════════════════════════════╗",
            "║  ██████   █████   ██████  ██████  ║",
            "║  ██   ██ ██   ██ ██      ██    ██ ║",
            "║  ██████  ███████ ██      ██    ██ ║",
            "║  ██      ██   ██ ██      ██    ██ ║",
            "║  ██      ██   ██  ██████  ██████  ║",
            "╚═══════════════════════════════════╝",
        ]

        start_r = max(2, (sh - 15) // 2)
        start_c = max(0, (sw - 37) // 2)

        for i, line in enumerate(title):
            try:
                self.stdscr.addstr(start_r + i, start_c, line, 
                                  curses.color_pair(5) | curses.A_BOLD)
            except:
                pass

        # Ghost line
        ghost_line = "  ᗣ Blinky    ᗣ Pinky    ᗣ Inky    ᗣ Clyde  "
        ghost_colors = [1, 2, 3, 4]
        ghost_start_c = (sw - len(ghost_line)) // 2

        try:
            pos = 0
            for i, name in enumerate(["Blinky", "Pinky", "Inky", "Clyde"]):
                prefix = "  ᗣ "
                self.stdscr.addstr(start_r + 8, ghost_start_c + pos, prefix,
                                  curses.color_pair(ghost_colors[i]) | curses.A_BOLD)
                pos += len(prefix)
                self.stdscr.addstr(start_r + 8, ghost_start_c + pos, name + "    ",
                                  curses.color_pair(ghost_colors[i]))
                pos += len(name) + 4
        except:
            pass

        # Controls
        controls = [
            "",
            "Controls:",
            "  Arrow Keys / WASD  -  Move",
            "  P                  -  Pause",
            "  Q                  -  Quit",
            "",
            "Press any key to start!"
        ]

        for i, line in enumerate(controls):
            try:
                self.stdscr.addstr(start_r + 10 + i, (sw - len(line)) // 2, line,
                                  curses.color_pair(7))
            except:
                pass

        self.stdscr.refresh()
        self.stdscr.getch()
        self.stdscr.nodelay(True)

    def _play(self):
        """Main game loop. Returns 'win' or 'lose'."""
        clock = time.time()

        while True:
            now = time.time()
            dt = now - clock
            clock = now

            # Handle input
            self._handle_input()

            if not self.running:
                return 'quit'

            if self.paused:
                self._draw()
                time.sleep(0.02)
                continue

            # Update frightened timer
            if self.frightened:
                self.frightened_timer -= dt
                if self.frightened_timer <= 0:
                    self.frightened = False

            # Update blink animation
            if now - self.last_blink > BLINK_INTERVAL:
                self.blink_state = not self.blink_state
                self.last_blink = now
                self.anim_toggle = not self.anim_toggle

            # Move Pac-Man
            if now - self.last_pac_move > PAC_MOVE_INTERVAL:
                self._move_pacman()
                self.last_pac_move = now

            # Move ghosts
            if now - self.last_ghost_move > GHOST_MOVE_INTERVAL:
                for ghost in self.ghosts:
                    ghost.move(self.maze, self.pac_r, self.pac_c, 
                              self.frightened, self.frightened_timer, 
                              GHOST_MOVE_INTERVAL)
                self.last_ghost_move = now

            # Check collisions
            result = self._check_collisions()
            if result == 'lose':
                return 'lose'
            if result == 'win':
                return 'win'

            # Draw
            self._draw()

            # Cap framerate
            time.sleep(0.02)

    def _handle_input(self):
        try:
            key = self.stdscr.getch()
        except:
            return

        if key == -1:
            return

        # Quit
        if key in (ord('q'), ord('Q')):
            self.running = False
            return

        # Pause
        if key in (ord('p'), ord('P')):
            self.paused = not self.paused
            return

        # Direction input
        new_dir = None
        if key in (curses.KEY_UP, ord('w'), ord('W')):
            new_dir = (-1, 0)  # UP
        elif key in (curses.KEY_DOWN, ord('s'), ord('S')):
            new_dir = (1, 0)   # DOWN
        elif key in (curses.KEY_LEFT, ord('a'), ord('A')):
            new_dir = (0, -1)  # LEFT
        elif key in (curses.KEY_RIGHT, ord('d'), ord('D')):
            new_dir = (0, 1)   # RIGHT

        if new_dir:
            self.next_dir = new_dir

    def _can_move(self, r, c):
        """Check if Pac-Man can move to this cell."""
        if r < 0 or r >= ROWS:
            return False
        c = c % COLS  # Handle tunnel wrap
        cell = self.maze[r][c]
        # Pac-Man cannot go through walls or ghost house doors
        return cell not in ('#', 'G')

    def _move_pacman(self):
        # Try next direction first (for smooth cornering)
        dirs_to_try = [self.next_dir, self.pac_dir]

        for d in dirs_to_try:
            nr, nc = self.pac_r + d[0], self.pac_c + d[1]
            if self._can_move(nr, nc):
                self.pac_r, self.pac_c = nr, nc
                self.pac_c = self.pac_c % COLS  # Tunnel wrap
                self.pac_dir = d
                self.pac_mouth_open = not self.pac_mouth_open

                # Check for dots
                cell = self.maze[self.pac_r][self.pac_c]
                if cell == '.':
                    self.maze[self.pac_r][self.pac_c] = ' '
                    self.score += 10
                    self.dots_eaten += 1
                elif cell == 'o':
                    self.maze[self.pac_r][self.pac_c] = ' '
                    self.score += 50
                    self.dots_eaten += 1
                    self.frightened = True
                    self.frightened_timer = FRIGHTENED_DURATION
                    # Reset eaten state for ghosts
                    for g in self.ghosts:
                        if g.eaten:
                            g.eaten = False
                return

    def _check_collisions(self):
        for ghost in self.ghosts:
            if ghost.r == self.pac_r and ghost.c == self.pac_c:
                if ghost.eaten:
                    continue
                if self.frightened and not ghost.eaten:
                    # Eat ghost
                    ghost.eaten = True
                    ghost.frightened = False
                    self.score += 200
                else:
                    # Lose life
                    if self._lose_life():
                        return 'lose'

        # Check win condition
        if self.dots_eaten >= self.total_dots:
            return 'win'

        return 'continue'

    def _lose_life(self):
        self.lives -= 1

        if self.lives <= 0:
            return True

        # Reset positions
        self.pac_r, self.pac_c = 23, 14
        self.pac_dir = (0, -1)
        self.next_dir = (0, -1)

        for ghost in self.ghosts:
            ghost.reset()

        self.frightened = False
        self.frightened_timer = 0

        # Brief pause
        self._draw()
        time.sleep(1)
        return False

    def _draw(self):
        self.stdscr.clear()
        sh, sw = self.stdscr.getmaxyx()

        # Calculate offset to center maze
        offset_r = max(0, (sh - ROWS - 3) // 2)
        offset_c = max(0, (sw - COLS) // 2)

        # Draw maze
        for r in range(ROWS):
            for c in range(COLS):
                cell = self.maze[r][c]
                char = ' '
                attr = curses.color_pair(7)

                if cell == '#':
                    char = CHAR_WALL
                    attr = curses.color_pair(7) | curses.A_DIM
                elif cell == '.':
                    char = CHAR_DOT
                    attr = curses.color_pair(7)
                elif cell == 'o':
                    char = CHAR_PELLET
                    attr = curses.color_pair(7) | curses.A_BOLD
                elif cell == 'G':
                    char = CHAR_DOOR
                    attr = curses.color_pair(7) | curses.A_DIM

                try:
                    self.stdscr.addstr(offset_r + r, offset_c + c, char, attr)
                except:
                    pass

        # Draw ghosts
        for ghost in self.ghosts:
            gr, gc = ghost.r, ghost.c
            char = CHAR_GHOST
            attr = ghost.color | curses.A_BOLD

            if ghost.eaten:
                char = CHAR_GHOST_EATEN
                attr = curses.color_pair(7) | curses.A_BOLD
            elif ghost.frightened:
                char = CHAR_GHOST
                # Blink white/blue in last 2 seconds
                if self.frightened_timer < BLINK_START and self.blink_state:
                    attr = curses.color_pair(7) | curses.A_BOLD
                else:
                    attr = curses.color_pair(6) | curses.A_BOLD

            try:
                self.stdscr.addstr(offset_r + gr, offset_c + gc, char, attr)
            except:
                pass

        # Draw Pac-Man
        dir_name = DIR_NAMES[DIRS.index(self.pac_dir)]
        if self.anim_toggle:
            char = CHAR_PACMAN[dir_name]
        else:
            char = CHAR_PACMAN['CLOSED']

        try:
            self.stdscr.addstr(offset_r + self.pac_r, offset_c + self.pac_c, 
                              char, curses.color_pair(5) | curses.A_BOLD)
        except:
            pass

        # Draw HUD
        hud_y = offset_r + ROWS + 1
        lives_str = '●' * self.lives
        hud_text = f"SCORE: {self.score:06d}   LIVES: {lives_str:>3}  LEVEL: {self.level}"

        try:
            self.stdscr.addstr(hud_y, offset_c, hud_text, 
                              curses.color_pair(8) | curses.A_BOLD)
        except:
            pass

        # Pause indicator
        if self.paused:
            self._center_msg("PAUSED", curses.color_pair(7) | curses.A_BOLD)

        self.stdscr.refresh()

    def _addch(self, r, c, char, attr):
        """Safe addch wrapper."""
        try:
            self.stdscr.addch(r, c, char, attr)
        except curses.error:
            pass

    def _center_msg(self, msg, attr):
        """Display centered message."""
        sh, sw = self.stdscr.getmaxyx()
        try:
            self.stdscr.addstr(sh // 2, (sw - len(msg)) // 2, msg, attr)
        except:
            pass

    def _game_over_screen(self, result):
        """Show game over screen. Returns True to play again."""
        self.stdscr.nodelay(False)
        self.stdscr.clear()

        sh, sw = self.stdscr.getmaxyx()

        if result == 'win':
            msg = "YOU WIN! 🎉"
            color = curses.color_pair(8) | curses.A_BOLD
        else:
            msg = "GAME OVER"
            color = curses.color_pair(1) | curses.A_BOLD

        try:
            self.stdscr.addstr(sh // 2 - 2, (sw - len(msg)) // 2, msg, color)
            self.stdscr.addstr(sh // 2, (sw - 12) // 2, 
                              f"Score: {self.score}", curses.color_pair(7))
            self.stdscr.addstr(sh // 2 + 2, (sw - 20) // 2, 
                              "Play again? (Y / N)", curses.color_pair(7))
        except:
            pass

        self.stdscr.refresh()

        while True:
            key = self.stdscr.getch()
            if key in (ord('y'), ord('Y')):
                self.stdscr.nodelay(True)
                return True
            elif key in (ord('n'), ord('N'), ord('q'), ord('Q')):
                return False


# ═══════════════════════════════════════════════════════════════
# ENTRY POINT
# ═══════════════════════════════════════════════════════════════

def main(stdscr):
    curses.curs_set(0)  # Hide cursor
    stdscr.nodelay(True)  # Non-blocking input

    game = PacmanGame(stdscr)
    game.run()


if __name__ == "__main__":
    curses.wrapper(main)
```

## MiMo  kinda working ✅
```
#!/usr/bin/env python3
"""Pac-Man for the terminal — Python 3, stdlib only (curses, time, random)."""

import curses
import time
import random

# ════════════════════════════════════════════════════════════════
#  CONSTANTS
# ════════════════════════════════════════════════════════════════

ROWS, COLS = 31, 28
FPS = 50
FRAME_DELAY = 1.0 / FPS

PAC_MOVE = 0.14
GHOST_MOVE = 0.22
BLINK_TOGGLE = 0.35
FRIGHTENED_DUR = 7.0
FRIGHTENED_WARN = 2.0

UP, DOWN, LEFT, RIGHT = 0, 1, 2, 3
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
OPPOSITE = [1, 0, 3, 2]
DIR_CHR = ["\u02c7", "\u02c6", "\u02c3", "\u02c2"]   # ᐱ ᐯ ᐸ ᐳ

GHOST_COLORS = [1, 2, 3, 4]       # Blinky-red, Pinky-magenta, Inky-cyan, Clyde-yellow
GHOST_NAMES = ["Blinky", "Pinky", "Inky", "Clyde"]
GHOME = [(14, 13), (14, 14), (15, 13), (15, 14)]
GSPAWN = [0, 2, 4, 6]
GCHASE = [(0, 0), (2, 2), (-2, 2), (2, -2)]

# ════════════════════════════════════════════════════════════════
#  MAZE TEMPLATE  (31 × 28)
# ════════════════════════════════════════════════════════════════

MAZE_TEMPLATE = [
    "############################",
    "#............##............#",
    "#.####.#####.##.#####.####.#",
    "#o####.#####.##.#####.####o#",
    "#.####.#####.##.#####.####.#",
    "#..........................#",
    "#.####.##.########.##.####.#",
    "#.####.##.########.##.####.#",
    "#......##....##....##......#",
    "######.##### ##.#####.######",
    "     #.##### ##.#####.#     ",
    "     #.##          ##.#     ",
    "     #.## ###GG### ##.#     ",
    "######.## #      # ##.######",
    "      .   #      #   .      ",
    "######.## #      # ##.######",
    "     #.## ######## ##.#     ",
    "     #.##          ##.#     ",
    "     #.## ######## ##.#     ",
    "######.## ######## ##.######",
    "#............##............#",
    "#.####.#####.##.#####.####.#",
    "#.####.#####.##.#####.####.#",
    "#o..##.......  .......##..o#",
    "###.##.##.########.##.##.###",
    "###.##.##.########.##.##.###",
    "#......##....##....##......#",
    "#.##########.##.##########.#",
    "#.##########.##.##########.#",
    "#..........................#",
    "############################",
]

# ════════════════════════════════════════════════════════════════
#  GHOST CLASS
# ════════════════════════════════════════════════════════════════

class Ghost:
    __slots__ = ("idx", "r", "c", "dir", "spawn_delay", "frightened",
                 "frightened_timer", "eaten", "ir", "ic", "game")

    def __init__(self, idx, r, c, game):
        self.idx = idx
        self.ir, self.ic = r, c
        self.game = game
        self.reset()

    # ── state ──────────────────────────────────────────────────

    def reset(self):
        self.r, self.c = self.ir, self.ic
        self.dir = UP
        self.spawn_delay = GSPAWN[self.idx]
        self.frightened = False
        self.frightened_timer = 0.0
        self.eaten = False

    # ── movement tick ──────────────────────────────────────────

    def move(self, dt, pac_r, pac_c):
        self.spawn_delay -= dt
        if self.spawn_delay > 0:
            return

        maze = self.game.maze

        # --- eaten → go home ---
        if self.eaten:
            if (self.r, self.c) == GHOME[self.idx]:
                self.eaten = False
                return
            tr, tc = GHOME[self.idx]

        # --- frightened → random scatter ---
        elif self.frightened:
            tr = random.randint(0, ROWS - 1)
            tc = random.randint(0, COLS - 1)

        # --- normal chase ---
        else:
            dr, dc = GCHASE[self.idx]
            tr = pac_r + dr
            tc = pac_c + dc

        # walkable test (class method defined below)
        def _wk(r, c, rev):
            if c < 0 or c >= COLS:
                return False
            if r < 0 or r >= ROWS:
                return False
            if maze[r][c] == "#":
                return False
            if c == rev:
                return False
            if self.dir != OPPOSITE[self.idx] and maze[r][c] == "G" and not self.eaten:
                return False          # normal ghost can't re-enter through G
            return True

        rev_c = self.c + DIRS[OPPOSITE[self.dir]][1]
        rev_r = self.r + DIRS[OPPOSITE[self.dir]][0]

        cands = []
        for d in (UP, DOWN, LEFT, RIGHT):
            dr, dc = DIRS[d]
            nr, nc = self.r + dr, self.c + dc
            if _wk(nr, nc, rev_c):
                cands.append((d, nr, nc))

        if not cands:
            for d in (UP, DOWN, LEFT, RIGHT):
                dr, dc = DIRS[d]
                nr, nc = self.r + dr, self.c + dc
                if 0 <= nr < ROWS and 0 <= nc < COLS and maze[nr][nc] != "#":
                    cands.append((d, nr, nc))

        if cands:
            random.shuffle(cands)
            best_d, best_nr, best_nc = min(
                cands,
                key=lambda x: (x[1] - tr) ** 2 + (x[2] - tc) ** 2,
            )
            self.r, self.c = best_nr, best_nc
            self.dir = best_d


# ════════════════════════════════════════════════════════════════
#  GAME CLASS
# ════════════════════════════════════════════════════════════════

class PacmanGame:
    def __init__(self, stdscr):
        self.stdscr = stdscr
        self._setup_colors()
        self.level = 1
        self._init_game()

    # ── curses colour pairs ────────────────────────────────────

    def _setup_colors(self):
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_RED, -1)
        curses.init_pair(2, curses.COLOR_MAGENTA, -1)
        curses.init_pair(3, curses.COLOR_CYAN, -1)
        curses.init_pair(4, curses.COLOR_YELLOW, -1)
        curses.init_pair(5, curses.COLOR_YELLOW, -1)
        curses.init_pair(6, curses.COLOR_BLUE, -1)
        curses.init_pair(7, curses.COLOR_WHITE, -1)
        curses.init_pair(8, curses.COLOR_GREEN, -1)

    # ── (re-)initialise a fresh game ──────────────────────────

    def _init_game(self):
        self.maze = [list(row) for row in MAZE_TEMPLATE]
        self.total_dots = sum(
            1 for r in self.maze for c in r if c in (".", "o")
        )
        self.dots_eaten = 0

        self.pac_r, self.pac_c = 23, 14
        self.pac_dir = LEFT
        self.next_dir = LEFT

        self.ghosts = [Ghost(i, *GHOME[i], self) for i in range(4)]

        self.score = 0
        self.lives = 3
        self.paused = False
        self.running = True
        self.game_over = False
        self.game_won = False

        self.frightened = False
        self.frightened_timer = 0.0
        self.blink_timer = 0.0
        self.mouth_timer = 0.0
        self.mouth_open = True
        self._collided = -1

        self._last_tick = time.time()

    # ── input ──────────────────────────────────────────────────

    def _handle_input(self):
        key_map = {
            curses.KEY_UP: UP, ord("w"): UP, ord("W"): UP,
            curses.KEY_DOWN: DOWN, ord("s"): DOWN, ord("S"): DOWN,
            curses.KEY_LEFT: LEFT, ord("a"): LEFT, ord("A"): LEFT,
            curses.KEY_RIGHT: RIGHT, ord("d"): RIGHT, ord("D"): RIGHT,
        }
        self.stdscr.nodelay(True)
        while True:
            ch = self.stdscr.getch()
            if ch == -1:
                break
            if ch in key_map:
                self.next_dir = key_map[ch]
            elif ch in (ord("p"), ord("P")):
                self.paused = not self.paused
            elif ch in (ord("q"), ord("Q")):
                self.running = False

    # ── pac-man movement ───────────────────────────────────────

    def _move_pac(self):
        moved = False
        for d in (self.next_dir, self.pac_dir):
            dr, dc = DIRS[d]
            nr, nc = self.pac_r + dr, self.pac_c + dc
            nc %= COLS
            if 0 <= nr < ROWS and self.maze[nr][nc] != "#":
                self.pac_r, self.pac_c = nr, nc
                self.pac_dir = d
                moved = True
                break
        return moved

    # ── dot / pellet consumption ───────────────────────────────

    def _eat(self):
        cell = self.maze[self.pac_r][self.pac_c]
        if cell == ".":
            self.maze[self.pac_r][self.pac_c] = " "
            self.score += 10
            self.dots_eaten += 1
        elif cell == "o":
            self.maze[self.pac_r][self.pac_c] = " "
            self.score += 50
            self.dots_eaten += 1
            self.frightened = True
            self.frightened_timer = FRIGHTENED_DUR
            for g in self.ghosts:
                if not g.eaten and g.spawn_delay <= 0:
                    g.frightened = True
                    g.frightened_timer = FRIGHTENED_DUR

    # ── collision ──────────────────────────────────────────────

    def _check_collisions(self):
        self._collided = -1
        for i, g in enumerate(self.ghosts):
            if g.spawn_delay > 0 or g.eaten:
                continue
            if g.r == self.pac_r and g.c == self.pac_c:
                if g.frightened:
                    g.eaten = True
                    g.frightened = False
                    g.spawn_delay = 3.0
                    self.score += 200
                else:
                    self._collided = i

    def _lose_life(self):
        self.lives -= 1
        if self.lives <= 0:
            self.game_over = True
            self.running = False
            return
        self.pac_r, self.pac_c = 23, 14
        self.pac_dir = LEFT
        self.next_dir = LEFT
        for g in self.ghosts:
            g.reset()
        self.frightened = False
        self.frightened_timer = 0.0
        time.sleep(1.0)
        self._last_tick = time.time()

    # ── win check ──────────────────────────────────────────────

    def _check_win(self):
        if self.dots_eaten >= self.total_dots:
            self.game_won = True
            self.running = False

    # ── one logic tick ─────────────────────────────────────────

    def _tick(self, dt):
        if self.paused:
            return

        self._move_pac()
        self._eat()

        if self.frightened:
            self.frightened_timer -= dt
            if self.frightened_timer <= 0:
                self.frightened = False
                for g in self.ghosts:
                    g.frightened = False

        for g in self.ghosts:
            if g.frightened:
                g.frightened_timer = self.frightened_timer
            g.move(dt, self.pac_r, self.pac_c)

        self._check_collisions()
        if self._collided >= 0:
            self._lose_life()
            return

        self._check_win()

    # ── main play loop ─────────────────────────────────────────

    def _play(self):
        self._last_tick = time.time()
        while self.running:
            t0 = time.time()
            self._handle_input()

            now = time.time()
            dt = now - self._last_tick
            self._last_tick = now

            self._tick(dt)

            # animations (always run so visuals don't freeze)
            self.mouth_timer += dt
            if self.mouth_timer >= PAC_MOVE:
                self.mouth_timer -= PAC_MOVE
                self.mouth_open = not self.mouth_open
            self.blink_timer += dt
            if self.blink_timer >= BLINK_TOGGLE * 2:
                self.blink_timer -= BLINK_TOGGLE * 2

            self._draw()

            elapsed = time.time() - t0
            time.sleep(max(0, FRAME_DELAY - elapsed))

    # ══════════════════════════════════════════════════════════
    #  RENDERING
    # ══════════════════════════════════════════════════════════

    def _put(self, r, c, ch, attr=0):
        try:
            self.stdscr.addch(r, c, ch, attr)
        except curses.error:
            pass

    def _puts(self, r, c, s, attr=0):
        try:
            self.stdscr.addstr(r, c, s, attr)
        except curses.error:
            pass

    def _centre_msg(self, row, msg, attr=0):
        sh, sw = self.stdscr.getmaxyx()
        self._puts(row, max(0, (sw - len(msg)) // 2), msg, attr)

    # ── draw frame ─────────────────────────────────────────────

    def _draw(self):
        stdscr = self.stdscr
        stdscr.erase()
        sh, sw = stdscr.getmaxyx()

        off_r = max(0, (sh - ROWS - 3) // 2)
        off_c = max(0, (sw - COLS) // 2)

        # --- maze cells ---
        wall_a = curses.color_pair(7) | curses.A_DIM
        for r in range(ROWS):
            for c in range(COLS):
                ch = self.maze[r][c]
                gy, gx = off_r + r, off_c + c
                if ch == "#":
                    self._put(gy, gx, "█", wall_a)
                elif ch == ".":
                    self._put(gy, gx, "·", curses.color_pair(7))
                elif ch == "o":
                    self._put(gy, gx, "●", curses.color_pair(7) | curses.A_BOLD)
                elif ch == "G":
                    self._put(gy, gx, "─", wall_a)

        # --- pac-man ---
        gy = off_r + self.pac_r
        gx = off_c + self.pac_c
        pa = curses.color_pair(5) | curses.A_BOLD
        if self.mouth_open:
            self._put(gy, gx, DIR_CHR[self.pac_dir], pa)
        else:
            self._put(gy, gx, "O", pa)

        # --- ghosts ---
        blink_on = self.blink_timer < BLINK_TOGGLE
        for g in self.ghosts:
            if g.spawn_delay > 0:
                continue
            gy = off_r + g.r
            gx = off_c + g.c
            if g.eaten:
                self._put(gy, gx, "◌", curses.color_pair(7))
            elif g.frightened:
                if self.frightened_timer < FRIGHTENED_WARN and not blink_on:
                    attr = curses.color_pair(7) | curses.A_BOLD
                else:
                    attr = curses.color_pair(6) | curses.A_BOLD
                self._put(gy, gx, "ᗣ", attr)
            else:
                self._put(gy, gx, "ᗣ",
                          curses.color_pair(GHOST_COLORS[g.idx]) | curses.A_BOLD)

        # --- HUD ---
        hud_r = off_r + ROWS + 1
        lives_str = "● " * max(0, self.lives)
        hud = f"SCORE: {self.score:06d}   LIVES: {lives_str}  LEVEL: {self.level}"
        self._puts(hud_r, off_c, hud, curses.color_pair(8) | curses.A_BOLD)

        # --- paused overlay ---
        if self.paused:
            self._centre_msg(off_r + ROWS // 2,
                             "  *** PAUSED ***  ",
                             curses.color_pair(8) | curses.A_BOLD | curses.A_BLINK)

        stdscr.refresh()

    # ══════════════════════════════════════════════════════════
    #  SPLASH SCREEN
    # ══════════════════════════════════════════════════════════

    def _show_splash(self):
        stdscr = self.stdscr
        stdscr.erase()
        stdscr.nodelay(False)
        curses.curs_set(0)

        sh, sw = stdscr.getmaxyx()

        title = [
            "╔══════════════════════════╗",
            "║       P A C - M A N      ║",
            "╚══════════════════════════╝",
        ]

        sr = max(0, (sh - 16) // 2)
        sc = max(0, (sw - 28) // 2)

        ya = curses.color_pair(5) | curses.A_BOLD
        for i, line in enumerate(title):
            self._puts(sr + i, sc, line, ya)

        row = sr + 5
        self._puts(row, sc + 2, "MEET THE GHOSTS:", curses.color_pair(7) | curses.A_BOLD)
        for i, name in enumerate(GHOST_NAMES):
            attr = curses.color_pair(GHOST_COLORS[i]) | curses.A_BOLD
            self._puts(row + 1 + i, sc + 4,
                       f"ᗣ  {name}", attr)

        row += 6
        self._puts(row, sc + 2, "CONTROLS:", curses.color_pair(7) | curses.A_BOLD)
        self._puts(row + 1, sc + 4, "Arrow Keys / WASD  - Move",
                   curses.color_pair(7))
        self._puts(row + 2, sc + 4, "P                  - Pause",
                   curses.color_pair(7))
        self._puts(row + 3, sc + 4, "Q                  - Quit",
                   curses.color_pair(7))

        row += 5
        self._centre_msg(row, "Press any key to start!",
                         curses.color_pair(8) | curses.A_BOLD | curses.A_BLINK)

        stdscr.refresh()

        # flush pending, then block for one key
        stdscr.nodelay(True)
        while stdscr.getch() != -1:
            pass
        stdscr.nodelay(False)
        stdscr.getch()

    # ══════════════════════════════════════════════════════════
    #  GAME-OVER SCREEN
    # ══════════════════════════════════════════════════════════

    def _game_over_screen(self):
        stdscr = self.stdscr
        stdscr.erase()
        stdscr.nodelay(False)

        sh, sw = stdscr.getmaxyx()
        sr = max(0, (sh - 7) // 2)

        if self.game_won:
            self._centre_msg(sr, "╔═══════════════════╗",
                             curses.color_pair(8) | curses.A_BOLD)
            self._centre_msg(sr + 1, "║   YOU WIN!  🎉    ║",
                             curses.color_pair(8) | curses.A_BOLD)
            self._centre_msg(sr + 2, "╚═══════════════════╝",
                             curses.color_pair(8) | curses.A_BOLD)
        else:
            self._centre_msg(sr, "╔═══════════════════╗",
                             curses.color_pair(1) | curses.A_BOLD)
            self._centre_msg(sr + 1, "║    GAME  OVER     ║",
                             curses.color_pair(1) | curses.A_BOLD)
            self._centre_msg(sr + 2, "╚═══════════════════╝",
                             curses.color_pair(1) | curses.A_BOLD)

        self._centre_msg(sr + 4,
                         f"Final Score: {self.score:06d}",
                         curses.color_pair(7) | curses.A_BOLD)
        self._centre_msg(sr + 6, "Play again?  (Y / N)",
                         curses.color_pair(8) | curses.A_BOLD)

        stdscr.refresh()

        while True:
            ch = stdscr.getch()
            if ch in (ord("y"), ord("Y")):
                self.running = True
                return True
            if ch in (ord("n"), ord("N"), ord("q"), ord("Q")):
                self.running = False
                return False

    # ══════════════════════════════════════════════════════════
    #  TOP-LEVEL RUN LOOP
    # ══════════════════════════════════════════════════════════

    def run(self):
        curses.curs_set(0)
        self._show_splash()

        while self.running:
            self._init_game()
            self._play()
            if not self.running:
                break
            if not self._game_over_screen():
                break


# ════════════════════════════════════════════════════════════════
#  ENTRY POINT
# ════════════════════════════════════════════════════════════════

def main(stdscr):
    game = PacmanGame(stdscr)
    game.run()


if __name__ == "__main__":
    curses.wrapper(main)
```

## Deepseek 3.2  ❌ not working 
```
 ~/temp/pacman   v3.14.3  11:36   in 18s232ms ❯ uv run deepseek.py
  File "/Users/junv/temp/pacman/deepseek.py", line 240
    self.dots_eaten = or
                      ^^
SyntaxError: invalid syntax

```
I'll create a complete terminal Pac-Man game using Python's curses library with no external dependencies. Below is the implementation:

```python
#!/usr/bin/env python3
import curses
import random
import time
import math

# Constants
ROWS = 31
COLS = 28
PAC_MOVE_INTERVAL = 0.14
GHOST_MOVE_INTERVAL = 0.22
BLINK_INTERVAL = 0.35
FRIGHTENED_TIME = 7.0
GHOST_SPAWN_DELAYS = [0, 2, 4, 6]

# Maze template
MAZE_TEMPLATE = [
    "############################",
    "#............##............#",
    "#.####.#####.##.#####.####.#",
    "#o####.#####.##.#####.####o#",
    "#.####.#####.##.#####.####.#",
    "#..........................#",
    "#.####.##.########.##.####.#",
    "#.####.##.########.##.####.#",
    "#......##....##....##......#",
    "######.##### ## #####.######",
    "######.##### ## #####.######",
    "######.##          ##.######",
    "######.## ###--### ##.######",
    "######.## #      # ##.######",
    "          # GGGG #          ",
    "######.## #      # ##.######",
    "######.## ###--### ##.######",
    "######.##          ##.######",
    "######.## ######## ##.######",
    "######.## ######## ##.######",
    "#............##............#",
    "#.####.#####.##.#####.####.#",
    "#.####.#####.##.#####.####.#",
    "#o......##........##......o#",
    "#.####.##.########.##.####.#",
    "#.####.##.########.##.####.#",
    "#..........................#",
    "#.####.#####.##.#####.####.#",
    "#.####.#####.##.#####.####.#",
    "#............##............#",
    "############################"
]

# Direction vectors and mapping
DIRS = [(0, -1), (-1, 0), (0, 1), (1, 0)]  # LEFT, UP, RIGHT, DOWN
DIR_CHARS = ['ᐸ', 'ᐱ', 'ᐳ', 'ᐯ']
REVERSE_DIRS = {0: 2, 1: 3, 2: 0, 3: 1}

class Ghost:
    def __init__(self, idx, name, color_pair):
        self.idx = idx
        self.name = name
        self.color_pair = color_pair
        self.row = 14  # Ghost house starting row
        self.col = 13 + idx  # Different columns in house
        self.dir = random.choice([0, 1, 2, 3])
        self.spawn_delay = GHOST_SPAWN_DELAYS[idx]
        self.frightened = False
        self.frightened_timer = 0.0
        self.eaten = False
        self.blink_timer = 0.0
        self.blink_state = False
        self.last_move_time = 0.0
        
    def reset(self, game_state):
        """Reset ghost to initial state"""
        self.row = 14
        self.col = j13 + self.idx
        self.dir = random.choice([0, 1, 2, 3])
        self.spawn_delay = GHOST_SPAWN_DELAYS[self.idx]
        self.frightened = False
        self.frightened_timer = 0.0
        self.eaten = False
        self.blink_timer = 0.0
        self.blink_state = False
        self.last_move_time = 0.0
        
    @staticmethod
    def _walkable(cell_char, for_ghost=True):
        """Check if a cell is walkable"""
        if cell_char == '#':
            return False
        if cell_char == 'G' and not for_ghost:
            return False  # Ghost house door is wall for Pac-Man
        return True
        
    def move(self, maze, pac_row, pac_col, dt):
        """Move ghost according to algorithm"""
        # Handle spawn delay
        if self.spawn_delay > 0:
            self.spawn_delay -= dt
            return
        
        # Update frightened timer
        if self.frightened:
            self.frightened_timer -= dt
            if self.frightened_timer <= 0:
                self.frightened = False
            elif self.frightened_timer <= 2.0:
                self.blink_timer += dt
                if self.blink_timer >= BLINK_INTERVAL:
                    self.blink_timer = 0
                    self.blink_state = not self.blink_state
        
        # Check if ready to move
        if time.time() - self.last_move_time < GHOST_MOVE_INTERVAL:
            return
        
        self.last_move_time = time.time()
        
        # Determine target cell
        if self.eaten:
            # Return to ghost house
            target_row = 14
            target_col = 13 + self.idx
        elif self.frightened:
            # Random scatter target
            target_row = random.randint(0, ROWS - 1)
            target_col = random.randint(0, COLS - 1)
        else:
            # Chase target with offset based on ghost index
            offsets = [(0, 0), (2, 2), (-2, 2), (2, -2)]
            dr, dc = offsets[self.idx]
            target_row = pac_row + dr
            target_col = pac_col + dc
        
        # Find best direction
        possible_dirs = []
        for i, (dr, dc) in enumerate(DIRS):
            # Don't reverse unless necessary (for eaten ghosts returning home)
            if i == REVERSE_DIRS.get(self.dir, -1) and not self.eaten:
                continue
            
            nr = self.row + dr
            nc = self.col + dc
            
            # Horizontal wrap in tunnel (row 14)
            if self.row == 14 and (nc < 0 or nc >= COLS):
                nc = nc % COLS
            
            if 0 <= nr < ROWS and 0 <= nc < COLS:
                cell_char = maze[nr][nc]
                if Ghost._walkable(cell_char, for_ghost=True):
                    possible_dirs.append(i)
        
        # If no possible directions (shouldn't happen), stay still
        if not possible_dirs:
            return
        
        # Shuffle for random tie-breaking
        random.shuffle(possible_dirs)
        
        # Find direction minimizing Euclidean distance to target
        best_dir = possible_dirs[0]
        best_dist = float('inf')
        
        for dir_idx in possible_dirs:
            dr, dc = DIRS[dir_idx]
            nr = self.row + dr
            nc = self.col + dc
            
            # Horizontal wrap
            if self.row == 14 and (nc < 0 or nc >= COLS):
                nc = nc % COLS
            
            dist = (nr - target_row) ** 2 + (nc - target_col) ** 2
            if dist < best_dist:
                best_dist = dist
                best_dir = dir_idx
        
        # Move in chosen direction
        dr, dc = DIRS[best_dir]
        self.row += dr
        self.col += dc
        self.dir = best_dir
        
        # Horizontal wrap for ghosts too
        if self.row == 14 and (self.col < 0 or self.col >= COLS):
            self.col = self.col % COLS

class PacmanGame:
    def __init__(self, stdscr):
        self.stdscr = stdscr
        self._setup_colors()
        self.maze = []
        self.total_dots = 0
        self.score = 0
        self.level = 1
        self.lives = 3
        self.dots_eaten = 0
        self.pac_row = 23
        self.pac_col = 14
        self.pac_dir = 0  # LEFT
        self.next_dir = 0  # LEFT
        self.pac_last_move = 0.0
        self.pac_anim_timer = 0.0
        self.pac_anim_state = 0  # 0=closed, 1=open
        self.ghosts = []
        self.frightened_mode = False
        self.frightened_timer = 0.0
        self.paused = False
        self.game_over = False
        self.win = False
        
    def _setup_colors(self):
        """Initialize color pairs"""
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_RED, -1)      # Blinky
        curses.init_pair(2, curses.COLOR_MAGENTA, -1)  # Pinky
        curses.init_pair(3, curses.COLOR_CYAN, -1)     # Inky
        curses.init_pair(4, curses.COLOR_YELLOW, -1)   # Clyde
        curses.init_pair(5, curses.COLOR_YELLOW, -1)   # Pac-Man
        curses.init_pair(6, curses.COLOR_BLUE, -1)     # Frightened ghost
        curses.init_pair(7, curses.COLOR_WHITE, -1)    # Maze/dots/HUD
        curses.init_pair(8, curses.COLOR_GREEN, -1)    # Score text
        
    def _init_game(self):
        """Initialize game state"""
        # Create mutable maze from template
        self.maze = [list(row) for row in MAZE_TEMPLATE]
        
        # Count total dots
        self.total_dots = 0
        for row in self.maze:
            for cell in row:
                if cell == '.' or cell == 'o':
                    self.total_dots += 1
        
        # Reset game stats
        self.score = 0
        self.level = 1
        self.lives = 3
        self.dots_eaten = or
        self.pac_row = 23
        self.pac_col = 14
        self.pac_dir = 0
        self.next_dir = 0
        self.pac_last_move = time.time()
        self.pac_anim_timer = 0.0
        self.pac_anim_state = 0
        self.frightened_mode = False
        self.frightened_timer = 0.0
        self.paused = False
        self.game_over = False
        self.win = False
        
        # Initialize ghosts
        ghost_names = ["Blinky", "Pinky", "Inky", "Clyde"]
        self.ghosts = [
            Ghost(0, ghost_names[0], 1),
            Ghost(1, ghost_names[1], 2),
            Ghost(2, ghost_names[2], 3),
            Ghost(3, ghost_names[3], 4)
        ]
        
    def _handle_input(self):
        """Process keyboard input"""
        try:
            key = self.stdscr.getch()
        except curses.error:
            return
        
        if key == curses.KEY_LEFT or key == ord('a') or key == ord('A'):
            self.next_dir = 0
        elif key == curses.KEY_UP or key == ord('w') or key == ord('W'):
            self.next_dir = 1
        elif key == curses.KEY_RIGHT or key == ord('d') or key == ord('D'):
            self.next_dir = 2
        elif key == curses.KEY_DOWN or key == ord('s') or key == ord('S'):
            self.next_dir = 3
        elif key == ord('p') or key == ord('P'):
            self.paused = not self.paused
        elif key == ord('q') or key == ord('Q'):
            self.game_over = True
    
    def _move_pacman(self):
        """Move Pac-Man according to buffered input"""
        if time.time() - self.pac_last_move < PAC_MOVE_INTERVAL:
            return
        
        # Update animation timer
        self.pac_anim_timer += PAC_MOVE_INTERVAL
        if self.pac_anim_timer >= PAC_MOVE_INTERVAL * 2:
            self.pac_anim_timer = 0
            self.pac_anim_state = 1 - self.pac_anim_state
        
        self.pac_last_move = time.time()
        
        # Try next_dir first, then pac_dir
        directions_to_try = [self.next_dir, self.pac_dir]
        
        for dir_idx in directions_to_try:
            dr, dc = DIRS[dir_idx]
            nr = self.pac_row + dr
            nc = self.pac_col + dc
            
            # Horizontal wrap in tunnel (row 14)
            if self.pac_row == 14 and (nc < 0 or nc >= COLS):
                nc = nc % COLS
            
            if 0 <= nr < ROWS and 0 <= nc < COLS:
                cell_char = self.maze[nr][nc]
                if Ghost._walkable(cell_char, for_ghost=False):
                    # Move successful
                    self.pac_row = nr
                    self.pac_col = nc
                    self.pac_dir = dir_idx
                    
                    # Horizontal wrap
                    if self.pac_row == 14 and (self.pac_col < 0 or self.pac_col >= COLS):
                        self.pac_col = self.pac_col % COLS
                    
                    # Check for dots/power pellets
                    if cell_char == '.':
                        self.score += 10
                        self.dots_eaten += 1
                        self.maze[nr][nc] = ' '
                    elif cell_char == 'o':
                        self.score += 50
                        self.dots_eaten += 1
                        self.maze[nr][nc] = ' '
                        self.frightened_mode = True
                        self.frightened_timer = FRIGHTENED_TIME
                        for ghost in self.ghosts:
                            ghost.frightened = True
                            ghost.frightened_timer = FRIGHTENED_TIME
                    
                    return
    
    def _check_collisions(self):
        """Check collisions between Pac-Man and ghosts"""
        for ghost in self.ghosts:
            if ghost.row == self.pac_row and ghost.col == self.pac_col:
                if ghost.eaten:
                    continue
                elif ghost.frightened:
                    # Eat ghost
                    self.score += 200
                    ghost.eaten = True
                    ghost.frightened = False
                else:
                    # Lose life
                    self._lose_life()
    
    def _lose_life(self):
        """Handle Pac-Man death"""
        self.lives -= 1
        if self.lives <= 0:
            self.game_over = True
            self.win = False
            return
        
        # Reset positions
        self.pac_row = 23
        self.pac_col = 14
        self.pac_dir = 0
        self.next_dir = 0
        self.pac_last_move = time.time()
        
        for ghost in self.ghosts:
            ghost.reset(self)
        
        # Brief pause
        self.stdscr.nodelay(False)
        self.stdscr.refresh()
        time.sleep(1)
        self.stdscr.nodelay(True)
    
    def _check_win(self):
        """Check if all dots have been eaten"""
        if self.dots_eaten >= self.total_dots:
            self.game_over = True
            self.win = True
    
    def _draw(self):
        """Draw the entire game screen"""
        self.stdscr.erase()
        sh, sw = self.stdscr.getmaxyx()
        
        # Calculate maze offset to center it
        offset_r = max(0, (sh - ROWS - 3) // 2)
        offset_c = max(0, (sw - COLS) // 2)
        
        # Draw maze
        for r in range(ROWS):
            for c in range(COLS):
                cell_char = self.maze[r][c]
                glyph = ' '
                attr = curses.color_pair(7)
                
                if cell_char == '#':
                    glyph = '█'
                    attr |= curses.A_DIM
                elif cell_char == '.':
                    glyph = '·'
                elif cell_char == 'o':
                    glyph = '●'
                    attr |= curses.A_BOLD
                elif cell_char == 'G':
                    glyph = '─'
                    attr |= curses.A_DIM
                
                try:
                    self.stdscr.addch(offset_r + r, offset_c + c, glyph, attr)
                except curses.error:
                    pass
        
        # Draw Pac-Man
        pac_char = 'O' if self.pac_anim_state == 0 else DIR_CHARS[self.pac_dir]
        try:
            self.stdscr.addch(
                offset_r + self.pac_row,
                offset_c + self.pac_col,
                pac_char,
                curses.color_pair(5) | curses.A_BOLD
            )
        except curses.error:
            pass
        
        # Draw ghosts
        for ghost in self.ghosts:
            if ghost.spawn_delay > 0:
                continue
            
            glyph = '◌' if ghost.eaten else 'ᗣ'
            attr = curses.A_BOLD
            
            if ghost.eaten:
                color = curses.color_pair(7)  # White
            elif ghost.frightened:
                if ghost.frightened_timer <= 2.0 and ghost.blink_state:
                    color = curses.color_pair(7)  # White blinking
                else:
                    color = curses.color_pair(6)  # Blue
            else:
                color = curses.color_pair(ghost.color_pair)  # Normal color
            
            try:
                self.stdscr.addch(
                    offset_r + ghost.row,
                    offset_c + ghost.col,
                    glyph,
                    color | attr
                )
            except curses.error:
                pass
        
        # Draw HUD
        hud_line = f"SCORE: {self.score:06d}   LIVES: {'● ' * self.lives}  LEVEL: {self.level}"
        hud_attr = curses.color_pair(8) | curses.A_BOLD
        
        try:
            # Center HUD
            hud_start = max(0, (sw - len(hud_line)) // 2)
            self.stdscr.addstr(offset_r + ROWS + 1, hud_start, hud_line, hud_attr)
        except curses.error:
            pass
        
        # Draw paused message
        if self.paused:
            msg = "PAUSED - Press P to resume"
            try:
                msg_start = max(0, (sw - len(msg)) // 2)
                self.stdscr.addstr(offset_r + ROWS + 2, msg_start, msg, curses.color_pair(7))
            except curses.error:
                pass
        
        self.stdscr.refresh()
    
    def _addch(self, row, col, ch, attr=0):
        """Safe wrapper for addch"""
        try:
            self.stdscr.addch(row, col, ch, attr)
        except curses.error:
            pass
    
    def _center_msg(self, row, msg, attr=0):
        """Display centered message"""
        sh, sw = self.stdscr.getmaxyx()
        start = max(0, (sw - len(msg)) // 2)
        try:
            self.stdscr.addstr(row, start, msg, attr)
        except curses.error:
            pass
    
    def _show_splash(self):
        """Display splash screen"""
        self.stdscr.erase()
        sh, sw = self.stdscr.getmaxyx()
        
        # Big "PAC-MAN" title
        title_lines = [
            "┏━━━┓ ┏━━━┓ ┏━━━┓",
            "┃ ┏━┛ ┃ ┏━┛ ┃ ┏━┛",
            "┃ ┃   ┃ ┃   ┃ ┃  ",
            "┃ ┃   ┃ ┃   ┃ ┃  ",
            "┗━┛   ┗━┛   ┗━┛  "
        ]
        
        for i, line in enumerate(title_lines):
            self._center_msg(sh // 3 + i, line, curses.color_pair(5) | curses.A_BOLD)
        
        # Ghost info
        ghost_info = "ᗣ Blinky (RED)   ᗣ Pinky (MAGENTA)   ᗣ Inky (CYAN)   ᗣ Clyde (YELLOW)"
        self._center_msg(sh // 3 + 7, ghost_info, curses.color_pair(7))
        
        # Controls
        controls = "CONTROLS: Arrow Keys/WASD to move, P to pause, Q to quit"
        self._center_msg(sh // 3 + 9, controls, curses.color_pair(7))
        
        # Start prompt
        start_msg = "Press any key to start!"
        self._center_msg(sh // 3 + 12, start_msg, curses.color_pair(8) | curses.A_BOLD)
        
        self.stdscr.refresh()
        self.stdscr.nodelay(False)
        self.stdscr.getch()  # Wait for any key
        self.stdscr.nodelay(True)
    
    def _game_over_screen(self):
        """Display game over screen"""
        self.stdscr.erase()
        sh, sw = self.stdscr.getmaxyx()
        
        if self.win:
            msg = "YOU WIN! 🎉"
            color = curses.color_pair(8) | curses.A_BOLD
        else:
            msg = "GAME OVER"
            color = curses.color_pair(1) | curses.A_BOLD
        
        self._center_msg(sh // 2 - 2, msg, color)
        
        score_msg = f"Final Score: {self.score}"
        self._center_msg(sh // 2, score_msg, curses.color_pair(7))
        
        prompt = "Play again? (Y / N)"
        self._center_msg(sh // 2 + 2, prompt, curses.color_pair(7))
        
        self.stdscr.refresh()
        self.stdscr.nodelay(False)
        
        while True:
            key = self.stdscr.getch()
            if key == ord('y') or key == ord('Y'):
                return True
            elif key == ord('n') or key == ord('N'):
                return False
        
    def _play(self):
        """Main game loop"""
        last_time = time.time()
        
        while not self.game_over:
            current_time = time.time()
            dt = current_time - last_time
            last_time = current_time
            
            # Handle input
            self._handle_input()
            
            if self.paused:
                self._draw()
                time.sleep(0.02)
                continue
            
            # Update frightened timer
            if self.frightened_mode:
                self.frightened_timer -= dt
                if self.frightened_timer <= 0:
                    self.frightened_mode = False
                    for ghost in self.ghosts:
                        ghost.frightened = False
            
            # Move Pac-Man
            self._move_pacman()
            
            # Move ghosts
            for ghost in self.ghosts:
                ghost.move(self.maze, self.pac_row, self.pac_col, dt)
            
            # Check collisions
            self._check_collisions()
            
            # Check win condition
            self._check_win()
            
            # Draw
            self._draw()
            
            # Frame rate control
            time.sleep(0.02)
    
    def run(self):
        """Run the full game"""
        self.stdscr.nodelay(True)
        curses.curs_set(0)
        
        # Show splash screen
        self._show_splash()
        
        # Main game loop with restart capability
        while True:
            self._init_game()
            self._play()
            
            # Show game over screen
            restart = self._game_over_screen()
            if not restart:
                break

def main(stdscr):
    """Main function wrapped by curses"""
    game = PacmanGame(stdscr)
    game.run()

if __name__ == "__main__":
    curses.wrapper(main)
```

## How to Run and Play

<details>
<summary><strong>📋 Installation & Running Instructions</strong></summary>

1. Save the code as `pacman.py`
2. Run with: `python3 pacman.py`
3. Make sure your terminal is at least **80×34** characters
4. If you get `ModuleNotFoundError` for curses, ensure you're on Mac/Linux (Windows doesn't have curses in stdlib)

</details>

<details>
<summary><strong>🎮 Game Features</strong></summary>

### Controls:
- **Arrow Keys** or **WASD** → Move Pac-Man
- **P** → Pause/Resume
- **Q** → Quit game

### Scoring:
- **Dot (·)** → +10 points
- **Power pellet (●)** → +50 points + frightens ghosts for 7 seconds
- **Eating frightened ghost** → +200 points

### Ghost Behavior:
- **Blinky (Red)** → Direct chase
- **Pinky (Magenta)** → Targets 2 cells ahead and 2 right of Pac-Man
- **Inky (Cyan)** → Targets 2 cells behind and 2 right of Pac-Man  
- **Clyde (Yellow)** → Targets 2 cells ahead and 2 left of Pac-Man
- **Frightened ghosts** → Move randomly and blink white/blue during last 2 seconds
- **Eaten ghosts** → Return to ghost house as ◌ (white circle)

</details>

<details>
<summary><strong>⚙️ Technical Implementation Details</strong></summary>

### Architecture:
- **Two classes**: `Ghost` (individual ghost logic) and `PacmanGame` (main game controller)
- **Time-based movement**: Uses timestamp comparisons instead of frame counting
- **Buffered input**: `next_dir` allows smooth cornering like original Pac-Man

### Maze Design:
- **31×28 grid** with walls (#), dots (.), power pellets (o), and ghost house door (G)
- **Four power pellets** at corners
- **Horizontal wrap** in tunnel row (row 14)
- **Ghost house** centered with single door

### Rendering:
- **Centered display** dynamically adapts to terminal size
- **Color pairs** defined for all entities
- **Safe drawing** with try/except to avoid curses errors on small terminals
- **Unicode symbols** for Pac-Man (ᐸ, ᐱ, ᐳ, ᐯ) and ghosts (ᗣ)

### Game Flow:
1. Splash screen → Any key to start
2. Main game loop at ~50 FPS
3. Win when all dots eaten
4. Lose when lives exhausted
5. Game over screen with restart option

</details>

The game faithfully recreates classic Pac-Man mechanics including:
- Directional input buffering
- Ghost AI with chase/scatter/frightened modes  
- Power pellet effects
- Score tracking
- Lives system
- Terminal-optimized visuals with proper centering

Enjoy playing!

## Deepseek 网页版 ✅

```python
#!/usr/bin/env python3
"""
Pac-Man for the terminal using Python's curses (stdlib only).
Run with: python3 pacman.py
"""

import curses
import time
import random
from typing import List, Tuple, Optional

# ----------------------------------------------------------------------
# Maze definition (31 rows x 28 cols)
# ----------------------------------------------------------------------
MAZE_TEMPLATE = [
    "############################",  # 0
    "#............##............#",  # 1
    "#.####.#####.##.#####.####.#",  # 2
    "#o####.#####.##.#####.####o#",  # 3
    "#.####.#####.##.#####.####.#",  # 4
    "#..........................#",  # 5
    "#.####.##.########.##.####.#",  # 6
    "#.####.##.########.##.####.#",  # 7
    "#......##....##....##......#",  # 8
    "######.##### ## #####.######",  # 9
    "     #.##### ## #####.#     ",  # 10
    "     #.##          ##.#     ",  # 11
    "     #.## ###  ### ##.#     ",  # 12
    "######.## #      # ##.######",  # 13
    "       .  #      #  .       ",  # 14
    "######.## #      # ##.######",  # 15
    "     #.## ######## ##.#     ",  # 16
    "     #.##          ##.#     ",  # 17
    "     #.##############.#     ",  # 18
    "######.##############.######",  # 19
    "#............##............#",  # 20
    "#.####.#####.##.#####.####.#",  # 21
    "#.####.#####.##.#####.####.#",  # 22
    "#o..##................##..o#",  # 23
    "###.##.##.########.##.##.###",  # 24
    "###.##.##.########.##.##.###",  # 25
    "#......##....##....##......#",  # 26
    "#.##########.##.##########.#",  # 27
    "#.##########.##.##########.#",  # 28
    "#..........................#",  # 29
    "############################",  # 30
]

ROWS = 31
COLS = 28
GHOST_HOUSE_TOP = 12
GHOST_HOUSE_BOTTOM = 17
GHOST_HOUSE_LEFT = 10
GHOST_HOUSE_RIGHT = 17
DOOR_ROW = 14
DOOR_COL = 13

# Directions: (dr, dc)
DIRS = {
    'UP': (-1, 0),
    'DOWN': (1, 0),
    'LEFT': (0, -1),
    'RIGHT': (0, 1),
}
DIR_KEYS = {
    curses.KEY_UP: 'UP', ord('w'): 'UP',
    curses.KEY_DOWN: 'DOWN', ord('s'): 'DOWN',
    curses.KEY_LEFT: 'LEFT', ord('a'): 'LEFT',
    curses.KEY_RIGHT: 'RIGHT', ord('d'): 'RIGHT',
}

# ----------------------------------------------------------------------
# Ghost class
# ----------------------------------------------------------------------
class Ghost:
    def __init__(self, idx: int, start_r: int, start_c: int, color_pair: int,
                 spawn_delay: float, home_r: int, home_c: int):
        self.idx = idx
        self.start_r = start_r
        self.start_c = start_c
        self.color_pair = color_pair
        self.spawn_delay = spawn_delay   # seconds until allowed to move
        self.home_r = home_r
        self.home_c = home_c

        self.row = start_r
        self.col = start_c
        self.dir = 'UP'          # initial direction
        self.state = 'normal'    # normal, frightened, eaten
        self.frightened_timer = 0.0
        self.eaten_timer = 0.0    # unused, but we use state

    def reset(self, start_r: int, start_c: int):
        """Reset ghost to its starting state."""
        self.row = start_r
        self.col = start_c
        self.dir = 'UP'
        self.state = 'normal'
        self.frightened_timer = 0.0

    @staticmethod
    def _walkable(cell: str, for_ghost: bool) -> bool:
        """Check if a cell is walkable."""
        if cell == '#':
            return False
        if not for_ghost and cell == 'G':
            return False   # Pac-Man cannot go through door
        return True

    def move(self, dt: float, pacman_r: int, pacman_c: int,
             maze: List[List[str]], ghosts: List['Ghost']) -> None:
        """Move ghost based on current state and timer."""
        # Spawn delay
        if self.spawn_delay > 0:
            self.spawn_delay -= dt
            return

        # Update frightened timer
        if self.state == 'frightened':
            self.frightened_timer -= dt
            if self.frightened_timer <= 0:
                self.state = 'normal'

        # Determine target cell
        if self.state == 'eaten':
            target_r, target_c = self.home_r, self.home_c
        elif self.state == 'frightened':
            # random walk: pick random target
            target_r = random.randint(0, ROWS-1)
            target_c = random.randint(0, COLS-1)
        else:  # normal chase
            # Offsets based on ghost index (classic behavior simplified)
            offsets = [(0, 0), (2, 2), (-2, 2), (2, -2)]
            dr, dc = offsets[self.idx % 4]
            target_r = pacman_r + dr
            target_c = pacman_c + dc
            # Clamp to maze boundaries
            target_r = max(0, min(ROWS-1, target_r))
            target_c = max(0, min(COLS-1, target_c))

        # Find best direction (avoid reversing)
        reverse_dir = {'UP': 'DOWN', 'DOWN': 'UP', 'LEFT': 'RIGHT', 'RIGHT': 'LEFT'}[self.dir]

        best_dir = None
        best_dist = float('inf')
        dir_list = list(DIRS.keys())
        random.shuffle(dir_list)   # random tie-breaking

        for d in dir_list:
            if d == reverse_dir:
                continue
            dr, dc = DIRS[d]
            nr = self.row + dr
            nc = self.col + dc
            if 0 <= nr < ROWS and 0 <= nc < COLS:
                cell = maze[nr][nc]
                if self._walkable(cell, for_ghost=True):
                    dist = (nr - target_r)**2 + (nc - target_c)**2
                    if dist < best_dist:
                        best_dist = dist
                        best_dir = d

        if best_dir is None:
            # stuck – try reverse as last resort
            dr, dc = DIRS[reverse_dir]
            nr = self.row + dr
            nc = self.col + dc
            if 0 <= nr < ROWS and 0 <= nc < COLS and self._walkable(maze[nr][nc], for_ghost=True):
                best_dir = reverse_dir

        if best_dir:
            self.dir = best_dir
            dr, dc = DIRS[self.dir]
            self.row += dr
            self.col += dc
            # Horizontal wrap for ghosts too
            if self.col < 0:
                self.col = COLS - 1
            elif self.col >= COLS:
                self.col = 0


# ----------------------------------------------------------------------
# Main game class
# ----------------------------------------------------------------------
class PacmanGame:
    def __init__(self, stdscr):
        self.stdscr = stdscr
        self.sh = curses.LINES
        self.sw = curses.COLS

        self._setup_colors()

        # Maze data
        self.maze = None
        self.dots_total = 0
        self.dots_eaten = 0
        self.score = 0
        self.lives = 3
        self.level = 1

        # Pac-Man
        self.pac_r = 23
        self.pac_c = 14
        self.pac_dir = 'LEFT'
        self.next_dir = 'LEFT'
        self.last_pac_move = 0.0
        self.pac_move_interval = 0.14

        # Ghosts
        self.ghosts = []
        self.last_ghost_move = 0.0
        self.ghost_move_interval = 0.22

        # Global frightened mode
        self.frightened_until = 0.0   # timestamp when frightened ends
        self.frightened_duration = 7.0
        self.blink_timer = 0.0
        self.blink_interval = 0.35

        # Game state
        self.paused = False
        self.running = True

    def _setup_colors(self):
        """Initialize curses color pairs."""
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_RED, -1)
        curses.init_pair(2, curses.COLOR_MAGENTA, -1)
        curses.init_pair(3, curses.COLOR_CYAN, -1)
        curses.init_pair(4, curses.COLOR_YELLOW, -1)
        curses.init_pair(5, curses.COLOR_YELLOW, -1)   # Pac-Man
        curses.init_pair(6, curses.COLOR_BLUE, -1)     # frightened
        curses.init_pair(7, curses.COLOR_WHITE, -1)    # maze / dots
        curses.init_pair(8, curses.COLOR_GREEN, -1)    # score text

    def _init_game(self):
        """Reset game state for a new game."""
        # Build maze from template and add ghost house
        self.maze = [list(row) for row in MAZE_TEMPLATE]
        # Carve ghost house interior
        for r in range(GHOST_HOUSE_TOP, GHOST_HOUSE_BOTTOM+1):
            for c in range(GHOST_HOUSE_LEFT, GHOST_HOUSE_RIGHT+1):
                self.maze[r][c] = ' '
        # Place door
        self.maze[DOOR_ROW][DOOR_COL] = 'G'

        # Count dots ('.' and 'o')
        self.dots_total = sum(row.count('.') + row.count('o') for row in self.maze)
        self.dots_eaten = 0
        self.score = 0
        self.lives = 3
        self.level = 1
        self.paused = False

        # Pac-Man start
        self.pac_r = 23
        self.pac_c = 14
        self.pac_dir = 'LEFT'
        self.next_dir = 'LEFT'
        self.last_pac_move = time.time()

        # Ghosts
        # Start positions inside ghost house
        start_positions = [(13, 12), (13, 14), (14, 12), (14, 14)]
        colors = [1, 2, 3, 4]   # red, magenta, cyan, yellow
        spawn_delays = [0.0, 2.0, 4.0, 6.0]
        home_cell = (DOOR_ROW, DOOR_COL)   # ghosts return to door area
        self.ghosts = []
        for i, (sr, sc) in enumerate(start_positions):
            g = Ghost(i, sr, sc, colors[i], spawn_delays[i], home_cell[0], home_cell[1])
            self.ghosts.append(g)

        self.frightened_until = 0.0
        self.blink_timer = 0.0

        self.last_ghost_move = time.time()
        self.running = True

    def _is_walkable(self, row: int, col: int, for_ghost: bool = False) -> bool:
        """Check if a cell is walkable."""
        if not (0 <= row < ROWS and 0 <= col < COLS):
            return False
        cell = self.maze[row][col]
        if cell == '#':
            return False
        if not for_ghost and cell == 'G':
            return False
        return True

    def _move_pacman(self):
        """Try to move Pac-Man in the buffered direction."""
        now = time.time()
        if now - self.last_pac_move < self.pac_move_interval:
            return
        self.last_pac_move = now

        # Try next_dir first
        dr, dc = DIRS[self.next_dir]
        nr = self.pac_r + dr
        nc = self.pac_c + dc
        if self._is_walkable(nr, nc, for_ghost=False):
            self.pac_dir = self.next_dir
            self.pac_r = nr
            self.pac_c = nc
        else:
            # Try current direction
            dr, dc = DIRS[self.pac_dir]
            nr = self.pac_r + dr
            nc = self.pac_c + dc
            if self._is_walkable(nr, nc, for_ghost=False):
                self.pac_r = nr
                self.pac_c = nc

        # Horizontal tunnel wrap
        if self.pac_c < 0:
            self.pac_c = COLS - 1
        elif self.pac_c >= COLS:
            self.pac_c = 0

        # Eat dot / power pellet
        cell = self.maze[self.pac_r][self.pac_c]
        if cell == '.':
            self.maze[self.pac_r][self.pac_c] = ' '
            self.score += 10
            self.dots_eaten += 1
        elif cell == 'o':
            self.maze[self.pac_r][self.pac_c] = ' '
            self.score += 50
            self.dots_eaten += 1
            self.frightened_until = time.time() + self.frightened_duration
            # Set all ghosts to frightened (unless eaten)
            for g in self.ghosts:
                if g.state != 'eaten':
                    g.state = 'frightened'
                    g.frightened_timer = self.frightened_duration

    def _move_ghosts(self):
        """Move all ghosts."""
        now = time.time()
        if now - self.last_ghost_move < self.ghost_move_interval:
            return
        self.last_ghost_move = now

        dt = self.ghost_move_interval   # approx dt per move
        for g in self.ghosts:
            # Update frightened state based on global timer
            if g.state == 'frightened' and now > self.frightened_until:
                g.state = 'normal'
                g.frightened_timer = 0.0

            # Move ghost
            g.move(dt, self.pac_r, self.pac_c, self.maze, self.ghosts)

            # If eaten ghost reaches home, reset it
            if g.state == 'eaten' and (g.row, g.col) == (g.home_r, g.home_c):
                g.state = 'normal'
                # Place at start position (will be moved out after spawn delay)
                g.row, g.col = g.start_r, g.start_c
                g.spawn_delay = 0.0   # immediate? we set 0 to re-enter play

    def _check_collisions(self):
        """Handle Pac-Man and ghost collisions."""
        for g in self.ghosts:
            if (g.row, g.col) == (self.pac_r, self.pac_c):
                if g.state == 'frightened':
                    # Eat ghost
                    g.state = 'eaten'
                    g.row, g.col = g.start_r, g.start_c   # teleport to start? but better to move home
                    # Actually we want ghost to go home, so set eaten state and target home
                    # Already in move() it will target home, but we need to ensure it moves.
                    # For simplicity, we reset its position and set state eaten.
                    self.score += 200
                elif g.state != 'eaten':
                    # Normal collision
                    self._lose_life()
                    return   # only one collision per frame

    def _lose_life(self):
        """Handle losing a life."""
        self.lives -= 1
        if self.lives <= 0:
            self.running = False
            return

        # Reset positions
        self.pac_r = 23
        self.pac_c = 14
        self.pac_dir = 'LEFT'
        self.next_dir = 'LEFT'
        for g in self.ghosts:
            g.reset(g.start_r, g.start_c)
            g.spawn_delay = [0.0, 2.0, 4.0, 6.0][g.idx]   # reset delays
        self.frightened_until = 0.0
        time.sleep(1)

    def _check_win(self):
        """Check if all dots are eaten."""
        if self.dots_eaten >= self.dots_total:
            self.running = False

    def _handle_input(self, key):
        """Process keyboard input."""
        if key == ord('p') or key == ord('P'):
            self.paused = not self.paused
        elif key == ord('q') or key == ord('Q'):
            self.running = False
        elif key in DIR_KEYS:
            self.next_dir = DIR_KEYS[key]

    def _draw(self):
        """Render the entire game screen."""
        self.stdscr.clear()

        # Center the maze
        offset_r = max(0, (self.sh - ROWS - 3) // 2)
        offset_c = max(0, (self.sw - COLS) // 2)

        # Draw maze
        for r in range(ROWS):
            for c in range(COLS):
                ch = self.maze[r][c]
                if ch == '#':
                    glyph = '█'
                    color = curses.color_pair(7) | curses.A_DIM
                elif ch == '.':
                    glyph = '·'
                    color = curses.color_pair(7)
                elif ch == 'o':
                    glyph = '●'
                    color = curses.color_pair(7) | curses.A_BOLD
                elif ch == 'G':
                    glyph = '─'
                    color = curses.color_pair(7) | curses.A_DIM
                else:
                    glyph = ' '
                    color = curses.color_pair(7)

                try:
                    self.stdscr.addstr(offset_r + r, offset_c + c, glyph, color)
                except curses.error:
                    pass

        # Draw ghosts
        blink = False
        if self.frightened_until > 0:
            now = time.time()
            if now - self.blink_timer >= self.blink_interval:
                self.blink_timer = now
            # blink during last 2 seconds
            if self.frightened_until - now < 2.0:
                blink = (int((now - self.blink_timer) / self.blink_interval) % 2) == 0

        for g in self.ghosts:
            if g.state == 'eaten':
                glyph = '◌'
                color = curses.color_pair(7) | curses.A_BOLD
            elif g.state == 'frightened':
                if blink:
                    color = curses.color_pair(7) | curses.A_BOLD   # white
                else:
                    color = curses.color_pair(6) | curses.A_BOLD   # blue
                glyph = 'ᗣ'
            else:
                glyph = 'ᗣ'
                color = curses.color_pair(g.color_pair) | curses.A_BOLD

            try:
                self.stdscr.addstr(offset_r + g.row, offset_c + g.col, glyph, color)
            except curses.error:
                pass

        # Draw Pac-Man with mouth animation
        now = time.time()
        mouth_open = (int(now / self.blink_interval) % 2) == 0
        if mouth_open:
            if self.pac_dir == 'UP':
                pac_glyph = 'ᐱ'
            elif self.pac_dir == 'DOWN':
                pac_glyph = 'ᐯ'
            elif self.pac_dir == 'LEFT':
                pac_glyph = 'ᐸ'
            else:  # RIGHT
                pac_glyph = 'ᐳ'
        else:
            pac_glyph = 'O'

        try:
            self.stdscr.addstr(offset_r + self.pac_r, offset_c + self.pac_c, pac_glyph,
                               curses.color_pair(5) | curses.A_BOLD)
        except curses.error:
            pass

        # HUD
        hud_str = f"SCORE: {self.score:06d}   LIVES: {'● ' * self.lives}   LEVEL: {self.level}"
        try:
            self.stdscr.addstr(offset_r + ROWS + 1, offset_c, hud_str,
                               curses.color_pair(8) | curses.A_BOLD)
        except curses.error:
            pass

        if self.paused:
            try:
                pause_msg = "PAUSED"
                x = offset_c + (COLS - len(pause_msg)) // 2
                self.stdscr.addstr(offset_r + ROWS // 2, x, pause_msg,
                                   curses.color_pair(7) | curses.A_BOLD)
            except curses.error:
                pass

        self.stdscr.refresh()

    def _center_msg(self, lines: List[Tuple[str, int]]):
        """Display a centered message (list of (text, color_pair))."""
        self.stdscr.clear()
        h, w = self.stdscr.getmaxyx()
        start_y = max(0, (h - len(lines)) // 2)
        for i, (text, color) in enumerate(lines):
            x = max(0, (w - len(text)) // 2)
            try:
                self.stdscr.addstr(start_y + i, x, text, color)
            except curses.error:
                pass
        self.stdscr.refresh()

    def _show_splash(self):
        """Display splash screen."""
        lines = [
            ("██████╗  █████╗  ██████╗      ███╗   ███╗ █████╗ ███╗   ██╗", curses.color_pair(5) | curses.A_BOLD),
            ("██╔══██╗██╔══██╗██╔════╝      ████╗ ████║██╔══██╗████╗  ██║", curses.color_pair(5) | curses.A_BOLD),
            ("██████╔╝███████║██║           ██╔████╔██║███████║██╔██╗ ██║", curses.color_pair(5) | curses.A_BOLD),
            ("██╔═══╝ ██╔══██║██║           ██║╚██╔╝██║██╔══██║██║╚██╗██║", curses.color_pair(5) | curses.A_BOLD),
            ("██║     ██║  ██║╚██████╗      ██║ ╚═╝ ██║██║  ██║██║ ╚████║", curses.color_pair(5) | curses.A_BOLD),
            ("╚═╝     ╚═╝  ╚═╝ ╚═════╝      ╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝", curses.color_pair(5) | curses.A_BOLD),
            ("", 0),
            ("ᗣ  ᗣ  ᗣ  ᗣ", curses.color_pair(1) | curses.A_BOLD),
            ("Blinky  Pinky  Inky  Clyde", curses.color_pair(7)),
            ("", 0),
            ("Controls: Arrow keys / WASD", curses.color_pair(7)),
            ("P: pause     Q: quit", curses.color_pair(7)),
            ("", 0),
            ("Press any key to start!", curses.color_pair(7) | curses.A_BOLD),
        ]
        self._center_msg([(text, color) for text, color in lines])
        self.stdscr.nodelay(False)
        self.stdscr.getch()
        self.stdscr.nodelay(True)

    def _game_over_screen(self, won: bool):
        """Display game over / win screen."""
        if won:
            lines = [
                ("YOU WIN! 🎉", curses.color_pair(8) | curses.A_BOLD),
                (f"Final score: {self.score}", curses.color_pair(7)),
                ("", 0),
                ("Play again? (Y/N)", curses.color_pair(7)),
            ]
        else:
            lines = [
                ("GAME OVER", curses.color_pair(1) | curses.A_BOLD),
                (f"Final score: {self.score}", curses.color_pair(7)),
                ("", 0),
                ("Play again? (Y/N)", curses.color_pair(7)),
            ]
        self._center_msg([(text, color) for text, color in lines])
        while True:
            key = self.stdscr.getch()
            if key in (ord('y'), ord('Y')):
                return True
            if key in (ord('n'), ord('N')):
                return False

    def _play(self):
        """Main game loop."""
        self._init_game()
        self.last_pac_move = time.time()
        self.last_ghost_move = time.time()
        self.blink_timer = time.time()

        self.stdscr.nodelay(True)
        self.stdscr.timeout(0)   # non-blocking getch

        while self.running:
            now = time.time()

            # Handle input
            key = self.stdscr.getch()
            if key != -1:
                self._handle_input(key)

            if not self.paused:
                self._move_pacman()
                self._move_ghosts()
                self._check_collisions()
                self._check_win()

            self._draw()
            time.sleep(0.02)   # ~50 fps

        # Determine win/loss
        won = (self.dots_eaten >= self.dots_total)
        return won

    def run(self):
        """Main entry point."""
        self._show_splash()
        while True:
            won = self._play()
            if not self._game_over_screen(won):
                break
        self.stdscr.clear()


# ----------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------
def main(stdscr):
    game = PacmanGame(stdscr)
    game.run()


if __name__ == "__main__":
    curses.wrapper(main)
```
