Как сделать игру «Камень, ножницы, бумага» на JavaScript и HTML с нуля? Мини-игра кажется простой, но в ней есть весь базовый цикл фронтенд-разработки: разметка интерфейса, стили, обработка действий пользователя, обновление состояния и вывод результата. В этом разборе мы соберём ту же финальную версию, которая уже работает в разделе «Поиграть».

Что получится в конце

Готовая HTML-игра на чистом JavaScript: выбор хода, случайный ответ компьютера, счёт игрока и компьютера, серия побед, история последних раундов и сброс без перезагрузки страницы.

Сначала попробуйте готовую игру

Сыграйте несколько раундов, чтобы зафиксировать целевое поведение. После этого код читается намного быстрее: вы сразу понимаете, зачем нужен каждый блок.

Открыть игру отдельно

Как сделать игру «Камень, ножницы, бумага»: структура проекта

Финальная версия состоит из трёх файлов. Именно их мы разбираем в статье, и именно они лежат в блоке исходников внизу.

kamen-nozhnitsy-bumaga/
├── source.html
├── style.css
└── game.js

HTML-разметка игры «Камень, ножницы, бумага»

Разметка описывает не только визуальные блоки, но и точки привязки логики. Атрибуты data-rps-* позволяют JavaScript точно находить элементы без жёсткой привязки к структуре класса или тега.

<main class="rps" data-rps-game>
  <header class="rps__header">
    <h1>Камень, ножницы, бумага</h1>
    <p>Выбирайте ход и набирайте серии побед против компьютера.</p>
  </header>

  <section class="rps__choices" aria-label="Выбор хода">
    <button type="button" data-choice="камень">Камень</button>
    <button type="button" data-choice="ножницы">Ножницы</button>
    <button type="button" data-choice="бумага">Бумага</button>
  </section>

  <p class="rps__result" data-rps-result aria-live="polite">Сделайте первый ход.</p>

  <section class="rps__stats" aria-label="Текущий счёт">
    <p data-rps-score>Счёт: 0 : 0</p>
    <p data-rps-streak>Серия побед: 0</p>
  </section>

  <button class="rps__reset" type="button" data-rps-reset>Сбросить счёт</button>

  <section class="rps__history-wrap" aria-label="История раундов">
    <h2>Последние раунды</h2>
    <ul class="rps__history" data-rps-history>
      <li>Пока нет раундов.</li>
    </ul>
  </section>
</main>

CSS-стили для игры: адаптивный интерфейс

Стили разделены по ролям: контейнер, управляющие элементы, статистика и история. На широком экране кнопки стоят в три колонки, на мобильном переходят в одну колонку для удобного нажатия.

.rps {
  width: min(720px, 100%);
  background: rgba(9, 14, 30, 0.84);
  border: 1px solid rgba(133, 157, 255, 0.35);
  border-radius: 20px;
  padding: 20px;
}

.rps__choices {
  margin-top: 18px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

.rps__choices button,
.rps__reset {
  border: 0;
  border-radius: 12px;
  cursor: pointer;
  font-weight: 600;
}

@media (max-width: 560px) {
  .rps__choices {
    grid-template-columns: 1fr;
  }
}

JavaScript-логика: правила «камень, ножницы, бумага»

Вместо длинной цепочки if/else используем таблицу победных связей. Такой подход компактный и проще расширяется, если позже добавить новые варианты.

let options = ["камень", "ножницы", "бумага"];
let winMap = {
  камень: "ножницы",
  ножницы: "бумага",
  бумага: "камень",
};

function roundResult(player, cpu) {
  if (player === cpu) return "draw";
  if (winMap[player] === cpu) return "win";
  return "lose";
}

JavaScript: счёт, серия побед и история раундов

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

let playerScore = 0;
let cpuScore = 0;
let winStreak = 0;
let history = [];

function playRound(playerChoice) {
  let cpuChoice = randomChoice();
  let result = roundResult(playerChoice, cpuChoice);

  if (result === "win") {
    playerScore += 1;
    winStreak += 1;
  } else if (result === "lose") {
    cpuScore += 1;
    winStreak = 0;
  } else {
    winStreak = 0;
  }

  history.unshift(
    "Вы: " + playerChoice + " / Компьютер: " + cpuChoice + " - " + resultLabel(result)
  );
  if (history.length > 6) {
    history = history.slice(0, 6);
  }

  updateScore();
  renderHistory();
}

Обработчики кликов и сброс

Все кнопки выбора используют один общий обработчик по атрибуту data-choice. Кнопка сброса возвращает игру к старту без перезагрузки страницы: очищает счёт, серию, историю и текст результата.

Array.prototype.forEach.call(choiceButtons, function (button) {
  button.addEventListener("click", function () {
    let choice = button.getAttribute("data-choice");
    if (!choice) return;
    playRound(choice);
  });
});

resetButton.addEventListener("click", function () {
  playerScore = 0;
  cpuScore = 0;
  winStreak = 0;
  history = [];
  resultEl.textContent = "Сделайте первый ход.";
  updateScore();
  renderHistory();
});

Быстрая самопроверка

Проверка Что должно произойти
Нажать любую кнопку выбора Появляется строка с ходом игрока, ходом компьютера и итогом раунда
Выиграть раунд Увеличиваются счёт игрока и серия побед
Проиграть или сыграть вничью Серия побед сбрасывается в 0
Сыграть больше шести раундов В истории остаются только последние шесть записей
Нажать «Сбросить счёт» Счёт, серия и история возвращаются в стартовое состояние

FAQ

Почему в этой версии используется game.js, а не app.js?

Потому что финальные исходники проекта собраны именно так. В статье сохранено то же именование, чтобы текст и реальные файлы совпадали один в один.

Можно ли сделать режим «до 5 побед»?

Да. Добавьте порог побед и после достижения нужного счёта блокируйте кнопки выбора до следующего сброса.

Как улучшить UX без изменения механики?

Добавьте анимацию активной кнопки, цветовое выделение победы/поражения и короткий таймер между раундами. Правила игры при этом останутся прежними.

Зачем ограничивать историю шестью пунктами?

Это держит интерфейс компактным. Игрок видит ближайший контекст партий, но экран не превращается в длинную ленту событий.

Как развивать игру дальше

Следующий шаг — режим матчей с финальным победителем, например «до 5 побед». Затем можно добавить сохранение статистики в localStorage и переключение тем оформления. Главное правило: добавляйте по одной механике за итерацию, чтобы не потерять читаемость кода.

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

Ниже — актуальные файлы финальной версии. Это тот же код, который работает во встроенной игре в начале статьи и на отдельной странице в разделе «Поиграть».

Обложка книги JavaScript с нуля: учимся программировать, создавая игры

Книга по JavaScript для начинающих

«JavaScript с нуля: учимся программировать, создавая игры»

Пошаговый формат с маленькими практическими задачами: от базового синтаксиса до первых игровых механик без перегруза теорией.

Читать книгу на ЛитРес →