Играем в двадцать одно на компьютере
hard

Играем в двадцать одно на компьютере

Свой проект с блек-джеком и JavaScript

Сегодня нас ждёт интересный проект — сделаем блек-джек с графическим интерфейсом, используя все механики фронтенд-разработки. Мы уже писали эту игру на Python, настало время перенести её в веб. Для реализации нам понадобится только браузер и редактор кода, поэтому почувствовать себя фронтендером сможет каждый.

В процессе будем активно использовать манипуляции с DOM для отображения карт, поработаем с массивами и объектами и используем популярный алгоритм Фишера — Йетса для перемешивания колоды. Летс плей.

Правила игры

  • Есть колода из 54 карт, масти не важны. В казино используют сразу до 8 колод, перемешивая их друг с другом, у нас будет одна — так алгоритм будет проще.
  • Каждому игроку сдаётся в открытую по две карты, раздающему (дилеру) — одна карта.
  • Карты с цифрами от 2 до 10 дают номинальное число очков (от 2 до 10), картинка — 10 очков, туз — 11 очков (или одно, если на руке больше 11 очков).
  • Цель игры — набрать как можно больше очков, но не больше 21. Если получается больше 21 — игрок автоматически проиграл.
  • Игрок смотрит свою карту и принимает решение, взять ещё или ему хватит. После каждой взятой карты игрок смотрит на сумму очков и решает, будет брать ещё или нет.
  • Когда игрок останавливается, крупье сдаёт себе карты по той же схеме. По классическим правилам дилер должен изначально тянуть карты до тех пор, пока не наберёт минимум 17 очков.
  • Если у игрока больше очков, чем у крупье, то он выиграл. Если очков больше у дилера — выиграл он.

Играем в двадцать одно на компьютере

Логика проекта

Вся механика игры будет реализована в JavaScript, а в HTML мы разместим контейнеры для отображения карт и блок с кнопками. 

Внутренняя логика игры будет такой: 

  1. Создаём массив со значениями каждой карты. 
  2. С помощью циклов создаём полный набор карт, добавляя каждую в массив. 
  3. Перемешиваем массив карт, карты раздаются игроку и дилеру. 
  4. Проходим по массиву карт, добавляя значения каждой карты к общей сумме очков игрока или дилера. 
  5. Дополнительные функции сразу проверяют, нужно ли пересчитать сумму очков, если в руке есть туз, и управляют логикой завершения игры, определяя, выиграл игрок или дилер.

А вот как мы это реализуем на практике.

  • Добавим в HTML-разметку контейнеры для отображения карт игрока и дилера, а также элементы для показа суммы очков и кнопок действий.
  • Добавим стили: сделаем красивый фон и раскрасим кнопки.
  • Подготовим картинки для колоды.
  • В JavaScript напишем функцию buildDeck для создания колоды карт, а затем функцию shuffleDeck для их перемешивания.
  • С помощью функции startGame раздадим начальные карты игроку и дилеру, скрыв одну из карт дилера.
  • Реализуем логику для действий «Взять карту» и «Остановиться» в функциях hit и stay. Эти функции будут обновлять сумму очков игрока и крупье и проверять, нужно ли завершить игру.
  • В функции stay будем проверять итоговые суммы очков игрока и дилера, определяя победителя или ничью.
  • Функция restartGame сбросит все значения и состояния игры, чтобы начать новую партию.
  • Функция getValue будет определять ценность карты на основе её значения (число, валет, дама, король или туз).
  • Вспомогательные функции checkAce и reduceAce будут определять, считать туз как 11 или как 1, чтобы избежать перебора очков.

Звучит как план, можно стартовать.

Создаём HTML-страницу

Создадим страницу с базовой разметкой и сразу подключим файлы со стилями и скриптом. Их пока нет, но дальше мы их наполним. Картинки будут лежать в папке img проекта — с ними тоже поработаем чуть позже. В общем, сделаем каркас из страницы и пустых файлов, чтобы потом к этому не возвращаться.

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Black Jack</title>
    <!-- Подключение файла стилей -->
    <link rel="stylesheet" href="style.css">
    <!-- Подключение скрипта с логикой игры -->
    <script src="main.js"></script>
</head>
<body>
</body>
</html>

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

