Сейчас мы научим ходить все оставшиеся фигуры: слона, ладью, ферзя, короля.
Ход слона
У слона, ладьи и ферзя одинаковая логика перемещения. Все они дальнобойные фигуры. Разница - в направлениях, куда они могут ходить. Поэтому напрашивается сделать один метод для получения ходов-кандидатов для этих трёх видов фигур. А передавать в этот метод можно будет массив с возможными смещениями координат фигуры.
Итак, открываем файл с классом слона, и наполняем метод getCandidateMoves:
engine/figures/bishop.php<?php
class Bishop extends Figure {
public function getCandidateMoves() {
$shifts = array(array(-1, -1), array(-1, 1), array(1, -1), array(1, 1));
return $this->getLongRangeCandidateMoves($shifts);
}
}
В строке 5 определяем массив с возможными смещениями координат (вертикаль и горизонталь) слона. А в следующей строке передаём этот массив со смещениями в метод getLongRangeCandidateMoves, который сейчас напишем. Это и будет единый метод получения ходов - кандидатов для дальнобойных фигур. Напишем этот метод в классе Figure:
engine/figure.phppublic function getLongRangeCandidateMoves(array $shifts) {
$moves = array();
foreach ($shifts as $shift) {
list($shift_col, $shift_row) = $shift;
$continue_shift = true;
$col = $this->col;
$row = $this->row;
while ($continue_shift) {
$col = $col + $shift_col;
if ($col < 0 || $col >= BOARD_SIZE) {
$continue_shift = false;
continue;
}
$row = $row + $shift_row;
if ($row < 0 || $row >= BOARD_SIZE) {
$continue_shift = false;
continue;
}
$to_index = Functions::colRowToPositionIndex($col, $row);
$to_figure = $this->game_state->position[$to_index];
if ($to_figure === FG_NONE) {
$moves[] = $to_index;
continue;
}
$to_figure_color = Functions::color($to_figure);
if ($to_figure_color == $this->enemy_color) {
$moves[] = $to_index;
}
$continue_shift = false;
}
}
return $moves;
}
Как обычно, в начале определяем переменную $moves - пустой массив, в который будем накапливать ходы - кандидаты, а в конце метода возвратим из метода.
В строке 3 организуем цикл по всем смещениям (направлениям). Каждый элемент "смещения" представляет собой массив из двух элементов - смещение по горизонтали и смещение по вертикали. В строке 4 мы и раскладываем эти смещения по переменным $shift_col, $shift_row.
Внутри цикла мы обрабатываем одно направление движения фигуры. Мы берём позицию фигуры, и смещаемся из этой позиции в текущем направлении, пока это можно. А когда можно? Когда следующее поле (куда сместилось наше "внимание") пустое. И продолжаем смещаться, пока не упрёмся или в край доски, или в фигуру. Если это чужая фигура, то можно и на её место сделать ход (взять её).
Детальнее… В строке 5 мы определяем булеву (логическую) переменную, в которой будем хранить отметку - можно ли дальше пробовать смещаться вдоль текущего направления? Как только при проверке очередного поля мы "упрёмся" во что-то, то запишем в эту переменную false.
В строках 6, 7 берём начальное положение фигуры, точнее - сохраняем в переменные $col, $row индексы вертикали и горизонтали. В строке 8 делаем цикл "пока можно смещаться".
Внутри цикла знакомая схема. В строках 9 - 13 делаем смещение по горизонтали, т.е. изменяем колонку (вертикаль). Если вышли за пределы доски - то отмечаем что продолжать смещение не нужно: $continue_shift = false; и завершаем итерацию цикла. То-же самое аналогично проделываем для изменения горизонтали в строках 14 - 18.
В строке 19 по получившимся координатам (вертикали и горизонтали) получаем индекс поля, и в следующей строке получаем фигуру, находящуюся на этом поле. В строках 21 - 24 проверяем - если поле пустое, то записываем индекс поля в результирующий массив "ходов-кандидатов" и завершаем итерацию цикла.
На строке 25 мы оказываемся если очередное поле не пустое. Т.е. дальше идти нельзя, поэтому в строке 29 отмечаем - "дальше - нельзя". Но перед этим надо проверить - что за фигура стоит на этом занятом поле. Может её можно взять? Берём цвет фигуры в строке 25, и если этот цвет совпадает с цветом фигр оппонента (строка 2126), то записываем индекс поля в результирующий массив "ходов-кандидатов".
Не запутались? У нас получилось два цикла, они вложен в другой. Снаружи цикл по направлениям смещения, т.е. перебираем все направления. Внутри цикл - двигаемся вдоль направления "пока не упрёмся".
Всё! Слон умеет ходить. Если следуете за мной в написании кода, можете на этом месте проверить проект.
Ход ладьи.
Берём более тяжёлую фигуру - ладью. Открываем её класс, изменяем метод getCandidateMoves:
engine/figures/rook.phppublic function getCandidateMoves() {
$shifts = array(array(0, -1), array(0, 1), array(-1, 0), array(1, 0));
return $this->getLongRangeCandidateMoves($shifts);
}
Здесь точно такая-же схема, как для слона. Только массив смещений $shifts другой.
Вот и всё! Мы научили ходить и ладью. Но есть один нюансик, связанный с ладьёй. При любом перемещении ладьи, король терят возможность рокировки с ней. Помните, у нас в классе состояния игры (GameState) есть четыре поля для учёта возможностей рокировок:
public $enable_castling_white_king = null; // возможность рокировки белого короля на королевский фланг (короткая рокировка)
public $enable_castling_white_queen = null; // возможность рокировки белого короля на ферзевый фланг (длинная рокировка)
public $enable_castling_black_king = null; // возможность рокировки чёрного короля на королевский фланг (короткая рокировка)
public $enable_castling_black_queen = null; // возможность рокировки чёрного короля на ферзевый фланг (длинная рокировка)
При создании новой партии, во все эти поля пишется true. А нам, при совершении хода ладьёй, надо поставить false в одно из этих полей, в зависимости от начального положения ладьи.
Как и в случае с пешкой, перекроем в классе ладьи родительский метод makeMove. Вызовем родительский метод, и допишем код для обработки флагов возможностей рокировок. Нам понадобятся индексы начальных позиций всех ладей. Чтобы каждый раз не вычислять их по индексам вертикали и горизонтали, запишем эти значения в константы, они ведь всегда одинаковые для шахмат.
Вспоминаем нумерацию клеток доски:

