Adding Game Controls
Right now the game starts immediately when the page loads, and there is no way to restart or pause. Let’s add a Start button and a pause toggle. The code is event-driven, so we can do this without changing the rendering code.
Adding the Button to HTML
Update index.html to wrap the score and a button in a HUD (heads-up display) row:
<div id="game">
- <div id="score">Score: 0</div>
+ <div id="hud">
+ <span id="score">Score: 0</span>
+ <button id="start-btn">Start</button>
+ </div>
<div id="board"></div>
</div>
Styling the HUD
Add styles for the HUD and button in src/style.css. Replace the #score rule and add the new rules:
+ #hud {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ }
#score {
color: #eee;
font-family: monospace;
font-size: 1.2rem;
}
+ #start-btn {
+ padding: 6px 16px;
+ font-family: monospace;
+ font-size: 0.9rem;
+ background: #4caf50;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ }
+
+ #start-btn:hover {
+ background: #66bb6a;
+ }
Also add a style for the paused state:
#board.paused {
opacity: 0.7;
}
Updating the Engine
The engine needs three changes: a reset function, a togglePause function, and a modified start that resets before starting. Update src/engine.js:
First, extract the initial snake position into a constant and change the initial state so the game does not auto-start:
+ const INITIAL_SNAKE = [
+ { x: 10, y: 10 },
+ { x: 9, y: 10 },
+ { x: 8, y: 10 },
+ ];
- let snake = [
- { x: 10, y: 10 },
- { x: 9, y: 10 },
- { x: 8, y: 10 },
- ];
- let direction = { x: 1, y: 0 };
- let food = placeFood();
+ let snake = [];
+ let direction = { x: 1, y: 0 };
+ let food = null;
let score = 0;
let gameOver = false;
+ let running = false;
let intervalId = null;
Extract a helper for emitting the state (since we will call it from multiple places):
function emitState() {
emit("snake:tick", { snake: [...snake], food: { ...food }, gameOver });
}
Update the tick function to use it:
function tick() {
update();
- emit("snake:tick", { snake: [...snake], food: { ...food }, gameOver });
+ emitState();
}
Also update the collision handler to set running = false:
if (checkCollision(newHead)) {
gameOver = true;
+ running = false;
clearInterval(intervalId);
Add a reset function, and update start to reset first:
function reset() {
snake = INITIAL_SNAKE.map((seg) => ({ ...seg }));
direction = { x: 1, y: 0 };
food = placeFood();
score = 0;
gameOver = false;
running = false;
clearInterval(intervalId);
}
export function start() {
reset();
running = true;
emit("snake:start");
emitState();
intervalId = setInterval(tick, TICK_RATE);
}
The reset function restores all state to its initial values. Notice INITIAL_SNAKE.map((seg) => ({ ...seg })). We shallow-copy each segment so resetting does not mutate the constant.
The start function emits a new snake:start event that the UI can listen for to reset its own state (score display, button text, etc.).
Add pause/resume and an isRunning helper:
export function togglePause() {
if (gameOver || !running) return;
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
emit("snake:pause");
} else {
intervalId = setInterval(tick, TICK_RATE);
emit("snake:resume");
}
}
export function isRunning() {
return running && !gameOver;
}
The togglePause function stops or restarts the interval and emits snake:pause or snake:resume. The guard at the top prevents pausing when the game is over or has not started.
Updating the UI
In src/ui.js, grab the button element:
const board = document.getElementById("board");
const scoreDisplay = document.getElementById("score");
+ const startBtn = document.getElementById("start-btn");
Add listeners for the new events:
document.addEventListener("snake:start", () => {
scoreDisplay.textContent = "Score: 0";
board.classList.remove("game-over");
startBtn.textContent = "Restart";
});
document.addEventListener("snake:die", (e) => {
board.classList.add("game-over");
startBtn.textContent = "Play Again";
});
document.addEventListener("snake:pause", () => {
board.classList.add("paused");
});
document.addEventListener("snake:resume", () => {
board.classList.remove("paused");
});
Export the button so main.js can attach a click handler:
export { startBtn };
Wiring It Up in Main
Update src/main.js to import the button and the new engine functions:
import "./style.css";
import { startBtn } from "./ui.js";
import { start, setDirection, togglePause, isRunning } from "./engine.js";
startBtn.addEventListener("click", () => {
start();
});
document.addEventListener("keydown", (e) => {
if (e.key === " " && isRunning()) {
togglePause();
return;
}
const directions = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
};
if (directions[e.key]) {
setDirection(directions[e.key]);
}
});
Pressing the Space bar toggles pause/resume. The isRunning() guard ensures Space does nothing before the game starts or after it ends.
Run the dev server. You should see a “Start” button next to the score. Click it to begin, press Space to pause/resume, and click “Play Again” after a game over to restart:

Checkpoint: Commit your progress.
git add .
git commit -m "snake-10: Add start, restart, and pause controls"
git push