Polish and Finishing Touches

The game is fully playable, but let’s add two finishing touches: progressive difficulty (the game speeds up as you score) and a persistent high score using localStorage. Both features show a benefit of the event-driven architecture. We can add them without changing any of the existing logic.

Progressive Difficulty

Right now the game runs at a fixed tick rate. Let’s make it speed up every time the player scores 50 points.

In src/engine.js, replace the single TICK_RATE constant with three:

- const TICK_RATE = 150;
+ const BASE_TICK_RATE = 150;
+ const MIN_TICK_RATE = 75;
+ const SPEED_INCREMENT = 5;

Add a helper function that computes the current tick rate based on the score:

function currentTickRate() {
  const speedUps = Math.floor(score / 50);
  return Math.max(MIN_TICK_RATE, BASE_TICK_RATE - speedUps * SPEED_INCREMENT);
}

Every 50 points, the interval drops by 5ms. Math.max keeps it from ever going below 75ms, which is twice the starting speed.

Add a helper to restart the interval at the new speed:

function restartInterval() {
  clearInterval(intervalId);
  intervalId = setInterval(tick, currentTickRate());
}

Call restartInterval() when the snake eats food, inside the update function:

  if (newHead.x === food.x && newHead.y === food.y) {
    score += 10;
    food = placeFood();
    emit("snake:eat", { score });
+   // Speed up the game
+   restartInterval();
  } else {

Also update start and togglePause to use currentTickRate() instead of the old constant:

  export function start() {
    reset();
    running = true;
    emit("snake:start");
    emitState();
-   intervalId = setInterval(tick, TICK_RATE);
+   intervalId = setInterval(tick, currentTickRate());
  }

  export function togglePause() {
    if (gameOver || !running) return;

    if (intervalId) {
      clearInterval(intervalId);
      intervalId = null;
      emit("snake:pause");
    } else {
-     intervalId = setInterval(tick, TICK_RATE);
+     intervalId = setInterval(tick, currentTickRate());
      emit("snake:resume");
    }
  }

High Score with localStorage

Now let’s add a persistent high score display. This is a UI feature, so we will add it entirely through a new event listener.

Add a <span> for the high score in index.html:

  <div id="hud">
    <span id="score">Score: 0</span>
+   <span id="high-score">Best: 0</span>
    <button id="start-btn">Start</button>
  </div>

Style it in src/style.css. Combine it with the score rule and add a muted color:

- #score {
+ #score,
+ #high-score {
    color: #eee;
    font-family: monospace;
    font-size: 1.2rem;
  }

+ #high-score {
+   color: #aaa;
+ }

In src/ui.js, grab the element and load the saved high score:

  const scoreDisplay = document.getElementById("score");
+ const highScoreDisplay = document.getElementById("high-score");
  const startBtn = document.getElementById("start-btn");

+ // Load high score from localStorage
+ let highScore = Number(localStorage.getItem("snake-high-score")) || 0;
+ highScoreDisplay.textContent = `Best: ${highScore}`;

Then update the snake:die listener to check and save the high score:

  document.addEventListener("snake:die", (e) => {
    board.classList.add("game-over");
    startBtn.textContent = "Play Again";
+
+   // Update high score
+   if (e.detail.score > highScore) {
+     highScore = e.detail.score;
+     localStorage.setItem("snake-high-score", highScore);
+     highScoreDisplay.textContent = `Best: ${highScore}`;
+   }
  });

Notice that adding the high score feature required:

  • 0 changes to engine.js (it already emits snake:die with the score)
  • 0 changes to main.js
  • Only a new listener in ui.js and a new element in HTML

Run the dev server and play a few rounds. The game should speed up as you score, and your best score should persist across page refreshes:

Checkpoint: Commit your progress.

git add .
git commit -m "snake-11: Add progressive speed and persistent high score"
git push