← Back to Blog Apr 23, 2026 · 11:00 PM · 6 min read

Frogger Scoring & Lives: Points, Timer, and Game Over

By Wuidi · Tutorial · Part 4

In Part 3, we added collision detection — the frog dies when hit by a car or when it falls in the water, and scores when it reaches a goal slot. But there is no score counter, no lives, and no way to lose. Now we add the systems that turn Frogger into a complete game: points, a countdown timer, lives, high scores, and a game over screen.

What You Will Learn

Step 1: Score System

Frogger awards points for two things: reaching a goal slot (+100) and moving forward (+10 per row). We track the frog's highest row reached so forward-step points are only awarded once per row per life.

let score = 0;
let highestRow = ROWS - 1;  // tracks forward progress

const SCORE_GOAL = 100;
const SCORE_STEP = 10;

function awardForwardStep() {
  if (frog.row < highestRow) {
    score += SCORE_STEP * (highestRow - frog.row);
    highestRow = frog.row;
  }
}

function awardGoal() {
  score += SCORE_GOAL;
  highestRow = ROWS - 1;  // reset for next frog
}
Tip: Tracking highestRow prevents the player from farming points by moving forward and backward repeatedly. Points are only awarded for new forward progress. When the frog dies or scores a goal, highestRow resets.

Step 2: Lives System

The player starts with 3 lives. Each death costs one life. When lives reach zero, the game is over. We decrement lives in the death handler and check for game over before resetting the frog.

let lives = 3;
let gameOver = false;

function onFrogDeath(type) {
  lives--;

  if (lives <= 0) {
    gameOver = true;
    checkHighScore();
    return;
  }

  // Reset frog position and progress tracker
  startDeath(type);
  highestRow = ROWS - 1;
}

// In the collision check:
const result = checkAllCollisions();
if (result === 'hit' || result === 'drown') {
  onFrogDeath(result);
}

The death animation from Part 3 still plays when the frog dies. The difference now is that we decrement lives first. If lives hit zero, we skip the animation and go straight to the game over screen.

Step 3: Countdown Timer

Classic Frogger has a time limit for each frog. If the timer runs out, the frog dies. Reaching the goal quickly awards bonus points based on remaining time.

const TIME_LIMIT = 30;  // seconds per frog
let timeLeft = TIME_LIMIT;
let lastTick = 0;

function updateTimer(timestamp) {
  if (gameOver || deathAnim) return;

  if (timestamp - lastTick >= 1000) {
    timeLeft--;
    lastTick = timestamp;

    if (timeLeft <= 0) {
      timeLeft = 0;
      onFrogDeath('drown');  // time's up = death
    }
  }
}

function awardGoal() {
  // Time bonus: 1 point per second remaining
  const timeBonus = timeLeft * 2;
  score += SCORE_GOAL + timeBonus;

  // Reset timer for next frog
  timeLeft = TIME_LIMIT;
  highestRow = ROWS - 1;
}
Tip: The time bonus rewards fast play. At 2 points per second remaining, a player who reaches the goal in 10 seconds gets 40 bonus points on top of the 100 base. This encourages aggressive play instead of waiting for safe gaps.

Step 4: High Score with localStorage

We save the best score in localStorage so it persists across browser sessions. The pattern is the same as any other game — load on start, save on game over if the score is higher.

const HI_KEY = 'frogger_hi';

function loadHighScore() {
  const saved = localStorage.getItem(HI_KEY);
  return saved ? parseInt(saved, 10) : 0;
}

function saveHighScore(newScore) {
  const current = loadHighScore();
  if (newScore > current) {
    localStorage.setItem(HI_KEY, newScore.toString());
    return true;  // new record
  }
  return false;
}

let highScore = loadHighScore();
let isNewRecord = false;

function checkHighScore() {
  isNewRecord = saveHighScore(score);
  if (isNewRecord) highScore = score;
}

We call checkHighScore() when the game ends. The isNewRecord flag lets us show a special message on the game over screen.

Step 5: Drawing the HUD

The HUD (heads-up display) shows score, lives, and timer. We draw it on top of everything else so it is always visible. Lives are shown as small frog icons, the timer as a colored bar that shrinks and changes color.

function drawHUD() {
  // Score (top-left)
  ctx.fillStyle = '#fff';
  ctx.font = '14px "Segoe UI", sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('Score: ' + score, 8, 18);

  // High score (top-right)
  ctx.textAlign = 'right';
  ctx.fillStyle = '#ffd700';
  ctx.fillText('Best: ' + highScore, WIDTH - 8, 18);

  // Lives (bottom-left, frog icons)
  ctx.textAlign = 'left';
  ctx.font = '14px sans-serif';
  for (let i = 0; i < lives; i++) {
    ctx.fillText('🐸', 8 + i * 22, HEIGHT - 8);
  }

  // Timer bar (bottom)
  const barW = WIDTH - 80;
  const barH = 6;
  const barX = 70;
  const barY = HEIGHT - 14;
  const pct = timeLeft / TIME_LIMIT;

  // Background
  ctx.fillStyle = '#333';
  ctx.fillRect(barX, barY, barW, barH);

  // Fill (green → yellow → red)
  if (pct > 0.5) ctx.fillStyle = '#2ecc40';
  else if (pct > 0.25) ctx.fillStyle = '#f1c40f';
  else ctx.fillStyle = '#e74c3c';

  ctx.fillRect(barX, barY, barW * pct, barH);
}
Tip: The timer bar changing color from green to yellow to red gives the player an instant visual cue about urgency without needing to read numbers. This is a common UX pattern in games — use color to communicate state.

