Делаем простую браузерную игру в шашки на двоих
hard

Делаем простую браузерную игру в шашки на двоих

DOM, DOM — и в дамки!

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

  • Доска 8 x 8 клеток, игроки ходят по очереди, начиная с белых. 
  • Шашки перемещаются по диагонали на одну клетку вперёд, а достигнув последней линии, становятся дамками. 
  • Дамки ходят на любое количество клеток по диагонали в любом направлении.
  • Захватывая фигуры противника, обычные шашки и дамки должны перепрыгивать через них по диагонали на свободную клетку. Захваченные фигуры убираются с доски.
  • Цель игры — захватить все шашки соперника или лишить его возможности хода.

Проект длинный: в нём 11 функций, в которых будет основная логика игры — проверка валидности ходов, захват шашек, превращение в дамки и завершение игры. А ещё мы будем активно использовать атрибуты данных и манипуляции с DOM для управления состоянием игры.

Делаем простую браузерную игру в шашки на двоих

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

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

  • Сделаем HTML-контейнер, куда добавим доску и шашки.
  • Добавим стили: для доски используем гриды и стилизуем фигуры.
  • В JavaScript создадим функцию initializeBoard для инициализации доски и размещения шашек. 
  • Используем обработчики событий для кликов по клеткам для перемещения шашек.
  • Реализуем проверку возможности хода и захвата в функциях canMove, canMoveRegular и canMoveKing.
  • В функции movePiece реализуем логику перемещения и превращения шашек в дамки.
  • Добавим функции, проверяющие доступность ходов: на основании этой проверки игра будет продолжаться или завершаться.
  • Сделаем функцию checkGameState завершения игры: она будет проверять текущее состояние игры, принимать результат проверки доступности ходов и выводить алерт в случае выигрыша.

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

Создаём страницу с базовой разметкой. Сразу подключим файлы со стилями и скриптом. В разметке добавим контейнер <div class="board" id="board">, в который с помощью JS будем добавлять доску и шашки.

<!DOCTYPE html>
<html lang="ru">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Шашки</title>
<!-- подключаем стили -->
 <link rel="stylesheet" href="style.css">
</head>
<body>
<!-- добавляем элемент для доски -->
 <div class="board" id="board"></div>
<!-- подключаем скрипт -->
 <script src="script.js"></script>
</body>
</html>

Пока что страница пустая; чтобы на ней что-то появилось, добавим CSS-стили.

Пишем скрипт и создаём доску с шашками

Сейчас у нас на странице есть контейнер для игры, но там пока ничего нет. Исправим это и создадим файл script.js, в котором пропишем функции создания доски и расстановки шашек.

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

// Получаем ссылку на HTML-элемент с id "board"
const board = document.getElementById("board");
// Определяем ряды и колонки
const rows = 8;
const cols = 8;
// Переменная для отслеживания шашки, которую игрок выбрал для перемещения
let selectedPiece = null;
// Переменная для хранения текущего хода
let turn = "white";

Теперь создадим доску: с помощью board получим доступ к разделу в HTML-странице и в нём создадим поле и нарисуем клетки. Для этого делаем так:

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

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

// Получаем ссылку на HTML-элемент с id "board"
const board = document.getElementById("board");
// Определяем ряды и колонки
const rows = 8;
const cols = 8;
// Переменная для отслеживания шашки, которую игрок выбрал для перемещения
let selectedPiece = null;
// Переменная для хранения текущего хода
let turn = "white";

function initializeBoard() {
 // Проходим по всем рядам доски
 for (let row = 0; row < rows; row++) {
   // Проходим по всем колонкам
   for (let col = 0; col < cols; col++) {
     // Создаём элемент div для каждой клетки доски
     const cell = document.createElement("div");
     // Добавляем общий класс для всех клеток
     cell.classList.add("cell");
     // Устанавливаем класс для цвета клетки (белая или чёрная)
     cell.classList.add((row + col) % 2 === 0 ? "white" : "black");
     // Сохраняем координаты клетки в атрибуты dataset
     cell.dataset.row = row;
     cell.dataset.col = col;
     // Добавление шашек на доску
     // Чёрные шашки на первых трёх рядах
     if (row < 3 && (row + col) % 2 !== 0) {
       // Добавляем чёрную шашку, если условие чётности позиции выполняется
       addPiece(cell, "black");
       // Белые шашки на последних трёх рядах
       // Добавляем белую шашку, если условие чётности позиции выполняется
     } else if (row > 4 && (row + col) % 2 !== 0) {
       addPiece(cell, "white");
     }
     // Добавляем обработчик события для кликов по клетке
     cell.addEventListener("click", onCellClick);
     // Добавляем клетку на доску
     board.appendChild(cell);
   }
 }
}
// пока пустой обработчик по клику на клетке
function onCellClick(){

}
// расставляем шашки
function addPiece(cell, color) {
 // Создаём div для шашки
 const piece = document.createElement("div");
 // Добавляем общий класс для всех шашек и класс, определяющий цвет
 piece.classList.add("piece", color);
 // Сохраняем цвет шашки в атрибут dataset
 piece.dataset.color = color;
 // Устанавливаем атрибут, указывающий, что шашка не является дамкой
 piece.dataset.king = "false";
 // Добавляем шашку в клетку
 cell.appendChild(piece);
}

