Создаем шахматы с «туманом войны» на JavaScript: от идеи до работающего прототипа
- вторник, 9 декабря 2025 г. в 00:00:07

Всё началось с внутреннего предложения на работе присоединиться к отделу по развитию и поддержке веб-сайтов (название другое, но это их обязанности, поэтому написал так). Это была одновременно лестная и пугающая перспектива. Возможность работать над реальными проектами манила, но мой опыт в веб-разработке был скорее теоретическим. Я понимал, что для старта мне нужен был собственный, понятный проект, который стал бы началом.
И тут я вспомнил о своей идее сделать сайт для консольной реализации шахмат на Python. Это была сухая игра, написанная на Pygame для двоих программистов, но не для людей, так как её нельзя было запустить на других устройствах. И у меня родилась идея: а что, если превратить этот скелет игры в веб-приложение на JS?
Из минусов было только то, что моя игра написана на языке Python, а для работы мне нужен JavaScript, и я решил, что это не проблема, и начал переписывать готовую логику на новый язык, параллельно добавляя новые функции.
Идея зацепила сразу, представьте: вы переносите свой проверенный, рабочий кейс, у которого вся шахматная логика (проверка ходов, матов и пат) уже работает и ничего нового придумывать не надо, нужно только сделать интерфейс для браузера и все новые идеи (например, таймер).
Но на пути к этой цели стояли два момента, которые нужно вспомнить или изучить заново:

1. JavaScript – должен был оживить фронтенд и дать проекту новые краски. Чтобы он работал на телефоне и на компе, ему нужно взять на себя все внутренние задачи.
2. PHP – должен был стать мозгом и опорой на стороне сервера, на нём стоит задача объединить все части кода (CSS, JS, PHP, HTML).
Конечной целью я вижу не публичный запуск очередного chess.com, а создание чистого, хорошо структурированного шаблона (codebase). Чтобы любой разработчик, будь то новичок, как я, или опытный специалист, мог взять мой проект за основу, доработать его и превратить в полноценный игровой сервис: с учётными записями, рейтингами, чатом и, почему бы нет, с тем самым «туманом войны», который так вдохновлял меня в самом начале моего проекта.
Таким образом, работа над этими шахматами – это мой шаг из мира алгоритмов и простого языка (Python) в мир веб-разработки и JS. Это мотивация, подкреплённая не абстрактными примерами, а конкретной задачей, результат которой будет полезен и мне, и моему руководителю, и, возможно, кому-то ещё в будущем.
Для работы шахмат с туманом войны потребуются PHP и JavaScript. Перед тем как приступить к запуску проекта, убедитесь, что у вас установлен Node.js (v20.17.0). Если нет, то вы можете скачать его с официального сайта nodejs.org. После скачивания Node.js нужно установить PHP. Можно через сайт, но я использовал chocolatey, поэтому напишу этот способ:
Этот код нужен для установки chocolatey через консоль:
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))Код, чтобы установить PHP и проверить, что он установился:
choco install php -y
php -v Структура проекта:
Cкачать фотографии фигур можно с GitHub.
chess-project/
├── index.html # Главная страница игры (должен быть и будет правильно файл php, но в github.io нужен html, поэтому так)
├── savegame.php # Обработчик сохранения/загрузки игры
├── chess-game.js # Основная логика шахматной игры
├── chess-pieces.js # Классы шахматных фигур
├── style.css # Стили оформления
└──images/ # Папка с графикой
├── 0P.png # Белая пешка
├── 0N.png # Белый конь
├── ... # Остальные белые фигуры
├── 1P.png # Чёрная пешка
├── 1N.png # Чёрный конь
├── ... # Остальные чёрные фигуры
└── fog.png # Изображение тумана войныВ этой статье я не вываливаю всю кодобазу разом, а фокусируюсь на ключевых механизмах вроде тумана войны и системы таймеров, потому что многим интересно, что получилось интересного и нового, а это находится обычно не в строчках кода, а в архитектурных решениях и уникальных игровых механиках. Полный исходный код будет доступен на GitHub. Кроме того, в статье для тех, кто не любит заходить на GitHub, я размещу код в скрытых элементах. Так желающие смогут легко скопировать нужные фрагменты, а остальных это не будет отвлекать от сути.
В этой статье мы не будем повторять базовые правила шахмат — я предполагаю, что вы с ними знакомы. Если это не так, то у вас есть три варианта: прошлая статья, файл README или найти спойлер, если он закрыт. Вместо этого мы сфокусируемся на JavaScript и работе самого сайта.
Все фигуры наследуются от базового класса ChessPiece. Такой подход мы использовали в Python-версии и перенесли в JavaScript для удобства поддержки сайта. Каждый класс-наследник реализует метод getValidMoves(), который возвращает массив ходов для любой фигуры из текущей позиции.
* Базовый класс для всех шахматных фигур
*/
class ChessPiece {
/**
* Инициализация шахматной фигуры
*
* Args:
* color: 0 - для белых, 1 - для черных
* symbol: символ фигуры (например: K - король)
*/
constructor(color, symbol) {
this.color = color;
this.symbol = symbol;
this.hasMoved = false;
}
/**
*Проверить, является ли другая фигура фигурой противника
*/
isOpponent(otherPiece) {
return otherPiece && otherPiece.color !== this.color;
}
/**
* Проверить, находится ли позиция в пределах доски
*/
isValidPosition(x, y) {
return x >= 0 && x < 8 && y >= 0 && y < 8;
}
/**
* Проверить, является ли клетка пустой или содержит фигуру противника
* */
isEmptyOrOpponent(board, x, y) {
if (!this.isValidPosition(x, y)) return false;
const target = board[y][x];
return target === null || this.isOpponent(target);
}
}
Пешка является самой многочисленной, но при этом одной из самых сложных в реализации фигур из-за особых правил перемещения. В отличие от других фигур, пешка имеет различные правила для обычного хода, для хода с первой клетки, для взятия фигур на проходе, также смены самой пешки на другую фигуру при достижении конца доски.
Белая пешка движется вверх по доске (уменьшение координаты Y), а чёрная — вниз (увеличение координаты Y). Со стартовой позиции пешка может сделать двойной ход на две клетки вперед, если путь свободен. Для взятия фигур противника пешка движется по диагонали на одну клетку вперед. Особый случай — «взятие на проходе» — позволяет пешке взять пешку противника, которая только что сделала двойной ход и переместилась на соседнюю вертикаль.

/**
* Класс пешки
*/
class Pawn extends ChessPiece {
constructor(color) {
super(color, 'P');
}
/**
* Получить допустимые ходы для пешки
*
* Args:
* board: шахматная доска
* x: текущая координата x
* y: текущая координата y
* enPassant: координаты для взятия на проходе
*
* Returns:
* array: список допустимых ходов
*/
getValidMoves(board, x, y, enPassant = null) {
const moves = [];
const direction = this.color === 0 ? -1 : 1;
const startRow = this.color === 0 ? 6 : 1;
// Ход вперед на одну клетку
if (this.isValidPosition(x, y + direction) && board[y + direction][x] === null) {
moves.push({x, y: y + direction});
// Двойной ход с начальной позиции
if (y === startRow && board[y + 2 * direction][x] === null) {
moves.push({x, y: y + 2 * direction});
}
}
// Взятия
for (let dx of [-1, 1]) {
const newX = x + dx;
const newY = y + direction;
if (this.isValidPosition(newX, newY)) {
const target = board[newY][newX];
if (target && this.isOpponent(target)) {
moves.push({x: newX, y: newY});
}
// Взятие на проходе
if (enPassant && enPassant.x === newX && enPassant.y === newY) {
moves.push({x: newX, y: newY});
}
}
}
return moves;
}
/**
* Проверить, должна ли пешка превратиться в другую фигуру
*
* Args:
* y: текущая координата y пешки
*
* Returns:
* boolean: True, если пешка достигла последней горизонтали и False, если нет
*/
shouldPromote(y) {
return (this.color === 0 && y === 0) || (this.color === 1 && y === 7);
}
}
Конь — единственная фигура, способная перепрыгивать через другие фигуры, его движение описывается характерной буквой Г, он перемещается на две клетки по одной оси и на одну клетку по другой. Это создаёт восемь возможных направлений движения из любой точки доски.

