В текущей реализации игры, пешка, достигнув последней горизонтали, превращается в ферзя. Сегодня мы дадим возможность превращать пешку в ладью, слона или коня. Реально такая возможность нужна крайне редко. Почти всегда ферзь - лучшее решение. Но без реализации этой возможности это будут уже не совсем шахматы.
Как мы будем это делать
Когда пешка достигает "последней" горизонтали, будем показывать модальный слой с картинками фигур, в которые можно превратить пешку. Чтобы продолжить игру, надо выбрать фигуру, кликнув по ней.
"Модальное" окно/слой/диалог - окно/слой/диалог, которое блокирует работу с остальным (родительским) интерфейсом до тех пор, пока это модальное окно не будет закрыто.
Обнаружение превращения пешки, общая схема работы
Когда надо начинать действия, связанные с превращением пешки? Когда мы выбрали ход пешкой и перемещаем её на последюю горизонталь. Т.е. пешка уже выделена. И среди допустимых ходов есть поле на последней горизонтали. И мы кликаем по такому полю.
Конечно это всё происходит на стороне браузера, всё это обрабатывается кодом в файле chess.js. Обрабатывается в функции onCellClick. Вот в этой ветке кода:
chess.jsif (available_moves_for_selected_cell.includes(index)) {
// кликнули по полю, куда можно переместиться с выделенного поля
deselectPrevMoveCells();
const cell_index_from = selected_cell_index;
makeMove(cell_index_from, index);
selected_cell_index = null;
available_moves_for_selected_cell = [];
is_our_move = false;
setGameStatus('Ждите мой ответ');
sendMoveToServer(cell_index_from, index);
return;
}
Нам надо изменить код внутри этого if. Напишем здесь условие - если выбирается ход пешкой на последнюю горизонталь, то показываем модальный слой выбора фигуры. Иначе - выполняем тот самый код, который сейчас там есть - код для совершения хода. Обозначим этот "код для совершения хода" через X. Т.е. X(index) - код для совершения хода выделенной фигуры на поле с индексом index. Тогда текущий код можно записать условно так:
chess.jsif (available_moves_for_selected_cell.includes(index)) {
// кликнули по полю, куда можно переместиться с выделенного поля
X(index);
return;
}
Мы его заменим кодом, который условно запишем так:
chess.jsif (available_moves_for_selected_cell.includes(index)) {
// кликнули по полю, куда можно переместиться с выделенного поля
// если это ход пешкой на последнюю горизонталь, то надо выбрать фигуру, в которую превратится пешка
if (это_ход_пешкой_на_последнюю_горизонталь) {
показываем_слой_выбора_фигуры_для_превращения;
} else {
X(index);
}
return;
}
В слое для выбора фигуры будут показаны картинки ферзя, ладьи, слона и коня соответствующего цвета. При клике по фигуре нам надо будет сделать ход на выбранное поле. А какой код у нас занимается этим? Да это и есть этот самый код X! Только теперь надо передать этому коду кроме индекса поля ещё и код фигуры, в которую надо превратить пешку.
Мы вырежем этот "код X" и поместим его в отдельную функцию, назовём её move_click_processing. Итак, рассматриваемую ветку кода переписываем так:
chess.jsif (available_moves_for_selected_cell.includes(index)) {
// кликнули по полю, куда можно переместиться с выделенного поля
// если это ход пешкой на последнюю горизонталь, то надо выбрать фигуру, в которую превратится пешка
if (is_pawn_to_last_row(index)) {
show_select_figure_layer(index);
} else {
move_click_processing(index, null);
}
return;
}
Здесь мы вызываем функцию move_click_processing со вторым параметром, равным null, т.е, говорим: "превращать фигуру во что-то другое - не нужно". Ниже, после функции onCellClick, добавляем новую функцию с перенесённым кодом:
chess.jsfunction move_click_processing(index, transform_to_figure) {
deselectPrevMoveCells();
const cell_index_from = selected_cell_index;
makeMove(cell_index_from, index);
selected_cell_index = null;
available_moves_for_selected_cell = [];
is_our_move = false;
setGameStatus('Ждите мой ответ');
sendMoveToServer(cell_index_from, index, transform_to_figure);
}
Это тот самый "код Х", который мы вынесли из функции onCellClick, но с добавленным параметром transform_to_figure. И этот параметр мы добавляем как дополнительный аргумент в вызов функции sendMoveToServer.
Сразу дополним функцию sendMoveToServer: добавим дополнительный параметр transform_to_figure, и будем его передавать на сервер при вызове скрипта make_move.php. Функция будет выглядеть так:
chess.jsfunction sendMoveToServer(cell_index_from, cell_index_to, transform_to_figure) {
createRequest({
method: 'POST',
url: 'make_move.php',
data: { cell_index_from: cell_index_from, cell_index_to: cell_index_to, transform_to: transform_to_figure },
callback: function(response) {
setGameState(response);
if (response && response.is_human_move == false) {
sendWaitComputerMove();
}
}
});
}
Теперь напишем функцию is_pawn_to_last_row, которую мы использовали выше. Она должна определить что "совершается ход пешкой на последнюю горизонталь", это другой ход. Нам понадобится ещё одна функция, которая по индексу поля определит номер горизонтали:
chess.jsfunction cell_index_to_row(index) {
return index >> 3;
}
Чтобы получить номер горизонтали можно разделить индекс позиции на 8 и взять целую часть результата. Или по другому - сдвинуть индекс позиции на три двоичных разряда вправо, что мы и делаем в коде.
Ну и сама функция is_pawn_to_last_row:
chess.jsfunction is_pawn_to_last_row(to_index) {
const from_index = selected_cell_index;
const figure = position[from_index];
let to_row = cell_index_to_row(to_index);
return (figure == WHITE_PAWN && to_row === 0) || (figure == BLACK_PAWN && to_row === BoardSize - 1);
}
В строке 2 мы берём индекс выделенного поля, т.е. поля, откуда совершается ход. В следующей строке из массива position берём фигуру, которая на ходится на выделенном поле. Т.е. это та фигура, которой собираемся ходить.
В строке 4 из индекса поля "куда ходим", получаем номер горизонтали. И в строке 5 собственно определяем ходит ли пешка на последнюю горизонталь или это другой ход. На человеческом языке: (фигура - белая пешка И перемещается на горизонталь №0) ИЛИ (фигура - чёрная пешка И перемещается на горизонталь №7)
В коде выше мы использовали неопределённые пока константы WHITE_PAWN, BLACK_PAWN, обозначающие белую и чёрную пешки. Определим их сразу после константы FIGURES:
chess.jsconst WHITE_PAWN = 25;
const BLACK_PAWN = 26;
Показ слоя с выбором фигуры
Выше мы использовали вызов функции show_select_figure_layer для показа слоя, в котором надо выбрать фигуру. Сейчас мы напишем эту функцию. Для ясности покажу что мы хотим создать в javascript - коде, и как это должно выглядеть:

Т.е. внутри доски (внутри div с классом board) будет блок div с классом select_pawn_to_figure. Внутри блока - список ul, в каждом элементе списка - ссылка, клики по которым будем обрабатывать. А внутри ссылок - картинки фигур соответствующего цвета. Перед списком (псевдокласс ::before) будет полупрозрачный элемент, закрывающий (затеняющий) всю доску.
Итак, добавляем функцию:
chess.jsfunction show_select_figure_layer(to_index) {
const layer = document.createElement('div');
layer.classList.add('select_pawn_to_figure');
const ul_element = document.createElement('ul');
layer.appendChild(ul_element);
let color = cell_index_to_row(to_index) == 0 ? 'white' : 'black';
let figures_for_transform = ['queen', 'rook', 'bishop', 'knight'];
for (let i = 0; i < figures_for_transform.length; i++) {
let figure = figures_for_transform[i];
const li_element = document.createElement('li');
const a_element = document.createElement('a');
a_element.innerHTML = `<img src="img/${figure}-${color}.svg"/>`;
a_element.addEventListener('click', (event) => onTransformFigureClick(to_index, figure));
li_element.appendChild(a_element);
ul_element.appendChild(li_element);
}
const board = document.querySelector('.board');
board.appendChild(layer);
}
В общем код простой, только несколько уточнений… В строке 6 по индексу поля "куда" узнаём, на какой ряд (горизонталь) встанет пешка, и по этому ряду определяем цвет фигуры.
В строке 7 определяем массив названий фигур для превращения. Эти "названия" будут частью имени картинки фигуры. И эти названия мы будем передавать в обработчик кликов по фигурам - в строке 13
А в строке 12 для конструирования пути к картинке фигуры мы используем интерполяцию строк в javascript. Будте внимательны - вокруг строки не одинарные кавычки, а обратные апострафы. Можете конечно обойтись просто конкатенацией (сложением) строк.
Обработчик кликов по "фигуре для превращения"
В слое выбора фигуры для превращения мы назначили обработчик кликов - функцию onTransformFigureClick. Добавим её:
chess.jsfunction onTransformFigureClick(to_index, figure) {
const layer = document.querySelector('.select_pawn_to_figure');
layer.remove();
move_click_processing(to_index, figure);
}
Всё просто - берём слой с выбором фигур, удаляем его. И вызываем тот самый "код X" для обработки перемещения фигуры, который мы перенесли в отдельную функцию - move_click_processing. И передаём в эту функцию вторым параметром выбранную фигуру (в виде строки).
Css - стили для оформления слоя выбора фигуры
Добавляем в chess.css правила для слоя, на котором будем показывать список выбора фигур:
chess.css.select_pawn_to_figure {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 11;
}
.select_pawn_to_figure:before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #000;
z-index: 10;
opacity: 0.5;
}
Т.е. мы слой позиционируем абсолютно и растягиваем его на всю родительскую область. И "под него ложим" слой - подложку, который тоже растягиваем на всю родительскую область, окрашиваем его в чёрный цвет и даём прозрачность 50%.
Но растяжение происходит на ближайшего родителя, имеющего позиционирование absolute, fixed или relative, а у нас такого нет, так что растяжение произойдёт на всё окно браузера. Чтобы слой растягивался только на доску, добавим к доске, т.е. классу .board правило:
chess.cssposition: relative;
Ну и теперь добавляем ещё правила для оформления списка фигур:
chess.css.select_pawn_to_figure ul {
position: absolute;
list-style-type: none;
display: flex;
padding: 7px;
width: 240px;
margin: 0;
top: calc(50% - 39px);
left: calc(50% - 127px);
background-color: #fff;
z-index: 11;
opacity: 1;
}
.select_pawn_to_figure ul li a {
display: inline-block;
}
.select_pawn_to_figure ul li a:hover {
background-color: #9f9;
border-radius: 20px;
}
.select_pawn_to_figure ul li a img{
width: 60px;
height: 60px;
}
Изменение в makeMove на стороне сервера
Что нужно изменить на стороне сервера (бекенда)? У нас стал приходить новый post - параметр transform_to в обработчик make_move.php Примем его, и будем передавать в метод makeMove объекта игры. Контроллер будет выглядеть так:
make_move.php<?php
require_once('engine/chess_game.php');
$cell_index_from = $_POST['cell_index_from'];
$cell_index_to = $_POST['cell_index_to'];
$to_figure_string = empty($_POST['transform_to']) ? null : $_POST['transform_to'];
$game = new ChessGame();
$game->loadGame();
$game->makeMove($cell_index_from, $cell_index_to, true, $to_figure_string);
$data = $game->getClientJsonGameState();
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
Т.е. добавлена строка 5, и в строке 8 в вызове метода добавлен параметр $to_figure_string
Теперь идём в класс ChessGame (файл engine/chess_game.php), в метод makeMove. Добавляем в определение метода параметр $to_figure_string со значением по умолчанию null:
engine/chess_game.phppublic function makeMove($cell_index_from, $cell_index_to, $validate_move=true, $to_figure_string=null) {
Метод у нас заканчивается так:
engine/chess_game.phpif ($figure->makeMove($cell_index_to, $validate_move)) {
$this->saveGame();
}
Перед этим кодом вставляем определение фигуры по строке $to_figure_string:
engine/chess_game.phpswitch ($to_figure_string) {
case 'rook':
$to_figure = FG_ROOK;
break;
case 'bishop':
$to_figure = FG_BISHOP;
break;
case 'knight':
$to_figure = FG_KNIGHT;
break;
default:
$to_figure = FG_QUEEN;
}
В этом коде мы определили фигуру $to_figure, и вставляем её как третий параметр в вызов метода makeMove:
engine/chess_game.phpif ($figure->makeMove($cell_index_to, $validate_move, $to_figure)) {
$this->saveGame();
}
Итоги
В этой статье мы научили превращаться пешек человека не только в ферзя, но и в ладью, слона или коня. "А как же пешки компьютера?" - спросите вы. Пока мы не дали ему эту возможность. Вряд-ли она вообще ему понадобится. Возможно вернёмся к этому вопросу позже.
И, как обычно: исходные коды проекта на github.