У викторины всего три состояния: текущий вопрос, число правильных ответов и выбран ли уже вариант. Если хранить их явно, кнопки не начислят два балла, следующий вопрос появится вовремя, а повторный раунд начнётся с нуля.
Новички часто создают отдельную HTML-кнопку для каждого вопроса. Тогда изменение текста требует править разметку, а логика расползается по обработчикам. Мы положим данные в массив и будем рисовать один универсальный экран.
Вопрос, варианты и индекс ответа лежат в одном объекте.
Одна renderQuestion обновляет заголовок, прогресс и кнопки.
Флаг answered блокирует повторный балл до перехода дальше.
Как представить вопросы в данных
У каждого вопроса есть text, answers и correct. Индекс текущего вопроса начинается с нуля, score тоже. После клика кнопки получают disabled, правильный вариант отмечается всегда, а выбранный неправильный получает отдельный класс.
| Поле | Тип | Пример |
|---|---|---|
| text | string | Что делает const? |
| answers | string[] | Массив из трёх вариантов |
| correct | number | Индекс правильного варианта |
| current | number | Какой объект сейчас показывать |
После этого шага у каждой части есть одна роль. Интерфейс собирает действие, логика меняет состояние, а вывод показывает признак успеха. Это проще отлаживать, чем один большой обработчик.

answered отделяет выбор от перехода. После последнего вопроса появляется итог
Пишем рендер и проверку ответа
Используйте делегирование событий: один обработчик стоит на контейнере ответов. Индекс ответа хранится в data-index. После выбора мы сначала ставим answered=true, а уже затем считаем и красим, поэтому быстрый двойной клик не изменит счёт дважды.
const questions = [
{text:'Как объявить неизменяемую ссылку?',answers:['let','const','var'],correct:1},
{text:'Что возвращает Array.isArray([])?',answers:['true','false','undefined'],correct:0},
{text:'Какое событие вызывается кнопкой?',answers:['click','submitter','press'],correct:0},
];
let current=0, score=0, answered=false;
const title=document.querySelector('#question');
const answers=document.querySelector('#answers');
const progress=document.querySelector('#progress');
function renderQuestion(){
answered=false;
const item=questions[current];
progress.textContent='Вопрос ' + (current+1) + ' из ' + questions.length;
title.textContent=item.text;
answers.innerHTML=item.answers.map((text,index)=>
'<button data-index="' + index + '">' + text + '</button>').join('');
}
answers.addEventListener('click',event=>{
const button=event.target.closest('button');
if(!button || answered) return;
answered=true;
const chosen=Number(button.dataset.index);
if(chosen===questions[current].correct) score++;
[...answers.children].forEach((item,index)=>{
item.disabled=true;
if(index===questions[current].correct) item.classList.add('correct');
else if(index===chosen) item.classList.add('wrong');
});
});
document.querySelector('#next').addEventListener('click',()=>{
if(!answered) return;
current++;
if(current<questions.length) renderQuestion();
else document.querySelector('#quiz').textContent='Результат: ' + score + '/' + questions.length;
});
renderQuestion();Вопросы отделены от разметки и легко дополняются. Один контейнер обрабатывает все варианты. После ответа кнопки блокируются и показывают правильный. Финальный экран считает результат из длины массива.

Данные не должны копировать интерфейс. Сравнение показывает, какой вариант легче поддерживать и проверять.
Как не начислить один балл дважды
innerHTML здесь получает строки из вашего статического массива. Если вопросы приходят от пользователей или внешнего API, не вставляйте их как HTML: создавайте кнопки через createElement и назначайте textContent. Это не даёт данным превратиться в разметку или скрипт.
| Состояние | Разрешённое действие | Следующий шаг |
|---|---|---|
| answered=false | Выбрать вариант | Проверить и заблокировать |
| answered=true | Нажать Далее | current + 1 |
| current < length | Показать вопрос | Сбросить answered |
| current = length | Показать итог | Предложить повтор |
Сохраняйте в одном месте то, что может измениться. Если одно и то же значение скопировано в три функции, после первой правки они начнут расходиться. Один объект состояния и одна функция отрисовки снимают эту проблему.
Практика: проходим все ветки
Добавьте вопрос, где правильный ответ стоит последним, затем первый и средний. Дважды быстро нажмите один вариант, попробуйте кнопку «Далее» до выбора и пройдите тест до конца без правильных ответов. Итог должен оставаться предсказуемым. После этого пройдите все вопросы правильно и убедитесь, что знаменатель результата всегда берётся из длины массива.
- Добавьте четвёртый вопрос.
- Проверьте правильный ответ с индексом 0.
- Нажмите Далее до ответа.
- Сделайте двойной клик по варианту.
- Пройдите все вопросы неправильно.
- Добавьте кнопку «Начать заново», обнуляющую три состояния.
Один ответ даёт максимум один балл. Правильный вариант виден даже после ошибки. Далее не работает до выбора. Новый раунд обнуляет current, score и answered.

Игра готова после крайних сценариев. Пройдите все четыре сценария перед публикацией проекта.
Доводим викторину до финального экрана
После базовой версии можно перемешать вопросы, добавить пояснение и сохранять лучший результат. Не добавляйте всё сразу. Сначала вынесите resetQuiz и проверьте, что новый раунд полностью независим от старого.
Любое изменение требует копировать разметку.
Двойной клик меняет счёт дважды.
Можно пропускать состояние без явного решения.
Внешние данные могут стать разметкой.
Что получится в итоге

Финальная викторина показывает прогресс, крупные варианты, обратную связь после клика и аккуратный итоговый счёт.
Зачем хранить вопросы в массиве?
Данные можно менять без копирования интерфейса и обработчиков.
Что защищает от двойного балла?
Флаг answered и disabled у кнопок.
Когда innerHTML опасен?
Когда внутрь попадают непроверенные внешние или пользовательские данные.
Как понять, что проект готов?
Он повторяет целевой сценарий несколько раз подряд, понятно реагирует на ошибочный ввод и не теряет состояние при обычных действиях.
Можно ли копировать код целиком?
Можно как стартовую точку, но сначала замените значения из примера, затем запустите проект по шагам. Если вы не можете объяснить одну строку, её лучше разобрать до следующего шага.
Массивы, объекты и события разбираются в курсе JavaScript. Если обработчик не находит кнопку, проверьте ошибку null.
Состояние между запусками можно сохранить по примеру Todo-листа, а таймер раунда добавить по статье об обратном отсчёте. В Кодике удобно тестировать каждый вопрос отдельно.