/**
* Класс коня (обозначается как N в шахматах)
*/
class Knight extends ChessPiece {
constructor(color) {
super(color, 'N');
}
/**
* Получить допустимые ходы для коня
*
* Args:
* board: шахматная доска
* x: текущая координата x
* y: текущая координата y
*
* Returns:
* array: список допустимых ходов
*/
getValidMoves(board, x, y) {
const moves = [];
const knightMoves = [
[2, 1], [2, -1], [-2, 1], [-2, -1],
[1, 2], [1, -2], [-1, 2], [-1, -2]
];
for (let [dx, dy] of knightMoves) {
const newX = x + dx;
const newY = y + dy;
if (this.isEmptyOrOpponent(board, newX, newY)) {
moves.push({x: newX, y: newY});
}
}
return moves;
}
}
Слон перемещается исключительно по диагоналям на любое количество клеток до тех пор, пока не встретит препятствие. Каждый слон остаётся на клетках одного цвета на протяжении всей игры.

/**
* Класс слона
*/
class Bishop extends ChessPiece {
constructor(color) {
super(color, 'B');
}
/**
* Получить допустимые ходы для слона
*
* Args:
* board: шахматная доска
* x: текущая координата x
* y: текущая координата y
*
* Returns:
* array: список допустимых ходов
*/
getValidMoves(board, x, y) {
const moves = [];
const directions = [[1, 1], [1, -1], [-1, 1], [-1, -1]];
for (let [dx, dy] of directions) {
for (let i = 1; i < 8; i++) {
const newX = x + i * dx;
const newY = y + i * dy;
if (!this.isValidPosition(newX, newY)) break;
if (board[newY][newX] === null) {
moves.push({x: newX, y: newY});
} else if (this.isOpponent(board[newY][newX])) {
moves.push({x: newX, y: newY});
break;
} else {
break;
}
}
}
return moves;
}
}
Ладья движется по горизонталям и вертикалям на любое количество клеток. В начальной позиции ладьи занимают угловые клетки доски и участвуют в специальном ходе рокировке. Алгоритм перемещения ладьи аналогичен слону, но использует четыре ортогональных направления вместо диагональных.

/**
* Класс ладьи
*/
class Rook extends ChessPiece {
constructor(color) {
super(color, 'R');
}
/**
* Получить допустимые ходы для ладьи
*
* Args:
* board: шахматная доска
* x: текущая координата x
* y: текущая координата y
*
* Returns:
* array: список допустимых ходов
*/
getValidMoves(board, x, y) {
const moves = [];
const directions = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (let [dx, dy] of directions) {
for (let i = 1; i < 8; i++) {
const newX = x + i * dx;
const newY = y + i * dy;
if (!this.isValidPosition(newX, newY)) break;
if (board[newY][newX] === null) {
moves.push({x: newX, y: newY});
} else if (this.isOpponent(board[newY][newX])) {
moves.push({x: newX, y: newY});
break;
} else {
break;
}
}
}
return moves;
}
}
Ферзь сочетает в себе возможности ладьи и слона, что делает его самой мощной фигурой на доске. Он может перемещаться на любое количество клеток по горизонтали, вертикали или диагонали. В реализации мы используем композицию, объединяя ходы ладьи и слона.

/**
* Класс ферзя
*/
class Queen extends ChessPiece {
constructor(color) {
super(color, 'Q');
}
/**
* Получить допустимые ходы для ферзя
*
* Args:
* board: шахматная доска
* x: текущая координата x
* y: текущая координата y
*
* Returns:
* array: список допустимых ходов
*/
getValidMoves(board, x, y) {
// Ферзь ходит как ладья + слон
const rookMoves = new Rook(this.color).getValidMoves(board, x, y);
const bishopMoves = new Bishop(this.color).getValidMoves(board, x, y);
return [...rookMoves, ...bishopMoves];
}
}
Король — самая важная фигура, чья потеря означает проигрыш партии. Он перемещается на одну клетку в любом направлении. Особый ход (рокировка) позволяет королю переместиться на две клетки в сторону ладьи, а ладье — перепрыгнуть через короля.
В реализации учитывается, что король не может перемещаться на атакованные клетки, что проверяется в основном игровом цикле. Рокировка возможна только если король и соответствующая ладья не двигались с начала игры, между ними нет других фигур и король не проходит через атакованные клетки.

/**
* Класс короля
*/
class King extends ChessPiece {
constructor(color) {
super(color, 'K');
}
/**
* Получить допустимые ходы для короля
*
* Args:
* board: шахматная доска
* x: текущая координата x
* y: текущая координата y
*
* Returns:
* array: список допустимых ходов
*/
getValidMoves(board, x, y) {
const moves = [];
const kingMoves = [
[1, 0], [-1, 0], [0, 1], [0, -1],
[1, 1], [1, -1], [-1, 1], [-1, -1]
];
for (let [dx, dy] of kingMoves) {
const newX = x + dx;
const newY = y + dy;
if (this.isEmptyOrOpponent(board, newX, newY)) {
moves.push({x: newX, y: newY});
}
}
// Рокировка
if (!this.hasMoved) {
// Короткая рокировка
if (board[y][x + 1] === null && board[y][x + 2] === null &&
board[y][x + 3] instanceof Rook && !board[y][x + 3].hasMoved) {
moves.push({x: x + 2, y, castling: 'short'});
}
// Длинная рокировка
if (board[y][x - 1] === null && board[y][x - 2] === null && board[y][x - 3] === null &&
board[y][x - 4] instanceof Rook && !board[y][x - 4].hasMoved) {
moves.push({x: x - 2, y, castling: 'long'});
}
}
return moves;
}
}
Первая идея «Тумана войны» была в стратегической игре, а если конкретнее — в пошаговой стратегии Empire 1977 года, разработанной Уолтером Брайтом, где она имитирует ограниченную видимость на поле боя (подобно тому, как в реальной войне командиры не видят все перемещения противника). Если интересно про историю игр.

В контексте шахмат эта механика кардинально меняет привычную игру: вместо расположения фигур противника игрок видит только пустоту и те участки доски, которые находятся в зоне контроля его фигур, что сильно усложняет геймплей и делает игру забавной. Также в интернете нашёл информацию, кто придумал шахматы «Туман войны» и как в них играть без программы (ссылка на статью).
Без помощи компьютерных технологий играть можно при помощи трёх шахматных наборов и рефери. Шахматы втёмную были изобретены датскими шахматистами-любителями — музыкантом Йенсом Беком Нильсеном (дат. Jens Bæk Nielsen; род. 1957) и художником Торбеном Остедом (дат. Torben Osted; род. 1945) — в 1989 году.

Класс ChessGame реализует механизм ограниченной видимости, известный как «туман войны» (Fog of War), который кардинально меняет стратегический подход к шахматам. При активации этого режима игрок видит только те клетки доски, которые находятся в зоне следующего хода всех его фигур, что создаёт атмосферу тактической неопределённости и заставляет полагаться на память и предвидение, ну или удачу, смотря как вы играете.
class ChessGame {
constructor() {
this.fogOfWar = true;
this.visibleCells = new Set();
// ...
}
/**
* Обновить множество видимых клеток для тумана войны
*/
updateVisibleCells() {
this.visibleCells.clear();
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = this.board[row][col];
if (piece && piece.color === this.currentPlayer) {
this.visibleCells.add(`${row}-${col}`);
const moves = piece.getValidMoves(this.board, col, row, this.enPassant);
moves.forEach(move => {
this.visibleCells.add(`${move.y}-${move.x}`);
});
}
}
}
}
/**
* Проверить, видна ли клетка текущему игроку
*/
isCellVisible(row, col) {
return this.visibleCells.has(`${row}-${col}`);
}Для создания атмосферы была разработана система отрисовки.
drawFogOfWar(ctx, squareSize) {
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
if (!this.isCellVisible(row, col)) {
if (this.fogImage) {
ctx.drawImage(this.fogImage, col * squareSize, row * squareSize, squareSize, squareSize);
} else {
ctx.fillStyle = '#2c3e50';
ctx.fillRect(col * squareSize, row * squareSize, squareSize, squareSize);
}
}
}
}
}
toggleFogOfWar() {
this.fogOfWar = !this.fogOfWar;
this.drawBoard();
}
}Также, чтобы туман войны хоть немного отличался от прошлого кода, решил добавить фотографию тумана.

