Привет! Сегодня мы напишем классическую игру «Крестики-нолики» с нуля, используя только HTML, CSS и JavaScript. Никаких фреймворков — чистый код, который работает сразу в браузере.
Это отличный проект для тренировки логики, массивов и работы с DOM. В конце у нас получится минималистичная, но симпатичная игра для двух игроков (за одним устройством).
Шаг 1. Скелет на HTML
Создайте файл index.html. Нам нужна сетка 3×3, заголовок и место для отображения результата.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Крестики-нолики</title>
<style>
/* Стили пока пропустим, добавим позже */
</style>
</head>
<body>
<div class="container">
<h1>Крестики-нолики</h1>
<div class="status" id="status">Ход: X</div>
<div class="board" id="board">
<!-- 9 ячеек заполним через JS, но можно и вручную -->
</div>
<button id="reset">Новая игра</button>
</div>
<script>
// Здесь будет логика
</script>
</body>
</html>
Шаг 2. Стилизуем поле (CSS)
Вставьте CSS-код внутрь тега <style>, чтобы игра выглядела аккуратно.
* {
box-sizing: border-box;
user-select: none;
}
body {
font-family: system-ui, 'Segoe UI', sans-serif;
background: linear-gradient(145deg, #1e2a3e 0%, #0f1724 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(4px);
border-radius: 48px;
padding: 30px 20px 30px 20px;
text-align: center;
box-shadow: 0 25px 40px rgba(0,0,0,0.3);
width: 400px;
}
h1 {
color: #f0f3f8;
font-weight: 600;
margin-top: 0;
margin-bottom: 20px;
font-size: 2rem;
text-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.status {
background: #0b1a2b;
color: #bbd9ff;
font-size: 1.5rem;
font-weight: bold;
padding: 10px 20px;
border-radius: 60px;
margin-bottom: 25px;
display: inline-block;
min-width: 160px;
letter-spacing: 1px;
}
.board {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
background-color: #2c3e4e;
padding: 15px;
border-radius: 36px;
margin-bottom: 25px;
box-shadow: inset 0 0 8px #1a2a36, 0 10px 20px rgba(0,0,0,0.2);
}
.cell {
aspect-ratio: 1 / 1;
background: #eef5ff;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3.8rem;
font-weight: 700;
font-family: monospace;
cursor: pointer;
transition: 0.1s linear;
box-shadow: 0 6px 0 #7f8c8d;
color: #1e2f3f;
}
.cell:active {
transform: translateY(2px);
box-shadow: 0 3px 0 #7f8c8d;
}
/* победные ячейки подсветим */
.cell.win {
background: #b8e4b0;
color: #1e5c1a;
box-shadow: 0 0 0 2px gold;
}
button {
background: #ffb347;
border: none;
font-size: 1.3rem;
font-weight: bold;
padding: 12px 28px;
border-radius: 48px;
cursor: pointer;
color: #2c2b28;
transition: 0.1s linear;
box-shadow: 0 5px 0 #a45d1a;
margin-top: 5px;
}
button:active {
transform: translateY(2px);
box-shadow: 0 2px 0 #a45d1a;
}
Шаг 3. JavaScript — логика сердца игры
Теперь самое интересное. Пишем код внутри <script>. Разобьём на задачи:
- Хранить состояние игрового поля (массив 3×3).
- Обрабатывать клики по клеткам.
- Проверять победу или ничью.
- Менять игрока (X или O).
// Ждём загрузки DOM
document.addEventListener('DOMContentLoaded', () => {
// --- элементы ---
const boardElement = document.getElementById('board');
const statusDiv = document.getElementById('status');
const resetBtn = document.getElementById('reset');
// --- переменные игры ---
let board = ['', '', '', '', '', '', '', '', ''];
let currentPlayer = 'X'; // X ходит первым
let gameActive = true; // игра идёт или закончена
// комбинации побед (индексы ячеек)
const winPatterns = [
[0,1,2], [3,4,5], [6,7,8], // горизонтали
[0,3,6], [1,4,7], [2,5,8], // вертикали
[0,4,8], [2,4,6] // диагонали
];
// --- вспомогательная функция проверки победы ---
function checkWinner() {
for (let pattern of winPatterns) {
const [a, b, c] = pattern;
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
// нашли победителя
gameActive = false;
statusDiv.textContent = `🏆 Победил ${board[a]}! 🏆`;
highlightWinningCells(pattern);
return true;
}
}
// ничья?
if (!board.includes('')) {
gameActive = false;
statusDiv.textContent = '🤝 Ничья! 🤝';
return true; // игра закончена (ничья)
}
return false;
}
// подсветка победных клеток
function highlightWinningCells(pattern) {
const cells = document.querySelectorAll('.cell');
pattern.forEach(index => {
cells[index].classList.add('win');
});
}
// снять подсветку победных клеток
function clearWinHighlight() {
const cells = document.querySelectorAll('.cell');
cells.forEach(cell => cell.classList.remove('win'));
}
// --- обработчик клика по ячейке ---
function handleCellClick(index) {
// если ячейка занята или игра окончена — ничего не делаем
if (board[index] !== '' || !gameActive) return;
// ставим символ
board[index] = currentPlayer;
renderBoard();
// проверяем победу или ничью
const finished = checkWinner();
if (finished) {
renderBoard(); // перерендер для подсветки
return;
}
// меняем игрока
currentPlayer = (currentPlayer === 'X') ? 'O' : 'X';
statusDiv.textContent = `Ход: ${currentPlayer}`;
}
// --- отрисовка игрового поля ---
function renderBoard() {
// очищаем контейнер
boardElement.innerHTML = '';
for (let i = 0; i < board.length; i++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.textContent = board[i];
// вешаем обработчик с индексом
cell.addEventListener('click', (function(idx) {
return function() { handleCellClick(idx); };
})(i));
boardElement.appendChild(cell);
}
}
// --- сброс игры ---
function resetGame() {
board = ['', '', '', '', '', '', '', '', ''];
currentPlayer = 'X';
gameActive = true;
clearWinHighlight();
renderBoard();
statusDiv.textContent = 'Ход: X';
}
// --- старт и кнопка сброса ---
renderBoard();
resetBtn.addEventListener('click', resetGame);
});
Шаг 4. Объясняем логику читателям
В статье после кода обязательно поясните ключевые моменты:
- Массив board хранит 9 строк. Пустая строка — свободная клетка.
- winPatterns — все 8 выигрышных комбинаций. После каждого хода мы проверяем, совпали ли символы в одной из комбинаций.
- Подсветка победных клеток через дополнительный CSS-класс win.
- Смена игрока — простая тернарная операция.
- Сброс очищает массив, возвращает gameActive = true и обновляет интерфейс.
Что можно улучшить?
- Добавить анимацию появления X и O.
- Режим «Игра с ботом» (минимакс алгоритм).
- Счёт побед (X — 3, O — 1).
- Адаптацию под мобильные устройства (у нас и так хорошо).
Заключение
Всего за 15 минут мы создали полноценную игру на чистом JavaScript без сторонних библиотек. Теперь вы понимаете, как работать с событиями, динамически создавать элементы и управлять состоянием приложения.
Попробуйте сами: поиграйте с друзьями или доработайте код. А если появятся вопросы — пишите в комментариях!
👇 А вы чаще играете за X или за O? Делитесь в комментариях!
Копируйте код и тестируйте прямо сейчас!