Шахматы на php и javascript: Запрос к бекенду и обработка ответа

Функция для общения с сервером

Сейчас напишем функцию, с помощью которой будем отправлять запросы на сервер (бекенд), и обрабатывать ответы. Эта функция довольно универсальна, её можно будет использовать в разных своих проектах. Итак, открываем в редакторе файл chess.js, и в самое начало добавляем такое:

chess.jsconst createRequest = (options = {}) => {
    const method = (options.method === undefined ? 'GET' : options.method.toUpperCase());
    let url = options.url;
    let formData;
    if (options.data) {
        if (options.method === 'GET') {
            url += url.indexOf('?') >= 0 ? '&' : '?';
            for (let key in options.data) {
                url += key + '=' + encodeURI(options.data[key])+ '&';
            }
            url = url.slice(0, -1);
        } else {
            formData = new FormData();
            for (let key in options.data) {
                formData.append(key, options.data[key]);
            }
        }
    }
    const xhr = new XMLHttpRequest();
    try {
        xhr.open(method, url);
        if (options.headers) {
            for (let key in options.headers) {
                xhr.setRequestHeader(key, options.headers[key]);
            }
        }
        xhr.responseType = 'json';
        if (options.callback) {
            xhr.addEventListener('readystatechange', function() {
                if (this.readyState == xhr.DONE) {
                    let response = this.response;
                    if (this.status == 200 || this.status == 201 || options.no_check_status) {
                        options.callback(response);
                    } else if (options.error_callback) {
                        options.error_callback(response);   
                    } else {
                        console.log(response);
                    }
                }
            });
        }
        xhr.send(formData);
    } catch (e) {
        console.log(e);
    }
    return xhr;
}

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

Но лучше конечно разобраться. Я кратко, "грубыми мазками", без подробностей, опишу что там делается. Если надо детально понять код, разложить его "по полочкам", вот он - перед Вами. Читайте! Очень важно развивать навыки чтения чужого кода.

Если хотите индивидуальных разъяснений, есть какие-то вопросы по коду, логике, алгоритмам, можно спросить здесь или записаться на консультацию

Какие парамеры обрабатывает функция

Вернёмся к коду. Функция createRequest принимает хеш - набор разных опций, который нам нужны. Самый простой вариант использования, когда мы просто шлём GET-запрос по адресу https://someurl.com, и никак не обрабатываем ответ:

createRequest({ url: 'https://someurl.com' });

Если надо сделать POST-запрос по этому адресу, с двумя параметрами, и ответ записать в консоль, вот пример:

createRequest({
    method: 'POST',
    url: 'https://someurl.com',
    data: {
        param1: "value1",
        param2: "value2"
    },
    callback: function(response) {
        console.log(response);
    }
});

В этом примере видны основные используемые опции, и как они используются. Вот список всех опций, обрабатываемых в функции:

method
метод запроса
url
URL / адрес куда шлём запрос
data
данные, которые надо передать
headers
заголовки запроса, которые надо отправить
callback
функция, которая будет вызываться при "успешном" ответе ("успешный" - значит http-статус равен 200 или 201)
no_check_status
логическое значение - проверять ли "успешность ответа" для запуска функции callback
error_callback
функция, которая будет вызвана при "неуспешном" ответе

Кратко пройдёмся по функции

Строки 5 - 18 - подготавливаем данные для передачи. Если метод запроса - GET, то данные пихаем конечно в url запроса. Иначе - пользуемся объектом FormData

В строке 19 создаём "объекта запроса" - экземпляр класса XMLHttpRequest - это центральный элемент функции, вокруг которого всё и крутится. Далее настраиваем разные параметры этого объекта запроса: метод запроса, URL (адрес), заголовки, указываем что ожидаем ответ в формате json.

Строки 28 - 41 относятся к обработке ответа. Если во входных опциях функции указан ключ callback, то к объекту запроса мы добавляем "слушателя" события "смены состояния готовности" (readystatechange). Обрабатываем только смену статуса на "полную готовность" (xhr.DONE)

