❌⭕ Я хочу сыграть с тобой в одну игру: пилим «Крестики-нолики» на JavaScript

Хватит теории – пора практиковаться! Мы разберем создание полноценной браузерной игры, затрагивая ключевые концепции frontend-разработки. Идеально для новичков, жаждущих применить свои знания в реальном проекте.

Оригинал статьи здесь.

Создадим папку для хранения файлов проекта. Назовем ее Tic-Tac-Toe-game.

В папке проекта создадим три основных файла:

  • index.html: HTML-файл проекта.
  • style.css: CSS-файл.
  • script.js: JavaScript-файл для построения логики игры.

Теперь встроим JS- и CSS-файлы в HTML-страницу.

Откроем index.html и добавим в тег <head> ссылку на CSS-файл.

<link rel="stylesheet" href="style.css">

Перед закрывающим тегом </body> добавим ссылку на JavaScript-файл.

<script src="script.js"></script>

Откроем index.html в браузере. Сейчас мы увидим пустую страницу.

Создаем игровое поле 3x3 с помощью HTML

Начнем с того, что убедимся в наличии базовой структуры HTML-документа.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tic-Tac-Toe Game</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- Код игрового поля добавим сюда -->
  <script src="script.js"></script>
</body>
</html>

Это базовая структура для любого HTML-проекта. Она включает объявление DOCTYPE, тег HTML, тег head с мета-тегами, заголовком и ссылкой на CSS.

Добавление игрового поля

Внутри тега <body> создадим сетку 3x3 для игры. Используем элементы div:

  • Основной контейнер с ID "Tic-Tac-Toe-board".
  • Внутри него три div с классом "row" для рядов сетки.
  • В каждом ряду три div с классом "cell" для ячеек.
  • Каждой ячейке присвоим уникальный id.
<div id="tic-tac-toe-board">
  <div class="row">
      <div class="cell" id="cell-1"></div>
      <div class="cell" id="cell-2"></div>
      <div class="cell" id="cell-3"></div>
  </div>
  <div class="row">
      <div class="cell" id="cell-4"></div>
      <div class="cell" id="cell-5"></div>
      <div class="cell" id="cell-6"></div>
  </div>
  <div class="row">
      <div class="cell" id="cell-7"></div>
      <div class="cell" id="cell-8"></div>
      <div class="cell" id="cell-9"></div>
  </div>
</div>

Сохраните изменения и обновите страницу. Сетка пока не видна, но убедитесь в отсутствии ошибок.

👨‍💻🎨 Библиотека фронтендера
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

Стилизация игрового поля с помощью CSS

Откроем файл style.css. Начнем с установки основных стилей для игрового поля:

