← Back to Blog Apr 23, 2026 · 10:00 AM · 7 min read

Pac-Man Ghost Chase AI: Make Ghosts Hunt the Player

In Part 1 we built the game, and in Part 2 we added score tracking. But our ghosts still wander randomly — not very scary. In this tutorial, we will give each ghost a unique chase personality, just like the original Pac-Man.

How the Original Ghosts Work

In the classic Pac-Man, each ghost has a different targeting strategy:

We will implement simplified versions of these behaviors that work well in our tile-based game.

Step 1: Define Ghost Objects

Expand the ghost objects to include a name, color, and AI type. Each ghost gets a target property that the AI updates every frame.

let ghosts = [
  { x: 9, y: 7, dir: 0, color: '#FF0000', name: 'blinky',
    ai: 'chase',   target: {x:0, y:0} },
  { x: 10, y: 7, dir: 2, color: '#FFB8FF', name: 'pinky',
    ai: 'ambush',  target: {x:0, y:0} },
  { x: 9, y: 8, dir: 0, color: '#00FFFF', name: 'inky',
    ai: 'flank',   target: {x:0, y:0} },
  { x: 10, y: 8, dir: 1, color: '#FFB852', name: 'clyde',
    ai: 'random',  target: {x:0, y:0} },
];

Step 2: Calculate Target Tiles

Each ghost calculates its target tile differently. This is the core of the AI — the ghost then moves toward its target using a simple distance check.

function updateGhostTarget(ghost) {
  const dirs = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];

  if (ghost.ai === 'chase') {
    // Blinky: go straight for Pac-Man
    ghost.target.x = pacman.x;
    ghost.target.y = pacman.y;

  } else if (ghost.ai === 'ambush') {
    // Pinky: target 4 tiles ahead of Pac-Man
    const d = dirs[pacman.dir];
    ghost.target.x = pacman.x + d.x * 4;
    ghost.target.y = pacman.y + d.y * 4;

  } else if (ghost.ai === 'flank') {
    // Inky: mirror Blinky's position through a point
    // 2 tiles ahead of Pac-Man
    const d = dirs[pacman.dir];
    const ax = pacman.x + d.x * 2;
    const ay = pacman.y + d.y * 2;
    const blinky = ghosts[0];
    ghost.target.x = ax + (ax - blinky.x);
    ghost.target.y = ay + (ay - blinky.y);

  } else if (ghost.ai === 'random') {
    // Clyde: chase when far, scatter when close
    const dist = Math.abs(ghost.x - pacman.x)
               + Math.abs(ghost.y - pacman.y);
    if (dist > 8) {
      ghost.target.x = pacman.x;
      ghost.target.y = pacman.y;
    } else {
      // Run to bottom-left corner
      ghost.target.x = 0;
      ghost.target.y = map.length - 1;
    }
  }
}
Tip: Manhattan distance (|dx| + |dy|) works great for tile-based games. It is fast to calculate and gives natural-looking movement on a grid.

Step 3: Move Toward the Target

Instead of picking a random direction, each ghost now picks the direction that brings it closest to its target tile. The ghost cannot reverse direction (just like the original game).

function moveGhost(ghost) {
  updateGhostTarget(ghost);

  const dirs = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
  const reverse = (ghost.dir + 2) % 4;

  let bestDir = ghost.dir;
  let bestDist = Infinity;

  for (let d = 0; d < 4; d++) {
    if (d === reverse) continue; // no reversing

    const nx = ghost.x + dirs[d].x;
    const ny = ghost.y + dirs[d].y;

    // Check if tile is walkable
    if (!map[ny] || map[ny][nx] === 1) continue;

    // Manhattan distance to target
    const dist = Math.abs(nx - ghost.target.x)
               + Math.abs(ny - ghost.target.y);

    if (dist < bestDist) {
      bestDist = dist;
      bestDir = d;
    }
  }

  const nx = ghost.x + dirs[bestDir].x;
  const ny = ghost.y + dirs[bestDir].y;

  if (map[ny] && map[ny][nx] !== 1) {
    ghost.x = nx;
    ghost.y = ny;
    ghost.dir = bestDir;
  }
}

Step 4: Add Scatter Mode

In the original Pac-Man, ghosts alternate between chase and scatter mode. During scatter, each ghost retreats to its assigned corner. This gives the player breathing room and makes the game feel fair.

let ghostMode = 'chase';
let modeTimer = 0;
const CHASE_DURATION = 20000;   // 20 seconds
const SCATTER_DURATION = 7000;  // 7 seconds

