Шахматы на php и javascript: Человек делает ход

Сценарий действий

А теперь научимся двигать фигуры! Какой будет сценарий? Сначала мы выделяем фигуру, кликом по ней. "Подсвечиваем" выделенную фигуру. Также подсвечиваем поля, куда можно перемещать выделенную фигуру. Помните, мы для этого заводили хеш available_moves в предыдущей части?

Если потом кликаем по полю, доступному для перемещения выделенной фигуры, то перемещаем её. Если кликаем по полю, куда нельзя переместить выделенную фигуру, или кликаем по уже выделенной фигуре, то просто снимаем выделение.

Также будем показывать информацию о предыдущем ходе - подсветим поля поля откуда и куда ходили. Это будем делать и для своего хода, и для хода компьютера.

Drag-and-drop (перетаскивание мышью) не будем делать. Может быть сделаем это позже, когда займёмся "украшательством".

Заводим переменные для хранения состояния

Открываем в редакторе наш chess.js. По описанию сценария уже понятно, какие переменные нужны. Добавляем их после константы FIGURES:

let cells;
let selected_cell_index = null;
let available_moves_for_selected_cell = [];
let prev_move_from = null;
let prev_move_to = null;

В переменной cells будет массив с dom-элементами, представлющими поля поля доски. Заполним эту переменную в функции createBoard, добавив в самом конце строчку:

cells = Array.from(document.querySelectorAll('.board .cell'));

Точно такая-же строчка у нас была в начале функции showPosition. Теперь она там не нужна, удаляем её.

Учим поля реагировать на клик

Как заставить поля реагировать на клик? Очень просто - добавить "слушателя событий". Идём в функцию createBoard. В конце цикла мы там добавляем клетку на доску:

board.appendChild(cell);

И вот прямо перед этой строкой добавляем "слушателя событий":

cell.addEventListener('click', (event) => onCellClick(event));

Теперь, при клике на поле, будет вызываться функция onCellClick с параметром, содержащим информацию о событии. Дело за малым - написать эту функцию. В ней и будет реализована основная логика интерфейса.

Основная логика в интерфейсе

Идём в самый конец файла chess.js, добавляем функцию, которая будет обрабатывать клики по клеткам доски, и содержать основную "интерфейс-логику":

chess.jsfunction onCellClick(event) {
    if (!is_our_move) {
        return;
    }
    
    const index = cells.indexOf(event.currentTarget);
    if (selected_cell_index !== null) {
        deselect_cell(selected_cell_index); // снимаем выделение с текущей выделенной клетки
    }
    if (index === selected_cell_index) {
        // кликнули по уже выделенной клетке
        selected_cell_index = null;
        available_moves_for_selected_cell = [];
        return;
    }
    if (index in available_moves) {
        // кликнули по клетке, с которой есть доступные ходы
        selected_cell_index = index;
        available_moves_for_selected_cell = available_moves[index];
        select_cell(index);
        return;
    }

    if (available_moves_for_selected_cell.includes(index)) {
        // кликнули по полю, куда можно переместиться с выделенного поля
        const cell_index_from = selected_cell_index;
        make_move(cell_index_from, index);
        selected_cell_index = null;
        available_moves_for_selected_cell = [];
        is_our_move = false;
        send_move_to_server(cell_index_from, index);
        return;
    }
    selected_cell_index = null;
}

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

В строке 6 берём клетку, по которой кликнули - event.currentTarget, определяем её индекс (порядковый номер) на доске, и записываем этот индекс в переменную index.

Если есть какая-то выделенная клетка, то при любом дальнейшем сценарии, это выделение надо снять. Это реализовано в строках 7 - 9. Чтобы убрать визуальное выделение, вызываем функцию deselect_cell. Эту функцию напишем чуть позже.

Если клик произошёл по выделенной клетке (строки 10 - 15), то в переменных помечаем, что выделенной клетки нет (selected_cell_index = null;), и делаем пустым массив с номерами клеток, доступных для перемещения из выделенной позиции: available_moves_for_selected_cell = [];. Всё, выходим из функции. Визуальные эффекты по снятию выделения уже были вызваны ранее, в строке 8.

Если клинули по клетке, откуда есть допустимые ходы (строки 16 - 22), то надо отметить, что клетка выделена:

selected_cell_index = index;

Записываем список доступных ходов:

available_moves_for_selected_cell = available_moves[index];

И вызываем функцию для визуального выделения:

select_cell(index);

Эту функцию напишем чуть ниже.

В строке 24 проверяем - есть ли номер клетки (по которой кликнули) в списке клеток, куда можно переместиться. Если есть, что это значит? Во первых - этот список не пуст. А он заполнется когда мы кликаем по какой-то клетке, откуда разрешено перемещение! Т.е. у нас уже есть выделенное поле с какой-то фигурой. И во вторых, на клетку, клик по которой обрабатываем, можно переместиться с той, выделенной ранее клетки. Т.е. надо делать ход!

Этот ход мы и делаем в строках 25 - 32. Сначала сохраним текущий номер выделенного поля:

const cell_index_from = selected_cell_index;

Делаем визуальное перемещение, вызвав не написанную пока функцию make_move(cell_index_from, index). Первый параметр - номер поля, откуда перемещаемся, это cell_index_from - номер выделенного поля. Второй параметр - номер поля куда перемещаемся. Это index - номер поля, клик по которому мы сейчас обрабатываем.

Дальше, после перемещения, указываем что теперь выделенного поля нет:

selected_cell_index = null;

