Шахматы на php и javascript: Генератор ходов, фабричный метод и классы фигур

Меняем кодировку фигур

Текущая кодировка фигур с их цветами (см constants.php) кажется не очень удобной. Например мы получили код фигры $figure, находящейся на какой-то клетке. Как проверить, что это ферзь? Вот так:

$figure == FG_QUEEN_WHITE || $figure == FG_QUEEN_BLACK

Как-то не очень хорошо, да? А если надо проверить, что это белая фигура? Или так:

$figure == FG_KING_WHITE || $figure == FG_QUEEN_WHITE || $figure == FG_ROOK_WHITE || $figure == FG_BISHOP_WHITE || $figure == FG_QUEEN_KNIGHT || $figure == FG_QUEEN_PAWN

Или, можно заметить, что белые фигуры у нас представлены нечётными числами, и проверять код фигуры на нечётность.

А давайте изменим представление фигур! Цвет представим двумя младшими двоичными разрядами числа. Да, для вариантов "белый-чёрный" можно обойтись одним разрядом. Но я решил сделать два. Так код фигуры "отсутствие фигуры" (FG_NONE) не будет определяться ни как белая фигура, ни как чёрная.

А вид фигуры (король, ферзь, ладья, слон, конь, пешка) пронумеруем от 1 (король) до 6 (пешка), но сдвинем это значение на два двоичных разряда влево. Теперь код фигуры будет выглядеть так:

Кодировка фигуры

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

constants.php<?php

    define('COLOR_WHITE', 0b01); // 1
    define('COLOR_BLACK', 0b10); // 2
    define('BOARD_SIZE', 8);

    define('FG_NONE', 0);
    define('FG_KING',   1 << 2); // 4
    define('FG_QUEEN',  2 << 2); // 8
    define('FG_ROOK',   3 << 2); // 12
    define('FG_BISHOP', 4 << 2); // 16
    define('FG_KNIGHT', 5 << 2); // 20
    define('FG_PAWN',   6 << 2); // 24

    

Т.е., к примеру, белый король будет представлен числом FG_KING + COLOR_WHITE = 0b100 + 0b01 = 0b101 = 5

chess.js

Раз уж мы изменили кодировку фигур и цветов, надо подкорректировать все места где она использовались эти константы. Начнём с константы FIGURES в файле chess.js. Она должна выглядеть так:

chess.jsconst FIGURES = {
    5:  'img/king-white.svg', // белый король
    6:  'img/king-black.svg', // чёрный король
    9:  'img/queen-white.svg', // белый ферзь
    10: 'img/queen-black.svg', // чёрный ферзь
    13: 'img/rook-white.svg', // белая ладья
    14: 'img/rook-black.svg', // чёрная ладья
    17: 'img/bishop-white.svg', // белый слон
    18: 'img/bishop-black.svg', // чёрный слон
    21: 'img/knight-white.svg', // белый конь
    22: 'img/knight-black.svg', // чёрный конь
    25: 'img/pawn-white.svg', // белая пешка
    26: 'img/pawn-black.svg', // чёрная пешка
}

chess_game.php

Открываем файл engine/chess_game.php Меняем коды фигур в функции, отдающей начальную позицию:

engine/chess_game.phpprivate function getInitPosition() {
    return array(
        FG_ROOK+COLOR_BLACK, FG_KNIGHT+COLOR_BLACK, FG_BISHOP+COLOR_BLACK, FG_QUEEN+COLOR_BLACK, FG_KING+COLOR_BLACK, FG_BISHOP+COLOR_BLACK, FG_KNIGHT+COLOR_BLACK, FG_ROOK+COLOR_BLACK,
        FG_PAWN+COLOR_BLACK, FG_PAWN+COLOR_BLACK,   FG_PAWN+COLOR_BLACK,   FG_PAWN+COLOR_BLACK,  FG_PAWN+COLOR_BLACK, FG_PAWN+COLOR_BLACK,   FG_PAWN+COLOR_BLACK,   FG_PAWN+COLOR_BLACK,
        FG_NONE,             FG_NONE,               FG_NONE,               FG_NONE,              FG_NONE,             FG_NONE,               FG_NONE,               FG_NONE,
        FG_NONE,             FG_NONE,               FG_NONE,               FG_NONE,              FG_NONE,             FG_NONE,               FG_NONE,               FG_NONE,
        FG_NONE,             FG_NONE,               FG_NONE,               FG_NONE,              FG_NONE,             FG_NONE,               FG_NONE,               FG_NONE,
        FG_NONE,             FG_NONE,               FG_NONE,               FG_NONE,              FG_NONE,             FG_NONE,               FG_NONE,               FG_NONE,
        FG_PAWN+COLOR_WHITE, FG_PAWN+COLOR_WHITE,   FG_PAWN+COLOR_WHITE,   FG_PAWN+COLOR_WHITE,  FG_PAWN+COLOR_WHITE, FG_PAWN+COLOR_WHITE,    FG_PAWN+COLOR_WHITE,  FG_PAWN+COLOR_WHITE,
        FG_ROOK+COLOR_WHITE, FG_KNIGHT+COLOR_WHITE, FG_BISHOP+COLOR_WHITE, FG_QUEEN+COLOR_WHITE, FG_KING+COLOR_WHITE, FG_BISHOP+COLOR_WHITE, FG_KNIGHT+COLOR_WHITE, FG_ROOK+COLOR_WHITE
    );
}

