Practice Questions

1. Write code that listens for keydown events and maps the four arrow keys to direction objects (e.g., { x: 0, y: -1 } for ArrowUp). How would you prevent the user from reversing direction — for example, pressing Left when currently moving Right?

Solution
const DIRECTIONS = {
  ArrowUp: { x: 0, y: -1 },
  ArrowDown: { x: 0, y: 1 },
  ArrowLeft: { x: -1, y: 0 },
  ArrowRight: { x: 1, y: 0 },
};

let direction = DIRECTIONS.ArrowRight; // initial direction

document.addEventListener("keydown", (event) => {
  const newDir = DIRECTIONS[event.key];
  if (!newDir) return;  // ignore non-arrow keys

  // Prevent reversing: new direction cannot be opposite of current
  const isOpposite =
    newDir.x + direction.x === 0 && newDir.y + direction.y === 0;

  if (!isOpposite) {
    direction = newDir;
  }
});

The reversal check works because opposite directions sum to zero on both axes (e.g., Left {-1, 0} + Right {1, 0} = {0, 0}). This prevents the player from immediately colliding with themselves by doubling back.

2. Write a function called createGrid that takes a container element and a size (number of columns/rows), dynamically creates size * size <div> elements, appends them to the container, and returns an array of references to the created elements.

Solution
function createGrid(container, size) {
  const cells = [];
  for (let i = 0; i < size * size; i++) {
    const cell = document.createElement("div");
    cell.classList.add("cell");
    container.appendChild(cell);
    cells.push(cell);
  }
  return cells;
}

The function uses document.createElement to create each cell, adds a class for styling, appends it to the container, and collects references in an array so individual cells can be accessed by index later.

3. Given a flat array of cells representing a 10x10 grid (row-major order), write an expression that converts an { x, y } coordinate to the corresponding array index. Then write the reverse: convert an index back to { x, y }.

Solution
// Coordinate to index
const index = y * 10 + x;

// Index to coordinate
const x = index % 10;
const y = Math.floor(index / 10);

In a row-major flat array, each row contains GRID_SIZE elements. Multiplying y by the row width and adding x gives the linear position. The reverse uses modulo for the column and integer division for the row.

4. Explain the “head-insert, tail-remove” pattern for moving a linked list of segments. Write code that moves an array of { x, y } segments one step in a given direction without creating a new array.

Solution

The pattern works by adding a new element at the front (the new head position) and removing the last element (the old tail). This makes the entire sequence appear to shift forward by one step, since every segment now occupies the position of the one that was in front of it.

function move(segments, direction) {
  const head = segments[0];
  const newHead = {
    x: head.x + direction.x,
    y: head.y + direction.y,
  };
  segments.unshift(newHead);
  segments.pop();
}

unshift inserts at the front; pop removes from the end. The array is mutated in place.

5. What advantage does dispatching a CustomEvent provide over simply calling a function directly? Write code where a "score:changed" event is dispatched with the new score in detail, and two independent listeners respond — one updating a display element and one saving to localStorage. Explain why this is better than having the score-update code call both functions directly.

Solution

Using CustomEvent decouples the code that changes state from the code that reacts to it. The producer does not depend on who is listening or how many listeners there are.

// Producer: only cares about announcing the change
function updateScore(newScore) {
  const event = new CustomEvent("score:changed", {
    detail: { score: newScore },
  });
  document.dispatchEvent(event);
}

// Listener 1: updates the display
document.addEventListener("score:changed", (e) => {
  document.getElementById("score").textContent = e.detail.score;
});

// Listener 2: persists to localStorage
document.addEventListener("score:changed", (e) => {
  localStorage.setItem("high-score", e.detail.score);
});

If updateScore called both functions directly, adding a third reaction (e.g., playing a sound effect) would require editing updateScore. With events, you register a new listener instead, and the producer stays unchanged.

This is the same principle behind addEventListener("click", ...): the button does not depend on what happens when it is clicked. Custom events let you apply this pattern to your own application logic.

6. What does it mean to “decouple” game logic from UI rendering? Describe the problem that arises when a single function both updates state and manipulates the DOM, and explain how custom events solve it.

Solution

When a single function handles both state changes and DOM updates, the two concerns become mixed together in one function. Adding a new feature (e.g., a sound effect when something happens) requires editing the state-update function, increasing its complexity and the risk of introducing bugs.

Custom events solve this by making the state-update function only responsible for modifying state and then dispatching an event that describes what happened. Separate listeners independently react to those events: one listener updates the DOM, another plays a sound, another logs analytics. Each can be added or removed without modifying the core logic. This is decoupling: the producer of the event does not depend on who is listening.

7. Consider the following module structure:

src/
  engine.js   -- exports: start(), setDirection()
  ui.js       -- exports: nothing (side-effect module)
  main.js     -- entry point

In main.js, the UI module is imported like this: import "./ui.js"; (with no named imports). What does it mean to import a module for its side effects? Give an example of a side effect that would run during this import.

Solution

Importing a module for its side effects means you are not importing any exported values. Instead, the module’s top-level code runs when it is imported, and that code performs some useful work as a side effect of being loaded.

Examples of side effects that run at import time:

  • Creating DOM elements and appending them to the page (e.g., building a grid of cells)
  • Registering event listeners on document (e.g., document.addEventListener("snake:tick", ...))
  • Setting global variables or modifying shared state

This pattern is common in event-driven architecture: each module registers its own listeners, and the entry point only imports the modules and starts the application.

8. Write a function randomPosition(gridSize, occupied) that returns a random { x, y } coordinate within a grid that is not in the occupied array. Use a do...while loop.

Solution
function randomPosition(gridSize, occupied) {
  let position;
  do {
    position = {
      x: Math.floor(Math.random() * gridSize),
      y: Math.floor(Math.random() * gridSize),
    };
  } while (
    occupied.some((pos) => pos.x === position.x && pos.y === position.y)
  );
  return position;
}

The do...while loop generates a random position and checks whether it overlaps with any occupied cell. If it does, it generates a new one. The loop is guaranteed to run at least once, which is why do...while is appropriate here rather than a regular while loop.

9. Explain how clearInterval works in conjunction with setInterval. Why is it important to store the return value of setInterval in a variable? Write code that starts a repeating timer and stops it after a condition is met.

Solution

setInterval returns a numeric ID that uniquely identifies the interval. clearInterval takes that ID and cancels the repeating execution. Without storing the ID, you have no way to stop the interval.

let count = 0;
const intervalId = setInterval(() => {
  count++;
  console.log(`Tick ${count}`);
  if (count >= 5) {
    clearInterval(intervalId);
    console.log("Stopped");
  }
}, 1000);

You need this pattern in a game loop. You store the interval ID so you can stop the loop when the game ends, stop it when the player pauses, or restart it at a different speed.

10. What is localStorage? Write code that saves a numeric high score to localStorage, and then retrieves it on page load, handling the case where no value has been stored yet.

Solution

localStorage is a browser API that stores key-value pairs as strings. Data persists across page refreshes and browser restarts (unlike session storage or in-memory variables).

// Saving
function saveHighScore(score) {
  localStorage.setItem("high-score", score);
}

// Loading (handling first visit)
function loadHighScore() {
  return Number(localStorage.getItem("high-score")) || 0;
}

localStorage.getItem returns null if the key does not exist. Number(null) evaluates to 0, so the || 0 fallback handles the first-ever visit. Note that localStorage stores everything as strings, so the value is automatically coerced when saved and must be converted back with Number() when loaded.

11. You have a working application where a start() function begins a game loop with setInterval. You want to add pause and resume functionality. Write a togglePause function that alternately stops and restarts the interval. What state variables do you need?

Solution

You need to track the interval ID (to know whether the loop is currently running) and optionally a running flag to prevent pausing before the game starts or after it ends.

let intervalId = null;
let running = false;

function tick() {
  // game update logic here
}

function start() {
  running = true;
  intervalId = setInterval(tick, 150);
}

function togglePause() {
  if (!running) return;

  if (intervalId) {
    clearInterval(intervalId);
    intervalId = null;
  } else {
    intervalId = setInterval(tick, 150);
  }
}

intervalId acts as a dual-purpose variable: when it is non-null, the game is running; when it is null, the game is paused. The guard if (!running) return prevents toggling before the game has started.

12. Why is it considered good practice to copy state before passing it through an event (e.g., using spread operators like [...array] or { ...object })? What could go wrong if you pass the original reference?

Solution

If you pass the original reference, any listener that receives the event could accidentally (or intentionally) mutate the source data. For example, a UI listener might sort or filter the array it receives, which would modify the engine’s internal state and cause bugs that are difficult to trace.

By creating a shallow copy with spread operators:

emit("tick", { items: [...items], config: { ...config } });

the engine’s original state is protected, because no listener can accidentally mutate the source data. Note that all listeners still share the same emitted snapshot, so one listener mutating the copy could affect another listener that reads it later. To fully isolate listeners from each other, you would need to freeze the snapshot (Object.freeze) or clone it again per listener. In practice, treating the emitted data as read-only is usually sufficient.

Note that spread creates only a shallow copy. If objects are deeply nested, you may need a deep copy (e.g., structuredClone or JSON.parse(JSON.stringify(...))) to fully protect the source data.