<!-- Сумма очков дилера -->
<h2>Дилер: <span id="dealer-sum">0</span></h2>
<div id="dealer-cards">
    <!-- Картинка скрытой карты дилера -->
    <img id="hidden" src="./img/back.png" class="card-img">
</div>
<!-- Сумма очков игрока -->
<h2>Игрок: <span id="your-sum"></span></h2>
<div id="your-cards" class="card-container"></div>

Наконец, создадим блок с кнопками и заголовок, который появляется по завершении игры. При запуске игры будут видны две кнопки. По нажатию на «Взять» (hit) игроку сдаётся ещё одна карта, по клику на «Остановиться» (stay) — подсчитывается сумма очков и выводится сообщение о завершении игры. После этого в блоке появляется третья кнопка — «Заново» (restart), по клику на неё игра перезапускается. Для каждой кнопки сделаем подсказку (data-tooltip) при наведении.

<!-- Кнопки управления игрой -->
<div class="buttons">
    <!-- Кнопка "Взять карту" -->
    <button id="hit" data-tooltip="Взять">
        <img src="./img/hit.png" alt="Icon">
    </button>
    <!-- Кнопка "Остановиться" -->
    <button id="stay" data-tooltip="Остановиться">
        <img src="./img/stay.png" alt="Icon">
    </button>
    <!-- Кнопка "Заново" для перезапуска игры, скрыта по умолчанию -->
    <button id="restart" style="display: none;" data-tooltip="Заново">
        <img src="./img/restart.png" alt="Icon">
    </button>
</div>
<!-- Отображение результата игры -->
<h2 id="results"></h2>

Вот что получилось. Пока негусто, но это потому что нет стилей и картинок:

Играем в двадцать одно на компьютере

Добавляем изображения карт и иконки в проект

Для проекта нам нужны изображения самих карт и иконки для кнопок. Карты можно использовать как локально, сохранив их в папку проекта, так и генерировать каждый раз, используя Deck of Cards API.

Мы возьмём готовые карты с OpenGameArt.org — это платформа, предоставляющая бесплатные графические ресурсы для разработки игр. Возьмём колоду карт художницы Ланеи Циммерман. Уже подготовленные (переименованные и сжатые) изображения можно скачать в папке проекта, там же лежат и иконки управления. Заходим и сохраняем всё, что там есть, в папку img.

Каждую карту нужно именовать по её значению и масти, чтобы мы могли обращаться к ним в коде и правильно считать их значения. Например, карта десятки пик должна называться 10-S (Spades — пики). Аналогично именуем оставшиеся карты: червы — Hearts (H), трефы — Clubs (C), бубны — Diamonds (D).

Обновляем страницу — теперь видна одна гигантская карта, без стилей дальше точно не обойтись:

Добавляем изображения карт и иконки в проект

Стилизуем и красим кнопки

Создаём файл style.css, в котором будут все наши стили, и сохраняем его в той же папке, что и страницу. Сначала стилизуем фон страницы, чтобы он имитировал игровой стол:

