Quiz chronométré
Cinq questions tirées d'un fichier JSON, quinze secondes pour chacune, et des points en plus pour les plus rapides. Le bon exercice pour séparer les données de l'interface et gérer un compte à rebours précis.
- Charger des questions depuis un JSON
- Mesurer le temps restant avec performance.now
- Mélanger un tableau avec l'algorithme de Fisher-Yates
Pourquoi un quiz chronométré ? Le quiz de base laisse tout le temps de réfléchir. Avec un chrono, il devient un jeu : on répond vite, et la vitesse rapporte des points.
Les questions ne sont plus écrites dans le code. Elles vivent dans un fichier JSON : on peut en ajouter, les traduire ou les faire écrire par quelqu'un d'autre sans toucher au JavaScript. À chaque partie, on en tire cinq dans un ordre différent.
Le temps restant se calcule à partir de l'heure de départ, et une barre se vide en continu. Au bout de quinze secondes, la question est comptée comme ratée et la bonne réponse s'affiche avec une explication.
Pourquoi en JavaScript simple ? Un fetch, un mélange, une boucle d'animation. Le clavier marche aussi : A, B, C, D ou 1 à 4.
LES QUESTIONS DANS UN FICHIER
questions.json : [{ "q": "...", "r": ["...", "..."], "b": 1, "e": "explication" }]
const questions = await fetch('/questions.json').then(r => r.json());MÉLANGER SANS BIAIS
Fisher-Yates : chaque ordre a exactement la même chance.
function melanger(tab) { const a = [...tab]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } const partie = melanger(questions).slice(0, 5);LE CHRONO
On note le départ, et on calcule le reste à chaque image.
const DUREE = 15; let debut, raf; const reste = () => Math.max(0, DUREE - (performance.now() - debut) / 1000); function tic() { const r = reste(); barre.style.transform = `scaleX(${r / DUREE})`; chiffre.textContent = Math.ceil(r); if (r <= 0) return repondre(-1); // temps écoulé raf = requestAnimationFrame(tic); }RÉPONDRE
Bonne réponse : 100 points, plus 10 par seconde restante.
function repondre(choix) { cancelAnimationFrame(raf); const q = partie[index], ok = choix === q.b; score += ok ? 100 + Math.round(reste() * 10) : 0; montrerCorrection(q, choix); }LANCER UNE QUESTION
function poser() { afficherQuestion(partie[index]); debut = performance.now(); tic(); }
PIÈGE 1 : Un mélange biaisé
Mauvais : tab.sort(() => Math.random() - 0.5). Certains ordres sortent bien plus souvent que d'autres.
Correct : l'algorithme de Fisher-Yates, qui échange chaque case avec une case tirée avant elle.
PIÈGE 2 : Oublier d'arrêter le chrono
Mauvais : répondre sans annuler l'animation. Le chrono atteint zéro plus tard et compte une deuxième réponse.
Correct : cancelAnimationFrame dès la réponse, et désactiver les boutons.
PIÈGE 3 : Mettre la question dans innerHTML
Mauvais : innerHTML = q.q. Une question sur la balise <title> casse l'affichage, et un fichier piégé peut injecter du code.
Correct : textContent pour tout ce qui vient du fichier.
- Des catégories, choisies au début de la partie.
- Un mode « mort subite » : la première erreur termine la partie.
- Les meilleurs scores, enregistrés comme dans le Snake avec classement.
- Validez le fichier JSON au chargement : une question sans bonne réponse doit être ignorée, pas faire planter le jeu.
- Chargez les questions une fois et gardez-les en mémoire pour les parties suivantes.
Les questions se chargent dans un useEffect ou un composant serveur avec Next.js.
L'index, le score et les réponses vivent dans un useReducer : chaque action, répondre ou passer, est un cas clair.
Le chrono reste dans un useRef pour ne pas provoquer soixante rendus par seconde.
Chargement des commentaires…