In Part 4 we added power pellets. Now our game needs progression โ each level should feel harder than the last. We will increase ghost speed, reduce power pellet duration, and add a level transition screen.
Define a configuration table that maps each level to its difficulty parameters. This makes it easy to tune the game balance.
let level = 1;
const LEVEL_CONFIG = [
// level 1
{ ghostSpeed: 150, frightenedTime: 8000,
scatterTime: 7000, chaseTime: 20000 },
// level 2
{ ghostSpeed: 130, frightenedTime: 6000,
scatterTime: 5000, chaseTime: 20000 },
// level 3
{ ghostSpeed: 110, frightenedTime: 4000,
scatterTime: 4000, chaseTime: 25000 },
// level 4
{ ghostSpeed: 100, frightenedTime: 3000,
scatterTime: 3000, chaseTime: 25000 },
// level 5+
{ ghostSpeed: 90, frightenedTime: 2000,
scatterTime: 2000, chaseTime: 30000 },
];
function getConfig() {
const idx = Math.min(level - 1, LEVEL_CONFIG.length - 1);
return LEVEL_CONFIG[idx];
}
Use the level config to control the game loop interval and frightened duration. Update these values whenever the level changes.
let gameInterval = null;
function applyLevelConfig() {
const cfg = getConfig();
FRIGHTENED_DURATION = cfg.frightenedTime;
SCATTER_DURATION = cfg.scatterTime;
CHASE_DURATION = cfg.chaseTime;
// Restart game loop with new speed
if (gameInterval) clearInterval(gameInterval);
gameInterval = setInterval(gameLoop, cfg.ghostSpeed);
}
// Call applyLevelConfig() at game start and level change
When all dots are eaten, instead of showing "YOU WIN", advance to the next level. Reset the map but keep the score.
function checkWin() {
if (dotsLeft <= 0) {
level++;
showLevelTransition();
}
}
function resetMapForNewLevel() {
// Restore all dots from original map
for (let r = 0; r < originalMap.length; r++) {
map[r] = [...originalMap[r]];
}
countDots();
// Reset positions
pacman.x = 1; pacman.y = 1; pacman.dir = 0;
ghosts.forEach((g, i) => {
g.x = 9 + (i % 2);
g.y = 7 + Math.floor(i / 2);
g.eaten = false;
});
frightened = false;
ghostMode = 'scatter';
modeTimer = 0;
applyLevelConfig();
}
Show a brief overlay between levels. This gives the player a moment to breathe and builds anticipation.
let inTransition = false;
function showLevelTransition() {
inTransition = true;
// Pause game loop
if (gameInterval) clearInterval(gameInterval);
// Draw transition overlay
function drawTransition() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#f0c040';
ctx.font = 'bold 32px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Level ' + level,
canvas.width / 2, canvas.height / 2 - 10);
ctx.fillStyle = '#888';
ctx.font = '14px "Segoe UI", sans-serif';
ctx.fillText('Get ready...',
canvas.width / 2, canvas.height / 2 + 25);
}
drawTransition();
// After 2 seconds, start the new level
setTimeout(() => {
inTransition = false;
resetMapForNewLevel();
}, 2000);
}
Show the current level in the score bar so the player always knows their progress.
function drawScore() {
const barY = map.length * TILE_SIZE;
ctx.fillStyle = '#111';
ctx.fillRect(0, barY, canvas.width, SCORE_BAR_HEIGHT);
// Level (left)
ctx.fillStyle = '#4cff72';
ctx.font = '13px "Segoe UI", sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Lv.' + level, 8, barY + 20);
// Score (center)
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.fillText('Score: ' + score,
canvas.width / 2, barY + 20);
// High score (right)
ctx.fillStyle = '#f0c040';
ctx.textAlign = 'right';
ctx.fillText('Best: ' + highScore,
canvas.width - 8, barY + 20);
}
In the original Pac-Man, a fruit appears after eating a certain number of dots. Each level has a different fruit worth different points. This is a nice touch for completeness.
const FRUITS = [
{ name: 'Cherry', points: 100, emoji: '๐' },
{ name: 'Strawberry', points: 300, emoji: '๐' },
{ name: 'Orange', points: 500, emoji: '๐' },
{ name: 'Apple', points: 700, emoji: '๐' },
{ name: 'Melon', points: 1000, emoji: '๐' },
];
let fruitActive = false;
let fruitPos = { x: 9, y: 11 };
let fruitTimer = 0;
const FRUIT_DURATION = 10000;
const FRUIT_TRIGGER = 70; // appear after 70 dots eaten
function checkFruitSpawn() {
const eaten = totalDots - dotsLeft;
if (!fruitActive && eaten >= FRUIT_TRIGGER) {
fruitActive = true;
fruitTimer = Date.now();
}
// Despawn after timeout
if (fruitActive &&
Date.now() - fruitTimer > FRUIT_DURATION) {
fruitActive = false;
}
}
function getFruit() {
const idx = Math.min(level - 1, FRUITS.length - 1);
return FRUITS[idx];
}
Your Pac-Man now has real progression. Each level feels different as ghosts get faster and power pellets last shorter. Next up:
Continue to Part 6: Sound Effects where we add audio feedback to make the game feel alive.