В этом-же файле, в функции getClientJsonGameState меняем выражение для human_color:

'human_color' =>> ($this->game_state->human_color == COLOR_BLACK ? 'b' : 'w'),

Т.е. мы на стороне php изменили кодировку цвета (1 - белый, 2 - чёрный), но на стороне клиента (браузера / фронтенда), цвет по прежнему будем обозначать 'w' или 'b'.

Изменяем имя ключа сессии

Мы сохраняем позицию в сессии, но раз мы изменили кодировку фигур, то при отображении уже сохранённой ранее позиции, получится, мягко говоря, неожиданная картина. Чтобы не получился конфликт версий разных форматов, давайте просто изменим имя ключа сессии, в котором будет храниться состояние игры. Идём в engine/session_storage.php и меняем константу с именем ключа сессии:

const SESSION_KEY = 'chess_game_state_v2';

Микрорефакторинг

В файле create_new_game.php, в вызове конструктора класса, мы передаём параметр $human_color, но в объявлении конструктора нет параметров. Так что уберём параметр в вызове конструктора:

$game = new ChessGame();

И в последней строке этого файла, в функцию json_encode добавим второй параметр - флаг JSON_UNESCAPED_UNICODE. Он нужен чтобы русские символы (многобайтные символы Unicode) не кодировались как "\uXXXX". Можно конечно обойтись и без этог флага, просто с ним удобнее смотреть ответы сервера в инспекторе объектов браузера.

Т.е. последняя строка файла create_new_game.php теперь будет выглядеть так:

echo json_encode($data, JSON_UNESCAPED_UNICODE);

Точно такое-же изменение, т.е. добавление параметра JSON_UNESCAPED_UNICODE в вызов функции json_encode сделаем в последних строках файлов: get_game_state.php, make_move.php

Получение вида фигуры и цвета по коду фигуры

Нам понадобится в дальнейшем по коду фигуры получать её цвет и вид фигуры. Например по коду 25 = 0b11001 нам надо определить что это белая пешка.

Кодировка фигуры

Для мелких вспомогательных функций, не привязанных явно к какой-либо сущности, создадим отдельный класс. Он будет выступать в роли "неймспейса". Создаём файлик engine/functions.php:

<?php

    class Functions {
        static public function color(int $figure) {
            return $figure & 0b11;
        }
    
        static public function figure_type(int $figure) {
            return $figure & 0b11100;
        }
    }

Тут всё просто. Для выделения цвета делаем операцию логическое "И" между кодом фигуры и маской, выделяющей первые два разряда. Тоже самое - для типа фигуры, только маска убирает разряды цвета, и выделяет три следующих (справа налево) разряда для типа фигуры: 0b11100.

Генератор ходов

Открываем файл engine/chess_game.php, идём к методу generateAvailableMoves. Он дожен отдавать массив со всеми возможными перемещениями (ходами) из текущей позиции. Сейчас там "заглушка" - метод возвращает пустой массив. Сейчас это исправим! Делаем так:

public function generateAvailableMoves() {
    $move_generator = new MoveGenerator($this->game_state);
    return $move_generator->generateAllMoves();
}

