Slides/Reference Code
Day 1Day 2Day 3Day 4
Instructor Reference · Day 3

Day 3 — Ghosts

Students add 4 ghosts with random-walk AI, scared mode when a power pellet is eaten, and game-over collision detection.

New today:Array of objects: ghosts[]for...of loopRandom direction AI with wall avoidanceTimer with frameCount: scaredTimer countdownGame state flag: gameOver
sketch.js▶ Run Output
// ════════════════════════════════════════════════════════════
//  DAY 3 — Ghosts
//  Adds: 4 ghosts, random-walk AI, scared mode, game over
// ════════════════════════════════════════════════════════════

const TILE_SIZE = 20;
const COLS = 21, ROWS = 21;
const PATH = 0, WALL = 1, PELLET = 2, POWER = 3;

let maze = [
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
  [1,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,1],
  [1,2,1,1,2,1,1,1,1,2,1,2,1,1,1,1,2,1,1,2,1],
  [1,3,1,1,2,1,1,1,1,2,1,2,1,1,1,1,2,1,1,3,1],
  [1,2,1,1,2,2,2,1,1,2,2,2,1,1,2,2,2,1,1,2,1],
  [1,2,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,1,2,1],
  [1,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,1],
  [1,1,1,1,2,1,1,1,0,0,1,0,0,1,1,1,2,1,1,1,1],
  [1,1,1,1,2,1,0,0,0,0,1,0,0,0,0,1,2,1,1,1,1],
  [1,1,1,1,2,1,0,1,1,0,0,0,1,1,0,1,2,1,1,1,1],
  [1,2,2,2,2,0,0,1,0,0,0,0,0,1,0,0,2,2,2,2,1],
  [1,1,1,1,2,1,0,1,1,1,1,1,1,1,0,1,2,1,1,1,1],
  [1,1,1,1,2,1,0,0,0,0,1,0,0,0,0,1,2,1,1,1,1],
  [1,1,1,1,2,1,1,1,0,0,1,0,0,1,1,1,2,1,1,1,1],
  [1,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,1],
  [1,2,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,1,2,1],
  [1,2,1,1,2,2,2,1,1,2,2,2,1,1,2,2,2,1,1,2,1],
  [1,3,1,1,2,1,1,1,1,2,1,2,1,1,1,1,2,1,1,3,1],
  [1,2,1,1,2,1,1,1,1,2,1,2,1,1,1,1,2,1,1,2,1],
  [1,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,1],
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];

let pacRow = 16, pacCol = 10, pacDir = 0;
let mouthAngle = 0, mouthOpen = true;
let score = 0;

// ── GHOST SPAWN POSITIONS ─────────────────────────────────────
// Change row and col to wherever YOU want each ghost to start.
// Must be a PATH tile (0 in your maze — not a wall).
let ghostSpawns = [
  { row: 9,  col: 9  },   // Ghost 1 — red (Blinky)
  { row: 9,  col: 11 },   // Ghost 2 — pink (Pinky)
  { row: 11, col: 9  },   // Ghost 3 — cyan (Inky)
  { row: 11, col: 11 },   // Ghost 4 — orange (Clyde)
];

// NEW: ghost objects (positions come from ghostSpawns above)
let ghosts = [
  { row: ghostSpawns[0].row, col: ghostSpawns[0].col, color: [255, 0, 0],     dr: 0,  dc: 1  },
  { row: ghostSpawns[1].row, col: ghostSpawns[1].col, color: [255, 184, 255],  dr: 0,  dc: -1 },
  { row: ghostSpawns[2].row, col: ghostSpawns[2].col, color: [0, 255, 255],    dr: -1, dc: 0  },
  { row: ghostSpawns[3].row, col: ghostSpawns[3].col, color: [255, 184, 81],   dr: 1,  dc: 0  },
];

let scaredTimer = 0;
const SCARED_TIME = 300;
let ghostMoveTimer = 0;
const GHOST_SPEED  = 15;
let gameOver = false;

function setup() {
  createCanvas(COLS * TILE_SIZE, ROWS * TILE_SIZE);
  frameRate(60); noSmooth();
}

function draw() {
  background(0);
  drawMaze();
  if (!gameOver) {
    ghostMoveTimer++;
    if (ghostMoveTimer >= GHOST_SPEED) { moveGhosts(); ghostMoveTimer = 0; }
    if (scaredTimer > 0) scaredTimer--;
    checkGhostCollision();
  }
  drawGhosts();
  animateMouth(); drawPacman();
  fill(255); noStroke(); textSize(12); textAlign(LEFT, TOP);
  text('SCORE: ' + score, 6, 4);
  if (gameOver) {
    fill(0, 0, 0, 160); rect(0, 0, width, height);
    fill(255, 0, 0); textSize(28); textAlign(CENTER, CENTER);
    text('GAME OVER', width/2, height/2);
    fill(255); textSize(14);
    text('Score: ' + score, width/2, height/2 + 28);
  }
}