Если код статуса 200 или 201 (т.е. успешный ответ), или мы указали в опциях, что проверять статус не нужно (options.no_check_status), то вызываем функцию, которую мы указали в опции options.callback, передавая ей ответ с сервера (бекенда).

Иначе, если указана функция для обработки ошибки (опция options.error_callback), вызываем её. Если такой фунции не указано, то просто пишем ответ сервера в консоль.

В строке 42 происходит собственно сама отправка запроса, управление передаётся дальше, не дожидаясь ответа сервера. И функция возвращает "объект запроса". При возникновении любой исключительной ситуации (не обработанной ошибки), срабатывает блок try - catch и ответ записывается в консоль.

Делаем запрос на сервер

Помните, в прошлой статье мы объявили javascrit-функцию send_move_to_server? Теперь у нас есть функция для общения с сервером. Используем её:

chess.jsfunction send_move_to_server(cell_index_from, cell_index_to) {
    createRequest({
        method: 'POST',
        url: 'make_move.php',
        data: { cell_index_from: cell_index_from, cell_index_to: cell_index_to },
        callback: function(response) {
            set_game_state(response);
        }
    });
}

Т.е., мы методом POST шлём запрос по адресу текущий_домен/make_move.php. Передаём в этом запросе параметры cell_index_from, cell_index_to. При успешном ответе вызываем javascript-функцию set_game_state. Этой функции пока нет, очень скоро мы её напишем. Так-же,как и отсутствующий пока php-скрипт make_move.php.

Php - скрипт для обработки хода

Пришла пора сделать уже что-то и на сторне бекенда. Создаём в веб-корне проекта файлик make_move.php, и пишем "верхне-уровнево", что сейчас хотим получить, обращаясь к ещё не написанный классам и функциям:

make_move.php<?php
require_once('engine/chess_game.php');
$cell_index_from = $_POST['cell_index_from'];
$cell_index_to = $_POST['cell_index_to'];
$game = new ChessGame();
$game->makeMove($cell_index_from, $cell_index_to);
$data = $game->getGameState();
header('Content-Type: application/json');
sleep(2);
echo json_encode($data);

Сначала подключаем скрипт, где предполагаем разместить "игровой движок" - engine/chess_game.php. Потом, в строке 5, создаём объект игры класса ChessGame:

$game = new ChessGame();

Класс ChessGame планируем объявить в ненаписанном пока файле engine/chess_game.php. В строке 6 вызываем опять-таки ненаписанный пока метод перемещения фигуры с поля $cell_index_from на поле $cell_index_to. Эти переменные "откуда" и "куда" заполняются в строках 3, 4 - значения берутся из данных, пришедших в POST - запросе.

Далее, в строке 7, получаем данные о состоянии игры (позиции), вызывая несуществующий пока метод getGameState. Этот метод мы напишем специально чтобы он отдавал все нужные данные для клиентской стороны (для javascript- фронтенда), и в нужном виде.

Шлём заголовок, говорящий что данные в теле ответа представлены в формате json. Потом, в строке 9 я поставил задержку на 2 секунды - это просто для демонстрации, чтобы вы успели заметить состояние полей доски после того как мы сделали ход, и до того, как компьютер ответил. Потом мы уберём эту задержку.

И, наконец, в строке 10 отдаём данные $data, закодировав JSON в виде строки.

Создаём класс игры

Создадим всё то, что мы понавызывали в только что описанном файле make_move.php. Пока просто создадим класс с методами - "заглушками", просто чтобы отладить обработку данных от сервера на стороне браузера. Итак, создаём в веб-корне проекта папку engine ("движок"), а в ней - файл chess_game.php:

engine/chess_game.php<?php

class ChessGame {
    function makeMove($cell_index_from, $cell_index_to) {

    }