// Инициализация игры
initializeBoard();

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

Добавляем стили

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

body {
 /* Установка flex-контейнера для выравнивания содержимого */
 display: flex;
 /* Выравнивание содержимого по горизонтальной оси в центре */
 justify-content: center;
 /* Выравнивание содержимого по вертикальной оси в центре */
 align-items: center;
 /* Высота тела страницы равна 100% видимой области экрана */
 height: 100vh;
 /* Удаление отступов у элемента */
 margin: 0;
 /* Цвет фона */
 background-color: #f0f0f0;
}

/* Доска */
.board {
 /* Используем grid-разметку для создания сетки */
 display: grid;
 /* Определяем 8 колонок по 50px каждая */
 grid-template-columns: repeat(8, 50px);
 /* Определяем 8 строк по 50px каждая */
 grid-template-rows: repeat(8, 50px);
 /* Устанавливаем, что между клетками не будет промежутков */
 gap: 0;
 /* Добавляем границу тёмного цвета */
 border: 2px solid #333;
}

/* Клетки */
.cell {
 /* Устанавливаем ширину и высоту клеток */
 width: 50px;
 height: 50px;
 /* Используем flex для выравнивания содержимого внутри клетки */
 display: flex;
 /* Горизонтальное выравнивание содержимого по центру */
 justify-content: center;
 /* Вертикальное выравнивание содержимого по центру */
 align-items: center;
}

/* Чёрные клетки */
.cell.black {
 /* Задаём цвет фона для чёрных клеток */
 background-color: #cccccc;
}

/* Белые клетки */
.cell.white {
 /* Задаём белый цвет фона для белых клеток */
 background-color: #fff;
}
Делаем простую браузерную игру в шашки на двоих

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

  • создадим общий стиль любых шашек;
  • белым и чёрным шашкам добавим свой цвет;
  • сразу сделаем стиль для дамки — добавим ей красивую обводку.

/* Белые шашки */
.piece.white {
 /* Цвет фона для белых шашек, ярко-зелёный */
 background-color: white;
}
/* Чёрные шашки */
.piece.black {
 /* Цвет фона для чёрных шашек, тёмно-фиолетовый */
 background-color: black;
}
/* Дамки */
.piece.king {
 /* Добавляем обводку для дамок, чтобы выделить их */
 border: 2px solid #fc7f49;
}
Делаем простую браузерную игру в шашки на двоих

Добавляем обработчик событий (клик по клетке)

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

// Обработка кликов по клеткам
function onCellClick(e) {
 // Находим ближайший элемент с классом "cell", который был нажат
 const cell = e.target.closest(".cell");
 // Если такой элемент не найден, выходим из функции
 if (!cell) return;
 // Находим шашку в этой клетке, если она есть
 const piece = cell.querySelector(".piece");
 // Если шашка уже выбрана
 if (selectedPiece) {
   // Проверяем, можно ли сделать ход
   if (canMove(selectedPiece, cell)) {
     // Перемещаем шашку
     movePiece(selectedPiece, cell);
     // Сбрасываем выбранную шашку
     selectedPiece = null;
   } else {
     // Сбрасываем выбранную шашку, если ход невозможен
     selectedPiece = null;
   }
 } else {
   // Если шашка не выбрана и текущий ход соответствует цвету шашки
   if (piece && piece.dataset.color === turn) {
     // Выбираем шашку
     selectedPiece = piece;
   }
 }
}

Делаем ход

Перед тем как сделать ход, нам нужно проверить, можем ли мы вообще туда походить. Для этого определяем направление для хода (белые — вверх, чёрные — вниз), потом проверяем, дамка это или нет (про дамки будет позже, но сразу предусмотрим), и если все условия выполнены, идём в проверки хода для обычных шашек и для дамок.

По правилам шашки могут двигаться только по диагонали. Шашка не может встать на ту клетку, где уже стоит другая шашка. Можно съесть шашку соперника, если за ней есть пустая клетка. Чтобы реализовать это, напишем функции для проверки возможности хода и перемещения.

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

  • canMove: определяет, допустим ли ход, вызывая соответствующие функции для проверки обычной шашки или дамки. Если обычная шашка может сделать заданный ход, вызываем функции canMoveRegular
  • canMoveRegular: проверяет, можно ли сделать ход обычной шашке.
  • movePiece: перемещает шашку на целевую клетку и обрабатывает превращение простой шашки в дамку, если та достигает последнего ряда противника. Удаляет захваченные шашки и переключает ход на следующего игрока.

Сейчас будет общая функция со всем проверками — от неё и будем отталкиваться.