Шахматные часы появились в конце XIX века, когда партии между сильнейшими игроками мира могли длиться по 10-12 часов, а иногда и вовсе прерываться на ночь. Первое официальное использование часов (не песочных, тогда это был 1852 год) зафиксировано в 1866 году на матче Андерсен против Стейница, правда они были не с двумя кнопками, как сейчас, а обычные, но настоящую популярность они обрели после лондонского турнира 1883 года (инф. из статьи).

Тогда игроки осознали, что часы — это не просто способ ускорить игру, а целая механика. Теперь стало важно не только думать над своим ходом, но и принимать решения быстро, т.к. время не бесконечно. Это добавило в шахматы интересный элемент психологического напряжения, из-за которого многие делаю поспешные шаги.
class ChessGame {
constructor() {
this.timers = {0: 600, 1: 600}; // 10 минут в секундах
this.timerInterval = null;
this.currentTimer = null;
// ...
}
startTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
}
this.currentTimer = this.currentPlayer;
this.timerInterval = setInterval(() => {
if (!this.gameOver && this.timers[this.currentTimer] > 0) {
this.timers[this.currentTimer]--;
this.updateTimersDisplay();
if (this.timers[this.currentTimer] <= 0) {
this.gameOver = true;
clearInterval(this.timerInterval);
this.updateGameInfo();
}
}
}, 1000);
this.updateTimersDisplay();
} /**
* Обновление отображения таймеров
*/
updateTimersDisplay() {
const whiteTimer = document.getElementById('white-timer');
const blackTimer = document.getElementById('black-timer');
if (whiteTimer && blackTimer) {
// Обновляем время
whiteTimer.textContent = this.formatTime(this.timers[0]);
blackTimer.textContent = this.formatTime(this.timers[1]);
// Сбрасываем стили
whiteTimer.className = 'timer';
blackTimer.className = 'timer';
// Подсвечиваем активный таймер
if (this.currentTimer === 0) {
whiteTimer.classList.add('active');
} else {
blackTimer.classList.add('active');
}
// Предупреждения при малом времени
if (this.timers[0] <= 120 && this.timers[0] > 0) {
whiteTimer.classList.add('warning');
}
if (this.timers[1] <= 120 && this.timers[1] > 0) {
blackTimer.classList.add('warning');
}
if (this.timers[0] <= 60 && this.timers[0] > 0) {
whiteTimer.classList.add('danger');
}
if (this.timers[1] <= 60 && this.timers[1] > 0) {
blackTimer.classList.add('danger');
}
}
}
/**
* Форматирование времени в мм:сс
*/
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}Система таймеров делает больше, чем просто отсчитывает время — она создаёт настоящее напряжение, похожее на то, что чувствуют игроки в турнире. Визуальные индикаторы, переходящие от синего к жёлтому, а затем к красному, напоминают о реальных шахматных часах, где последние минуты часто оказываются самыми критическими и могут решить исход партии.

