DEV Community

Akbo Ichou
Akbo Ichou

Posted on

Building a Fair Trivia Quiz Engine: Unbiased Shuffling, Answer Position Balance and Team Scoring

A trivia quiz looks like the simplest app imaginable: show a question, show four options, check the answer. But the details decide whether it feels fair — and whether a room full of students (or friends at a party) quickly figures out that "the answer is usually C."

Trivia Questions and Answers is a free collection of trivia quizzes by category, used for everything from game nights to classroom review games for middle schoolers and teens. Here's what goes into a quiz engine that holds up.

1. Shuffle correctly (Fisher–Yates)

The classic mistake is array.sort(() => Math.random() - 0.5). It produces a biased shuffle — some orders appear far more often than others. Use Fisher–Yates:

function shuffle(arr, rng = Math.random) {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}
Enter fullscreen mode Exit fullscreen mode

Shuffle both the question order and each question's options.

2. Balance where the correct answer lands

Even with an unbiased shuffle, a 10-question round can randomly put the right answer in slot B five times. Players notice. You can balance positions across a round so each slot gets roughly equal use:

function balancedPositions(nQuestions, nOptions, rng) {
  const slots = [];
  while (slots.length < nQuestions) {
    slots.push(...shuffle([...Array(nOptions).keys()], rng));
  }
  return slots.slice(0, nQuestions);
}

function placeAnswer(q, slot, rng) {
  const wrong = shuffle(q.options.filter((o) => o !== q.answer), rng);
  wrong.splice(slot, 0, q.answer);
  return { ...q, options: wrong };
}
Enter fullscreen mode Exit fullscreen mode

3. Watch out for "all of the above"

Options like "All of the above" or "Both A and B" break when shuffled. Mark them as pinned and keep them last:

const pinned = q.options.filter((o) => /all of the above|none of the above/i.test(o));
const free = q.options.filter((o) => !pinned.includes(o));
const options = [...shuffle(free), ...pinned];
Enter fullscreen mode Exit fullscreen mode

4. Normalize typed answers

For free-response rounds, "The Beatles", "beatles" and "Beatles!" should all count:

const norm = (s) =>
  s.toLowerCase()
   .normalize("NFD").replace(/[\u0300-\u036f]/g, "")  // strip accents
   .replace(/^(the|a|an)\s+/, "")
   .replace(/[^a-z0-9]/g, "");

const isCorrect = (input, answers) => answers.some((a) => norm(a) === norm(input));
Enter fullscreen mode Exit fullscreen mode

5. Team scoring for classrooms

In a classroom the quiz is projected and the class plays in teams. A tiny scoring model covers it, including bonus points when a team explains why an answer is right — which turns a quiz into actual review:

function createScoreboard(teams) {
  const scores = Object.fromEntries(teams.map((t) => [t, 0]));
  return {
    correct: (team, pts = 1) => (scores[team] += pts),
    explainBonus: (team) => (scores[team] += 1),
    standings: () => Object.entries(scores).sort((a, b) => b[1] - a[1]),
  };
}
Enter fullscreen mode Exit fullscreen mode

6. Write for the audience

The engine is only half of it. Questions for middle schoolers and teens need to be age-appropriate, unambiguous and checkable; every answer should have one clearly defensible correct option. Organizing by category and audience makes it easy for a teacher to grab a relevant round at the end of a lesson.

Takeaways

  • Never shuffle with sort(() => Math.random() - 0.5).
  • Balance the correct answer's position across a round.
  • Pin "all/none of the above" options.
  • Normalize typed answers generously.

Grab a quiz at triviaquestionsandanswers.net. What's the sneakiest bias you've found in a quiz or game?

Top comments (0)