Build a Capital–Country game
Multiple-choice quiz: given a country, pick the right capital from 4 options. State: questions list, current index, selected answer, score. Generate distractors from other countries' capitals (random, avoid the answer). Show feedback after select, then advance. End with a score summary + restart. Keyboard support and accessibility (radiogroup).
A capital-country game is a tight machine-coding round: data, randomization, state machine, and feedback UI in ~80 lines.
1. Data
const COUNTRIES = [
{ country: "France", capital: "Paris" },
{ country: "Japan", capital: "Tokyo" },
// ...
];2. Question generation
For each round, pick a country and 3 distractor capitals from other countries. Shuffle the 4 options.
function makeQuestions(data, count = 10) {
const shuffled = [...data].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count).map((entry) => {
const distractors = shuffled
.filter((d) => d.capital !== entry.capital)
.sort(() => Math.random() - 0.5)
.slice(0, 3)
.map((d) => d.capital);
const options = [...distractors, entry.capital].sort(() => Math.random() - 0.5);
return { country: entry.country, answer: entry.capital, options };
});
}(Math.random() - 0.5 is biased; Fisher-Yates is correct. Mention it.)
3. State machine
asking → answered (correct/wrong) → next | finishedfunction Game() {
const [questions] = useState(() => makeQuestions(COUNTRIES, 10));
const [i, setI] = useState(0);
const [selected, setSelected] = useState(null);
const [score, setScore] = useState(0);
const q = questions[i];
const done = i >= questions.length;
const pick = (opt) => {
if (selected) return; // lock after answering
setSelected(opt);
if (opt === q.answer) setScore((s) => s + 1);
};
const next = () => {
setSelected(null);
setI((x) => x + 1);
};
if (done) return <Summary score={score} total={questions.length} onRestart={() => location.reload()} />;
return (
<fieldset>
<legend>What is the capital of {q.country}?</legend>
{q.options.map((opt) => {
const isAnswer = selected && opt === q.answer;
const isWrong = selected === opt && opt !== q.answer;
return (
<label key={opt}>
<input
type="radio"
name="opt"
value={opt}
disabled={!!selected}
checked={selected === opt}
onChange={() => pick(opt)}
/>
{opt} {isAnswer && "✓"} {isWrong && "✗"}
</label>
);
})}
{selected && <button onClick={next}>{i === questions.length - 1 ? "Finish" : "Next"}</button>}
</fieldset>
);
}4. Feedback
After selection:
- Correct option highlighted green.
- Wrong selection highlighted red.
- Options disabled — you can't change your mind.
- "Next" button appears.
5. Score summary
function Summary({ score, total, onRestart }) {
return (
<div>
<h2>You scored {score} / {total}</h2>
<button onClick={onRestart}>Play again</button>
</div>
);
}6. Accessibility
<fieldset>+<legend>for the question — proper radio group semantics.- Labels wrapping inputs (clickable).
- Don't rely on color alone for feedback — add icon or text.
- Announce correctness via a polite live region.
- Keyboard: arrow keys to navigate options (built-in for radio groups), Enter to pick.
7. The polish
- Timer per question with auto-fail on timeout.
- Streak bonus.
- Difficulty levels (more distractors).
- Persistent high score (localStorage).
- Reverse mode — given a capital, pick the country.
- Hints (continent / population).
- Fisher-Yates shuffle instead of biased
sort(() => Math.random() - 0.5).
Interview framing
"Generate N questions upfront: for each, pick a country and 3 distractor capitals from other entries (avoid duplicates), shuffle the 4 options. State machine is asking → answered → next, with a score counter. After selecting, lock the options, show feedback (color + icon, not color-only), then enable Next. End screen shows score and a restart. Use <fieldset>+<legend> + labeled radios for a proper radio group with built-in keyboard nav. Fisher-Yates for the shuffle — Math.random() - 0.5 is biased."
Follow-up questions
- •Why is `sort(() => Math.random() - 0.5)` biased?
- •How would you persist score and add a leaderboard?
- •How would you add a timer per question?
- •Why use fieldset/legend for the question?
Common mistakes
- •Biased shuffle.
- •Allowing re-answering after seeing feedback.
- •Picking the answer as a distractor (duplicate option).
- •Color-only feedback.
- •Re-randomizing distractors per render.
Performance considerations
- •Trivial — small data. Memoize the questions array (generated once on mount). Avoid regenerating distractors on every render.
Edge cases
- •Country with multiple acceptable answers.
- •Fewer than 4 distractors available.
- •Empty data set.
Real-world examples
- •Duolingo-style multiple-choice quizzes, Sporcle, geography apps.