Т.е. мы создаём объект ненаписанного пока класса MoveGenerator и вызываем метод generateAllMoves. Вот так всё очень просто, остался сущий пустяк - написать новый класс. Так в принципе можно решать все задачи - просто вызвать несуществующий метод несуществующего класса .

Создаём в папке engine файл move_generator.php и пишем там:

engine/move_generator.php<?php

class MoveGenerator {
    private $game_state;

    public function __construct(GameState $game_state) {
        $this->game_state = $game_state;
    }

    public function generateAllMoves() {
        $moves = array();
        $color = $this->game_state->current_player_color;
        for($i = 0; $i < BOARD_SIZE * BOARD_SIZE; $i++) {
            $figure_code = $this->game_state->position[$i];
            if ($figure_code === FG_NONE || Functions::color($figure_code) != $color) {
                continue;
            }
            $figure_factory = new FigureFactory();
            $figure = $figure_factory->create($this->game_state, $i);
            $figure_moves = $figure->getAvailableMoves();
            if (count($figure_moves) > 0) {
                $moves[$i] = $figure_moves;
            }
        }
        return $moves;
    }
}

Конструктор класса (строки 6 - 8 принимает один параметр - состояние игры (объект класса GameState). Это состояние игры записывается в приватное поле класса - $game_state.

Метод generateAllMoves

В строке 11 объявляем переменную $moves, где будем накапливать допустимые ходы, и которую и возвратим в конце функции в качестве результата. Записываем туда пока пустой массив.

В строке 12 в переменную $color запишем цвет фигур "текущего игрока", т.е. тот цвет, который сейчас "ходит". Именно для фигур этого цвета и будем генерировать перемещения.

Проходим по всем полям доски (цикл, начинающийся на строке 13). Далее рассмотрим, что мы делаем в теле цикла (строки 14 - 23)

Строка 14 - получаем код фигуры, находящейся на текущем поле (поле, для которого выполняется текущая итерация тела цикла).

Строки 15 - 17 - если фигура отсутствует (FG_NONE), или её цвет не совпадает с цветом фигур, для которых мы генерируем ходы, то завершаем текущую итерацию цикла, переходим к следующей.

Таким образом, к строке 18 мы пришли, зная, что в текущем поле (поле с индексом $i находится фигура нужного нам цвета. И для этой фигуры надо сгенерировать все возможные варианты ходов.

Каждый вид фигур ходит по своим правилам, и выглядит логичным, чтобы ходы для каждого вида генерировались экземплярами соответствующих разных классов фигур. Ходы короля формируются экземпляром класса короля, ходы коня - экземпляром класса коня, …

В строке 18 мы создаём экземпляр класса "фабрики фигур" (FigureFactory), а в следующей строке вызываем его метод create, получая экземпляр нужного класса фигур. И этот экземпляр записываем в переменную $figure. Класс "фабрики фигур" мы напишем чуть ниже. Это пример реализации порождающего паттерна проектирования "фабричный метод".

В метод create мы передаём состояние игры ($this->game_state) и индекс ($i) поля, на котором находится фигура. Нам-же надо знать где находится фигура, для которой надо сгенерировать ходы. И состояние игры нужно знать. Например мы генерируем ходы для короля, и для одного и того-же распложения фигур, в одном состоянии игры король уже уходил со своей начальной позиции, и рокировка невозможна ни в одну сторону. А в другом состоянии (в другой партии) - король не двигался, и рокировка возможна. Или для пешки, генерация хода "взятие на проходе" зависит от предыдущего "полухода".

Итак, мы получили экземпляр класса фигуры, и в строке 20 вызываем его метод getAvailableMoves. Этот метод напишем позже так, чтобы он возвращал массив с индексом полей, куда возможно перемещение. Если полученный массив не пустой, то записываем его под индексом $i в итоговый ассоциативный массив $moves. Т.е. "говорим" - "с поля $i можно пойти на такие вот поля".

Класс "фабрика фигур"

Напишем класс "фабрика фигур", который выше мы использовали для получения экземпляров классов разных видов фигур. В папке engine создаём файл figure_factory.php:

engine/figure_factory.php<?php

class FigureFactory {
    public function create(GameState $game_state, int $position_index) {
        $figure_code = $game_state->position[$position_index];
        $figure_type = Functions::figure_type($figure_code);
        switch($figure_type) {
            case FG_KING:
                $class_name = 'King';
                break;
            case FG_QUEEN:
                $class_name = 'Queen';
                break;
            case FG_ROOK:
                $class_name = 'Rook';
                break;
            case FG_BISHOP:
                $class_name = 'Bishop';
                break;
            case  FG_KNIGHT:
                $class_name = 'Knight';
                break;
            case FG_PAWN:
                $class_name = 'Pawn';
                break;
            default:
                throw new Exception('Unknown figure type');
        }
        return new $class_name($game_state, $position_index);
    }
}

Очень простой код. В классе всего один метод create, принимающий два параметра - состояние игры (экземпляр класса GameState, и индекс поля. В строке 5 берём код фигуры, расположенной в поле с указанным индексом. В строке 6 определяем вид фигуры, и далее в блоке switch (строки 7 - 28), в зависимости от вида фигуры, записываем в переменную $class_name имя класса. Все эти классы (King, Queen, Rook, Bishop, Knight, Pawn) мы очень скоро создадим. В ветке "default", т.е. когда в поле оказалось "нечто неожиданное", мы генерируем исключение "Unknown figure type".

В конце метода (строка 29) мы создаём экземпляр класса по имени класса. В качестве параметров передаём состояние игры и индекс поля, т.е., то-же самое, что и приняли в качестве входных параметров в метод create.

Общий класс фигуры

Все перечисленные выше классы фигур унаследуем от одного общего абстрактного класса. Напишем его в файле engine/figure.php:

engine/figure.php<?php

require_once('figures/king.php');
require_once('figures/queen.php');
require_once('figures/rook.php');
require_once('figures/bishop.php');
require_once('figures/knight.php');
require_once('figures/pawn.php');

abstract class Figure {
    protected GameState $game_state;
    protected int $position_index;

    public function __construct(GameState $game_state, int $position_index) {
        $this->game_state = $game_state;
        $this->position_index = $position_index;
    }

    abstract public function getAvailableMoves();
}
    

В этом файле сначала подключаем шесть файлов, в которых мы совсем скоро напишем классы конкретных виов фигур. В строке 10 объявляем наш общий класс фигуры, указывая что он абстрактный, т.к. в нём будут абстрактные методы.

В классе объявляем два поля, в которых будут храниться состояние игры (экземпляр класса GameState, и индекс поля, на котором находится фигура, представляемая экземпляром класса. В конструкторе мы заполняем эти поля (строки 14 - 17).

Ну и объявляем абстрактный метод getAvailableMoves.

Подключаем новые классы

Мы уже написали 4 новых класса, но нигде не подключали файлы, в которых они содержатся. Идём в файл engine/chess_game.php и после подключения файла game_state.php, подключаем новые файлы:

require_once('functions.php');
require_once('figure_factory.php');
require_once('figure.php');
require_once('move_generator.php');

Классы фигур

Сейчас напишем классы конкретных видов фигур, унаследовав их от класса Figure. Этот класс - предок содержит абстрактный метод getAvailableMoves, так что напишем его реализацию в каждом из классов - потомков. Пока сделаем "заглушку" - будем возвращать пустой массив допустимых ходов.

Итак, внутри папки engine создаём папку figures. И создаём в ней шесть похожих файлов. Для короля:

engine/figures/king.php<?php

class King extends Figure {
    public function getAvailableMoves() {
        return array();
    }
}

Для ферзя:

engine/figures/queen.php<?php

class Queen extends Figure {
    public function getAvailableMoves() {
        return array();
    }
}

Для ладьи:

engine/figures/rook.php<?php

class Rook extends Figure {
    public function getAvailableMoves() {
        return array();
    }
}

Для слона:

engine/figures/bishop.php<?php

class Bishop extends Figure {
    public function getAvailableMoves() {
        return array();
    }
}

Для коня:

engine/figures/knight.php<?php

class Knight extends Figure {
    public function getAvailableMoves() {
        return array();
    }
}

Для пешки:

engine/figures/pawn.php<?php

class Pawn extends Figure {
    public function getAvailableMoves() {
        return array();
    }
}

Итоги

В этой части мы написали "скелет" генератора ходов (перемещений). Написали общий класс фигуры, и унаследовали от него шесть классов конкретных видов фигур. В генераторе перемещений создаём экземпляры конкретных классов фигур, реализовав шаблон проектирования "фабричный метод".

Как обычно, выложил исходный код этой части на github.

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

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