In the previous tutorial, we built a basic Pac-Man game with a map, movement, and simple ghost AI. Now it is time to make the game feel rewarding by adding a score system. We will count dots eaten, display the score on the canvas, save high scores, and detect when the player has cleared all dots.
We need a few variables to track the current score, the high score, and how many dots are left on the map. Regular dots are worth 10 points, and power pellets are worth 50.
let score = 0;
let highScore = parseInt(localStorage.getItem('pacman_hi')) || 0;
let dotsLeft = 0;
const SCORE_DOT = 10;
const SCORE_PELLET = 50;
// Count total dots at game start
function countDots() {
dotsLeft = 0;
for (let r = 0; r < map.length; r++) {
for (let c = 0; c < map[r].length; c++) {
if (map[r][c] === 2 || map[r][c] === 3) dotsLeft++;
}
}
}
countDots();
Modify the movePacman() function from the previous tutorial. When Pac-Man moves onto a dot or power pellet tile, add points and decrease the dot counter.
function movePacman() {
const dirs = [{x:1,y:0},{x:0,y:1},{x:-1,y:0},{x:0,y:-1}];
const d = dirs[nextDir];
const nx = pacman.x + d.x;
const ny = pacman.y + d.y;
if (map[ny] && map[ny][nx] !== 1) {
pacman.x = nx;
pacman.y = ny;
pacman.dir = nextDir;
const tile = map[ny][nx];
if (tile === 2) {
score += SCORE_DOT;
dotsLeft--;
map[ny][nx] = 0;
} else if (tile === 3) {
score += SCORE_PELLET;
dotsLeft--;
map[ny][nx] = 0;
}
// Update high score
if (score > highScore) {
highScore = score;
localStorage.setItem('pacman_hi', String(highScore));
}
}
}
We draw the score at the bottom of the canvas. This keeps everything self-contained without needing HTML elements outside the canvas.
function drawScore() {
const y = canvas.height - 8;
// Current score (left side)
ctx.fillStyle = '#fff';
ctx.font = '14px "Segoe UI", sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Score: ' + score, 8, y);
// High score (right side)
ctx.textAlign = 'right';
ctx.fillStyle = '#f0c040';
ctx.fillText('Best: ' + highScore, canvas.width - 8, y);
}
To avoid the score overlapping with the game, add extra height to the canvas and adjust the map drawing to leave room at the bottom.
// In your HTML, increase canvas height:
// <canvas id="game" width="400" height="430"></canvas>
const SCORE_BAR_HEIGHT = 30;
function drawMap() {
for (let row = 0; row < map.length; row++) {
for (let col = 0; col < map[row].length; col++) {
const x = col * TILE_SIZE;
const y = row * TILE_SIZE; // map starts at top
// ... draw tiles as before
}
}
}
function drawScore() {
const barY = map.length * TILE_SIZE;
// Score bar background
ctx.fillStyle = '#111';
ctx.fillRect(0, barY, canvas.width, SCORE_BAR_HEIGHT);
// Score text
ctx.fillStyle = '#fff';
ctx.font = '13px "Segoe UI", sans-serif';
ctx.textAlign = 'left';
ctx.fillText('Score: ' + score, 8, barY + 20);
ctx.textAlign = 'right';
ctx.fillStyle = '#f0c040';
ctx.fillText('Best: ' + highScore, canvas.width - 8, barY + 20);
}
When all dots are eaten, the player wins the level. We check dotsLeft in the game loop and show a win message.
let gameOver = false;
let gameWon = false;
function checkWin() {
if (dotsLeft <= 0) {
gameWon = true;
gameOver = true;
}
}
function drawGameOver() {
if (!gameOver) return;
ctx.fillStyle = 'rgba(0, 0, 0, 0.75)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = gameWon ? '#4cff72' : '#ff4040';
ctx.font = 'bold 28px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.fillText(
gameWon ? 'YOU WIN!' : 'GAME OVER',
canvas.width / 2,
canvas.height / 2 - 20
);
ctx.fillStyle = '#fff';
ctx.font = '16px "Segoe UI", sans-serif';
ctx.fillText(
'Score: ' + score,
canvas.width / 2,
canvas.height / 2 + 15
);
ctx.fillStyle = '#888';
ctx.font = '13px "Segoe UI", sans-serif';
ctx.fillText(
'Press R to restart',
canvas.width / 2,
canvas.height / 2 + 45
);
}
Add the score drawing and win check to the main game loop. When the game is over, stop updating movement but keep drawing.
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawMap();
if (!gameOver) {
movePacman();
ghosts.forEach(g => moveGhost(g));
checkWin();
}
drawPacman();
ghosts.forEach(g => drawGhost(g));
drawScore();
drawGameOver();
}
setInterval(gameLoop, 150);
Let the player restart by pressing R. Reset the score, reload the map, and reposition Pac-Man and ghosts.
document.addEventListener('keydown', (e) => {
if (e.key === 'r' || e.key === 'R') {
if (gameOver) restartGame();
}
});
function restartGame() {
score = 0;
gameOver = false;
gameWon = false;
// Reset map (reload from original)
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[0].x = 9; ghosts[0].y = 9;
ghosts[1].x = 10; ghosts[1].y = 9;
}
originalMap) so you can reset it. If you modify the map array directly when eating dots, you need the original to restore it on restart.
For extra polish, show a floating "+10" or "+50" text that rises and fades when Pac-Man eats something. This gives satisfying visual feedback.
let scorePopups = [];
function addScorePopup(x, y, points) {
scorePopups.push({
x: x * TILE_SIZE + TILE_SIZE / 2,
y: y * TILE_SIZE,
text: '+' + points,
life: 1.0
});
}
function drawScorePopups() {
for (let i = scorePopups.length - 1; i >= 0; i--) {
const p = scorePopups[i];
p.y -= 1; // float up
p.life -= 0.03; // fade out
if (p.life <= 0) {
scorePopups.splice(i, 1);
continue;
}
ctx.globalAlpha = p.life;
ctx.fillStyle = '#f0c040';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(p.text, p.x, p.y);
ctx.globalAlpha = 1;
}
}
// Call addScorePopup(nx, ny, SCORE_DOT) in movePacman()
// Call drawScorePopups() in gameLoop() after drawPacman()
Now your Pac-Man game has a complete score system with high score persistence and a win condition. In the next parts of this series, we can explore:
Now your Pac-Man game has a complete score system with high score persistence and a win condition. Continue to Part 3: Ghost Chase AI where we give each ghost a unique personality and make them hunt the player.