Смотрим на углы, в которых стоят ладьи в начале партии, и добавляем константы в самое начало класса Rook:
engine/figures/rook.phpclass Rook extends Figure {
const WHITE_KING_ROOK_INIT_POSITION = 63;
const WHITE_QUEEN_ROOK_INIT_POSITION = 56;
const BLACK_KING_ROOK_INIT_POSITION = 7;
const BLACK_QUEEN_ROOK_INIT_POSITION = 0;
И ниже добавляем метод makeMove, перекрывающий родительский:
engine/figures/rook.phppublic function makeMove($to_cell_index, $validate_move=true) {
$cell_from = $this->position_index;
if (!parent::makeMove($to_cell_index, $validate_move)) {
return false;
}
if ($this->color == COLOR_WHITE) {
switch ($cell_from) {
case self::WHITE_KING_ROOK_INIT_POSITION:
$this->game_state->enable_castling_white_king = false;
break;
case self::WHITE_QUEEN_ROOK_INIT_POSITION:
$this->game_state->enable_castling_white_queen = false;
break;
}
} elseif ($this->color == COLOR_BLACK) {
switch ($cell_from) {
case self::BLACK_KING_ROOK_INIT_POSITION:
$this->game_state->enable_castling_black_king = false;
break;
case self::BLACK_QUEEN_ROOK_INIT_POSITION:
$this->game_state->enable_castling_black_queen = false;
break;
}
}
return true;
}
В строке 3 вызываем родительский метод, который делает все основные действия по "совершению хода", и если он возвратил false, то мы сразу возвращаем false и из перекрытого метода. Дальше, в зависимости от цвета ладьи, проверяем поле откуда она уходит на совпадение с начальными позициями ладей нужного цвета. Если оказывается что ладья уходит с "начальной позиции" - ставим false в одной из полей возможностей рокировок.
Ход ферзя
Открываем класс ферзя, изменяем метод getCandidateMoves:
engine/figures/queen.php<?php
class Queen extends Figure {
public function getCandidateMoves() {
$shifts = array(
array(-1, -1), array(-1, 0), array(-1, 1),
array(0, -1), array(0, 1),
array(1, -1), array(1, 0), array(1, 1)
);
return $this->getLongRangeCandidateMoves($shifts);
}
}
Точно такая-же схема как у слона и ладьи, отличия только в массиве возможных смещений. Этот массив для ферзя я расписал на три строчки просто для удобства воспиятия. В первой строке (строка номер 6 в листинге кода) описал смещения влево: влево-вниз, просто влево, влево-вверх. На второй строке вертикаль не меняется, там только два перемещения - вверх и вниз. Ну и третья строка - вправо-вниз, вправо, вправо-вверх.
Вот и всё! Теперь и ферзь может ходить, можете попробовать, поиграть. Для самой грозной фигуры, в итоге, кода понадобилось написать меньше всего.
Ходы недальнобойных фигур, микрорефакторинг
У нас теперь не умеет ходить только король. У него есть два типа хода - "обычный ход", и рокировка. "Обычный" ход делается точно по такой-же схеме как и ход коня, просто допустимые смещения другие. Давайте не будем копипастить, и сделаем метод получения ходов-кандидатов для "недальнобойных фигур". Открываем класс Figure, и добавляем метод, почти полностью скопировав код из метода getCandidateMoves класса коня:
engine/figure.phppublic function getShortRangeCandidateMoves(array $shifts) {
$moves = array();
foreach($shifts as $shift) {
$col = $this->col + $shift[0];
if ($col < 0 || $col >= BOARD_SIZE) {
continue;
}
$row = $this->row + $shift[1];
if ($row < 0 || $row >= BOARD_SIZE) {
continue;
}
$to_index = Functions::colRowToPositionIndex($col, $row);
$to_figure = $this->game_state->position[$to_index];
if ($this->color == Functions::color($to_figure)) {
continue;
}
$moves[] = $to_index;
}
return $moves;
}
А в коде класса коня надо убрать этот код и вместо него вызвать новый метод. После такого изменения весь класс коня становится таким:
engine/figures/knight.php<?php
class Knight extends Figure {
public function getCandidateMoves() {
$shifts = array(
array(-2, -1), array(-2, 1), array(-1, -2), array(-1, 2), array(1, -2), array(1, 2), array(2, -1),array(2, 1)
);
return $this->getShortRangeCandidateMoves($shifts);
}
}
Как видите, классы коня, слона и ферзя получились очень компактными.
Ход короля
В оставшейся части статьи будем работать только с файлом с классом короля - engine/figures/king.php. В методе getCandidateMoves будем отдавать только ходы-кандидаты "простых перемещений", без рокировок:
engine/figures/king.php// Метод отдаёт только поля простых перемещений. Возможность рокировок будет проверена в методе getAvailableMoves
public function getCandidateMoves() {
$shifts = array(
array(-1, -1), array(-1, 0), array(-1, 1),
array(0, -1), array(0, 1),
array(1, -1), array(1, 0), array(1, 1)
);
return $this->getShortRangeCandidateMoves($shifts);
}
Всё просто - допустимые смещения берём точно такие-же как у ферзя, и передаём их в метод получения ходов-кандидатов недальнобойных фигур.
Условия для рокировки
Какие условия должны быть выполнены, чтобы рокировка была возможна? Во первых, король до этого не должен был ходить. И ладья участвующая в рокировке не должны была ходить для этого. Для проверки этих условий у нас есть 4 флага в объекте - "состоянии игры".
Во вторых, поля между королём и ладьёй должны быть свободны. Т.е. надо проверить на "свободность" два поля для короткой рокировке, и три поля - для длинной. В третьих, король должен быть не под шахом. Поле, которое он перескочит, должно быть не под угрозой. И итоговая позиция короля должна быть не под угрозой.
Константы для проверки условий
Необходимость проверки "под угрозой или нет" склоняет сделать эти проверки в методе getAvailableMoves. Этот метод определён в роительском классе, и мы его перекроем.
Для начала определим константы с разными индексами полей, которые понадобятся при проверках. И добавим поле класса для объекта класса BoardPosition:
engine/figures/king.phpclass King extends Figure {
// индексы полей между королём и ладьёй. Порядок важен! От короля - до ладьи
const FIELDS_CASTLING_WHITE_KING = array(61, 62);
const FIELDS_CASTLING_WHITE_QUEEN = array(59, 58, 57);
const FIELDS_CASTLING_BLACK_KING = array(5, 6);
const FIELDS_CASTLING_BLACK_QUEEN = array(3, 2, 1);
const FIELD_INIT_WHITE = 60; // начальная позиция белого короля
const FIELD_INIT_BLACK = 4; // начальная позиция чёрного короля
const TO_COLUMN_CASTLING_KING = 6; // индекс вертикали на которую встаёт король при короткой рокировке
protected BoardPosition $board_position;
Ещё раз напомню нумерацию полей доски:

Проверка условий рокировки
Напишем отдельный метод для проверки условий рокировки. Будем в него передавать позицию (индекс поля) короля, и массив индексов полей между королём и ладьёй. Причём мы считаем что поля в этом массиве идут по порядку - от короля до ладьи. Не будем здесь проверять валидность этих данных, в данном случае это будет потерей времени. Это метод для "внутреннего испольования". Вызывающий код сам "позаботится" о данных.
Итак, добавляем метод проверки условий рокировки:
engine/figures/king.php// Проверка условий возможности рокировки c поля $init_position, поля до ладьи - $fields_to_rook, порядок полей важен!
protected function checkCastlingConditions(int $init_position, array $fields_to_rook) {
// поля должны быть свободны
foreach ($fields_to_rook as $cell) {
if ($this->game_state->position[$cell] !== FG_NONE) {
return false;
}
}
// поле, на котором стоит король, и поля, которые он пройдёт, не должны быть под атакой
if (
$this->board_position->isFieldUnderAttack($init_position, $this->enemy_color) ||
$this->board_position->isFieldUnderAttack($fields_to_rook[0], $this->enemy_color) ||
$this->board_position->isFieldUnderAttack($fields_to_rook[1], $this->enemy_color)
) {
return false;
}
return true;
}
Получение допустимых ходов - getAvailableMoves
Перекроем родительский метод. Сначала проверим допустимость простых перемещений, потом - рокировок. В родительском методе как проверялась допустимость хода? Запоминалось положение короля, потом на "отдельной доске" (BoardPosition) перемещалась фигура, и проверялось - не находится ли король в той, запомненной, позиции под атакой. Но сейчас мы рассматриваем ходы самого короля. Надо проверять то поле, куда король пойдёт. Так что вызывать перекрываемый родительский метод мы не будем, не так много одинакового кода получается.
engine/figures/king.phppublic function getAvailableMoves() {
$moves = array();
$this->board_position = new BoardPosition();
// проверим "обычные" перемещения
$candidate_moves = $this->getCandidateMoves();
foreach($candidate_moves as $to_index) {
$to_position = $this->game_state->position; // копируем позицию
$to_position[$this->position_index] = FG_NONE; // убираем фигуру из текущей позиции
$to_position[$to_index] = $this->figure;
$this->board_position->setPosition($to_position);
if ($this->board_position->isFieldUnderAttack($to_position, $this->enemy_color)) {
// ход недопустим, т.к. после него наш король оказывается под атакой
continue;
}
$moves[] = $to_index;
}
// теперь проверим возможность рокировок
$this->board_position->setPosition($this->game_state->position);
if ($this->color == COLOR_WHITE) {
if ($this->game_state->enable_castling_white_king && $this->checkCastlingConditions(self::FIELD_INIT_WHITE, self::FIELDS_CASTLING_WHITE_KING)) {
$moves[] = self::FIELDS_CASTLING_WHITE_KING[1];
}
if ($this->game_state->enable_castling_white_queen && $this->checkCastlingConditions(self::FIELD_INIT_WHITE, self::FIELDS_CASTLING_WHITE_QUEEN)) {
$moves[] = self::FIELDS_CASTLING_WHITE_QUEEN[1];
}
} else {
if ($this->game_state->enable_castling_black_king && $this->checkCastlingConditions(self::FIELD_INIT_BLACK, self::FIELDS_CASTLING_BLACK_KING)) {
$moves[] = self::FIELDS_CASTLING_BLACK_KING[1];
}
if ($this->game_state->enable_castling_black_queen && $this->checkCastlingConditions(self::FIELD_INIT_BLACK, self::FIELDS_CASTLING_BLACK_QUEEN)) {
$moves[] = self::FIELDS_CASTLING_BLACK_QUEEN[1];
}
}
return $moves;
}
Код достаточно прокомменирован, должно быть нетрудно его понять. В строках 5 - 17 проверяется допустимость "обычных" перемещений короля по уже знакомой схеме. Берутся "ходы-кандидаты", для каждого строится получившаяся после такого хода позиция, и проверется, не находится ли король под ударом. Если нет, то соответствующий "ход-кандидат" добавлется в список допустимых ходов $moves, который будет возвращён из метода.
В строках 20 - 35 проверяется возможность совершения рокировок. Сначала устанавливаем позицию (строка 20). Потом выясняем цвет фигуры (короля). Для белого короля проверки делаем в строках 22 - 27.
В строке 22 проверяем, можно ли делать рокировку белым королём на королевский фланг (короткая рокировка). В первой части условия проверяем флаг enable_castling_white_king в "состоянии игры". Если флаг запрещает эту рокировку, то дальше и смысла проверять нету.
Во второй части условия вызываем только что написанный метод checkCastlingConditions проверки условий рокировки. В первом параметре надо передать позицию короля. Мы передаём константу self::FIELD_INIT_WHITE, в которой хранится начальная позиция белого короля. Раз мы дошли до этого условия, значит флаг enable_castling_white_king не запрещает рокировку, а значит король ещё не двигался, а значит он находится на начальной позиции.
Во втором параметре передаём массив self::FIELDS_CASTLING_WHITE_KING индексов полей, расположенных между белым королём и королевской ладьёй.
Если условия для короткой рокировки белого короля выполняются, то надо добавить этот ход в результирующий список допустимых ходов. Это мы делаем в строке 23. Берём массив индексов полей между королём и ладьёй, и берём оттуда второй элемент (с индексом 1) - король при рокировке всегда перемещается на две позиции. Именно для этой операции и было важно, чтобы поля в константах FIELDS_CASTLING_XXX располагались в определённом порядке - от короля до ладьи.
Аналогично, в строках 25 - 27 проверяется возможность рокировки белого короля на ферзевый фланг (длинная рокировка).
Ну и в строках 29 - 34 обрабатывается вариант, когда король - чёрный. Аналогично проверется допустимость сначала короткой рокировки, потом - длинной.
Совершение хода - makeMove
Остался один метод - совершение самого хода, makeMove. Для перемещения самого короля возможностей родительского класса хватает. Но надо ещё установить в false флаги возможностей рокировок - ведь после своего хода король теряет возможность рокировки. И надо ещё передвинуть ладью, если ход короля является рокировкой.
Добавляем ещё один метод в класс короля:
engine/figures/king.phppublic function makeMove($to_cell_index, $validate_move=true) {
$col = $this->col;
if (!parent::makeMove($to_cell_index, $validate_move)) {
return false;
}
// король делает ход, пометим что теперь рокировки невозможны
if ($this->color == COLOR_WHITE) {
$this->game_state->enable_castling_white_king = false;
$this->game_state->enable_castling_white_queen = false;
} else {
$this->game_state->enable_castling_black_king = false;
$this->game_state->enable_castling_black_queen = false;
}
$to_col = Functions::positionToCol($to_cell_index);
if (abs($col - $to_col) == 2) {
// вертикаль изменилась на 2, значит это рокировка, надо передвинуть ладью
if ($to_col == self::TO_COLUMN_CASTLING_KING) {
// короткая рокировка (на королевский фланг)
$rook_from_position = Functions::colRowToPositionIndex(BOARD_SIZE-1, $this->row);
$rook_to_position = $to_cell_index - 1;
} else {
// длинная рокировка (на ферзевый фланг)
$rook_from_position = Functions::colRowToPositionIndex(0, $this->row);
$rook_to_position = $to_cell_index + 1;
}
$this->game_state->position[$rook_from_position] = FG_NONE;
$this->game_state->position[$rook_to_position] = FG_ROOK + $this->color;
}
return true;
}
Как обычно, сначала вызываем родительский метод. Потом, в строках 8 - 14 записываем false в нужные флаги возможностей рокировок в зависимости от цвета короля.
В строке 16 вычисляем колонку (вертикаль) на которой оказывается король после хода. В следующей строке проверяем - если вертикаль изменяется на две единицы, значит это рокировка, и в строках 18 - 29 обрабатываем эту ситуацию.
Сначала, в строке 19, проверяем что это за рокировка - короткая или длинная. Если верикаль, на которой оказывается король, равна self::TO_COLUMN_CASTLING_KING (6), значит это короткая рокировка. Дальше получаем индексы двух полей - откуда ладья ушла, и куда должна встать.
На примере ветки кода для короткой рокировки (20 - 22). Где стоит в начале ладья при короткой рокировке? На последней вертикали - с индексом 7. А горизонталь совпадает с горизонталью короля. Имея эти коодинаты, получаем индекс поля "откуда", как обычно, используя Functions::colRowToPositionIndex.
А где окажется ладья после короткой рокировки? Можно конечно опять взять координаты, посчитать по ним индекс. Но можно сообразить - ладья будет слева от короля. Мы всегда рассматриваем доску "с позиции белых", поэтому и чёрная ладья будет "слева" от короля при короткой рокировке. Вспомните, как у нас нумеруются поля. Слева от короля значит просто что индекс этог поля будет на 1 меньше чем у короля. Что мы и использовали в строке 22
Аналогично, в строках 24 - 26 рассчитываем индексы полей "откуда" и "куда" для ладьи при длинной рокировке. Там ладья будет стоять справа от короля, поэтому для получения поля "куда", прибавляем единицу к позиции короля.
Итак, мы получили поля "откуда" и "куда" для ладьи. В строке 28 убираем ладью с поля "откуда", а в строке 29 ставим ладью в поле "куда".
Итоги
В этой статье мы научили двигаться все оставшиеся фигуры - слона, ладью, ферзя, короля. Теперь можно играть всеми фигурами! Правда пока король не боится нападений, и компьютер выбирает ход случайным образом.

Исходные коды этой части на github.
Демо №10 игры - играть можно всеми фигурами.