// Проверка возможности хода на целевую клетку
function canMove(piece, targetCell) {
 // Получаем координаты начальной клетки (где стоит выбранная шашка)
 const startRow = parseInt(piece.parentElement.dataset.row);
 const startCol = parseInt(piece.parentElement.dataset.col);

 // Получаем координаты целевой клетки (куда хотим переместить шашку)
 // Преобразуем строковое значение номера ряда в целое число
 const endRow = parseInt(targetCell.dataset.row);
 // Преобразуем строковое значение номера колонки в целое число
 const endCol = parseInt(targetCell.dataset.col);

 // Определяем направление хода (белые шашки идут вверх, чёрные — вниз)
 const direction = piece.dataset.color === "white" ? -1 : 1;

 // Проверяем, является ли шашка дамкой
 const isKing = piece.dataset.king === "true";

 // Если целевая клетка уже занята другой шашкой, ход невозможен
 if (targetCell.querySelector(".piece")) {
   // Целевая клетка занята
   return false;
 }

 // Проверяем возможность хода для обычной шашки или дамки
 if (!isKing) {
   // Если шашка не дамка, проверяем возможность хода для обычной шашки
   return canMoveRegular(piece, startRow, startCol, endRow, endCol, direction);
 } else {
   // Если шашка дамка, проверяем возможность хода для дамки
   return canMoveKing(piece, startRow, startCol, endRow, endCol);
 }
}

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

// Проверка возможности хода для обычной шашки
function canMoveRegular(piece, startRow, startCol, endRow, endCol, direction) {
 // Проверка на обычный ход (одна клетка по диагонали)
 if (
   Math.abs(endRow - startRow) === 1 &&
   Math.abs(endCol - startCol) === 1 &&
   endRow - startRow === direction
 ) {
   // Если ход соответствует правилам обычного хода
   return true;
 }
 // Проверка на ход с захватом (две клетки по диагонали)
 if (
   Math.abs(endRow - startRow) === 2 &&
   Math.abs(endCol - startCol) === 2 &&
   endRow - startRow === 2 * direction
 ) {
   // Находим координаты средней клетки (между начальной и целевой)
   const middleRow = (startRow + endRow) / 2;
   const middleCol = (startCol + endCol) / 2;

   // Находим среднюю клетку по координатам
   const middleCell = document.querySelector(
     `.cell[data-row="${middleRow}"][data-col="${middleCol}"]`
   );
   // Проверяем, есть ли в средней клетке шашка и если есть, то принадлежит ли она противнику
   // Ищем шашку в средней клетке
   const middlePiece = middleCell.querySelector(".piece");
   // Проверяем, занята ли средняя клетка противником
   if (middlePiece && middlePiece.dataset.color !== piece.dataset.color) {
     // Если допустим ход с захватом
     return true;
   }
 }
 // Невозможный ход
 return false;
}

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

// Перемещение шашки на новую клетку
function movePiece(piece, targetCell) {
 // Получаем начальную строку, где находилась шашка
 const startRow = parseInt(piece.parentElement.dataset.row);
 // Получаем начальный столбец, где находилась шашка
 const startCol = parseInt(piece.parentElement.dataset.col);
 // Получаем конечную строку, куда шашка будет перемещена
 const endRow = parseInt(targetCell.dataset.row);
 // Получаем конечный столбец, куда шашка будет перемещена
 const endCol = parseInt(targetCell.dataset.col);

 // Проверяем, осуществляется ли захват (перемещение на две клетки)
 if (Math.abs(endRow - startRow) === 2 && Math.abs(endCol - startCol) === 2) {
   // Вычисляем координаты серединной клетки между начальной и конечной
   const middleRow = (startRow + endRow) / 2;
   // Вычисляем координаты серединного столбца между начальным и конечным
   const middleCol = (startCol + endCol) / 2;
   // Получаем доступ к серединной клетке
   const middleCell = document.querySelector(
     `.cell[data-row="${middleRow}"][data-col="${middleCol}"]`
   );
   // Проверяем, есть ли шашка в серединной клетке
   const middlePiece = middleCell.querySelector(".piece");
   // Удаляем шашку, если она там есть
   if (middlePiece) {
     middleCell.removeChild(middlePiece);
   }
 }

 // Перемещаем шашку в целевую клетку
 targetCell.appendChild(piece);

 // Проверяем, достигла ли шашка последнего ряда для превращения в дамку
 if (
   (piece.dataset.color === "white" && endRow === 0) ||
   (piece.dataset.color === "black" && endRow === 7)
 ) {
   // Отмечаем шашку как дамку
   piece.dataset.king = "true";
   // Добавляем класс, указывающий, что шашка теперь дамка
   piece.classList.add("king");
 }

 //  Переход хода к следующему игроку
 turn = turn === "white" ? "black" : "white";
 // Сбрасываем выбор текущей шашки
 selectedPiece = null;
 // Вызываем функцию для проверки состояния игры после хода
 checkGameState();
}

Проверяем, закончилась игра или нет

Последней командой в предыдущей функции прописана функция проверки состояния игры после очередного хода. Эта функция checkGameState будет вызываться после каждого хода и проверять, остались ли у противника шашки и возможные ходы.  Для проверки возможности дальнейших ходов используется функция hasValidMoves. В зависимости от результата проверки объявляется победитель и вызывается функция disableBoard, чтобы заблокировать доску.

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

