In Part 3 we gave ghosts unique chase personalities. Now we add the most iconic Pac-Man mechanic — power pellets that turn the tables. When Pac-Man eats a power pellet, all ghosts turn blue, reverse direction, and become edible for bonus points.
Track whether power mode is active, how long it lasts, and a combo counter for escalating ghost eat bonuses.
let frightened = false;
let frightenedTimer = 0;
const FRIGHTENED_DURATION = 8000; // 8 seconds
const FLASH_START = 6000; // start flashing at 6s
let ghostEatCombo = 0;
const GHOST_SCORES = [200, 400, 800, 1600];
When Pac-Man eats a power pellet, activate frightened mode and reverse all ghost directions.
function activatePowerMode() {
frightened = true;
frightenedTimer = Date.now();
ghostEatCombo = 0;
// Reverse all ghost directions
ghosts.forEach(g => {
if (!g.eaten) {
g.dir = (g.dir + 2) % 4;
}
});
}
// In movePacman(), when eating a power pellet:
if (tile === 3) {
score += SCORE_PELLET;
dotsLeft--;
map[ny][nx] = 0;
activatePowerMode();
}
Frightened ghosts move randomly instead of chasing. They also move slower — we skip every other frame for them.
let frameCount = 0;
function moveGhost(ghost) {
if (ghost.eaten) {
moveToGhostHouse(ghost);
return;
}
// Frightened ghosts move slower (skip frames)
if (frightened && frameCount % 2 !== 0) return;
if (frightened) {
moveGhostRandom(ghost);
} else {
updateGhostTarget(ghost);
moveGhostToTarget(ghost);
}
}
function moveGhostRandom(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;
const valid = [];
for (let d = 0; d < 4; d++) {
if (d === reverse) continue;
const nx = ghost.x + dirs[d].x;
const ny = ghost.y + dirs[d].y;
if (map[ny] && map[ny][nx] !== 1) valid.push(d);
}
if (valid.length === 0) valid.push(reverse);
const pick = valid[Math.floor(Math.random() * valid.length)];
ghost.x += dirs[pick].x;
ghost.y += dirs[pick].y;
ghost.dir = pick;
}
When Pac-Man collides with a frightened ghost, award bonus points and send the ghost back to the ghost house.
function checkGhostCollision() {
for (const ghost of ghosts) {
if (ghost.eaten) continue;
if (ghost.x !== pacman.x || ghost.y !== pacman.y) continue;
if (frightened) {
// Eat the ghost
const bonus = GHOST_SCORES[
Math.min(ghostEatCombo, GHOST_SCORES.length - 1)
];
score += bonus;
ghostEatCombo++;
ghost.eaten = true;
// Show score popup
addScorePopup(ghost.x, ghost.y, bonus);
} else {
onPacmanCaught();
return;
}
}
}
Eaten ghosts travel back to the ghost house at double speed, then respawn with their normal color.
const GHOST_HOUSE = { x: 9, y: 8 };
function moveToGhostHouse(ghost) {
const dx = GHOST_HOUSE.x - ghost.x;
const dy = GHOST_HOUSE.y - ghost.y;
// Move 2 tiles per frame (fast return)
if (Math.abs(dx) > Math.abs(dy)) {
ghost.x += Math.sign(dx);
} else {
ghost.y += Math.sign(dy);
}
// Arrived at ghost house
if (ghost.x === GHOST_HOUSE.x &&
ghost.y === GHOST_HOUSE.y) {
ghost.eaten = false;
}
}
Frightened ghosts are blue. Near the end of the timer, they flash between blue and white to warn the player.
function drawGhost(ghost) {
if (ghost.eaten) {
drawGhostEyes(ghost); // eyes only
return;
}
const cx = ghost.x * TILE_SIZE + TILE_SIZE / 2;
const cy = ghost.y * TILE_SIZE + TILE_SIZE / 2;
const r = TILE_SIZE / 2 - 2;
if (frightened) {
const elapsed = Date.now() - frightenedTimer;
// Flash white/blue in last 2 seconds
if (elapsed > FLASH_START) {
const flash = Math.floor(elapsed / 200) % 2;
ctx.fillStyle = flash ? '#fff' : '#2121DE';
} else {
ctx.fillStyle = '#2121DE';
}
} else {
ctx.fillStyle = ghost.color;
}
// Draw body (same shape as before)
ctx.beginPath();
ctx.arc(cx, cy - 2, r, Math.PI, 0);
ctx.lineTo(cx + r, cy + r);
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();
// Frightened face (wavy mouth)
if (frightened) {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(cx - 3, cy - 3, 2, 0, Math.PI * 2);
ctx.arc(cx + 3, cy - 3, 2, 0, Math.PI * 2);
ctx.fill();
} else {
drawGhostEyes(ghost);
}
}
Check the frightened timer each frame. When it expires, return all ghosts to normal chase mode.
function updateFrightened() {
if (!frightened) return;
const elapsed = Date.now() - frightenedTimer;
if (elapsed > FRIGHTENED_DURATION) {
frightened = false;
ghostEatCombo = 0;
}
}
// Add to gameLoop:
// updateFrightened();
Power pellet mode adds a huge layer of strategy. Players now have to decide when to eat pellets for maximum ghost combos. In the next parts:
Continue to Part 5: Multiple Levels where we add level progression with increasing difficulty.