The Codex / Projects / Quiz / JavaScript
Tier 0 — Absolute Beginner

Quiz

Build a multiple-choice quiz — question bank, async terminal input, grading, and score feedback.

🐍 Python 🟨 JavaScript

1. The Problem

Build a multiple-choice quiz that presents questions one at a time (in the terminal or browser), accepts answers, and shows the final score with correct/incorrect feedback.

Why this project? Separating data (the question bank) from logic (the quiz runner) from presentation (the UI) — the three-layer pattern used in every real application.

2. How to Think About This

Store questions as an array of objects — each with a question string, an options array, and the index of the correct answer. The quiz runner is a pure function that takes the questions and user answers and returns results. The CLI or browser UI handles the back-and-forth with the user. Three separate layers, each testable independently.

3. The Build

quiz.mjs
import * as readline from "readline/promises";
import { stdin, stdout } from "process";

// ── Question bank ──────────────────────────────────────────────────────────
const questions = [
  {
    q:       "What keyword declares a block-scoped variable in modern JS?",
    options: ["var", "let", "dim", "val"],
    answer:  1,
  },
  {
    q:       "Which value is NOT falsy in JavaScript?",
    options: ["0", """", "[]", "null"],
    answer:  2,   // [] is truthy!
  },
  {
    q:       "What does typeof null return?",
    options: [""null"", ""undefined"", ""object"", ""boolean""],
    answer:  2,
  },
  {
    q:       "Which method adds an item to the END of an array?",
    options: ["unshift", "shift", "push", "pop"],
    answer:  2,
  },
  {
    q:       "What does === check?",
    options: ["Value only", "Type only", "Value AND type", "Reference"],
    answer:  2,
  },
];

// ── Pure quiz engine ───────────────────────────────────────────────────────
export function gradeAnswers(answers, qs = questions) {
  let score = 0;
  const results = qs.map((q, i) => {
    const correct = answers[i] === q.answer;
    if (correct) score++;
    return { question: q.q, correct,
             yourAnswer: q.options[answers[i]] ?? "not answered",
             correctAnswer: q.options[q.answer] };
  });
  return { score, total: qs.length, percent: Math.round(score / qs.length * 100), results };
}

// ── CLI presentation ───────────────────────────────────────────────────────
const rl = readline.createInterface({ input: stdin, output: stdout });
const answers = [];

console.log("\n=== JavaScript Quiz ===");
console.log(`${questions.length} questions. Enter the number of your answer.\n`);

for (let i = 0; i < questions.length; i++) {
  const q = questions[i];
  console.log(`Q${i + 1}: ${q.q}`);
  q.options.forEach((opt, idx) => console.log(`  ${idx + 1}. ${opt}`));

  let answer = null;
  while (answer === null) {
    const raw = await rl.question("Your answer (1-4): ");
    const n = parseInt(raw, 10) - 1;   // convert 1-based to 0-based index
    if (n >= 0 && n < q.options.length) {
      answer = n;
    } else {
      console.log("Please enter a number between 1 and", q.options.length);
    }
  }
  answers.push(answer);
  console.log();
}

rl.close();

// Display results
const { score, total, percent, results } = gradeAnswers(answers);
console.log("=== Results ===");
results.forEach((r, i) => {
  const icon = r.correct ? "✓" : "✗";
  console.log(`\n${icon} Q${i + 1}: ${r.question}`);
  if (!r.correct) {
    console.log(`   Your answer: ${r.yourAnswer}`);
    console.log(`   Correct:     ${r.correctAnswer}`);
  }
});

console.log(`\nScore: ${score}/${total} (${percent}%)`);
if (percent === 100) console.log("Perfect score! 🎉");
else if (percent >= 80) console.log("Great work! 🌟");
else if (percent >= 60) console.log("Good effort — review the questions you missed.");
else console.log("Keep studying — you've got this!");

4. Test & Prove

quiz.test.mjs
import { gradeAnswers } from "./quiz.mjs";

const sampleQs = [
  { q:"Q1", options:["a","b","c"], answer:0 },
  { q:"Q2", options:["x","y","z"], answer:2 },
  { q:"Q3", options:["p","q"],     answer:1 },
];

let pass=0, fail=0;
function test(desc,fn){try{fn();console.log("  ✓",desc);pass++;}catch(e){console.log("  ✗",desc,"—",e.message);fail++;}}

test("all correct = 100%", () => {
  const r = gradeAnswers([0,2,1], sampleQs);
  if(r.score!==3||r.percent!==100) throw new Error(JSON.stringify(r));
});
test("all wrong = 0%", () => {
  const r = gradeAnswers([1,0,0], sampleQs);
  if(r.score!==0||r.percent!==0) throw new Error(JSON.stringify(r));
});
test("2/3 = 66%", () => {
  const r = gradeAnswers([0,2,0], sampleQs);
  if(r.score!==2) throw new Error(r.score);
});
test("results array length matches questions", () => {
  const r = gradeAnswers([0,0,0], sampleQs);
  if(r.results.length!==3) throw new Error(r.results.length);
});
test("correct flag set correctly", () => {
  const r = gradeAnswers([0,2,1], sampleQs);
  if(!r.results.every(x=>x.correct)) throw new Error("not all correct");
});

console.log(\`
\${pass} passed, \${fail} failed\`);

5. The Interface

Input: one number per question (1–4), entered at the terminal.

Output: each question graded (✓/✗), wrong answers shown with the correct one, final score and percentage.

6. Run It

node quiz.mjs        # run the interactive quiz
node quiz.test.mjs   # run the tests (non-interactive)

🎯 Try this next

  1. Shuffle the questions using the Fisher-Yates algorithm so each run is different
  2. Add a time limit per question using setTimeout + Promise.race
  3. Save the high score to a JSON file using fs.writeFile
  4. Build a browser version — render questions as HTML, accept clicks instead of keypresses

What you practised

Three-layer architecture (data / engine / presentation), async terminal input with readline/promises, input validation with a while loop, array of objects as a data structure, and exporting pure functions for isolated testing.

Reference: Arrays · Objects · Async & Promises

Try It Yourself