body {
  /* Устанавливаем основной шрифт всей страницы */
  font-family: 'Arial', sans-serif;

  /* Настраиваем стили так, чтобы центрировать все содержимое по вертикали */
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  /* Растягиваем страницу на всю высоту экрана */
  height: 100vh;

  /* Убираем внешние отступы */
  margin: 0;

  /* Добавляем фоновый градиент */
  background: linear-gradient(to right, #74ebd5, #ACB6E5);

  /* Устанавливаем основной цвет текста */
  color: #333;
}

Здесь мы:

  1. Устанавливаем основной шрифт всей страницы. В данном случае это Arial.
  2. Настраиваем стили так, чтобы центрировать все содержимое по вертикали.
  3. Растягиваем страницу на всю высоту экрана.
  4. Убираем внешние отступы.
  5. Добавляем фоновый градиент.
  6. Устанавливаем основной цвет текста.

Сохраните style.css и обновите страницу в браузере. Вы должны увидеть четко оформленное интерактивное игровое поле.

Адаптивный дизайн

Для корректного отображения на всех устройствах добавьте элементы адаптивного дизайна. Используйте @-правило @media в CSS:

/* Стили будут применяться только для экранов шириной 600 пикселей или меньше */
@media (max-width: 600px) {
  /* Выбирает все элементы с классом "cell" */
  .cell {
      /* Устанавливает ширину и высоту каждой ячейки в 60 пикселей */
      width: 60px;
      height: 60px;
  }

  #tic-tac-toe-board {
      /* Задает три колонки в сетке, каждая шириной 60 пикселей */
      grid-template-columns: repeat(3, 60px);
      /* Задает три строки в сетке, каждая высотой 60 пикселей. */
      grid-template-rows: repeat(3, 60px);
  }
}
  • @media (max-width: 600px) корректирует размер ячеек и сетки на экранах меньше 600 пикселей, делая игру удобной на мобильных устройствах.
  • .cell { выбирает все элементы с классом "cell".
  • width: 60px; и height: 60px; устанавливают ширину и высоту каждой ячейки в 60 пикселей. Это уменьшает размер ячеек для маленьких экранов.
  • grid-template-columns: repeat(3, 60px); задает три колонки в сетке, каждая шириной 60 пикселей.
  • grid-template-rows: repeat(3, 60px); задает три строки в сетке, каждая высотой 60 пикселей.

Разработка игровой логики

На этом этапе мы создадим логику игры:

  1. Обработаем ходы игроков.
  2. Определим условия победы.
  3. Настроим сброс игры после ее завершения.

Инициализация состояния игры

Откройте файл script.js. Сначала нужно создать переменные для отслеживания состояния игры. Это включает псевдоним текущего игрока, состояние игрового поля и статус игры (выиграна или продолжается).

let currentPlayer = 'X'; // Player X always starts
let gameBoard = ['', '', '', '', '', '', '', '', '']; // 3x3 game board
let gameActive = true;

Эти строки кода задают базовые переменные:

  • currentPlayer будет чередоваться между игроками 'X' и 'O'.
  • gameBoard – массив, представляющий нашу сетку 3x3.
  • gameActive показывает, продолжается ли игра.

Обработка ходов игроков

Теперь напишем функцию для обработки ходов игроков. Эта функция будет обновлять игровое поле и переключать ход на другого игрока.

function handlePlayerTurn(clickedCellIndex) {
  if (gameBoard[clickedCellIndex] !== '' || !gameActive) {
      return;
  }
  gameBoard[clickedCellIndex] = currentPlayer;
  currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
}

Функция проверяет, пуста ли выбранная ячейка и активна ли игра. Если да, она устанавливает символ текущего игрока и переключает ход.

Управление взаимодействием с игроками

На этом этапе мы поднимем нашу игру на новый уровень, реализовав взаимодействие с игроками.

В файле script.js добавим слушателя событий к каждой ячейке игрового поля. Сначала выберем все элементы ячеек.

const cells = document.querySelectorAll('.cell');

Теперь добавим слушателя событий к каждой ячейке. Этот слушатель будет вызывать функцию cellClicked при клике на ячейку.

cells.forEach(cell => {
  cell.addEventListener('click', cellClicked, false);
});

Обработка кликов по ячейкам

Создадим функцию cellClicked для обработки логики при клике на ячейку. Она проверит индекс ячейки, обновит состояние игры и обновит интерфейс.

function cellClicked(clickedCellEvent) {
  const clickedCell = clickedCellEvent.target;
  const clickedCellIndex = parseInt(clickedCell.id.replace('cell-', '')) - 1;
 
  if (gameBoard[clickedCellIndex] !== '' || !gameActive) {
      return;
  }

  handlePlayerTurn(clickedCellIndex);
  updateUI();
}

В этой функции мы:

  1. Получаем ID ячейки, по которой кликнул игрок.
  2. Проверяем, не занята ли уже ячейка и активна ли игра.
  3. Вызываем handlePlayerTurn для обновления состояния игры и updateUI для отображения изменений на доске.

Обновление пользовательского интерфейса

После каждого хода нужно обновлять игровую доску, чтобы показать ходы игроков. Напишем функцию updateUI. Эта функция обновляет каждую ячейку соответствующим значением из массива gameBoard, отображая X и O на доске.

function updateUI() {
  for (let i = 0; i < cells.length; i++) {
      cells[i].innerText = gameBoard[i];
  }
}

Тестирование

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

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

Реализация условия победы

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

Выигрышные комбинации

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

const winConditions = [
  [0, 1, 2], // Верхний ряд
  [3, 4, 5], // Средний ряд
  [6, 7, 8], // Нижний ряд
  [0, 3, 6], // Левый столбец
  [1, 4, 7], // Средний столбец
  [2, 5, 8], // Правый столбец
  [0, 4, 8], // Диагональ слева направо
  [2, 4, 6]  // Диагональ справа налево
];

Проверка на победу или ничью

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

function checkForWinOrDraw() {
  let roundWon = false;

  for (let i = 0; i < winConditions.length; i++) {
      const [a, b, c] = winConditions[i];
      if (gameBoard[a] && gameBoard[a] === gameBoard[b] && gameBoard[a] === gameBoard[c]) {
          roundWon = true;
          break;
      }
  }

  if (roundWon) {
      announceWinner(currentPlayer);
      gameActive = false;
      return;
  }

  let roundDraw = !gameBoard.includes('');
  if (roundDraw) {
      announceDraw();
      gameActive = false;
      return;
  }
}

В этой функции мы:

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

Объявление победителя и обработка ничьей

Теперь напишем функции для отображения сообщений о победе игрока или ничьей.

Сначала добавим новый div в HTML для отображения игровых сообщений.

<div id="gameMessage" class="game-message"></div>

Добавим также стили в CSS-файл.

.game-message {
  text-align: center;
  margin-top: 20px;
  font-size: 20px;
  color: #333;
}

Теперь займемся JavaScript-функциями.

function announceWinner(player) {
  const messageElement = document.getElementById('gameMessage');
  messageElement.innerText = `Player ${player} Wins!`;
}

function announceDraw() {
  const messageElement = document.getElementById('gameMessage');
  messageElement.innerText = 'Game Draw!';
}

Эти функции обновят интерфейс, чтобы информировать игроков о результате игры.

Добавление функции сброса игры

Полноценная игра «Крестики-нолики» должна позволять игрокам начать заново после завершения партии. Это может быть после победы, ничьей или просто по желанию игроков.

Cоздадим функцию для сброса игры. Она очистит доску и сбросит все необходимые переменные и позволит игрокам начать новую игру без перезагрузки страницы.

function resetGame() {
  gameBoard = ['', '', '', '', '', '', '', '', '']; // Clear the game board
  gameActive = true; // Set the game as active
  currentPlayer = 'X'; // Reset to player X
  // Clear all cells on the UI
  cells.forEach(cell => {
      cell.innerText = '';
  });
  document.getElementById('gameMessage').innerText = '';
}

Добавление кнопки сброса

В HTML файле добавьте кнопку сброса:

<button id="resetButton">Reset Game</button>

Затем в script.js добавьте слушатель событий для этой кнопки:

const resetButton = document.getElementById('resetButton');
resetButton.addEventListener('click', resetGame, false);

Этот код выбирает кнопку сброса и присоединяет к ней слушатель событий. При нажатии кнопки вызывается функция resetGame.

Также добавим CSS-стили для новой кнопки:

#resetButton {
  padding: 10px 20px;
  font-size: 1rem;
  color: #fff;
  background-color: #333;
  border: none;
  cursor: pointer;
  border-radius: 5px;
  transition: background-color 0.3s ease;
}