// Corner targets for scatter mode
const scatterTargets = {
  blinky: { x: 19, y: 0 },   // top-right
  pinky:  { x: 0,  y: 0 },   // top-left
  inky:   { x: 19, y: 19 },  // bottom-right
  clyde:  { x: 0,  y: 19 },  // bottom-left
};

function updateGhostMode() {
  const now = Date.now();
  if (modeTimer === 0) modeTimer = now;

  const elapsed = now - modeTimer;

  if (ghostMode === 'chase' && elapsed > CHASE_DURATION) {
    ghostMode = 'scatter';
    modeTimer = now;
  } else if (ghostMode === 'scatter' && elapsed > SCATTER_DURATION) {
    ghostMode = 'chase';
    modeTimer = now;
  }
}

// Modify updateGhostTarget to respect scatter mode:
function updateGhostTarget(ghost) {
  if (ghostMode === 'scatter') {
    const corner = scatterTargets[ghost.name];
    ghost.target.x = corner.x;
    ghost.target.y = corner.y;
    return;
  }

  // ... chase logic from Step 2
}
Tip: The scatter/chase cycle is what makes Pac-Man feel dynamic. Without scatter mode, the game feels relentless. Without chase mode, it feels too easy. The balance between the two is key.

Step 5: Detect Ghost Collision

Check if any ghost occupies the same tile as Pac-Man. If so, the player loses a life or the game ends.

function checkGhostCollision() {
  for (const ghost of ghosts) {
    if (ghost.x === pacman.x && ghost.y === pacman.y) {
      onPacmanCaught();
      return;
    }
  }
}

function onPacmanCaught() {
  lives--;
  if (lives <= 0) {
    gameOver = true;
  } else {
    // Reset positions, keep score
    pacman.x = 1;
    pacman.y = 1;
    pacman.dir = 0;
    ghosts[0].x = 9;  ghosts[0].y = 7;
    ghosts[1].x = 10; ghosts[1].y = 7;
    ghosts[2].x = 9;  ghosts[2].y = 8;
    ghosts[3].x = 10; ghosts[3].y = 8;
  }
}

// Call checkGhostCollision() in gameLoop after movement

Step 6: Draw Ghost with Eyes

Make the ghosts look proper with a body and eyes that point in their movement direction. This gives visual feedback about where each ghost is heading.

function drawGhost(ghost) {
  const cx = ghost.x * TILE_SIZE + TILE_SIZE / 2;
  const cy = ghost.y * TILE_SIZE + TILE_SIZE / 2;
  const r = TILE_SIZE / 2 - 2;

  // Body
  ctx.fillStyle = ghost.color;
  ctx.beginPath();
  ctx.arc(cx, cy - 2, r, Math.PI, 0);
  ctx.lineTo(cx + r, cy + r);
  // Wavy bottom
  for (let i = 0; i < 3; i++) {
    const w = (r * 2) / 3;
    const bx = cx + r - w * i;
    ctx.quadraticCurveTo(bx - w/4, cy + r - 4,
                         bx - w/2, cy + r);
  }
  ctx.fill();

  // Eyes
  const dirs = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
  const d = dirs[ghost.dir];
  const eyeOff = 3;

  for (const side of [-1, 1]) {
    const ex = cx + side * 3;
    const ey = cy - 3;
    // White
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(ex, ey, 3, 0, Math.PI * 2);
    ctx.fill();
    // Pupil (shifts toward direction)
    ctx.fillStyle = '#00f';
    ctx.beginPath();
    ctx.arc(ex + d.x * 1.5, ey + d.y * 1.5,
            1.5, 0, Math.PI * 2);
    ctx.fill();
  }
}

Step 7: Update the Game Loop

Put it all together by adding the mode update and collision check to the game loop.

function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#000';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  drawMap();

  if (!gameOver) {
    updateGhostMode();
    movePacman();
    ghosts.forEach(g => moveGhost(g));
    checkGhostCollision();
    checkWin();
  }

  drawPacman();
  ghosts.forEach(g => drawGhost(g));
  drawScore();
  drawGameOver();
}

setInterval(gameLoop, 150);

What's Next?

Your ghosts now have personality and the game feels like a real chase. In the next parts we can explore:

Continue to Part 4: Power Pellet Mode where ghosts turn blue and become edible for bonus points.

More Articles

💊
Tutorial Pac-Man Power Pellet Mode: Ghosts Turn Blue Tutorial · Apr 23, 2026 · 11:00 AM
📈
Tutorial Pac-Man Levels: Speed Up Ghosts Each Level Tutorial · Apr 23, 2026 · 12:00 PM
🔊
Tutorial Pac-Man Sound Effects: Waka-Waka and Beyond Tutorial · Apr 23, 2026 · 01:00 PM
← Back to Blog