А раз нет выделенного поля, то и разрешённых ходов с него нету:

available_moves_for_selected_cell = [];

Далее говорим, что теперь ход - не наш:

is_our_move = false;

После этого мы ничего не сможем изменить на доске, пока очередь хода опять не перейдёт к нам.

И, наконец, вызываем функцию (которую тоже пока не написали) для передачи нашего хода на сервер (на бекенд):

send_move_to_server(cell_index_from, index);

В качестве параметров передаём, те-же значения, что передавали в функцию make_move - индексы полей - откуда идём, и куда.

Ну и в конце нашей функции говорим, что нет текущего выделенного поля:

selected_cell_index = null;

Это нужно для случаев, когда не сработало ни одно из вышеперечисленных условий - когда мы кликнули по клетке, откуда и ходить нельзя, и на неё нельзя пойти с выделенного поля, и эта клетка сама не выделена. Например мы кликнули сначала по клетке e2, она стала выделенной, а потом кликнули по "совершенно левой" клетке, например по a5. Вот тут нам и надо снять выделение с поля e2, и указать, что выделенного поля - нет.

Выделение клетки select_cell

В предыдущем коде мы вызывали функции, которых ещё нет. Пришло время написать их. Визуальное выделение ячейки:

function select_cell(cell_index) {
    let cell = cells[cell_index];
    cell.classList.add('figure_selected');
    for (let i = 0; i < available_moves_for_selected_cell.length; i += 1) {
        cell = cells[available_moves_for_selected_cell[i]];
        cell.classList.add('available_for_move');
    }
}

Функция принимает параметр - индекс поля доски. Берём клетку по её индексу, и добавляем ей css-класс figure_selected:

let cell = cells[cell_index];
cell.classList.add('figure_selected');

Потом в цикле проходим по всем полям, куда можно переместиться (available_moves_for_selected_cell), и для каждой клетки добавляем css-класс available_for_move

Снятие выделения с клетки

function deselect_cell(cell_index) {
    let cell = cells[cell_index];
    cell.classList.remove('figure_selected');
    for (let i = 0; i < available_moves_for_selected_cell.length; i += 1) {
        cell = cells[available_moves_for_selected_cell[i]];
        cell.classList.remove('available_for_move');
    }
}

Здесь всё аналогично функции select_cell, только вместо добавления css-классов, происходит их удаление

Функция перемещения фигуры

function make_move(cell_index_from, cell_index_to) {
    const cell_from = cells[cell_index_from];
    const figure = cell_from.querySelector('.figure');
    const cell_to = cells[cell_index_to];
    cell_to.appendChild(figure);

    prev_move_from = cell_index_from;
    prev_move_to = cell_index_to;
    cell_from.classList.add('prev_move');
    cell_to.classList.add('prev_move');
}

Тут всё просто. У нас есть индексы полей - "откуда" и "куда". По этим индексам берём сами поля (строки 2, 4).

В строке 3 в поле "откуда" ищем элемент с классом figure, запоминаем его в константе figure. В строке 5 к полю "куда" добавляем элемент figure как дочерний элемент. При этом у figure меняется родитель. Был - cell_from, стал - cell_to. Визуально - фигура исчезла из поля "откуда", появилась в поле "куда". Делать анимацию с плавным перемещением фигуры сейчас не будем.

В строках 7, 8 запоминаем индексы полей "откуда" и "куда", а ниже к этим полям добавляем css-класс prev_move.

Объявление функции передачи на сервер

Ранее мы вызвали функцию для передачи хода на сервер. Сейчас мы просто объявим эту функцию, чтобы не было ошибки, но само её тело напишем позже, в следующей части:

function send_move_to_server(cell_index_from, cell_index_to) {

}

Оформляем выделение полей в css

Открываем файл стилей, добавляем в конец правило для выделенного поля:

.figure_selected {
    border: 2px solid #061;
}

Т.е. выделенное поле будет иметь зелёную рамку. А чтобы при добавлении границы, общий размер поля не увеличивался, "ломая" при этом всю доску, в правило для .cell добавим:

box-sizing: border-box;

Ещё для этого-же .cell давайте изменим вид курсора над полем, добавив правило:

cursor: pointer;

Для "подсвечивания" полей, на которых был предыдущий ход, тоже сделаем рамку вокруг поля, но уже синего цвета:

.prev_move {
    border: 2px solid #06f;
}

Теперь оформим поля, на которые можно переместить выделенную фигуру. Сделаем посередине поля зелёный полупрозрачный круг. Создадим псевдоэлемент ::after - абсолюно позиционированный блок, растянутый на всё поле, с радиальным градиентом:

.available_for_move::after {
    content: "";
    display: block;
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background: radial-gradient(circle, rgba(0, 102, 17, 0.5) 19%, rgba(0, 0, 0, 0) 20%);
}

А чтобы этот absolute - блок правильно позиционировался, т.е. отсчитывал координаты от родительского элемента .cell, добавим к этому .cell ещё правило:

position: relative;

Итоги

В этой части мы научились делать ходы на "стороне человека". При этом "подсвечиваем" поля, куда можно ходить выделенной фигурой, а также поля, на которых был предыдущий ход. Как всё работает на этом этапе, можно посмотреть на демо-3 - странице

Выделение фигуры, показ возможных ходов, показ предыдущего хода

Можете попробовать сделать свои варианты выделения полей. Можно добавить обработчики событий "Мышь над полем", чтобы менять вид курсора. Экспериментируйте!

Исходные коды текущего этапа разработки можно посмотреть на гитхабе