#resetButton:hover {
  background-color: #555;
}

Готово.

Полный код

HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tic-Tac-Toe Game</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="tic-tac-toe-board">
      <div class="row">
          <div class="cell" id="cell-1"></div>
          <div class="cell" id="cell-2"></div>
          <div class="cell" id="cell-3"></div>
      </div>
      <div class="row">
          <div class="cell" id="cell-4"></div>
          <div class="cell" id="cell-5"></div>
          <div class="cell" id="cell-6"></div>
      </div>
      <div class="row">
          <div class="cell" id="cell-7"></div>
          <div class="cell" id="cell-8"></div>
          <div class="cell" id="cell-9"></div>
      </div>
  </div>
  <div id="gameMessage" class="game-message"></div>
  <button id="resetButton">Reset Game</button>  
  <script src="script.js"></script>
</body>
</html>

CSS

body {
  font-family: 'Arial', sans-serif;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
  margin: 0;
  background: linear-gradient(to right, #74ebd5, #ACB6E5);
  color: #333;
}

#tic-tac-toe-board {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  gap: 10px;
}

.cell {
  background-color: #fff;
  border: 2px solid #333;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 2rem;
  cursor: pointer;
  transition: background-color 0.3s ease;
  width: 100px;
  height: 100px;
}

.cell:hover {
  background-color: #e3e3e3;
}