Класс ChessGame представляет собой ядро шахматного движка, который управляет всей логикой игры. Он отвечает за инициализацию доски, обработку ходов, проверку правил шахмат, управление временем и визуализацию игрового состояния. Движок поддерживает все стандартные шахматные правила, включая рокировку, взятие на проходе, превращение пешек и завершение игры при мате или пате.
/**
* Основной класс шахматной игры
*/
class ChessGame {
/**
* Инициализация шахматной игры
* Создает пустую доску, устанавливает начальные параметры игры,
* загружает графические ресурсы и запускает таймеры
*/
constructor() {
this.board = this.createEmptyBoard();
this.currentPlayer = 0;
this.selectedPiece = null;
this.validMoves = [];
this.moveHistory = [];
this.capturedPieces = {0: [], 1: []};
this.gameOver = false;
this.check = false;
this.enPassant = null;
this.promotionPending = null;
this.boardFlipped = false;
this.fogOfWar = true;
this.visibleCells = new Set();
// Загрузка изображений
this.imagesLoaded = false;
this.pieceImages = {};
this.fogImage = null;
// Таймеры (10 минут в секундах)
this.timers = {
0: 600, // Белые: 10 минут
1: 600 // Черные: 10 минут
};
this.timerInterval = null;
this.currentTimer = null;
this.initializeBoard();
this.updateVisibleCells();
// Загружаем изображения и только потом инициализируем игру
this.loadAllImages().then(() => {
this.imagesLoaded = true;
this.startTimer();
this.drawBoard();
this.updateGameInfo();
});
}
Система загрузки ресурсов. Асинхронная загрузка изображений фигур и эффектов гарантирует, что все визуальные компоненты будут готовы до начала игрового процесса, что предотвращает появление отсутствующих текстур.
/**
* Загрузка всех изображений с ожиданием завершения
* Обеспечивает полную загрузку всех графических ресурсов перед началом игры
*/
async loadAllImages() {
await this.loadPieceImages();
await this.loadFogImage();
}
/**
* Загрузка изображений фигур с ожиданием завершения
* Загружает изображения для всех типов фигур обоих цветов
*/
loadPieceImages() {
return new Promise((resolve) => {
const pieces = ['P', 'N', 'B', 'R', 'Q', 'K'];
const colors = ['white', 'black'];
let imagesToLoad = pieces.length * colors.length;
let imagesLoaded = 0;
const checkAllLoaded = () => {
imagesLoaded++;
if (imagesLoaded === imagesToLoad) {
resolve();
}
};
pieces.forEach(piece => {
colors.forEach(color => {
const img = new Image();
img.onload = checkAllLoaded;
img. => {
console.warn(`Не удалось загрузить изображение: images/${color === 'white' ? '0' : '1'}${piece}.png`);
checkAllLoaded();
};
// Используем ваши файлы с изображениями
img.src = `images/${color === 'white' ? '0' : '1'}${piece}.png`;
this.pieceImages[`${color}_${piece}`] = img;
});
});
// Если нет изображений для загрузки, сразу резолвим
if (imagesToLoad === 0) {
resolve();
}
});
}
/**
* Загрузка изображения тумана войны с ожиданием завершения
* Загружает текстуру для эффекта скрытия невидимых клеток
*/
loadFogImage() {
return new Promise((resolve) => {
this.fogImage = new Image();
this.fogImage.onload = resolve;
this.fogImage. => {
console.warn('Не удалось загрузить изображение тумана: images/fog.png');
this.fogImage = null;
resolve();
};
this.fogImage.src = 'images/fog.png';
});
}
Методы инициализации доски создают стандартную шахматную расстановку фигур. Система использует объектно-ориентированный подход, где каждая фигура представлена экземпляром соответствующего класса, что упрощает управление поведением фигур и проверку правил.
/**
* Создать пустую шахматную доску
* Генерирует двумерный массив 8x8, представляющий пустую шахматную доску
*/
createEmptyBoard() {
return Array(8).fill().map(() => Array(8).fill(null));
}
/**
* Начальная расстановка фигур на доске
* Размещает все фигуры в их стандартных начальных позициях
*/
initializeBoard() {
// Пешки
for (let i = 0; i < 8; i++) {
this.board[1][i] = new Pawn(1);
this.board[6][i] = new Pawn(0);
}
// Остальные фигуры
const backRowOrder = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook];
// Черные фигуры
for (let i = 0; i < 8; i++) {
this.board[0][i] = new backRowOrder[i](1);
}
// Белые фигуры
for (let i = 0; i < 8; i++) {
this.board[7][i] = new backRowOrder[i](0);
}
}
Таймеры игры реализуют контроль времени для каждого игрока с визуальной индикацией оставшегося времени. Система автоматически переключает активный таймер после завершения хода и предоставляет визуальные предупреждения при малом остатке времени.
/**
* Запуск таймера для текущего игрока
* Активирует обратный отсчет для активного игрока и приостанавливает таймер противника
*/
startTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
}
this.currentTimer = this.currentPlayer;
this.timerInterval = setInterval(() => {
if (!this.gameOver && this.timers[this.currentTimer] > 0) {
this.timers[this.currentTimer]--;
this.updateTimersDisplay();
// Проверка на окончание времени
if (this.timers[this.currentTimer] <= 0) {
this.gameOver = true;
clearInterval(this.timerInterval);
this.updateGameInfo();
}
}
}, 1000);
this.updateTimersDisplay();
}
/**
* Обновление отображения таймеров
* Визуализирует оставшееся время с цветовой индикацией критических состояний
*/
updateTimersDisplay() {
const whiteTimer = document.getElementById('white-timer');
const blackTimer = document.getElementById('black-timer');
if (whiteTimer && blackTimer) {
// Обновляем время
whiteTimer.textContent = this.formatTime(this.timers[0]);
blackTimer.textContent = this.formatTime(this.timers[1]);
// Сбрасываем стили
whiteTimer.className = 'timer';
blackTimer.className = 'timer';
// Подсвечиваем активный таймер
if (this.currentTimer === 0) {
whiteTimer.classList.add('active');
} else {
blackTimer.classList.add('active');
}
// Предупреждения при малом времени
if (this.timers[0] <= 120 && this.timers[0] > 0) {
whiteTimer.classList.add('warning');
}
if (this.timers[1] <= 120 && this.timers[1] > 0) {
blackTimer.classList.add('warning');
}
if (this.timers[0] <= 60 && this.timers[0] > 0) {
whiteTimer.classList.add('danger');
}
if (this.timers[1] <= 60 && this.timers[1] > 0) {
blackTimer.classList.add('danger');
}
}
}
/**
* Форматирование времени в мм:сс
* Преобразует количество секунд в читаемый строковый формат
*/
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
Система тумана войны ограничивает видимость игрока только теми клетками, которые находятся в зоне контроля его фигур.
/**
* Обновить множество видимых клеток для тумана войны
* Вычисляет все клетки, которые должны быть видимы для текущего игрока
*/
updateVisibleCells() {
this.visibleCells.clear();
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = this.board[row][col];
if (piece && piece.color === this.currentPlayer) {
this.visibleCells.add(`${row}-${col}`);
const moves = piece.getValidMoves(this.board, col, row, this.enPassant);
moves.forEach(move => {
this.visibleCells.add(`${move.y}-${move.x}`);
});
}
}
}
}
/*
* Проверить, видна ли клетка текущему игроку
* Определяет, должна ли конкретная клетка отображаться или быть скрыта туманом
*/
isCellVisible(row, col) {
return this.visibleCells.has(`${row}-${col}`);
}
Механизм сохранения позволяет сохранять текущее состояние игры как локально в браузере, так и на сервере. Система запоминает все аспекты игрового состояния, включая позиции фигур, историю ходов и таймеры.
/**
* Сохранить состояние игры
* Сохраняет текущее состояние игры в localStorage и на сервер
*/
saveGame() {
const gameState = {
board: this.serializeBoard(),
currentPlayer: this.currentPlayer,
moveHistory: this.moveHistory,
capturedPieces: this.capturedPieces,
timers: this.timers,
gameOver: this.gameOver,
check: this.check,
enPassant: this.enPassant,
boardFlipped: this.boardFlipped,
fogOfWar: this.fogOfWar
};
// Сохраняем в localStorage
localStorage.setItem('chessGameSave', JSON.stringify(gameState));
// Отправляем на сервер через AJAX
this.saveToServer(gameState);
alert('Игра сохранена!');
}
/**
* Сериализация доски для сохранения
* Преобразует объекты фигур в простые объекты для хранения
*/
serializeBoard() {
const serialized = [];
for (let row = 0; row < 8; row++) {
const rowData = [];
for (let col = 0; col < 8; col++) {
const piece = this.board[row][col];
if (piece) {
rowData.push({
type: piece.constructor.name,
color: piece.color,
hasMoved: piece.hasMoved,
symbol: piece.symbol
});
} else {
rowData.push(null);
}
}
serialized.push(rowData);
}
return serialized;
}
/**
* Десериализация доски из сохранения
* Восстанавливает объекты фигур из сериализованных данных
*/
deserializeBoard(serialized) {
const board = this.createEmptyBoard();
const pieceClasses = {
'Pawn': Pawn, 'Knight': Knight, 'Bishop': Bishop,
'Rook': Rook, 'Queen': Queen, 'King': King
};
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const pieceData = serialized[row][col];
if (pieceData) {
const PieceClass = pieceClasses[pieceData.type];
if (PieceClass) {
const piece = new PieceClass(pieceData.color);
piece.hasMoved = pieceData.hasMoved;
board[row][col] = piece;
}
}
}
}
return board;
}
/**
* Сохранение на сервер через AJAX
* Отправляет состояние игры на сервер для постоянного хранения
*/
async saveToServer(gameState) {
try {
const response = await fetch('savegame.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'save',
gameState: gameState
})
});
const result = await response.json();
if (result.success) {
console.log('Game saved to server');
}
} catch (error) {
console.error('Error saving to server:', error);
}
}
/**
* Загрузить состояние игры
* Восстанавливает игру из сохранения, сначала проверяя локальное хранилище, затем сервер
*/
loadGame() {
// Пытаемся загрузить из localStorage
const saved = localStorage.getItem('chessGameSave');
if (saved) {
try {
const gameState = JSON.parse(saved);
this.loadFromState(gameState);
alert('Игра загружена из локального сохранения!');
return;
} catch (error) {
console.error('Error loading from localStorage:', error);
}
}
// Если нет локального сохранения, пробуем загрузить с сервера
this.loadFromServer();
}
/**
* Загрузить состояние из объекта
* Восстанавливает все аспекты игрового состояния из объекта сохранения
*/
loadFromState(gameState) {
this.board = this.deserializeBoard(gameState.board);
this.currentPlayer = gameState.currentPlayer;
this.moveHistory = gameState.moveHistory || [];
this.capturedPieces = gameState.capturedPieces || {0: [], 1: []};
this.timers = gameState.timers || {0: 600, 1: 600};
this.gameOver = gameState.gameOver || false;
this.check = gameState.check || false;
this.enPassant = gameState.enPassant || null;
this.boardFlipped = gameState.boardFlipped || false;
this.fogOfWar = gameState.fogOfWar !== undefined ? gameState.fogOfWar : true;
this.selectedPiece = null;
this.validMoves = [];
this.promotionPending = null;
this.updateVisibleCells();
this.startTimer();
this.drawBoard();
this.updateGameInfo();
}
Система отрисовки отвечает за визуальное представление шахматной доски, фигур и игровых эффектов. Она поддерживает различные режимы отображения, включая обычный вид, перевёрнутую доску и режим с туманом войны, обеспечивая гибкость представления игровой информации.
/**
* Отрисовка шахматной доски и фигур
* Рендерит все визуальные элементы игры на canvas элементе
*/
drawBoard() {
const canvas = document.getElementById('chess-board');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const squareSize = 80;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const displayRow = this.boardFlipped ? 7 - row : row;
const displayCol = this.boardFlipped ? 7 - col : col;
const x = col * squareSize;
const y = row * squareSize;
const isVisible = !this.fogOfWar || this.isCellVisible(displayRow, displayCol);
if (isVisible) {
ctx.fillStyle = (displayRow + displayCol) % 2 === 0 ? '#f0d9b5' : '#b58863';
ctx.fillRect(x, y, squareSize, squareSize);
} else {
// Темные клетки для невидимых областей
ctx.fillStyle = '#2c3e50';
ctx.fillRect(x, y, squareSize, squareSize);
}
if (isVisible) {
if (row === 7) {
ctx.fillStyle = displayCol % 2 === 0 ? '#b58863' : '#f0d9b5';
ctx.font = '14px Times New Roman';
ctx.fillText(String.fromCharCode(97 + displayCol), x + 5, y + squareSize - 5);
}
if (col === 0) {
ctx.fillStyle = displayRow % 2 === 0 ? '#b58863' : '#f0d9b5';
ctx.font = '14px Times New Roman';
ctx.fillText((8 - displayRow).toString(), x + 5, y + 15);
}
}
const piece = this.board[displayRow][displayCol];
if (piece && isVisible) {
this.drawPieceImage(ctx, piece, x, y, squareSize);
}
}
}
if (this.selectedPiece) {
let {x: selX, y: selY} = this.selectedPiece;
if (this.boardFlipped) {
selX = 7 - selX;
selY = 7 - selY;
}
ctx.strokeStyle = '#ffeb3b';
ctx.lineWidth = 4;
ctx.strokeRect(selX * squareSize, selY * squareSize, squareSize, squareSize);
ctx.fillStyle = 'rgba(76, 175, 80, 0.5)';
this.validMoves.forEach(move => {
let displayX = move.x;
let displayY = move.y;
if (this.boardFlipped) {
displayX = 7 - displayX;
displayY = 7 - displayY;
}
ctx.fillRect(displayX * squareSize, displayY * squareSize, squareSize, squareSize);
});
}
if (this.check && !this.gameOver) {
const kingPos = this.findKing(this.currentPlayer);
if (kingPos) {
let {x: kingX, y: kingY} = kingPos;
if (this.boardFlipped) {
kingX = 7 - kingX;
kingY = 7 - kingY;
}
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.fillRect(kingX * squareSize, kingY * squareSize, squareSize, squareSize);
}
}
if (this.fogOfWar) {
this.drawFogOfWar(ctx, squareSize);
}
// Отрисовка меню превращения пешки
if (this.promotionPending) {
this.drawPromotionMenu(ctx, squareSize);
}
}
/**
* Отрисовка изображения фигуры
* Отображает графическое представление фигуры или fallback символ
*/
drawPieceImage(ctx, piece, x, y, squareSize) {
const color = piece.color === 0 ? 'white' : 'black';
const imageKey = `${color}_${piece.symbol}`;
const img = this.pieceImages[imageKey];
// Используем изображение только если оно загружено
if (img && img.complete && img.naturalWidth > 0) {
// Рисуем изображение с небольшим отступом
const padding = 5;
ctx.drawImage(img, x + padding, y + padding, squareSize - padding * 2, squareSize - padding * 2);
} else {
// Fallback на символы, если изображение не загружено
this.drawPieceSymbol(ctx, piece, x, y, squareSize);
}
}
/**
* Отрисовка символа фигуры (fallback)
* Рендерит Unicode символы фигур когда изображения недоступны
*/
drawPieceSymbol(ctx, piece, x, y, squareSize) {
ctx.fillStyle = piece.color === 0 ? '#ffffff' : '#000000';
ctx.strokeStyle = piece.color === 0 ? '#000000' : '#ffffff';
ctx.lineWidth = 2;
ctx.font = 'bold 48px Times New Roman';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const symbol = this.getPieceSymbol(piece);
ctx.strokeText(symbol, x + squareSize / 2, y + squareSize / 2);
ctx.fillText(symbol, x + squareSize / 2, y + squareSize / 2);
}
/**
* Отрисовка тумана войны с использованием изображения
* Накладывает текстуру тумана на невидимые клетки доски
*/
drawFogOfWar(ctx, squareSize) {
// Рисуем изображение тумана для каждой невидимой клетки
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const displayRow = this.boardFlipped ? 7 - row : row;
const displayCol = this.boardFlipped ? 7 - col : col;
// Если клетка не видна рисуем на ней туман
if (!this.isCellVisible(displayRow, displayCol)) {
const x = col * squareSize;
const y = row * squareSize;
if (this.fogImage && this.fogImage.complete && this.fogImage.naturalWidth > 0) {
// Рисуем изображение тумана для этой клетки
ctx.drawImage(this.fogImage, x, y, squareSize, squareSize);
} else {
// Fallback - темная клетка
ctx.fillStyle = '#2c3e50';
ctx.fillRect(x, y, squareSize, squareSize);
}
}
}
}
}
Система превращения пешки предоставляет интерфейс для выбора новой фигуры, когда пешка достигает последней горизонтали. Механизм включает визуальное меню с четырьмя вариантами фигур и обработку пользовательского вывода как через клики, так и через клавиатуру.
/**
* Отрисовка меню превращения пешки
* Отображает интерфейс выбора фигуры для превращения пешки
*/
drawPromotionMenu(ctx, squareSize) {
const {x, y} = this.promotionPending;
const displayX = this.boardFlipped ? 7 - x : x;
const displayY = this.boardFlipped ? 7 - y : y;
const menuX = displayX * squareSize;
const menuY = displayY * squareSize;
const isMenuDown = (this.currentPlayer === 1 && !this.boardFlipped) ||
(this.currentPlayer === 0 && this.boardFlipped);
// Позиция меню
const menuStartY = isMenuDown ? menuY - squareSize * 4 : menuY + squareSize;
const menuHeight = squareSize * 4;
// Фон меню
ctx.fillStyle = this.currentPlayer === 1 ? 'rgba(255, 255, 255, 0.95)' : 'rgba(0, 0, 0, 0.95)';
ctx.fillRect(menuX, menuStartY, squareSize, menuHeight);
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.strokeRect(menuX, menuStartY, squareSize, menuHeight);
// Фигуры для выбора
const pieces = [
{ type: 'Q', symbol: 'Q', name: 'Ферзь' },
{ type: 'R', symbol: 'R', name: 'Ладья' },
{ type: 'B', symbol: 'B', name: 'Слон' },
{ type: 'N', symbol: 'N', name: 'Конь' }
];
pieces.forEach((piece, index) => {
// Позиция фигуры в меню
const pieceY = isMenuDown ?
menuStartY + index * squareSize :
menuStartY + (3 - index) * squareSize;
// Подсветка при наведении
const mouseX = this.promotionMouseX;
const mouseY = this.promotionMouseY;
if (mouseX && mouseY &&
mouseX >= menuX && mouseX <= menuX + squareSize &&
mouseY >= pieceY && mouseY <= pieceY + squareSize) {
ctx.fillStyle = 'rgba(0, 123, 255, 0.2)';
ctx.fillRect(menuX, pieceY, squareSize, squareSize);
}
// Рисуем фигуру в меню
const color = this.currentPlayer === 0 ? 'white' : 'black';
const imageKey = `${color}_${piece.symbol}`;
const img = this.pieceImages[imageKey];
if (img && img.complete && img.naturalWidth > 0) {
const padding = 10;
ctx.drawImage(img, menuX + padding, pieceY + padding, squareSize - padding * 2, squareSize - padding * 2);
} else {
// Fallback на символы
this.drawPieceSymbol(ctx, {symbol: piece.symbol, color: this.currentPlayer}, menuX, pieceY, squareSize);
}
// Название фигуры
ctx.fillStyle = this.currentPlayer === 1 ? '#000000' : '#FFFFFF';
ctx.font = '12px Times New Roman';
ctx.textAlign = 'center';
// Позиция названия в зависимости от направления меню
const nameY = isMenuDown ?
pieceY + squareSize - 8 :
pieceY + 10;
ctx.fillText(piece.name, menuX + squareSize/2, nameY);
});
}
/**
* Обработка клика по меню превращения
* Определяет выбранную фигуру на основе координат клика
*/
handlePromotionClick(x, y) {
if (!this.promotionPending) return false;
const squareSize = 80;
const {x: pawnX, y: pawnY} = this.promotionPending;
const displayX = this.boardFlipped ? 7 - pawnX : pawnX;
const displayY = this.boardFlipped ? 7 - pawnY : pawnY;
const menuX = displayX * squareSize;
const menuY = displayY * squareSize;
// Определяем направление меню (такая же логика как в drawPromotionMenu)
const isMenuDown = (this.currentPlayer === 1 && !this.boardFlipped) ||
(this.currentPlayer === 0 && this.boardFlipped);
// Область меню
const menuStartY = isMenuDown ? menuY - squareSize * 4 : menuY + squareSize;
const menuEndY = isMenuDown ? menuY : menuY + squareSize * 5;
// Проверяем, был ли клик в области меню
if (x >= menuX && x <= menuX + squareSize &&
y >= menuStartY && y <= menuEndY) {
const pieces = ['Q', 'R', 'B', 'N'];
// Вычисляем индекс выбранной фигуры
let clickIndex;
if (isMenuDown) {
// Меню снизу - индекс увеличивается сверху вниз
clickIndex = Math.floor((y - menuStartY) / squareSize);
} else {
// Меню сверху - индекс увеличивается снизу вверх
clickIndex = 3 - Math.floor((y - menuStartY) / squareSize);
}
if (clickIndex >= 0 && clickIndex < pieces.length) {
this.promotePawn(pieces[clickIndex]);
return true;
}
}
return false;
}
/**
* Превратить пешку в другую фигуру
* Заменяет пешку на выбранную фигуру и завершает ход
*/
promotePawn(pieceType) {
if (!this.promotionPending) return;
const {x, y} = this.promotionPending;
const pawn = this.board[y][x];
const color = pawn.color;
const pieceClasses = {
'Q': Queen, 'R': Rook, 'B': Bishop, 'N': Knight
};
this.board[y][x] = new pieceClasses[pieceType](color);
this.promotionPending = null;
this.promotionMouseX = null;
this.promotionMouseY = null;
this.currentPlayer = 1 - this.currentPlayer;
this.updateVisibleCells();
this.startTimer();
this.check = this.isInCheck(this.currentPlayer);
if (this.isCheckmate()) {
this.gameOver = true;
clearInterval(this.timerInterval);
} else if (this.isStalemate()) {
this.gameOver = true;
clearInterval(this.timerInterval);
}
this.drawBoard();
this.updateGameInfo();
}
Система ввода обрабатывает взаимодействие пользователя с игрой через различные устройства ввода, включая мышь, сенсорные экраны и клавиатуру. Механизм обеспечивает точное определение выбранных клеток и корректную обработку жестов на мобильных устройствах.
/**
* Обработка клика мыши по шахматной доске
* Определяет цель клика и выполняет соответствующее игровое действие
*/
handleClick(x, y) {
if (this.gameOver) return;
// Сначала проверяем клик по меню превращения
if (this.promotionPending) {
if (this.handlePromotionClick(x, y)) {
return;
}
}
if (this.promotionPending) return;
let col = Math.floor(x / 80);
let row = Math.floor(y / 80);
if (this.boardFlipped) {
col = 7 - col;
row = 7 - row;
}
if (!this.isValidPosition(col, row)) return;
if (this.fogOfWar && !this.isCellVisible(row, col)) {
return;
}
if (this.selectedPiece) {
const move = this.validMoves.find(m => m.x === col && m.y === row);
if (move) {
this.makeMove(this.selectedPiece, move);
this.selectedPiece = null;
this.validMoves = [];
} else {
this.selectPiece(col, row);
}
} else {
this.selectPiece(col, row);
}
this.drawBoard();
this.updateGameInfo();
}
/**
* Выбор фигуры на доске
* Устанавливает текущую фигуру для перемещения и вычисляет доступные ходы
*/
selectPiece(x, y) {
const piece = this.board[y][x];
if (piece && piece.color === this.currentPlayer) {
this.selectedPiece = {x, y};
this.validMoves = this.getValidMovesForPiece(x, y);
} else {
this.selectedPiece = null;
this.validMoves = [];
}
}
Движок перемещения фигур реализует все стандартные шахматные правила, включая специальные ходы, такие как рокировка и взятие на проходе. Система проверяет допустимость каждого хода и предотвращает ходы, которые оставляют короля под шахом.
/**
* Выполнить ход на доске
* Перемещает фигуру, обрабатывает специальные правила и обновляет игровое состояние
*/
makeMove(from, to) {
const movingPiece = this.board[from.y][from.x];
const targetPiece = this.board[to.y][to.x];
const moveNotation = this.getMoveNotation(from, to, movingPiece, targetPiece);
this.moveHistory.push(moveNotation);
if (targetPiece) {
this.capturedPieces[this.currentPlayer].push(targetPiece);
}
if (movingPiece instanceof Pawn && to.x !== from.x && !targetPiece) {
const captureY = from.y;
const capturedPawn = this.board[captureY][to.x];
if (capturedPawn) {
this.capturedPieces[this.currentPlayer].push(capturedPawn);
this.board[captureY][to.x] = null;
}
}
this.board[to.y][to.x] = movingPiece;
this.board[from.y][from.x] = null;
movingPiece.hasMoved = true;
if (movingPiece instanceof King && Math.abs(to.x - from.x) === 2) {
if (to.x > from.x) {
this.board[to.y][5] = this.board[to.y][7];
this.board[to.y][7] = null;
this.board[to.y][5].hasMoved = true;
} else {
this.board[to.y][3] = this.board[to.y][0];
this.board[to.y][0] = null;
this.board[to.y][3].hasMoved = true;
}
}
if (movingPiece instanceof Pawn && movingPiece.shouldPromote(to.y)) {
this.promotionPending = {x: to.x, y: to.y};
this.drawBoard();
return;
}
if (movingPiece instanceof Pawn && Math.abs(to.y - from.y) === 2) {
this.enPassant = {x: from.x, y: (from.y + to.y) / 2};
} else {
this.enPassant = null;
}
this.currentPlayer = 1 - this.currentPlayer;
this.updateVisibleCells();
this.startTimer();
this.check = this.isInCheck(this.currentPlayer);
if (this.isCheckmate()) {
this.gameOver = true;
clearInterval(this.timerInterval);
} else if (this.isStalemate()) {
this.gameOver = true;
clearInterval(this.timerInterval);
}
}
/**
* Получить допустимые ходы для фигуры с учетом шаха
* Фильтрует потенциальные ходы, исключая те которые оставляют короля под шахом
*/
getValidMovesForPiece(x, y) {
const piece = this.board[y][x];
if (!piece) return [];
const moves = piece.getValidMoves(this.board, x, y, this.enPassant);
return moves.filter(move => this.isMoveValid({x, y}, move));
}
/**
* Проверить, является ли ход допустимым
* Создает временную доску для проверки не оставляет ли ход короля под шахом
*/
isMoveValid(start, end) {
const tempBoard = this.deepCopyBoard();
const movingPiece = tempBoard[start.y][start.x];
tempBoard[end.y][end.x] = movingPiece;
tempBoard[start.y][start.x] = null;
return !this.isInCheck(this.currentPlayer, tempBoard);
}
Система определения игровых состояний отслеживает критические ситуации, такие как шах, мат и пат. Алгоритмы анализируют все возможные ходы для определения возможности продолжения игры и автоматически завершают игру при достижении финального состояния.
/**
* Проверить, находится ли король под шахом
* Определяет атакован ли король текущего игрока фигурами противника
*/
isInCheck(color, board = this.board) {
const kingPos = this.findKing(color, board);
if (!kingPos) return false;
const opponentColor = 1 - color;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece && piece.color === opponentColor) {
const moves = piece.getValidMoves(board, col, row, this.enPassant);
if (moves.some(move => move.x === kingPos.x && move.y === kingPos.y)) {
return true;
}
}
}
}
return false;
}
/**
* Проверить мат
* Определяет находится ли текущий игрок в положении мата
*/
isCheckmate() {
return this.isCheckmateForColor(this.currentPlayer);
}
/**
* Проверить мат для указанного цвета
* Анализирует все возможные ходы чтобы определить есть ли спасение от шаха
*/
isCheckmateForColor(color, board = this.board) {
if (!this.isInCheck(color, board)) return false;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece && piece.color === color) {
const moves = piece.getValidMoves(board, col, row, this.enPassant);
for (const move of moves) {
const tempBoard = this.deepCopyBoard();
tempBoard[move.y][move.x] = piece;
tempBoard[row][col] = null;
if (!this.isInCheck(color, tempBoard)) {
return false;
}
}
}
}
}
return true;
}
/**
* Проверить пат
* Определяет находится ли текущий игрок в положении пата (нет допустимых ходов но шаха нет)
*/
isStalemate() {
if (this.isInCheck(this.currentPlayer)) return false;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = this.board[row][col];
if (piece && piece.color === this.currentPlayer) {
const moves = this.getValidMovesForPiece(col, row);
if (moves.length > 0) return false;
}
}
}
return true;
}
Система обновления игровой информации синхронизирует визуальные элементы интерфейса с текущим состоянием игры. Она отображает историю ходов, захваченные фигуры, текущий статус игры и обеспечивает обратную связь с игроком.
/**
* Обновить информацию о состоянии игры
* Синхронизирует текстовые элементы интерфейса с текущим игровым состоянием
*/
updateGameInfo() {
const statusElement = document.getElementById('game-status');
const historyElement = document.getElementById('move-history');
const capturedWhite = document.getElementById('captured-white');
const capturedBlack = document.getElementById('captured-black');
if (this.gameOver) {
if (this.timers[0] <= 0) {
statusElement.textContent = 'Время вышло, победа черных!';
} else if (this.timers[1] <= 0) {
statusElement.textContent = 'Время вышло, победа белых!';
} else if (this.isCheckmate()) {
statusElement.textContent = `Мат! Победа ${this.currentPlayer === 0 ? 'черных' : 'белых'}`;
} else {
statusElement.textContent = 'Пат, ничья!';
}
statusElement.style.color = '#dc3545';
} else {
let statusText = `Ход ${this.currentPlayer === 0 ? 'белых' : 'черных'}`;
if (this.check) {
statusText += ' (ШАХ)';
}
if (this.promotionPending) {
statusText += ' - Выберите фигуру для превращения';
}
statusElement.textContent = statusText;
statusElement.style.color = this.check ? '#dc3545' : '#495057';
}
historyElement.innerHTML = this.moveHistory.map((move, index) =>
`<div>${index + 1}. ${move}</div>`
).join('');
capturedWhite.textContent = this.capturedPieces[0].map(p => this.getPieceSymbol(p)).join(' ');
capturedBlack.textContent = this.capturedPieces[1].map(p => this.getPieceSymbol(p)).join(' ');
}
Вспомогательные методы предоставляют базовую функциональность, используемую throughout игровым движком. Эти утилиты включают проверки валидности, форматирование данных и управление состоянием доски, обеспечивая чистоту и поддерживаемость основного кода.
/**
* Найти короля указанного цвета
* Сканирует доску для определения позиции короля заданного цвета
*/
findKing(color, board = this.board) {
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece instanceof King && piece.color === color) {
return {x: col, y: row};
}
}
}
return null;
}
/**
* Создать глубокую копию шахматной доски
* Создает независимую копию доски для симуляции ходов без изменения оригинального состояния
*/
deepCopyBoard() {
const newBoard = this.createEmptyBoard();
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = this.board[row][col];
if (piece) {
const pieceClass = piece.constructor;
const newPiece = new pieceClass(piece.color);
newPiece.hasMoved = piece.hasMoved;
newBoard[row][col] = newPiece;
}
}
}
return newBoard;
}
/**
* Получить символ Unicode для отображения фигуры (fallback)
* Возвращает Unicode символы для визуального представления фигур когда изображения недоступны
*/
getPieceSymbol(piece) {
const symbols = {
'P': '♙', 'N': '♘', 'B': '♗', 'R': '♖', 'Q': '♕', 'K': '♔'
};
const blackSymbols = {
'P': '♟', 'N': '♞', 'B': '♝', 'R': '♜', 'Q': '♛', 'K': '♚'
};
return piece.color === 0 ? symbols[piece.symbol] : blackSymbols[piece.symbol];
}
/**
* Проверить валидность позиции
* Определяет находятся ли координаты в пределах шахматной доски
*/
isValidPosition(x, y) {
return x >= 0 && x < 8 && y >= 0 && y < 8;
}
/**
* Перевернуть доску
* Изменяет perspective доски для отображения с точки зрения другого игрока
*/
flipBoard() {
this.boardFlipped = !this.boardFlipped;
this.drawBoard();
}
/**
* Включить/выключить туман войны
* Переключает режим ограниченной видимости на доске
*/
toggleFogOfWar() {
this.fogOfWar = !this.fogOfWar;
this.drawBoard();
}
Функции инициализации и управления игрой обеспечивают корректный запуск и контроль игрового процесса. Они обрабатывают события пользовательского интерфейса, инициализируют игровые объекты и обеспечивают связь между HTML-элементами и игровой логикой.
let game;
/**
* Инициализация игры с улучшенной обработкой касаний
* Настраивает игровое поле и обработчики событий для различных устройств ввода
*/
function initGame() {
game = new ChessGame();
const canvas = document.getElementById('chess-board');
if (!canvas) {
console.error('Canvas element not found!');
return;
}
// Функция для обработки взаимодействия
function handleInteraction(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width; // Масштаб по X
const scaleY = canvas.height / rect.height; // Масштаб по Y
const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
game.handleClick(x, y);
}
// Обработчик кликов для компьютеров
canvas.addEventListener('click', (event) => {
handleInteraction(event.clientX, event.clientY);
});
// Обработчики для мобильных устройств
canvas.addEventListener('touchstart', (event) => {
event.preventDefault();
if (event.touches.length === 1) { // Только одиночное касание
handleInteraction(event.touches[0].clientX, event.touches[0].clientY);
}
}, { passive: false });
// Предотвращаем стандартное поведение жестов
document.addEventListener('touchmove', (event) => {
if (game.promotionPending) {
event.preventDefault();
}
}, { passive: false });
// Обновление позиции для меню превращения
canvas.addEventListener('touchmove', (event) => {
if (game.promotionPending && event.touches.length === 1) {
event.preventDefault();
const rect = canvas.getBoundingClientRect();
const touch = event.touches[0];
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
game.updatePromotionMouse(x, y);
}
}, { passive: false });
canvas.addEventListener('mousemove', (event) => {
if (game.promotionPending) {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
game.updatePromotionMouse(x, y);
}
});
document.addEventListener('keydown', (event) => {
if (!game.promotionPending) return;
const key = event.key.toUpperCase();
const validPieces = {'Q': 'Q', 'R': 'R', 'B': 'B', 'N': 'N'};
if (validPieces[key]) {
game.promotePawn(validPieces[key]);
}
});
// Предотвращение контекстного меню на мобильных
canvas.addEventListener('contextmenu', (event) => {
event.preventDefault();
return false;
});
}
/**
* Начать новую игру
* Сбрасывает игровое состояние и начинает новую партию
*/
function startNewGame() {
if (confirm('Начать новую игру? Текущий прогресс будет потерян.')) {
initGame();
}
}
/**
* Сохранить игру
* Сохраняет текущее состояние игры для последующего восстановления
*/
function saveGame() {
if (game && !game.gameOver) {
game.saveGame();
} else {
alert('Нет активной игры для сохранения');
}
}
/**
* Загрузить игру
* Восстанавливает ранее сохраненное состояние игры
*/
function loadGame() {
if (game) {
if (confirm('Загрузить сохраненную игру? Текущий прогресс будет потерян.')) {
game.loadGame();
}
}
}
Первое, что может вызвать вопрос: почему главный файл имеет расширение .html, хотя внутри него активно используется PHP-код?

