Компьютер уже выбирает ход из массива. Теперь добавим три кнопки игрока и правило определения победителя. Сначала заработает один раунд, затем счёт, матч до трёх побед и полный перезапуск. Проверим правила на всех девяти сочетаниях, чтобы не пропустить ошибку из-за случайного выбора ходов.

Открыть эту версию отдельно

Попробуй сыграть матч и начать новый. Здесь мы шаг за шагом собираем игру с тремя кнопками и счётом.

Продолжаем игру из прошлого урока

Нужен урок о массивах и случайном выборе. Папка start этого комплекта содержит те же файлы, что его final: список moves, функция chooseComputerMove и кнопка выбора компьютера. Продолжай свою папку или возьми start; эксперимент с репликами оставь отдельно.

Исходники урока

Распакуй ZIP целиком. У каждого этапа своя папка: index.html, style.css и game.js лежат рядом. Открывай HTML двойным щелчком; после правки сохраняй файл и обновляй браузер. Установка, аккаунт и сервер для этого примера не нужны. Инструкция в просмотрщике объясняет версии; solutions/ оставь до самостоятельной попытки.

Скачать ZIP

Версии: step-01 — один раунд; step-02 — счёт без окончания; step-03 — матч до трёх побед; final — новая партия. Каждый этап можно запускать. Внимательно следи за файлом и местом вставки, чтобы не оставить старый обработчик удалённой кнопки.

Игрок выбирает ход

В index.html поменяй заголовок h1 и название вкладки title на «Камень, ножницы, бумага». Удали старый абзац с computer-move и кнопку choose-button. На их место добавь:

<div class="choices">
  <button id="rock-button" type="button">Камень</button>
  <button id="scissors-button" type="button">Ножницы</button>
  <button id="paper-button" type="button">Бумага</button>
</div>
<div class="stats">
  <p>Твой ход: <strong id="player-move">—</strong></p>
  <p>Ход компьютера: <strong id="computer-move">—</strong></p>
</div>
<p id="round-result" class="result" role="status">Выбери ход.</p>

Здесь снова только обычные кнопки. Их разные id позволяют подключить три понятных действия. computer-move остался, но его элемент заменён вместе с разметкой — на странице не должно быть двух одинаковых id. Пустое состояние показываем знаком «—», пока игрок ещё не ходил.

В game.js сохрани массив moves и функцию chooseComputerMove из предыдущего занятия. Удали старые render, handleComputerChoice, chooseButton, его регистрацию и последний вызов render();. Вместо прежнего состояния и поиска элементов добавь сразу после массива:

let playerMove = '—';
let computerMove = '—';
let roundResult = '';

const playerMoveElement = document.getElementById('player-move');
const computerMoveElement = document.getElementById('computer-move');
const resultElement = document.getElementById('round-result');
const rockButton = document.getElementById('rock-button');
const scissorsButton = document.getElementById('scissors-button');
const paperButton = document.getElementById('paper-button');

roundResult пока пустая строка. Дальше договоримся хранить в ней короткий код результата: draw — ничья, player — победил игрок, computer — победил компьютер. Код правила не будет зависеть от текста сообщения на экране.

Переводим словесное правило в функцию

До написания кода сформулируй правило. Одинаковые ходы дают ничью. Игрок выигрывает в трёх случаях: камень против ножниц, ножницы против бумаги, бумага против камня. Для остальных допустимых сочетаний выигрывает компьютер.

После chooseComputerMove добавь функцию:

function determineResult(playerChoice, computerChoice) {
  if (playerChoice === computerChoice) {
    return 'draw';
  }
  if (playerChoice === 'камень' && computerChoice === 'ножницы') {
    return 'player';
  }
  if (playerChoice === 'ножницы' && computerChoice === 'бумага') {
    return 'player';
  }
  if (playerChoice === 'бумага' && computerChoice === 'камень') {
    return 'player';
  }
  return 'computer';
}

Два параметра — два входных хода. Первый if обрабатывает сразу три ничьи и завершает вызов через return. Затем каждое сочетание победы описано отдельным понятным условием; && требует совпадения обоих ходов. Если ни одно не подошло, возвращаем computer.

В каждый аргумент передаём одно из трёх слов из moves, без изменений в написании. Она не проверяет произвольный текст пользователя: его здесь нет, потому что ход игрока задают наши кнопки, а ход компьютера берётся из массива. При добавлении четвёртого жеста одних данных уже не хватит — придётся определить новые правила.

Все девять сочетаний

Не пытайся проверить правило только случайными партиями. Из трёх вариантов каждого участника получается девять пар. В таблице результат указан с точки зрения игрока:

Игрок ↓ / компьютер →КаменьНожницыБумага
Каменьdrawplayercomputer
Ножницыcomputerdrawplayer
Бумагаplayercomputerdraw

Сохрани game.js, обнови страницу и выполни эти вызовы в Console. Они проверяют правило отдельно от кнопок, не меняя состояние игры:

console.log(determineResult('камень', 'камень'));   // draw
console.log(determineResult('камень', 'ножницы'));  // player
console.log(determineResult('камень', 'бумага'));   // computer
console.log(determineResult('ножницы', 'камень'));  // computer
console.log(determineResult('ножницы', 'ножницы')); // draw
console.log(determineResult('ножницы', 'бумага'));  // player
console.log(determineResult('бумага', 'камень'));   // player
console.log(determineResult('бумага', 'ножницы'));  // computer
console.log(determineResult('бумага', 'бумага'));   // draw

Сверь каждую строку с таблицей. Если не совпала одна пара, сначала проверь именно её условие и порядок аргументов. Не исправляй случайный выбор ради ошибки сравнения. Здесь всего девять сочетаний, поэтому каждое можно проверить отдельным вызовом.

Проводим и показываем один раунд

После determineResult добавь отображение и игровое действие:

function render() {
  playerMoveElement.textContent = playerMove;
  computerMoveElement.textContent = computerMove;
  if (roundResult === '') {
    resultElement.textContent = 'Выбери ход.';
  } else if (roundResult === 'draw') {
    resultElement.textContent = 'Ничья в раунде.';
  } else if (roundResult === 'player') {
    resultElement.textContent = 'Ты выиграл раунд.';
  } else {
    resultElement.textContent = 'Компьютер выиграл раунд.';
  }
}
function playRound(playerChoice) {
  playerMove = playerChoice;
  computerMove = chooseComputerMove();
  roundResult = determineResult(playerMove, computerMove);
  render();
}

Вызов playRound сначала запоминает ход игрока, затем один раз выбирает компьютерный. Только после этого определяет результат по этим двум значениям и вызывает render. Функция render лишь переводит код результата в сообщение; повторная отрисовка не должна проводить новый раунд.

Внизу файла добавь именованные обработчики и однократную регистрацию:

function handleRock() {
  playRound('камень');
}


function handleScissors() {
  playRound('ножницы');
}


function handlePaper() {
  playRound('бумага');
}


rockButton.addEventListener('click', handleRock);
scissorsButton.addEventListener('click', handleScissors);
paperButton.addEventListener('click', handlePaper);
render();

Обработчик передаётся браузеру без скобок, а уже внутри него вызывается playRound с конкретным аргументом. Так обработчик кнопки передаёт выбранный ход в функцию раунда. Повторяющиеся три короткие регистрации сейчас помогают видеть связь каждой кнопки с действием; обобщать их заранее не требуется.

Проверка step-01: при загрузке «Выбери ход», после нажатия видны оба хода и соответствующее сообщение. Проверь каждую кнопку и одну активацию через Tab/Enter. На следующем нажатии начинается новый раунд. Счёта и матча пока нет.

Начисляем очко победителю

В index.html после сообщения round-result добавь:

<p>Счёт: ты <strong id="player-score">0</strong> : <strong id="computer-score">0</strong> компьютер</p>

После остальных переменных состояния добавь два числа, а рядом с поиском элементов — две ссылки:

let playerScore = 0;
let computerScore = 0;

const playerScoreElement = document.getElementById('player-score');
const computerScoreElement = document.getElementById('computer-score');

Внутри playRound после присваивания roundResult и перед render добавь:

if (roundResult === 'player') {
  playerScore = playerScore + 1;
} else if (roundResult === 'computer') {
  computerScore = computerScore + 1;
}

У ничьей нет ветки начисления, поэтому оба числа остаются прежними. Не начисляй очко в render: тогда каждый вывод или изменение текста превращались бы в ещё одну победу. В конце render, перед закрывающей скобкой, добавь:

playerScoreElement.textContent = playerScore;
computerScoreElement.textContent = computerScore;

Это step-02: счёт накапливается, но верхней границы пока нет. Проверь хотя бы одну ничью: после неё счёт не растёт. Чтобы отделить проблему правила от проблемы отображения, сравни playerScore и computerScore в Console с надписями. Перезагрузка страницы пока обнуляет их.

Матч заканчивается на трёх победах

После счёта в HTML добавь <p id="match-status">Матч до 3 побед.</p>. В JS добавь к состоянию и ссылкам:

let matchOver = false;
const targetScore = 3;
const matchStatusElement = document.getElementById('match-status');

В самое начало playRound поставь защиту. После начисления очков, перед render, проверь цель:

// В начале playRound:
if (matchOver) {
  return;
}