function checkGameState() {
 // Инициализация переменных для подсчёта шашек и проверки наличия ходов
 // Количество белых шашек на доске
 let whiteCount = 0;
 // Количество чёрных шашек на доске
 let blackCount = 0;
 // Индикатор наличия допустимых ходов у белых
 let whiteHasMoves = false;
 // Индикатор наличия допустимых ходов у чёрных
 let blackHasMoves = false;

 // Перебор всех клеток на доске для проверки шашек и доступных ходов
 for (let row = 0; row < rows; row++) {
   for (let col = 0; col < cols; col++) {
     // Получение клетки по текущим координатам
     const cell = document.querySelector(
       `.cell[data-row="${row}"][data-col="${col}"]`
     );
     // Попытка найти шашку в данной клетке
     const piece = cell.querySelector(".piece");

     // Проверка наличия шашки в клетке
     if (piece) {
       // Увеличение соответствующего счётчика в зависимости от цвета шашки
       if (piece.dataset.color === "white") {
         // Увеличиваем счётчик белых шашек
         whiteCount++;
         // Проверка наличия доступных ходов для белой шашки
         if (!whiteHasMoves && hasValidMoves(piece)) {
           // Обновление флага доступных ходов для белых
           whiteHasMoves = true;
         }
       } else if (piece.dataset.color === "black") {
         // Увеличиваем счётчик чёрных шашек
         blackCount++;
         // Проверка наличия доступных ходов для чёрной шашки
         if (!blackHasMoves && hasValidMoves(piece)) {
           // Обновление флага доступных ходов для чёрных
           blackHasMoves = true;
         }
       }
     }
   }
 }

 // Проверка валидности позиции на доске
function isValidPosition(row, col) {
 // Проверяем, что координаты находятся внутри границ доски
 return row >= 0 && row < rows && col >= 0 && col < cols;
}

 // Проверка условий окончания игры и объявление победителя
 if (whiteCount === 0 || !whiteHasMoves) {
   // Проверка отсутствия шашек или ходов у белых
   // Объявление победы чёрных
   alert("Чёрные выигрывают!");
   // Отключение доски для завершения игры
   disableBoard();
 } else if (blackCount === 0 || !blackHasMoves) {
   // Проверка отсутствия шашек или ходов у чёрных
   // Объявление победы белых
   alert("Белые выигрывают!");
   // Отключение доски для завершения игры
   disableBoard();
 }
}

// Проверка наличия допустимых ходов для шашки
function hasValidMoves(piece) {
 // Получаем начальные координаты шашки
 const startRow = parseInt(piece.parentElement.dataset.row);
 const startCol = parseInt(piece.parentElement.dataset.col);

 // Определяем возможные направления для перемещения
 const directions =
   piece.dataset.king === "true"
     ? [
         // Для дамки: возможность движения во всех направлениях по диагонали
         [1, 1],
         [1, -1],
         [-1, 1],
         [-1, -1],
       ]
     : piece.dataset.color === "white"
     ? [
         // Для белой шашки: движение только вверх по диагонали
         [-1, 1],
         [-1, -1],
       ]
     : [
         // Для чёрной шашки: движение только вниз по диагонали
         [1, 1],
         [1, -1],
       ];

 // Перебираем все направления движения
 for (let [dr, dc] of directions) {
   // Вычисляем координаты новой строки после хода
   const newRow = startRow + dr;
   // Вычисляем координаты нового столбца после хода
   const newCol = startCol + dc;

   // Проверяем, находится ли новая позиция в пределах доски
   if (isValidPosition(newRow, newCol)) {
     // Находим клетку по новым координатам
     const targetCell = document.querySelector(
       `.cell[data-row="${newRow}"][data-col="${newCol}"]`
     );
     // Проверяем, свободна ли целевая клетка
     if (!targetCell.querySelector(".piece")) {
       return true; // Возвращаем true, если клетка свободна
     }
     // Вычисляем координаты клетки после возможного захвата
     const jumpRow = startRow + 2 * dr;
     const jumpCol = startCol + 2 * dc;
     if (isValidPosition(jumpRow, jumpCol)) {
       // Находим клетку середины, где может находиться шашка противника
       const middleCell = document.querySelector(
         `.cell[data-row="${newRow}"][data-col="${newCol}"]`
       );
       // Находим клетку после захвата
       const jumpCell = document.querySelector(
         `.cell[data-row="${jumpRow}"][data-col="${jumpCol}"]`
       );
       // Проверяем, есть ли в середине шашка противника
       const middlePiece = middleCell.querySelector(".piece");
       // Проверяем, свободна ли целевая клетка после захвата и находится ли в середине шашка противника
       if (
         middlePiece &&
         middlePiece.dataset.color !== piece.dataset.color &&
         !jumpCell.querySelector(".piece")
       ) {
         return true; // Возвращаем true, если возможен ход с захватом
       }
     }
   }
 }
 // Если ни один из возможных ходов не подошёл, возвращаем false
 return false;
}

Чтобы по завершению игры игроки не могли больше двигать фигуры, блокируем дальнейшие ходы, отключая доску. Функция disableBoard выбирает все элементы .cell и снимает с них обработчик событий:

function disableBoard() {
 document
  // Удаление обработчиков событий клика со всех клеток
   .querySelectorAll(".cell")
   .forEach((cell) => cell.removeEventListener("click", onCellClick));
}

Добавляем дамки

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

function canMoveKing(piece, startRow, startCol, endRow, endCol) {
 // Проверяем, что конечная клетка находится на той же диагонали, что и начальная
 if (Math.abs(endRow - startRow) === Math.abs(endCol - startCol)) {
   // Переменная для проверки, свободен ли путь
   let pathClear = true;
   // Переменная для отслеживания захвата шашки
   let middlePieceCaptured = false;
   // Определяем шаги по строкам и столбцам в зависимости от направления движения
   const rowStep = endRow > startRow ? 1 : -1;
   const colStep = endCol > startCol ? 1 : -1;
   // Проходим по всем клеткам на пути перемещения
   for (let i = 1; i < Math.abs(endRow - startRow); i++) {
     // Вычисляем координаты промежуточной клетки на пути
     const intermediateCell = document.querySelector(
       `.cell[data-row="${startRow + i * rowStep}"][data-col="${
         startCol + i * colStep
       }"]`
     );
     // Ищем шашку на этой клетке
     const pieceOnPath = intermediateCell.querySelector(".piece");
     // Если на пути есть шашка
     if (pieceOnPath) {
       // Проверяем, принадлежит ли шашка противнику
       if (pieceOnPath.dataset.color !== piece.dataset.color) {
         // Если шашка противника ещё не была захвачена
         if (middlePieceCaptured) {
           // Путь заблокирован
           pathClear = false;
           // Прерываем цикл проверки
           break;
         } else {
           // Отмечаем, что шашка противника была захвачена
           middlePieceCaptured = true;
         }
       } else {
         // Путь заблокирован другой шашкой
         pathClear = false;
         // Прерываем цикл проверки
         break;
       }
     }
   }
   // Если путь свободен, возвращаем true
   if (pathClear) {
     return true;
   }
 }
 // Если путь не свободен или ход не по диагонали, возвращаем false
 return false;
}

Уф, готово, можно собирать всё вместе и играть. Или можно поиграть на странице проекта.

Что дальше

Технически мы сделали игру в шашки, но до полноценной игры ещё далеко:

  • нет проверки, что нужно обязательно съесть шашку противника, если есть такая возможность;
  • нельзя за один ход забрать более одной шашки у противника;
  • нет визуального выделения выбранной шашки;
  • нельзя «съесть» шашку противника в обратную сторону (например, наверх для чёрных);
  • непонятно, чей сейчас ход.

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

<!DOCTYPE html>
<html lang="ru">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Шашки</title>
 <link rel="stylesheet" href="style.css">
</head>
<body>
 <div class="board" id="board"></div>
 <script src="script.js"></script>
</body>
</html>

body {
 /* Установка flex-контейнера для выравнивания содержимого */
 display: flex;
 /* Выравнивание содержимого по горизонтальной оси в центре */
 justify-content: center;
 /* Выравнивание содержимого по вертикальной оси в центре */
 align-items: center;
 /* Высота тела страницы равна 100% видимой области экрана */
 height: 100vh;
 /* Удаление отступов у элемента */
 margin: 0;
 /* Цвет фона */
 background-color: #f0f0f0;
}

/* Доска */
.board {
 /* Используем grid-разметку для создания сетки */
 display: grid;
 /* Определяем 8 колонок по 50px каждая */
 grid-template-columns: repeat(8, 50px);
 /* Определяем 8 строк по 50px каждая */
 grid-template-rows: repeat(8, 50px);
 /* Устанавливаем, что между клетками не будет промежутков */
 gap: 0;
 /* Добавляем границу тёмного цвета */
 border: 2px solid #333;
}

/* Клетки */
.cell {
 /* Устанавливаем ширину и высоту клеток */
 width: 50px;
 height: 50px;
 /* Используем flex для выравнивания содержимого внутри клетки */
 display: flex;
 /* Горизонтальное выравнивание содержимого по центру */
 justify-content: center;
 /* Вертикальное выравнивание содержимого по центру */
 align-items: center;
}

/* Чёрные клетки */
.cell.black {
 /* Задаём цвет фона для чёрных клеток */
 background-color: #cccccc;
}

/* Белые клетки */
.cell.white {
 /* Задаём белый цвет фона для белых клеток */
 background-color: #fff;
}
/* Общий стиль шашек */
.piece {
 /* Устанавливаем ширину и высоту каждой шашки */
 width: 40px;
 height: 40px;
 /* Закругляем края шашек */
 border-radius: 50%;
 /* добавляем границу */
 border: 1px;
 border-style: solid;
 /* Курсор в виде указателя при наведении для интерактивности */
 cursor: pointer;
 /* Тень для визуального выделения шашек */
 box-shadow: 2px 6px 8px 1px rgba(34, 60, 80, 0.2);
}
/* Белые шашки */
.piece.white {
 /* Цвет фона для белых шашек, ярко-зелёный */
 background-color: white;
}
/* Чёрные шашки */
.piece.black {
 /* Цвет фона для чёрных шашек, тёмно-фиолетовый */
 background-color: black;
}
/* Дамки */
.piece.king {
 /* Добавляем обводку для дамок, чтобы выделить их */
 border: 2px solid #fc7f49;
}