    function getGameState() {
        return [
            'is_our_move' => true,
            'prev_move_from' => 1,
            'prev_move_to' => 18,
            'position' => [
                6,  0, 8,  4,  2,  8,  10, 6,
                12, 12, 12, 12, 12, 12, 12, 12,
                0,  0,  10,  0,  0,  0,  0,  0,
                0,  0,  0,  0,  0,  0,  0,  0,
                0,  0,  0,  0,  0,  0,  0,  0,
                0,  0,  0,  0,  0,  0,  0,  0,
                11, 11, 11, 11, 11, 11, 11, 11,
                5,  9,  7,  3,  1,  7,  9,  5
            ],
            'available_moves' => [
                48 => [40, 32],
                49 => [41, 33],
                50 => [42, 34],
                51 => [43, 35],
                52 => [44, 36],
                53 => [45, 37],
                54 => [46, 38],
                55 => [47, 39],
                57 => [40, 42],
                62 => [45, 47]
            ]
        ];
    }
}

Мы объявили класс ChessGame с двумя методами.

Метод makeMove пока ничего не делает, он просто объявлен.

Метод getGameState возвращает ассоциативный массив со всей нужной для фронтенда (нашего javascrit - кода) информацией. Пока здесь "захардкоженные" данные. В них мы говорим, что очередь хода - у человека ('is_our_move' => true). Что компьютер сделал ход с поля с индексом 1 на поле 18 ('prev_move_from' => 1, 'prev_move_to' => 18)

В ключе position передаём результирующую позицию. Я её скопировал из файла chess.js, из функции initPosition. Белые фигуры оставил в начальном положении, как будто белые и не делали ход, а чёрного коня передвинул с поля 1 на поле 18:

кодировка позиции с имитацией хода чёрным конём

Да, позиция странная - как будто белые не ходили вовсе. Но это пока заглушка функции, а на этих данных мы на стороне фронтенда научимся обрабатывать ответ сервера.

Ну и допустимые ходы (available_moves) я тоже скопировал из файла chess.js.

Обработка ответа сервера

Возвращаемся к нашему "фронтенду" - chess.js. Что надо сделать? Записать пришедшие данные в javascript - переменные (очередь хода, откуда и куда был предыдущий ход, позицию, и допустимые перемещения). И перерисовать позицию. Всё!

Но когда я сделал полную перерисовку позиции, заметил неприятное "моргание" при перерисовке. Что-ж, будем перерисовывать только фигуры в изменённых полях. Для этого надо изменять переменную position при перемещении фигуры о отправки на сервер. Сделаем небольшие правки и рефакторинг.

Небольшой рефакторинг

1. Отмечаем смену позиции

В функции make_move эту строку

cell_to.appendChild(figure);
уберём под условие существования фигуры, и сразу следом отметим в позиции, что фигура ушла с поля "откуда", и появилась в поле "куда":

if (figure) {
    cell_to.appendChild(figure);
}
position[cell_index_to] = position[cell_index_from];
position[cell_index_from] = 0;

2. Убираем выделение преыдущего хода

В большой функции onCellClick ищем строку с комментарием

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

Эта функция будет снимать выделение полей "предыдущего хода". Зачем это здесь надо? Представьте - компьютер сделал ход, и сейчас выделены поля откуда и куда ходила его фигура. Мы выделили свою фигуру, и кликнули по полю, куда ей можно сходить. Т.е. мы сделали ход, и тут надо выделить поля откуда и куда мы пошли. И тут как раз и надо снять "выделение предыдущего хода" противника (компьютера).

Пишем функцию снятия "выделения предыдущего кода", расположив её например после функции deselect_cell:

function deselect_prev_move_cells() {
    const prev_cells = Array.from(document.querySelectorAll('.board .cell.prev_move'));
    for (let i=0; i < prev_cells.length; i++) {
        prev_cells[i].classList.remove('prev_move');
    }
}

Тут всё просто - берём все элементы - клетки доски, у которых есть css-класс prev_move, проходим по ним в цикле и убираем этот класс.

3. Установка фигуры на поле

Мы уже устанавливали фигуры на поля в функции showPosition. Нам понадобится в другом месте этот код. Конечно можно каждый раз, когда надо отобразить изменения в позиции, перерисовывать все поля доски в функции showPosition (слегка изменив её для очистки поля, где фигуры нет). Но, как я писал выше, картинка при этом "моргает". Очень быстро, но всё равно заметно. Так что будем вызывать установку фигур "точечно", не всем скопом.

