Меняем кодировку фигур
Текущая кодировка фигур с их цветами (см 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 в классах фигур. Попробуйте возвращать вместо пустых массивов, массивы с какими-нибудь индексами полей. Создайте игру "за белых", и выделите фигуру, в классе которой указали непустые массивы "допустимых полей". Соответствующие поля "подсветятся" зелёными кружками, и на них можно будет переместить фигуру.