Step 6: Game Over Screen

When lives reach zero, we show a game over overlay with the final score, high score, and a restart option. The overlay draws on top of the final game state so the player can see where they died.

function drawGameOver() {
  // Dark overlay
  ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
  ctx.fillRect(0, 0, WIDTH, HEIGHT);

  // Title
  ctx.fillStyle = '#e74c3c';
  ctx.font = 'bold 28px "Segoe UI", sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText('GAME OVER', WIDTH / 2, HEIGHT / 2 - 50);

  // Final score
  ctx.fillStyle = '#fff';
  ctx.font = '18px "Segoe UI", sans-serif';
  ctx.fillText('Score: ' + score, WIDTH / 2,
    HEIGHT / 2 - 15);

  // High score or new record
  if (isNewRecord) {
    ctx.fillStyle = '#ffd700';
    ctx.font = 'bold 18px "Segoe UI", sans-serif';
    ctx.fillText('★ New Record! ★', WIDTH / 2,
      HEIGHT / 2 + 15);
  } else {
    ctx.fillStyle = '#aaa';
    ctx.font = '14px "Segoe UI", sans-serif';
    ctx.fillText('Best: ' + highScore, WIDTH / 2,
      HEIGHT / 2 + 15);
  }

  // Restart hint
  ctx.fillStyle = '#888';
  ctx.font = '12px "Segoe UI", sans-serif';
  ctx.fillText('Press R to restart', WIDTH / 2,
    HEIGHT / 2 + 55);
}

// Restart handler
document.addEventListener('keydown', (e) => {
  if ((e.key === 'r' || e.key === 'R') && gameOver) {
    score = 0;
    lives = 3;
    timeLeft = TIME_LIMIT;
    highestRow = ROWS - 1;
    filledGoals = [];
    gameOver = false;
    isNewRecord = false;
    highScore = loadHighScore();
    resetFrog();
    createObstacles();
    createLogs();
  }
});

The restart handler resets everything: score, lives, timer, filled goals, and obstacle positions. We reload the high score from localStorage in case another tab updated it.

Updated Game Loop

Here is the complete game loop with scoring, lives, timer, and game over integrated:

function gameLoop(timestamp) {
  if (gameOver) {
    drawGameOver();
    requestAnimationFrame(gameLoop);
    return;
  }

  // Death animation
  if (updateDeathAnim()) {
    ctx.clearRect(0, 0, WIDTH, HEIGHT);
    drawLanes();
    drawLaneDetails();
    drawLogs();
    drawObstacles();
    drawFilledGoals();
    drawDeathAnim();
    drawHUD();
    requestAnimationFrame(gameLoop);
    return;
  }

  // Update
  updateTimer(timestamp);
  update();
  awardForwardStep();

  // Collisions
  const result = checkAllCollisions();
  if (result === 'hit' || result === 'drown') {
    onFrogDeath(result);
  } else if (result === 'goal') {
    if (checkGoal()) {
      awardGoal();
      resetFrog();
    } else {
      onFrogDeath('drown');
    }
  }

  // Draw
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  drawLanes();
  drawLaneDetails();
  drawLogs();
  drawObstacles();
  drawFilledGoals();
  if (!deathAnim) drawFrog();
  drawHUD();

  requestAnimationFrame(gameLoop);
}
Game feel tip: The timer creates urgency, lives create tension, and the high score creates motivation. Together, these three systems transform a simple crossing game into something players want to replay. Every good game has a loop: try → fail → learn → try again. Scoring and lives create that loop.

What's Next?

The game is now fully playable with scoring, lives, a timer, and a game over screen. But every round feels the same — the traffic never gets harder. In the next part, we add level progression so the game gets faster and more challenging as the player advances.

Continue to Part 5: Levels & Difficulty where we add level progression with faster traffic and shorter timers.

More Articles

📈
Tutorial Frogger Levels & Difficulty: Faster Traffic Each Level Tutorial · Apr 23, 2026 · 12:00 AM
🔊
Tutorial Frogger Sound Effects: Hop, Splash, and Level Up Sounds Tutorial · Apr 23, 2026 · 01:00 AM
💥
Tutorial Frogger Collision Detection: Cars, Water, and Safe Zones Tutorial · Apr 23, 2026 · 10:00 PM
← Back to Blog