// После начисления очка, перед render:
if (playerScore >= targetScore || computerScore >= targetScore) {
  matchOver = true;
}

Это две разные вставки в одну функцию. Ничья может увеличить число сыгранных раундов, но не приближает счёт к трём. Поэтому матч не обязательно длится три раунда. Проверка через || завершает его при достижении цели любым участником. Защита стоит до случайного выбора и всех изменений: лишнее действие после окончания не должно даже менять показанные ходы.

В конец render добавь:

if (matchOver) {
  if (playerScore >= targetScore) {
    matchStatusElement.textContent = 'Ты выиграл матч!';
  } else {
    matchStatusElement.textContent = 'Компьютер выиграл матч.';
  }
} else {
  matchStatusElement.textContent = 'Матч до ' + targetScore + ' побед.';
}
rockButton.disabled = matchOver;
scissorsButton.disabled = matchOver;
paperButton.disabled = matchOver;

Вложенный if проверяется только внутри завершённого матча: сначала выясняем, закончилась ли игра, затем кто достиг цели. Это те же условия и фигурные скобки, только одна проверка расположена внутри другой. Проверки выполняются в том порядке, в котором мы их вложили.

Проверка step-03: при 2 очках матч продолжается, при 3 — заканчивается. Сброс пока через обновление страницы. После результата попробуй playRound('камень') в Console: ни счёт, ни последние ходы не должны измениться. Кнопки disabled подсказывают запрет, а ранний return обеспечивает его.

Добавляем кнопку нового матча

Добавь после match-status в HTML:

<div class="actions">
  <button id="reset-button" class="secondary" type="button">Новый матч</button>
</div>

Рядом с другими ссылками в JS найди reset-button, ниже игровых обработчиков добавь сброс, а его регистрацию поставь вместе с остальными перед последним render:

const resetButton = document.getElementById('reset-button');

function handleReset() {
  playerMove = '—';
  computerMove = '—';
  roundResult = '';
  playerScore = 0;
  computerScore = 0;
  matchOver = false;
  render();
}


resetButton.addEventListener('click', handleReset);

Это final. Сбрасываем не только цифры: старый код результата и последние ходы тоже относятся к состоянию. Флаг matchOver снова false, поэтому render разблокирует три кнопки. Массив moves и цель targetScore не менялись в течение матча, их восстанавливать не нужно. Обработчики при сбросе повторно не регистрируем.

СценарийОжидаемый результат
Свежий старт0:0, ходы «—», кнопки доступны
НичьяСчёт прежний, оба хода видны
Победный раунд при счёте 2 у участникаСчёт становится 3, матч завершён
Повторное действие после окончанияДанные не меняются
Новый матч после победы или посреди игрыПолное начальное состояние
Пять быстрых сбросов, затем один ходРовно один раунд; не пять начислений

Самостоятельно: матч из пяти раундов

Сохрани базовый final, затем в отдельной копии измени условие окончания: матч длится ровно пять раундов, включая ничьи. Выигрывает тот, у кого больше очков. Если очки равны, весь матч заканчивается вничью. До пятого раунда игра продолжается даже при трёх победах подряд. Новый матч должен обнулить и счётчик раундов.

Теперь нужно отдельно считать раунды: победы и ничьи вместе. Сначала запиши, какие данные понадобятся и где они меняются. Предскажи результат на четвёртом и пятом раундах. Проверь, что шестой вызов не меняет состояние, а пять ничейных раундов завершают матч со счётом 0:0.

Подсказка 1: отдели раунд от победы

Два существующих счётчика считают победы. Потребуется ещё одно число для всех раундов. Увеличивай его один раз после получения результата, независимо от ничьей.

Подсказка 2: конец и победитель — разные проверки

Сравни число сыгранных раундов с пределом 5 для окончания. После окончания отдельно сравни playerScore и computerScore. Если ни один не больше другого, результат матча — ничья. Новый счётчик также добавь в handleReset.

Решение после попытки

Запустить вариант на пять раундов. Исходники ответа и разбор находятся в solutions скачанного архива. Сначала сравни своё правило завершения, затем сброс. Если воспользовался готовой функцией, повтори другой ограниченный вариант без неё и объясни каждое изменение.

Проверь готовую игру

Ты можешь прочитать функцию и объяснить правило своими словами, проверяешь все девять пар, различаешь результат раунда и матча, меняешь условие завершения и проверяешь границы. Покажи несколько новых матчей: сброс не оставляет старые ходы или очки. Сохрани рабочий результат и свою вариацию в Git, как в предыдущем модуле.

Для продолжения сохрани базовый final — матч до трёх побед. Расширенная игра в «Поиграть» остаётся отдельным дополнением.