body {
  /* Основной шрифт страницы */
  font-family: Arial, Helvetica, sans-serif;
    /* Выравнивание текста по центру */
  text-align: center;
  /* Фон с радиальным градиентом, имитирующим цвет сукна */
  background: radial-gradient(circle at center, #35654d, #1b3120, #050c05);
    /* Сбрасываем внешние отступы */
  margin: 0;
    /* Сбрасываем внутренние отступы */
  padding: 0;
    /* Высота страницы на всю высоту экрана */
  height: 100vh;
}

Появилось сукно и выравнивание, но карты по-прежнему большие:

Играем в двадцать одно на компьютере

Зададим стиль заголовка и установим адаптивный размер для карт, чтобы они правильно выглядели на разных экранах:

h2 {
  /* Цвет текста заголовков */
  color: aliceblue;
  /* Убираем нижний отступ для заголовков */
  margin-bottom: 0;
}

.card-img {
  /* Ширина изображения карты относительно ширины экрана */
  width: 20vw;
  /* Сохраняем соотношение сторон при изменении ширины */
  height: auto;
    /* Максимальная ширина изображения карты */
  max-width: 125px;
  /* Отступы вокруг изображения карты */
  margin: 1em;
}

Карты стали выглядеть адекватно, но с иконками управления по-прежнему беда, надо продолжать настраивать стили:

Играем в двадцать одно на компьютере

Установим стили для контейнера с кнопками и для самих кнопок:

.buttons {
  /* Выравнивание кнопок по центру и расположение их в строку */
  display: flex;
  
  /* Выравнивание кнопок по вертикали */
  align-items: center;
  
  /* Выравнивание кнопок по горизонтали */
  justify-content: center;
  
  /* Задаём расстояние между кнопками */
  gap: 2em;
}

button {
  /* Относительное позиционирование кнопки, чтобы тултип отображался корректно */
  position: relative;
    /* Выравниваем содержимое по центру */
  display: flex;
  /* Центрируем содержимое по вертикали */
  align-items: center;
  /* Центрируем содержимое по горизонтали */
  justify-content: center;
    /* Размеры кнопки */
  width: 60px;
  height: 60px;
  /* Скругляем углы */
  border-radius: 50%;
    /* Задаём цвет фона */
  background-color: #c5c5c5;
  /* Граница кнопки */
  border: 1px solid #333333;
    /* Курсор меняется на указатель при наведении */
  cursor: pointer;
  /* Добавляем тень*/
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

button img {
  /* Размер иконок внутри кнопки */
  width: 30px;
  height: 30px;
}

Вот, так намного лучше:

Играем в двадцать одно на компьютере

И напоследок добавим через псевдоэлемент after стилизацию для подсказки на кнопке при наведении:

button::after {
  /* Отображаем подсказку, используя атрибут data-tooltip */
  content: attr(data-tooltip);
  /* Абсолютное позиционирование подсказки */
  position: absolute;
    /* Позиционируем подсказку под кнопкой */
  bottom: -30px;
  /* Центрируем подсказку относительно кнопки */
  left: 50%;
  transform: translateX(-50%);
  /* Цвет фона */
  background-color: #343434;
  /* Цвет текста */
  color: white;
    /* Отступы внутри подсказки */
  padding: 0.5em;
    /* Закруглённые углы */
  border-radius: 5px;
  /* Отключаем перенос текста в подсказке */
  white-space: nowrap;
  /* Изначально скрываем подсказку */
  opacity: 0;
    /* Отключаем возможность взаимодействия с подсказкой */
  pointer-events: none;
  /* Плавное появление подсказки при наведении */
  transition: opacity 0.3s;
}

button:hover::after {
  /* Показываем подсказку при наведении */
  opacity: 1;
}

С дизайном закончили, теперь переходим к логике игры. Вот что получилось после применения всех стилей:

Играем в двадцать одно на компьютере

Создаём колоду

Теперь нам понадобится знание JavaScript. Создадим файл script.js и дальше будем работать в нём.

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

// Текущая суммы очков дилера
let dealerSum = 0;
// Текущая сумма очков игрока
let yourSum = 0;
// Отслеживание числа тузов у дилера
let dealerAceCount = 0;
// Отслеживание числа тузов у игрока
let yourAceCount = 0;
// Скрытая карта дилера
let hidden;
// Колода карт
let deck;
// Позволяет игроку брать карты, пока сумма очков не превысит 21
let canHit = true;

Затем напишем функции, которые будут управлять колодой карт. При загрузке страницы вызывается функция buildDeck() для создания полной колоды, затем shuffleDeck() для перемешивания карт и, наконец, startGame() для начала игры.

// Код, который выполняется при загрузке страницы
window.onload = function () {
  // Создаём колоду карт
  buildDeck();
  // Перемешиваем колоду
  shuffleDeck();
  // Начинаем игру, раздавая карты дилеру и игроку
  startGame();
};
// Создание колоды карт
function buildDeck() {
  // Значения карт: от туза (Ace) до короля (King)
  let values = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"];
  // Масти карт
  let types = ["C", "D", "H", "S"];
  // Пустой массив для колоды карт
  deck = [];
  // Создаём комбинации значений и мастей, добавляя их в колоду
  for (let i = 0; i < types.length; i++) {
    for (let j = 0; j < values.length; j++) {
      deck.push(values[j] + "-" + types[i]); 
    }
  }
}
// Перемешивание колоды с использованием алгоритма Фишера — Йетса
function shuffleDeck() {
  for (let i = 0; i < deck.length; i++) {
    // Выбираем случайный индекс для обмена с текущей картой
    let j = Math.floor(Math.random() * deck.length); 
    // Меняем местами текущую карту со случайной картой
    let temp = deck[i];
    deck[i] = deck[j];
    deck[j] = temp;
  }

Функция shuffleDeck() перемешивает колоду, используя алгоритм Фишера — Йетса. Это классический и широко используемый алгоритм для случайного перемешивания элементов массива. Алгоритм проходит по массиву с конца к началу и для каждого элемента случайно выбирает один из элементов, который находится перед ним, включая его же, и меняет их местами. Это гарантирует, что каждый элемент будет равновероятно находиться в любом месте в массиве.

Отображение карт

Чтобы элементы карт отображались на странице, добавим функцию createCard():

function createCard(cardSrc) {
  // Создаём новый элемент <img>, который будет представлять карту
  let cardImg = document.createElement("img");
  // Устанавливаем атрибут src, чтобы указать путь к изображению карты
  cardImg.src = cardSrc;
  // Добавляем класс card-img для стилизации изображения карты
  cardImg.classList.add("card-img");
  // Возвращаем созданный элемент изображения
  return cardImg;
}

Начало игры

Функция startGame() начинает игру, раздаёт карты игроку и дилеру, а кроме этого добавляет обработчики событий для кнопок «Взять карту» и «Остановиться».

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

function startGame() {
  // Извлекаем скрытую карту дилера из колоды
  hidden = deck.pop();
  // Добавляем значение скрытой карты к сумме очков дилера
  dealerSum += getValue(hidden);
  // Проверяем скрытую карту на туз и увеличиваем счётчик тузов
  dealerAceCount += checkAce(hidden);
  // Дилер берёт карты, пока его сумма очков не достигнет 17
  while (dealerSum < 17) {
    // Создаём изображение карты и добавляем его на экран
    let cardImg = createCard("./img/" + deck.pop() + ".png");
        // Добавляем значение карты к сумме очков дилера
    dealerSum += getValue(cardImg.src);
  // Проверяем карту на туз и увеличиваем счётчик тузов
    dealerAceCount += checkAce(cardImg.src);
    // Добавляем изображение карты дилера в контейнер
    document.getElementById("dealer-cards").append(cardImg);
  }
  // Игроку раздаются две карты
  for (let i = 0; i < 2; i++) {
    // Создаём изображение карты и добавляем его
    let cardImg = createCard("./img/" + deck.pop() + ".png");
    // Добавляем значение карты к сумме очков игрока
    yourSum += getValue(cardImg.src);
  // Проверяем карту на туз и увеличиваем счётчик тузов
    yourAceCount += checkAce(cardImg.src);
    // Добавляем изображение карты игрока в контейнер
    document.getElementById("your-cards").append(cardImg);
  }
  // Обновляем отображение суммы очков игрока на экране
  document.getElementById("your-sum").innerText = yourSum;
  // Добавляем обработчик событий для кнопки "Взять карту"
  document.getElementById("hit").addEventListener("click", hit);
    // Добавляем обработчик событий для кнопки "Остановиться"
  document.getElementById("stay").addEventListener("click", stay);
}

Мы добавили обработчики событий на кнопки — дальше реализуем логику этих кнопок в функциях hit() и stay().

Обработка действий игрока

Функция hit() позволяет игроку взять дополнительную карту, добавляет её к сумме очков и проверяет, не превысила ли сумма 21. Если игрок превысил 21 очко, то больше не может брать карты (canHit = false). 

Функция stay() завершает ход игрока, раскрывает скрытую карту дилера и проверяет, нужно ли дилеру брать дополнительные карты. После этого сравниваются суммы очков игрока и дилера, определяется результат игры (победа, проигрыш или ничья), отображается на экране. Также stay() меняет видимость кнопки «Заново» после завершения игры.

function hit() {
  // Проверяем, может ли игрок дальше брать карты
  if (!canHit) {
    return; // Если нет, прерываем выполнение
  }
  // Создаём элемент изображения для новой карты, извлекая карту из колоды
  let cardImg = createCard("./img/" + deck.pop() + ".png");
    // Добавляем значение новой карты к общей сумме очков игрока
  yourSum += getValue(cardImg.src);
  // Проверяем карту на туз и увеличиваем счётчик тузов
  yourAceCount += checkAce(cardImg.src);
  // Добавляем изображение новой карты на экран в контейнер
  document.getElementById("your-cards").append(cardImg);
  // Обновляем отображение суммы очков игрока на экране
  document.getElementById("your-sum").innerText = yourSum;
  // Если сумма очков игрока с учётом тузов превышает 21, игрок больше не может брать карты
  if (reduceAce(yourSum, yourAceCount) > 21) {
    canHit = false;
  }
}

function stay() {
  // Применяем функцию для корректировки суммы очков дилера с учётом тузов
  dealerSum = reduceAce(dealerSum, dealerAceCount);
  // Применяем функцию для корректировки суммы очков игрока с учётом тузов
  yourSum = reduceAce(yourSum, yourAceCount);
  // Игрок завершил ход, больше нельзя брать карты
  canHit = false;
  // Раскрываем скрытую карту дилера на экране
  document.getElementById("hidden").src = "./img/" + hidden + ".png";
  // Инициализируем переменную для сообщения о результате игры
  let message = "";
  // Проверяем условия для определения результата игры
  if (yourSum > 21) {
// Игрок проиграл, если его сумма очков больше 21
    message = "Вы проиграли!"; 
  } else if (dealerSum > 21) {
// Игрок выиграл, если дилер превысил 21
    message = "Дилер проиграл, вы выиграли!"; 
  } else if (yourSum == dealerSum) {
// Если суммы очков равны, объявляется ничья
    message = "Ничья!"; 
  } else if (yourSum > dealerSum) {
// Игрок выиграл, если его сумма очков больше суммы дилера
    message = "Вы выиграли!"; 
  } else if (yourSum < dealerSum) {
// Игрок проиграл, если его сумма очков меньше суммы дилера
    message = "Вы проиграли!"; 
  }
  // Обновляем отображение суммы очков дилера
  document.getElementById("dealer-sum").innerText = dealerSum;
    // Обновляем отображение суммы очков игрока
  document.getElementById("your-sum").innerText = yourSum;
    // Выводим сообщение о результате игры
  document.getElementById("results").innerText = message;
  // Показываем кнопку "Заново", чтобы игрок мог начать новую игру
  document.getElementById("restart").style.display = "inline-block";
    // Добавляем обработчик события для кнопки "Заново", чтобы она сбрасывала игру при нажатии
  document.getElementById("restart").addEventListener("click", restartGame);
}

Осталось совсем чуть-чуть. Дальше реализуем функцию restartGame(), которая будет сбрасывать все игровые значения и состояния.

Начало новой игры

Функция restartGame() сбрасывает все значения до начальных: обнуляются суммы очков игрока и дилера, очищаются поля с картами и сбрасываются счётчики тузов. После этого заново создаётся и перемешивается колода, и с помощью функции startGame() игра запускается.

function restartGame() {
  // Сбрасываем сумму очков дилера
  dealerSum = 0;
  // Сбрасываем сумму очков игрока
  yourSum = 0;
  // Сбрасываем количество тузов у дилера
  dealerAceCount = 0;
  // Сбрасываем количество тузов у игрока
  yourAceCount = 0;
  // Разрешаем игроку снова брать карты
  canHit = true;
  // Очищаем поле с картами дилера и добавляем изображение скрытой карты
  document.getElementById("dealer-cards").innerHTML =
    '<img id="hidden" src="./img/back.png" class="card-img">';
  // Очищаем поле с картами игрока
  document.getElementById("your-cards").innerHTML = "";
  // Сбрасываем отображаемую сумму очков дилера до нуля
  document.getElementById("dealer-sum").innerText = "0";
  // Сбрасываем отображаемую сумму очков игрока до нуля
  document.getElementById("your-sum").innerText = "0";
  // Очищаем сообщение о результате игры
  document.getElementById("results").innerText = "";
  // Заново создаём колоду 
  buildDeck();
  // Перемешиваем колоду 
  shuffleDeck();
  // Запускаем новую игру
  startGame();
  // Скрываем кнопку "Заново", пока игра не завершится снова
  document.getElementById("restart").style.display = "none";
}

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

Вспомогательные функции

Осталось реализовать три функции, без которых игра не будет корректно работать:

  • getValue() определяет ценность карты по её значению (число, валет, дама, король или туз). Функция читает название файла каждой карты и извлекает из него значение.
  • checkAce() проверяет, является ли карта тузом, и возвращает соответствующее значение.
  • reduceAce() корректирует сумму очков игрока или дилера, если в руке есть туз.

Напишем их:

function getValue(cardSrc) {
  // Разбиваем строку пути к карте, чтобы получить её значение и масть
  let data = cardSrc.split("/").pop().split("-");
// Берём первую часть строки, которая содержит значение карты
  let value = data[0]; 
  // Проверяем, является ли значение карты числом
  if (isNaN(value)) {
    // Если это не число и карта — туз, возвращаем 11
    if (value == "A") {
      return 11;
    }
    // Для карт J, Q, K (валет, дама, король) возвращаем 10
    return 10;
  }
  // Для числовых карт возвращаем их числовое значение
  return parseInt(value);
}
function checkAce(cardSrc) {
  // Проверяем, является ли первая буква в названии карты буквой A (туз)
  if (cardSrc[0] == "A") {
// Если да, возвращаем 1
    return 1; 
  }
// Если нет, возвращаем 0
  return 0; 
}
function reduceAce(playerSum, playerAceCount) {
  // Пока сумма очков больше 21 и есть тузы, которые можно пересчитать как 1
  while (playerSum > 21 && playerAceCount > 0) {
// Уменьшаем сумму очков на 10 (делаем туз 1 вместо 11)
    playerSum -= 10; 
// Уменьшаем количество тузов, посчитанных как 11
    playerAceCount -= 1; 
  }
// Возвращаем скорректированную сумму очков
  return playerSum; 
}

Всё готово! Можно собирать всё вместе и играть. Или поиграть на странице проекта.

Что можно улучшить

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

  • добавить несколько колод;
  • добавить анимацию;
  • сделать режим для нескольких игроков;
  • прикрутить звуковые эффекты;
  • реализовать сохранение прогресса в LocalStorage.

Исправим это в следующих версиях, а вы пока подпишитесь, чтобы не пропустить продолжение. А ещё лучше — скидывайте в комментарии свои варианты реализации новой логики.

<!DOCTYPE html>
<html lang="ru">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Black Jack</title>
   <link rel="stylesheet" href="style.css">
   <script src="main.js"></script>
</head>
<body>
   <h2>Дилер: <span id="dealer-sum">0</span></h2>
   <div id="dealer-cards">
       <img id="hidden" src="./img/back.png" class="card-img">
   </div>
   <h2>Игрок: <span id="your-sum"></span></h2>
   <div id="your-cards" class="card-container"></div>
   <div class="buttons">
       <button id="hit" data-tooltip="Взять">
           <img src="./img/hit.png" alt="Icon">
       </button>
       <button id="stay" data-tooltip="Остановиться">
           <img src="./img/stay.png" alt="Icon">
       </button>
       <button id="restart" style="display: none;" data-tooltip="Заново">
           <img src="./img/restart.png" alt="Icon">
       </button>
   </div>
   <h2 id="results"></h2>
</body>
</html>

body {
 font-family: Arial, Helvetica, sans-serif;
 text-align: center;
 background: radial-gradient(circle at center, #35654d, #1b3120, #050c05);
 margin: 0;
 padding: 0;
 height: 100vh;
}

h2 {
 color: aliceblue;
 margin-bottom: 0;
}

.card-img {
 width: 20vw;
 height: auto;
 max-width: 125px;
 margin: 1em;
}

.buttons {
 display: flex;
 align-items: center;
 justify-content: center;
 gap: 2em;
}

button {
 position: relative;
 display: flex;
 align-items: center;
 justify-content: center;
 width: 60px;
 height: 60px;
 border-radius: 50%;
 background-color: #c5c5c5;
 border: 1px solid #333333;
 cursor: pointer;
 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

button img {
 width: 30px;
 height: 30px;
}

button::after {
 content: attr(data-tooltip);
 position: absolute;
 bottom: -30px;
 left: 50%;
 transform: translateX(-50%);
 background-color: #343434;
 color: white;
 padding: 0.5em;
 border-radius: 5px;
 white-space: nowrap;
 opacity: 0;
 pointer-events: none;
 transition: opacity 0.3s;
}

button:hover::after {
 opacity: 1;
}

let dealerSum = 0;
let yourSum = 0;
let dealerAceCount = 0;
let yourAceCount = 0;
let hidden;
let deck;
let canHit = true;

window.onload = function () {
 buildDeck();
 shuffleDeck();
 startGame();
};

function buildDeck() {
 let values = [
   "A",
   "2",
   "3",
   "4",
   "5",
   "6",
   "7",
   "8",
   "9",
   "10",
   "J",
   "Q",
   "K",
 ];
 let types = ["C", "D", "H", "S"];
 deck = [];

 for (let i = 0; i < types.length; i++) {
   for (let j = 0; j < values.length; j++) {
     deck.push(values[j] + "-" + types[i]);
   }
 }
}

function shuffleDeck() {
 for (let i = 0; i < deck.length; i++) {
   let j = Math.floor(Math.random() * deck.length);
   let temp = deck[i];
   deck[i] = deck[j];
   deck[j] = temp;
 }
 console.log(deck);
}

function createCard(cardSrc) {
 let cardImg = document.createElement("img");
 cardImg.src = cardSrc;
 cardImg.classList.add("card-img");
 return cardImg;
}

function startGame() {
 hidden = deck.pop();
 dealerSum += getValue(hidden);
 dealerAceCount += checkAce(hidden);

 while (dealerSum < 17) {
   let cardImg = createCard("./img/" + deck.pop() + ".png");
   dealerSum += getValue(cardImg.src);
   dealerAceCount += checkAce(cardImg.src);
   document.getElementById("dealer-cards").append(cardImg);
 }

 for (let i = 0; i < 2; i++) {
   let cardImg = createCard("./img/" + deck.pop() + ".png");
   yourSum += getValue(cardImg.src);
   yourAceCount += checkAce(cardImg.src);
   document.getElementById("your-cards").append(cardImg);
 }

 document.getElementById("your-sum").innerText = yourSum;

 document.getElementById("hit").addEventListener("click", hit);
 document.getElementById("stay").addEventListener("click", stay);
}

function hit() {
 if (!canHit) {
   return;
 }

 let cardImg = createCard("./img/" + deck.pop() + ".png");
 yourSum += getValue(cardImg.src);
 yourAceCount += checkAce(cardImg.src);
 document.getElementById("your-cards").append(cardImg);

 document.getElementById("your-sum").innerText = yourSum;

 if (reduceAce(yourSum, yourAceCount) > 21) {
   canHit = false;
 }
}

function stay() {
 dealerSum = reduceAce(dealerSum, dealerAceCount);
 yourSum = reduceAce(yourSum, yourAceCount);

 canHit = false;
 document.getElementById("hidden").src = "./img/" + hidden + ".png";

 let message = "";
 if (yourSum > 21) {
   message = "Вы проиграли!";
 } else if (dealerSum > 21) {
   message = "Дилер проиграл, вы выиграли!";
 } else if (yourSum == dealerSum) {
   message = "Ничья!";
 } else if (yourSum > dealerSum) {
   message = "Вы выиграли!";
 } else if (yourSum < dealerSum) {
   message = "Вы проиграли!";
 }

 document.getElementById("dealer-sum").innerText = dealerSum;
 document.getElementById("your-sum").innerText = yourSum;
 document.getElementById("results").innerText = message;

 document.getElementById("restart").style.display = "inline-block";
 document.getElementById("restart").addEventListener("click", restartGame);
}

function restartGame() {
 dealerSum = 0;
 yourSum = 0;
 dealerAceCount = 0;
 yourAceCount = 0;
 canHit = true;

 document.getElementById("dealer-cards").innerHTML =
   '<img id="hidden" src="./img/back.png" class="card-img">';
 document.getElementById("your-cards").innerHTML = "";
 document.getElementById("dealer-sum").innerText = "0";
 document.getElementById("your-sum").innerText = "0";
 document.getElementById("results").innerText = "";

 buildDeck();
 shuffleDeck();
 startGame();

 document.getElementById("restart").style.display = "none";
}

function getValue(cardSrc) {
 let data = cardSrc.split("/").pop().split("-");
 let value = data[0];

 if (isNaN(value)) {
   if (value == "A") {
     return 11;
   }
   return 10;
 }
 return parseInt(value);
}

function checkAce(cardSrc) {
 if (cardSrc[0] == "A") {
   return 1;
 }
 return 0;
}

function reduceAce(playerSum, playerAceCount) {
 while (playerSum > 21 && playerAceCount > 0) {
   playerSum -= 10;
   playerAceCount -= 1;
 }
 return playerSum;
}

Код:

Кенни Йип

Обложка:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
hard