Это практичное решение для хостингов вроде GitHub Pages, где для запуска сайта обязательно нужен HTML-файл. Однако когда этот файл размещается на PHP-совместимом хостинге, сервер без проблем обрабатывает PHP-код, встроенный в страницу.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Шахматы Online</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>♞ Шахматы Online ♘</h1>
<div class="game-info">
<div id="game-status">
<?php
session_start();
if (isset($_SESSION['game_status'])) {
echo htmlspecialchars($_SESSION['game_status']);
} else {
echo "Ход белых";
}
?>
</div>
<div class="timers">
<div class="timer" id="white-timer">
<?php
echo isset($_SESSION['white_time']) ? $_SESSION['white_time'] : '10:00';
?>
</div>
<div class="timer" id="black-timer">
<?php
echo isset($_SESSION['black_time']) ? $_SESSION['black_time'] : '10:00';
?>
</div>
</div>
<div class="captured-pieces">
<div class="captured-white" id="captured-white">
<?php
if (isset($_SESSION['captured_white'])) {
echo htmlspecialchars($_SESSION['captured_white']);
}
?>
</div>
<div class="captured-black" id="captured-black">
<?php
if (isset($_SESSION['captured_black'])) {
echo htmlspecialchars($_SESSION['captured_black']);
}
?>
</div>
</div>
<div id="move-history">
<?php
if (isset($_SESSION['move_history'])) {
foreach ($_SESSION['move_history'] as $move) {
echo "" . htmlspecialchars($move) . "";
}
}
?>
</div>
</div>
<div class="chess-container">
<canvas id="chess-board" width="640" height="640"></canvas>
</div>
<div class="controls">
<button onclick="startNewGame()">Новая игра</button>
<button onclick="toggleFlipBoard()">Перевернуть доску</button>
<button onclick="toggleFogOfWar()">Туман войны</button>
<button onclick="saveGame()">Сохранить игру</button>
<button onclick="loadGame()">Загрузить игру</button>
</div>
</div>
<script src="chess-pieces.js"></script>
<script src="chess-game.js"></script>
</body>
</html>Наш index.html — это гибридный файл, который сервер обрабатывает перед отправкой пользователю. Внутри него мы видим смесь HTML-разметки и PHP-кода, заключённого в теги <?php ... ?>. Когда пользователь заходит на сайт, сервер выполняет весь этот PHP-код: он начинает или продолжает сессию, извлекает из неё актуальное игровое состояние (чей ход, время, история ходов) и встраивает данные в HTML-страницу.
Одним из ключевых преимуществ такого подхода является кросс-платформенность. Независимо от того, открываете ли вы сайт на настольном компьютере, ноутбуке, планшете или смартфоне, страница будет отображаться корректно. Это достигается за счёт адаптивной вёрстки в файле style.css, который автоматически подстраивает размер шахматной доски, расположение таймеров и элементов управления под размер экрана устройства.
Для настольных компьютеров с шириной экрана более 768 пикселей интерфейс настроен так, что шахматная доска занимает центральное место и имеет фиксированный размер 640x640 пикселей, что позволяет сосредоточиться на игре и не смотреть на что-то лишнее. Таймеры удобно расположены сверху, а кнопки управления внизу расположены в сетку шириной 120 пикселей.
На планшете или мобильном устройстве с экраном до 768 пикселей включается мобильный режим. Шахматная доска автоматически подстраивается, занимая всю доступную ширину. Таймеры перестраиваются в вертикальную колонку, что помогает эффективно использовать узкое пространство. Кнопки управления становятся адаптивными, на устройствах среднего размера они располагаются в две колонки, а на маленьких смартфонах (до 480 пикселей) переходят в одну колонку.
body {
font-family: 'Times New Roman', Times, serif;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 10px;
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 15px;
padding: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
h1 {
color: #333;
margin-bottom: 15px;
font-size: 2em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
}
.chess-container {
display: inline-block;
margin: 15px auto;
border: 3px solid #333;
border-radius: 5px;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
background: #8B4513;
max-width: 100%;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
#chess-board {
width: 100%;
height: auto;
max-width: 640px;
max-height: 640px;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
.game-info {
margin: 15px;
padding: 12px;
background: #f8f9fa;
border-radius: 10px;
border: 2px solid #dee2e6;
}
#game-status {
font-size: 1.3em;
font-weight: bold;
margin: 8px 0;
color: #495057;
}
.timers {
display: flex;
justify-content: space-between;
margin: 12px 0;
gap: 10px;
}
.timer {
padding: 8px 15px;
background: #e9ecef;
border-radius: 10px;
border: 2px solid #dee2e6;
font-size: 1.1em;
font-weight: bold;
flex: 1;
}
.timer.active {
background: #007bff;
color: white;
border-color: #0056b3;
}
#move-history {
max-height: 120px;
overflow-y: auto;
background: white;
padding: 8px;
border-radius: 5px;
border: 1px solid #ced4da;
font-size: 0.9em;
}
.controls {
margin: 15px;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 18px;
border-radius: 20px;
cursor: pointer;
font-size: 0.9em;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
flex: 1;
min-width: 120px;
min-height: 44px;
}
.captured-pieces {
display: flex;
justify-content: space-between;
margin: 12px 0;
gap: 10px;
}
.captured-white, .captured-black {
min-height: 25px;
padding: 5px 8px;
background: #e9ecef;
border-radius: 5px;
border: 1px solid #dee2e6;
font-size: 1em;
flex: 1;
}
.timer.warning {
background: #ffc107;
color: #856404;
border-color: #ffc107;
}
.timer.danger {
background: #dc3545;
color: white;
border-color: #c82333;
}
/* Мобильные устройства */
@media (max-width: 768px) {
body {
padding: 5px;
}
.container {
padding: 10px;
border-radius: 10px;
touch-action: pan-x pan-y;
}
h1 {
font-size: 1.6em;
margin-bottom: 10px;
}
#chess-board {
max-width: 100%;
height: auto;
}
.game-info {
margin: 10px;
padding: 10px;
}
#game-status {
font-size: 1.1em;
}
.timers {
flex-direction: column;
}
.timer {
font-size: 1em;
padding: 6px 12px;
}
.controls {
margin: 10px;
gap: 5px;
}
button {
padding: 8px 12px;
font-size: 0.85em;
min-width: 100px;
flex: 1 1 calc(50% - 10px);
}
.captured-pieces {
flex-direction: column;
}
}
@media (max-width: 480px) {
h1 {
font-size: 1.4em;
}
button {
flex: 1 1 100%;
min-width: auto;
}
#move-history {
max-height: 80px;
font-size: 0.8em;
}
.timer {
font-size: 0.9em;
}
}
savegame.php — это специализированный обработчик, отвечающий исключительно за работу с игровыми данными. Его задачи: принимать запросы от клиента и управлять состоянием игры на сервере.
<?php
session_start();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
switch ($action) {
case 'save':
if (isset($input['gameState'])) {
$_SESSION['chess_game_state'] = $input['gameState'];
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'No game state provided']);
}
break;
case 'load':
if (isset($_SESSION['chess_game_state'])) {
echo json_encode([
'success' => true,
'gameState' => $_SESSION['chess_game_state']
]);
} else {
echo json_encode(['success' => false, 'error' => 'No saved game found']);
}
break;
case 'clear':
unset($_SESSION['chess_game_state']);
echo json_encode(['success' => true]);
break;
default:
echo json_encode(['success' => false, 'error' => 'Invalid action']);
}
} else {
echo json_encode(['success' => false, 'error' => 'Invalid request method']);
}
?>Этот файл работает как RESTful API: он ожидает POST запросы с определенными действиями, такими как 'save', 'load' или 'clear'. Когда вы нажимаете кнопку «Сохранить игру», JavaScript из chess-game.js формирует запрос к savegame.php с полным состоянием партии. Серверный скрипт сохраняет эти данные в PHP-сессию ($_SESSION['chess_game_state']), которая действует как защищённое хранилище на время вашей игровой сессии.