function drawMaze() {
  for (let row = 0; row < ROWS; row++) {
    for (let col = 0; col < COLS; col++) {
      let cell = maze[row][col], x = col*TILE_SIZE, y = row*TILE_SIZE;
      if (cell===WALL)   { fill(0,0,180); noStroke(); rect(x,y,TILE_SIZE,TILE_SIZE); }
      else if (cell===PELLET) { fill(0); rect(x,y,TILE_SIZE,TILE_SIZE); fill(255,255,200); noStroke(); circle(x+TILE_SIZE/2,y+TILE_SIZE/2,4); }
      else if (cell===POWER)  { fill(0); rect(x,y,TILE_SIZE,TILE_SIZE); fill(255,180,0); noStroke(); circle(x+TILE_SIZE/2,y+TILE_SIZE/2,12); }
      else { fill(0); rect(x,y,TILE_SIZE,TILE_SIZE); }
    }
  }
}

// NEW: draw ghost body + eyes
function drawGhosts() {
  for (let g of ghosts) {
    let x = g.col*TILE_SIZE, y = g.row*TILE_SIZE;
    let cx = x+TILE_SIZE/2, cy = y+TILE_SIZE/2;
    let col = g.color;
    if (scaredTimer > 0)
      col = (scaredTimer < 60 && frameCount%20 < 10) ? [255,255,255] : [0,0,200];
    noStroke(); fill(col[0], col[1], col[2]);
    arc(cx, cy, TILE_SIZE-2, TILE_SIZE-2, PI, 0, CHORD);
    rect(x+1, cy, TILE_SIZE-2, TILE_SIZE/2-1);
    let w = (TILE_SIZE-2)/3;
    for (let i = 0; i < 3; i++)
      arc(x+1+i*w+w/2, y+TILE_SIZE-2, w, w*1.2, 0, PI, PIE);
    if (scaredTimer <= 0) {
      fill(255); ellipse(cx-3,cy-1,5,6); ellipse(cx+3,cy-1,5,6);
      fill(0,0,180); ellipse(cx-2,cy-1,2,3); ellipse(cx+4,cy-1,2,3);
    }
  }
}

// NEW: random-walk AI
function moveGhosts() {
  const dirs = [{dr:-1,dc:0},{dr:1,dc:0},{dr:0,dc:-1},{dr:0,dc:1}];
  for (let g of ghosts) {
    if (maze[g.row+g.dr][g.col+g.dc] === WALL) {
      let s = [...dirs].sort(() => random(-1,1));
      for (let d of s) {
        if (maze[g.row+d.dr][g.col+d.dc] !== WALL) { g.dr=d.dr; g.dc=d.dc; break; }
      }
    }
    let nr=g.row+g.dr, nc=g.col+g.dc;
    if (maze[nr][nc] !== WALL) { g.row=nr; g.col=nc; }
  }
}

// NEW: ghost hits Pacman → game over (or eat ghost if scared)
function checkGhostCollision() {
  for (let i = 0; i < ghosts.length; i++) {
    let g = ghosts[i];
    if (g.row===pacRow && g.col===pacCol) {
      if (scaredTimer > 0) {
        // Send eaten ghost back to its spawn point
        g.row = ghostSpawns[i].row; g.col = ghostSpawns[i].col; score += 200;
      } else { gameOver = true; }
    }
  }
}

function animateMouth() {
  if (mouthOpen) { mouthAngle+=4; if (mouthAngle>=40) mouthOpen=false; }
  else           { mouthAngle-=4; if (mouthAngle<=0)  mouthOpen=true;  }
}

function drawPacman() {
  let x=pacCol*TILE_SIZE+TILE_SIZE/2, y=pacRow*TILE_SIZE+TILE_SIZE/2;
  fill(255,220,0); noStroke();
  arc(x,y,TILE_SIZE-2,TILE_SIZE-2,radians(pacDir+mouthAngle),radians(pacDir+360-mouthAngle),PIE);
}

function keyPressed() {
  if (key==='r'||key==='R') { window.location.reload(); return; }
  if (gameOver) return;
  let nr=pacRow, nc=pacCol;
  if (keyCode===UP_ARROW)    { nr-=1; pacDir=270; }
  if (keyCode===DOWN_ARROW)  { nr+=1; pacDir=90;  }
  if (keyCode===LEFT_ARROW)  { nc-=1; pacDir=180; }
  if (keyCode===RIGHT_ARROW) { nc+=1; pacDir=0;   }
  if (maze[nr][nc] !== WALL) {
    pacRow=nr; pacCol=nc;
    if (maze[pacRow][pacCol]===PELLET) { maze[pacRow][pacCol]=PATH; score+=10; }
    else if (maze[pacRow][pacCol]===POWER) { maze[pacRow][pacCol]=PATH; score+=50; scaredTimer=SCARED_TIME; }
  }
}
← Back to SlidesRun This ▶