Students add 4 ghosts with random-walk AI, scared mode when a power pellet is eaten, and game-over collision detection.
// ════════════════════════════════════════════════════════════
// 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; }
}
}