// Получаем ссылку на HTML-элемент с id "board"
const board = document.getElementById("board");
// Определяем ряды и колонки
const rows = 8;
const cols = 8;
// Переменная для отслеживания шашки, которую игрок выбрал для перемещения
let selectedPiece = null;
// Переменная для хранения текущего хода
let turn = "white";

function initializeBoard() {
 // Проходим по всем рядам доски
 for (let row = 0; row < rows; row++) {
   // Проходим по всем колонкам
   for (let col = 0; col < cols; col++) {
     // Создаём элемент div для каждой клетки доски
     const cell = document.createElement("div");
     // Добавляем общий класс для всех клеток
     cell.classList.add("cell");
     // Устанавливаем класс для цвета клетки (белая или чёрная)
     cell.classList.add((row + col) % 2 === 0 ? "white" : "black");
     // Сохраняем координаты клетки в атрибуты dataset
     cell.dataset.row = row;
     cell.dataset.col = col;
     // Добавление шашек на доску
     // Чёрные шашки на первых трёх рядах
     if (row < 3 && (row + col) % 2 !== 0) {
       // Добавляем чёрную шашку, если условие чётности позиции выполняется
       addPiece(cell, "black");
       // Белые шашки на последних трёх рядах
       // Добавляем белую шашку, если условие чётности позиции выполняется
     } else if (row > 4 && (row + col) % 2 !== 0) {
       addPiece(cell, "white");
     }
     // Добавляем обработчик события для кликов по клетке
     cell.addEventListener("click", onCellClick);
     // Добавляем клетку на доску
     board.appendChild(cell);
   }
 }
}

function addPiece(cell, color) {
 // Создаём div для шашки
 const piece = document.createElement("div");
 // Добавляем общий класс для всех шашек и класс, определяющий цвет
 piece.classList.add("piece", color);
 // Сохраняем цвет шашки в атрибут dataset
 piece.dataset.color = color;
 // Устанавливаем атрибут, указывающий, что шашка не является дамкой
 piece.dataset.king = "false";
 // Добавляем шашку в клетку
 cell.appendChild(piece);
}
// Обработка кликов по клеткам
function onCellClick(e) {
 // Находим ближайший элемент с классом "cell", который был нажат
 const cell = e.target.closest(".cell");
 // Если такой элемент не найден, выходим из функции
 if (!cell) return;
 // Находим шашку в этой клетке, если она есть
 const piece = cell.querySelector(".piece");
 // Если шашка уже выбрана
 if (selectedPiece) {
   // Проверяем, можно ли сделать ход
   if (canMove(selectedPiece, cell)) {
     // Перемещаем шашку
     movePiece(selectedPiece, cell);
     // Сбрасываем выбранную шашку
     selectedPiece = null;
   } else {
     // Сбрасываем выбранную шашку, если ход невозможен
     selectedPiece = null;
   }
 } else {
   // Если шашка не выбрана и текущий ход соответствует цвету шашки
   if (piece && piece.dataset.color === turn) {
     // Выбираем шашку
     selectedPiece = piece;
   }
 }
}

// Проверка возможности хода на целевую клетку
function canMove(piece, targetCell) {
 // Получаем координаты начальной клетки (где стоит выбранная шашка)
 const startRow = parseInt(piece.parentElement.dataset.row);
 const startCol = parseInt(piece.parentElement.dataset.col);

 // Получаем координаты целевой клетки (куда хотим переместить шашку)
 // Преобразуем строковое значение номера ряда в целое число
 const endRow = parseInt(targetCell.dataset.row);
 // Преобразуем строковое значение номера колонки в целое число
 const endCol = parseInt(targetCell.dataset.col);

 // Определяем направление хода (белые шашки идут вверх, чёрные — вниз)
 const direction = piece.dataset.color === "white" ? -1 : 1;

 // Проверяем, является ли шашка дамкой
 const isKing = piece.dataset.king === "true";

 // Если целевая клетка уже занята другой шашкой, ход невозможен
 if (targetCell.querySelector(".piece")) {
   // Целевая клетка занята
   return false;
 }

 // Проверяем возможность хода для обычной шашки или дамки
 if (!isKing) {
   // Если шашка не дамка, проверяем возможность хода для обычной шашки
   return canMoveRegular(piece, startRow, startCol, endRow, endCol, direction);
 } else {
   // Если шашка дамка, проверяем возможность хода для дамки
   return canMoveKing(piece, startRow, startCol, endRow, endCol);
 }
}

