Шахматы на php и javascript: Пешка может стать не только ферзём

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

Как мы будем это делать

Когда пешка достигает "последней" горизонтали, будем показывать модальный слой с картинками фигур, в которые можно превратить пешку. Чтобы продолжить игру, надо выбрать фигуру, кликнув по ней.

"Модальное" окно/слой/диалог - окно/слой/диалог, которое блокирует работу с остальным (родительским) интерфейсом до тех пор, пока это модальное окно не будет закрыто.

Обнаружение превращения пешки, общая схема работы

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

Конечно это всё происходит на стороне браузера, всё это обрабатывается кодом в файле 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.

Демо №12 игры