.game-message {
  text-align: center;
  margin-top: 20px;
  font-size: 20px;
  color: #333;
}

#resetButton {
  padding: 10px 20px;
  font-size: 1rem;
  color: #fff;
  background-color: #333;
  border: none;
  cursor: pointer;
  border-radius: 5px;
  transition: background-color 0.3s ease;
}

#resetButton:hover {
  background-color: #555;
}

JavaScript

let currentPlayer = 'X';
let gameBoard = ['', '', '', '', '', '', '', '', ''];
let gameActive = true;

function handlePlayerTurn(clickedCellIndex) {
  if (gameBoard[clickedCellIndex] !== '' || !gameActive) {
      return;
  }
  gameBoard[clickedCellIndex] = currentPlayer;
  checkForWinOrDraw();
  currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
}

function cellClicked(clickedCellEvent) {
  const clickedCell = clickedCellEvent.target;
  const clickedCellIndex = parseInt(clickedCell.id.replace('cell-', '')) - 1;
  if (gameBoard[clickedCellIndex] !== '' || !gameActive) {
      return;
  }
  handlePlayerTurn(clickedCellIndex);
  updateUI();
}

const cells = document.querySelectorAll('.cell');

cells.forEach(cell => {
  cell.addEventListener('click', cellClicked, false);
});

function updateUI() {
  for (let i = 0; i < cells.length; i++) {
      cells[i].innerText = gameBoard[i];
  }
}

function announceWinner(player) {
  const messageElement = document.getElementById('gameMessage');
  messageElement.innerText = `Player ${player} Wins!`;
}

function announceDraw() {
  const messageElement = document.getElementById('gameMessage');
  messageElement.innerText = 'Game Draw!';
}

const winConditions = [
  [0, 1, 2], // Top row
  [3, 4, 5], // Middle row
  [6, 7, 8], // Bottom row
  [0, 3, 6], // Left column
  [1, 4, 7], // Middle column
  [2, 5, 8], // Right column
  [0, 4, 8], // Left-to-right diagonal
  [2, 4, 6]  // Right-to-left diagonal
];

function checkForWinOrDraw() {
  let roundWon = false;

  for (let i = 0; i < winConditions.length; i++) {
      const [a, b, c] = winConditions[i];
      if (gameBoard[a] && gameBoard[a] === gameBoard[b] && gameBoard[a] === gameBoard[c]) {
          roundWon = true;
          break;
      }
  }

  if (roundWon) {
      announceWinner(currentPlayer);
      gameActive = false;
      return;
  }

  let roundDraw = !gameBoard.includes('');
  if (roundDraw) {
      announceDraw();
      gameActive = false;
      return;
  }
}

function resetGame() {
  gameBoard = ['', '', '', '', '', '', '', '', ''];
  gameActive = true;
  currentPlayer = 'X';
  cells.forEach(cell => {
      cell.innerText = '';
  });
  document.getElementById('gameMessage').innerText = '';
}

const resetButton = document.getElementById('resetButton');
resetButton.addEventListener('click', resetGame, false);
***

Если ты задумываешься о том, чтобы освоить веб-разработку, курс Frontend Basic – это то, что тебе нужно:

  • Изучишь HTML, CSS и JavaScript с нуля.
  • Создашь свой первый интернет-магазин, применяя полученные знания.
  • Освоишь работу с Git и GitHub – важные инструменты для разработчиков.
  • Получишь навыки адаптивной верстки, чтобы сайты выглядели отлично на всех устройствах.
  • Сформируешь портфолио, которое поможет в поиске работ.

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
01 марта 2020

ТОП-15 книг по JavaScript: от новичка до профессионала

В этом посте мы собрали переведённые на русский язык книги по JavaScript – ...
admin
10 июня 2018

Лайфхак: в какой последовательности изучать JavaScript

Огромный инструментарий JS и тонны материалов по нему. С чего начать? Расск...