// Проверка возможности хода для обычной шашки
function canMoveRegular(piece, startRow, startCol, endRow, endCol, direction) {
 // Проверка на обычный ход (одна клетка по диагонали)
 if (
   Math.abs(endRow - startRow) === 1 &&
   Math.abs(endCol - startCol) === 1 &&
   endRow - startRow === direction
 ) {
   // Если ход соответствует правилам обычного хода
   return true;
 }
 // Проверка на ход с захватом (две клетки по диагонали)
 if (
   Math.abs(endRow - startRow) === 2 &&
   Math.abs(endCol - startCol) === 2 &&
   endRow - startRow === 2 * direction
 ) {
   // Находим координаты средней клетки (между начальной и целевой)
   const middleRow = (startRow + endRow) / 2;
   const middleCol = (startCol + endCol) / 2;

   // Находим среднюю клетку по координатам
   const middleCell = document.querySelector(
     `.cell[data-row="${middleRow}"][data-col="${middleCol}"]`
   );
   // Проверяем, есть ли в средней клетке шашка и если есть, то принадлежит ли она противнику
   // Ищем шашку в средней клетке
   const middlePiece = middleCell.querySelector(".piece");
   // Проверяем, занята ли средняя клетка противником
   if (middlePiece && middlePiece.dataset.color !== piece.dataset.color) {
     // Если допустим ход с захватом
     return true;
   }
 }
 // Невозможный ход
 return false;
}

function canMoveKing(piece, startRow, startCol, endRow, endCol) {
 // Проверяем, что конечная клетка находится на той же диагонали, что и начальная
 if (Math.abs(endRow - startRow) === Math.abs(endCol - startCol)) {
   // Переменная для проверки, свободен ли путь
   let pathClear = true;
   // Переменная для отслеживания захвата шашки
   let middlePieceCaptured = false;
   // Определяем шаги по строкам и столбцам в зависимости от направления движения
   const rowStep = endRow > startRow ? 1 : -1;
   const colStep = endCol > startCol ? 1 : -1;
   // Проходим по всем клеткам на пути перемещения
   for (let i = 1; i < Math.abs(endRow - startRow); i++) {
     // Вычисляем координаты промежуточной клетки на пути
     const intermediateCell = document.querySelector(
       `.cell[data-row="${startRow + i * rowStep}"][data-col="${
         startCol + i * colStep
       }"]`
     );
     // Ищем шашку на этой клетке
     const pieceOnPath = intermediateCell.querySelector(".piece");
     // Если на пути есть шашка
     if (pieceOnPath) {
       // Проверяем, принадлежит ли шашка противнику
       if (pieceOnPath.dataset.color !== piece.dataset.color) {
         // Если шашка противника ещё не была захвачена
         if (middlePieceCaptured) {
           // Путь заблокирован
           pathClear = false;
           // Прерываем цикл проверки
           break;
         } else {
           // Отмечаем, что шашка противника была захвачена
           middlePieceCaptured = true;
         }
       } else {
         // Путь заблокирован другой шашкой
         pathClear = false;
         // Прерываем цикл проверки
         break;
       }
     }
   }
   // Если путь свободен, возвращаем true
   if (pathClear) {
     return true;
   }
 }
 // Если путь не свободен или ход не по диагонали, возвращаем false
 return false;
}

// Перемещение шашки на новую клетку
function movePiece(piece, targetCell) {
 // Получаем начальную строку, где находилась шашка
 const startRow = parseInt(piece.parentElement.dataset.row);
 // Получаем начальный столбец, где находилась шашка
 const startCol = parseInt(piece.parentElement.dataset.col);
 // Получаем конечную строку, куда шашка будет перемещена
 const endRow = parseInt(targetCell.dataset.row);
 // Получаем конечный столбец, куда шашка будет перемещена
 const endCol = parseInt(targetCell.dataset.col);

 // Проверяем, осуществляется ли захват (перемещение на две клетки)
 if (Math.abs(endRow - startRow) === 2 && Math.abs(endCol - startCol) === 2) {
   // Вычисляем координаты серединной клетки между начальной и конечной
   const middleRow = (startRow + endRow) / 2;
   // Вычисляем координаты серединного столбца между начальным и конечным
   const middleCol = (startCol + endCol) / 2;
   // Получаем доступ к серединной клетке
   const middleCell = document.querySelector(
     `.cell[data-row="${middleRow}"][data-col="${middleCol}"]`
   );
   // Проверяем, есть ли шашка в серединной клетке
   const middlePiece = middleCell.querySelector(".piece");
   // Удаляем шашку, если она там есть
   if (middlePiece) {
     middleCell.removeChild(middlePiece);
   }
 }

 // Перемещаем шашку в целевую клетку
 targetCell.appendChild(piece);

 // Проверяем, достигла ли шашка последнего ряда для превращения в дамку
 if (
   (piece.dataset.color === "white" && endRow === 0) ||
   (piece.dataset.color === "black" && endRow === 7)
 ) {
   // Отмечаем шашку как дамку
   piece.dataset.king = "true";
   // Добавляем класс, указывающий, что шашка теперь дамка
   piece.classList.add("king");
 }

 //  Переход хода к следующему игроку
 turn = turn === "white" ? "black" : "white";
 // Сбрасываем выбор текущей шашки
 selectedPiece = null;
 // Вызываем функцию для проверки состояния игры после хода
 checkGameState();
}