В функции showPosition находим строку

let image = FIGURES[figure];
начиная с неё, и до конца функции убираем всё (можно в буфер обмена). Перенесём этот код в отдельную функцию. А здесь, где мы удалили код, вставляем вызов этой нашей новой функции:
set_figure_to_cell(figure, i);

Саму новую функцию расположим, к примеру, перед функцией showPosition:

function set_figure_to_cell(figure, cell_index) {
    let image = FIGURES[figure];
    if (!image) {
        return;
    }
    const figure_cell = document.createElement('div');
    const image_tag = document.createElement('img');
    image_tag.src = image;
    figure_cell.appendChild(image_tag);
    figure_cell.classList.add('figure');
    cells[cell_index].appendChild(figure_cell);
}

Если вы перенесли текст функции через буфер обмена, не забудте изменить переменную i на cell_index - именно так мы назвали второй входной параметр.

Обработка ответа сервера

Помните, когда мы отсылали наш ход на сервер (на бекенд), мы указали, что при успешном ответе надо вызвать функцию set_game_state? Сейчас мы её напишем. Задача этой функции - обновить информацию о позиции в соответствии с тем, что прислал нам сервер, и показать обновлённое положение фигур на доске. Идём в самый конец файла chess.js, и пишем:

function set_game_state(game_state) {
    // снимаем выделение с полей "предыдущего хода"
    deselect_prev_move_cells();

    // обновляем позицию
    for (let i = 0; i < BoardSize**2; i += 1) {
        if (position[i] === game_state.position[i]) {
            continue;
        }
        cells[i].textContent = '';
        set_figure_to_cell(game_state.position[i], i);
        position[i] = game_state.position[i]
    }

    // обновляю допустимые ходы
    available_moves = game_state.available_moves;

    // Делаем выделение полей "предыдущего хода"
    prev_move_from = game_state.prev_move_from;
    prev_move_to = game_state.prev_move_to;
    if (prev_move_from !== null) {
        cells[prev_move_from].classList.add('prev_move');
    }
    if (prev_move_to != null) {
        cells[prev_move_to].classList.add('prev_move');
    }
    // передаём очередь хода человеку
    is_our_move = game_state.is_our_move;
}

Мы вызывали эту функцию, передавая ей ответ сервера. Здесь мы назвали входной параметр как game_state. Мы ведь ожидаем, что в ответе нам придёт именно состояние игры.

По комментариям в коде понятно что делает функция, она написана достаточно просто. Отдельно наверное стоит пояснить обновление позиции (строки 6 - 13)

Проходим по всем клеткам, от 0 до 63. Что мы сравниваем в строке 7? В массиве position хранится состояние доски на стороне клиента (фронтенда, браузера). Т.е. какие фигуры находились в клетках доски до отправки нашего хода на сервер (бекенд).

В массиве game_state.position сервер нам прислал информацию о том, какие фигуры находятся в полях доски после хода компьютера. Если в клетке до ответа компьютера была та-же фигура, что и после ответа - ничего делать в этой клетке не надо, мы идём на следующую итерацию цикла с помощью continue.

Если же в поле что-то поменялось (фигура ушла с этого поля, встала на это поле), то очищаем содержимое поля, вызываем функцию для установки нужной фигуры в поле, и обновляем информацию об этом поле:

cells[i].textContent = '';
set_figure_to_cell(game_state.position[i], i);
position[i] = game_state.position[i]

Итоги

Мы сделали отправку нашего хода на сервер. Получаем ответный ход компьютера, обновляем позицию, и можем снова ходить. Правда пока ответ компьютера захардкожен - он просто спустя две секунды отдаёт нам позицию, где наши белые фигуры находятся на начальных позициях, а чёрный конь пошёл на поле с6

Посмотреть что получилось, можно на демо - странице №4

Ну и исходные коды можно посмотреть на гитхабе