Skip to the solution
HardMachine Coding14 min read

Build Whack-a-Mole

483 views

The problem

Question presented to candidate: "Build Whack-a-Mole. There is a 3 by 3 field of holes. After the player presses Start, moles pop up in random holes for a short time; clicking a mole scores a point. The game lasts 15 seconds, it should get harder as time runs out, and when time is up the board stops and shows the score. I also want to be able to test it without waiting for real randomness."

Requirements:

  • A 3 x 3 field of holes, a score, a countdown and a Start / Play again button.
  • One mole at a time appears in a random hole (never the same hole twice in a row), stays up for a limited time, then hides.
  • Clicking the hole with the mole scores once and hides it; clicking an empty hole or the same mole twice does not add points.
  • The time a mole stays up shrinks as the game goes on.
  • At 0 seconds the game ends, no mole is shown, clicks do nothing, and every timer is stopped.
  • The random source is injectable so a test can replay a game deterministically.

What a strong answer should cover:

  • A state machine: idle, playing, over; and within playing, a gap between moles and a mole being up.
  • Game rules as pure functions (tick(state, dt, random), whack(state, hole)) with no timers or Math.random inside, so tests can drive them with a seeded generator.
  • A single game clock (one interval) instead of a pile of independent timeouts, and cleaning it up on game over and unmount.
  • Avoiding stale state in timer callbacks, and why impure updates are dangerous where a framework may run update functions more than once.
  • Preventing double scoring, and a difficulty curve expressed as data.

Clarifying questions expected:

  • "One mole at a time, or several at once?"
  • "Should missing a mole or clicking an empty hole cost points?"
  • "Does the game need to pause when the tab is hidden?"

Code / implementation expected: Yes. A working game; expect to be asked how you tested it and how timers are cleaned up.

  • React
  • Game
  • Timers

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

14 min read ยท 9 sections

React solution

Every file of the working solution. Open it in the playground to run and change it.

ReactComplete solution
import { useEffect, useRef, useState } from "react";
import Hole from "./src/components/Hole";
import {
  HOLES,
  TICK_MS,
  initialState,
  start,
  statusText,
  tick,
  whack,
} from "./src/utils/mole";

// The random source the game uses. Read at call
// time, so a test can replace Math.random.
const random = () => Math.random();

export default function App() {
  const [game, setGame] = useState(initialState);
  // The interval callback is created once per
  // game, so it must not read `game` from its
  // closure (that copy would be stale). It reads
  // the latest state from this ref instead.
  const gameRef = useRef(game);
  const commit = (next) => {
    gameRef.current = next;
    setGame(next);
  };

  useEffect(() => {
    if (game.status !== "playing") return;
    const id = setInterval(
      () => commit(tick(gameRef.current, TICK_MS, random)),
      TICK_MS,
    );
    // runs on game over, on unmount, and in StrictMode re-runs
    return () => clearInterval(id);
  }, [game.status]);

  const playing = game.status === "playing";

  return (
    <main className="wam">
      <h1>Whack-a-Mole</h1>
      <div className="hud">
        <span>
          Score: <strong data-testid="score">{game.score}</strong>
        </span>
        <span>
          Time:{" "}
          <strong data-testid="time">
            {Math.ceil(game.timeLeft / 1000) + "s"}
          </strong>
        </span>
      </div>
      <p className="status" data-testid="status" aria-live="polite">
        {statusText(game)}
      </p>
      <div className="field" role="group" aria-label="Mole holes">
        {Array.from({ length: HOLES }, (_, i) => (
          <Hole
            key={i}
            index={i}
            up={Boolean(game.mole) && game.mole.hole === i}
            disabled={!playing}
            onWhack={(h) => commit(whack(gameRef.current, h))}
          />
        ))}
      </div>
      <button
        type="button"
        className="start"
        data-testid="start"
        disabled={playing}
        onClick={() => commit(start())}
      >
        {game.status === "over" ? "Play again" : "Start"}
      </button>
    </main>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 95 of 107 in Machine coding

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

Build a Chat UI

Continue