Архитектура приложения построена по принципу разделения обязанностей. Клиентская часть, состоящая из chess-game.js и chess-pieces.js, отвечает за всё, что видит пользователь: отрисовку самой игры на <canvas>, обработку кликов и отображение «тумана войны».
Серверная же часть, представленная savegame.php, работает как «хранилище состояния». Она не знает о правилах шахмат и ничего не делает по самой игре, её единственные задачи — хранить и возвращать данные по запросу. Это делает систему лёгкой и понятной.
В результате разработки была создана веб-платформа для игры в шахматы, которая полностью соответствует правилам, которые реализовали в прошлой статье. Основным достижением проекта стала надёжная система сохранения игрового прогресса, т.к. это что-то новое для меня, что я делал первый раз.
Что нового? Платформа отличается универсальностью (если сравнивать с прошлым проектом), дизайн интерфейса обеспечивает комфортное использование как на компьютерах, так и на мобильных устройствах с сенсорным управлением.
Также стоит отметить, что в файл index.html интегрирован PHP-код. Это позволяет платформе функционировать не только на простых хостингах, которые поддерживают лишь статические файлы, но и на полноценных PHP-серверах с динамическими возможностями.
☆ Одним из перспективных направлений является реализация сетевой игры (многопользовательская) через интернет или локальную сеть.
☆ Создание или подключение искусственного интеллекта различного уровня сложности представляет собой интересную вычислительную задачу.
☆ Аудиальные улучшения включают возможность выбора различных тем звуковых эффектов для разных действий.
На представленных скриншотах можно наблюдать ключевые особенности реализации:

Этот проект стал для меня ценным практическим опытом в веб-разработке. Полный код проекта доступен в репозитории GitHub. Если у вас есть идеи по улучшению или вы обнаружили какие-либо недочёты, буду рад обратной связи. Лучшие предложения могут быть реализованы в следующих версиях проекта или стать идеей для следующей статьи.
Если кто-то хочет увидеть проект в жизни — Запуск игры.
Проблемой (случайной, которую заметил) стала некорректная работа на мобильных устройствах. Пришлось реализовать обработку касаний, чтобы обеспечить плавный игровой процесс на сенсорных экранах, и сделать структуру для разных размеров экрана.

© 2025 ООО «МТ ФИНАНС»