function checkGameState() {
 // Инициализация переменных для подсчёта шашек и проверки наличия ходов
 // Количество белых шашек на доске
 let whiteCount = 0;
 // Количество чёрных шашек на доске
 let blackCount = 0;
 // Индикатор наличия допустимых ходов у белых
 let whiteHasMoves = false;
 // Индикатор наличия допустимых ходов у чёрных
 let blackHasMoves = false;

 // Перебор всех клеток на доске для проверки шашек и доступных ходов
 for (let row = 0; row < rows; row++) {
   for (let col = 0; col < cols; col++) {
     // Получение клетки по текущим координатам
     const cell = document.querySelector(
       `.cell[data-row="${row}"][data-col="${col}"]`
     );
     // Попытка найти шашку в данной клетке
     const piece = cell.querySelector(".piece");

     // Проверка наличия шашки в клетке
     if (piece) {
       // Увеличение соответствующего счётчика в зависимости от цвета шашки
       if (piece.dataset.color === "white") {
         // Увеличиваем счётчик белых шашек
         whiteCount++;
         // Проверка наличия доступных ходов для белой шашки
         if (!whiteHasMoves && hasValidMoves(piece)) {
           // Обновление флага доступных ходов для белых
           whiteHasMoves = true;
         }
       } else if (piece.dataset.color === "black") {
         // Увеличиваем счётчик чёрных шашек
         blackCount++;
         // Проверка наличия доступных ходов для чёрной шашки
         if (!blackHasMoves && hasValidMoves(piece)) {
           // Обновление флага доступных ходов для чёрных
           blackHasMoves = true;
         }
       }
     }
   }
 }

 // Проверка валидности позиции на доске
function isValidPosition(row, col) {
 // Проверяем, что координаты находятся внутри границ доски
 return row >= 0 && row < rows && col >= 0 && col < cols;
}

 // Проверка условий окончания игры и объявление победителя
 if (whiteCount === 0 || !whiteHasMoves) {
   // Проверка отсутствия шашек или ходов у белых
   // Объявление победы чёрных
   alert("Чёрные выигрывают!");
   // Отключение доски для завершения игры
   disableBoard();
 } else if (blackCount === 0 || !blackHasMoves) {
   // Проверка отсутствия шашек или ходов у чёрных
   // Объявление победы белых
   alert("Белые выигрывают!");
   // Отключение доски для завершения игры
   disableBoard();
 }
}

// Проверка наличия допустимых ходов для шашки
function hasValidMoves(piece) {
 // Получаем начальные координаты шашки
 const startRow = parseInt(piece.parentElement.dataset.row);
 const startCol = parseInt(piece.parentElement.dataset.col);

 // Определяем возможные направления для перемещения
 const directions =
   piece.dataset.king === "true"
     ? [
         // Для дамки: возможность движения во всех направлениях по диагонали
         [1, 1],
         [1, -1],
         [-1, 1],
         [-1, -1],
       ]
     : piece.dataset.color === "white"
     ? [
         // Для белой шашки: движение только вверх по диагонали
         [-1, 1],
         [-1, -1],
       ]
     : [
         // Для чёрной шашки: движение только вниз по диагонали
         [1, 1],
         [1, -1],
       ];

 // Перебираем все направления движения
 for (let [dr, dc] of directions) {
   // Вычисляем координаты новой строки после хода
   const newRow = startRow + dr;
   // Вычисляем координаты нового столбца после хода
   const newCol = startCol + dc;

   // Проверяем, находится ли новая позиция в пределах доски
   if (isValidPosition(newRow, newCol)) {
     // Находим клетку по новым координатам
     const targetCell = document.querySelector(
       `.cell[data-row="${newRow}"][data-col="${newCol}"]`
     );
     // Проверяем, свободна ли целевая клетка
     if (!targetCell.querySelector(".piece")) {
       return true; // Возвращаем true, если клетка свободна
     }
     // Вычисляем координаты клетки после возможного захвата
     const jumpRow = startRow + 2 * dr;
     const jumpCol = startCol + 2 * dc;
     if (isValidPosition(jumpRow, jumpCol)) {
       // Находим клетку середины, где может находиться шашка противника
       const middleCell = document.querySelector(
         `.cell[data-row="${newRow}"][data-col="${newCol}"]`
       );
       // Находим клетку после захвата
       const jumpCell = document.querySelector(
         `.cell[data-row="${jumpRow}"][data-col="${jumpCol}"]`
       );
       // Проверяем, есть ли в середине шашка противника
       const middlePiece = middleCell.querySelector(".piece");
       // Проверяем, свободна ли целевая клетка после захвата и находится ли в середине шашка противника
       if (
         middlePiece &&
         middlePiece.dataset.color !== piece.dataset.color &&
         !jumpCell.querySelector(".piece")
       ) {
         return true; // Возвращаем true, если возможен ход с захватом
       }
     }
   }
 }
 // Если ни один из возможных ходов не подошёл, возвращаем false
 return false;
}

// Отключение доски (блокировка дальнейших ходов)
function disableBoard() {
 document
   // Удаление обработчиков событий клика со всех клеток
   .querySelectorAll(".cell")
   .forEach((cell) => cell.removeEventListener("click", onCellClick));
}
// Инициализация игры
initializeBoard();

Обложка:

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

Корректор:

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

Вёрстка:

Маша Климентьева

Соцсети:

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

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