В этой части научим ходить пешки. У них самая сложная "бизнес - логика" среди шахматных фигур. Ходят вперёд, бьют наискоcок. Плюс взятие "на проходе", плюс превращение…
Для начала чуть-чуть изменим файл engine/figure.php. В методе makeMove есть такое:
// изменяем счётчик числа полуходов, после последнего взятия фигуры или движения пешки
if ($to_figure !== FG_NONE || Functions::figureType($this->figure) == FG_PAWN) {
$this->game_state->non_action_semimove_counter = 0;
} else {
$this->game_state->non_action_semimove_counter++;
}
Обратите внимание на условие, на вторую его часть, после логического "ИЛИ". Я подумал - а зачем в общем классе фигур проверять - не пешка ли это? Можно же будет в классе пешки при совершении хода обнулить счётчик non_action_semimove_counter. В общем, убираем вторую часть условия. Вышеприведённый кусок кода теперь должен выглядеть так:
// изменяем счётчик числа полуходов, после последнего взятия фигуры или движения пешки
if ($to_figure !== FG_NONE) {
$this->game_state->non_action_semimove_counter = 0;
} else {
$this->game_state->non_action_semimove_counter++;
}
Все остальные изменения в этой статье будем делать в классе пешки.
Ходы - кандидаты для пешки
Итак, открываем файл engine/figures/pawn.php. Там у нас пока только пустой метод getCandidateMoves. Делаем его таким:
engine/figures/pawn.phppublic function getCandidateMoves() {
$moves = array();
if ($this->color == COLOR_WHITE) {
$direction = -1;
$first_row = 6;
$last_row = 0;
} else {
$direction = 1;
$first_row = 1;
$last_row = 7;
}
if ($this->row === $last_row) {
throw new Exception('Pawn in last row');
}
// проверим поле перед пешкой
$row = $this->row + $direction;
$to_index = Functions::colRowToPositionIndex($this->col, $row);
$to_figure = $this->game_state->position[$to_index];
if ($to_figure === FG_NONE) {
$moves[] = $to_index;
if ($this->row === $first_row) {
// пешка находится на начальной позиции, проверим ещё одно поле "вперёд"
$row = $row + $direction;
$to_index = Functions::colRowToPositionIndex($this->col, $row);
$to_figure = $this->game_state->position[$to_index];
if ($to_figure === FG_NONE) {
$moves[] = $to_index;
}
}
}
// теперь проверим возможность взятий
$row = $this->row + $direction;
$col = $this->col - 1;
if ($col >= 0) {
$to_index = Functions::colRowToPositionIndex($col, $row);
$to_figure = $this->game_state->position[$to_index];
if (Functions::color($to_figure) === $this->enemy_color || $to_index === $this->game_state->crossed_field) {
$moves[] = $to_index;
}
}
$col = $this->col + 1;
if ($col < BOARD_SIZE) {
$to_index = Functions::colRowToPositionIndex($col, $row);
$to_figure = $this->game_state->position[$to_index];
if (Functions::color($to_figure) === $this->enemy_color || $to_index === $this->game_state->crossed_field) {
$moves[] = $to_index;
}
}
return $moves;
}
Как и для коней, сначала объявляем переменную $moves, для накапливания в ней ходов, пишем туда пустой массив, а в конце метода (строка 49) возвращаем.
В строках 3 - 11, в зависимости от цвета фигуры, заполняем три переменные. $direction - "направление", в котором изменяется индекс ряда (горизонтали) при движении пешки. Напомню, как у нас кодируются поля доски:

Чёрные фигуры в начале партии стоят в ряду #0, пешки - в ряду #1. И при их движении, номер ряда увеличивается, $direction = 1. Белые пешки стоят в ряду #6, и номер ряда при их движении уменьшается, $direction = -1.
В переменной $first_row записали индекс начального ряда, на котором стоят пешки. В $last_row - индекс последнего ряда (горизонтали), на которой пешка превращается в другую фигуру.
Строки 12 - 14 - проверяем, если вдруг пешка стоит на последней горизонтали, то возбуждаем исключение с фразой 'Pawn in last row'.
Ходы вперёд
Сначала проверяем, может ли пешка пойти вперёд на одно поле. В строке 16 вычисляем горизонталь поля перед пешкой - к текущей горизонтали прибавляем "направление". В строке 17 по колонке (вертикали) на которой стоит пешка, и по горизонтали "перед" пешкой, вычисляем индекс поля, которое находится перед пешкой ($to_index). На следующей строке получаем фигуру, которая находится на поле перед пешкой.
В строке 19 проверяем - если "фигура" перед пешкой - "пустое поле" (FG_NONE), значит на это поле можно пойти, и в строке 20 мы добавляем индекс поля перед пешкой ($to_index) в список ходов - кандидатов $moves.
Тут-же, находясь "под условием" что пешка может пойти на поле вперёд, проверяем (в строке 21), находится ли пешка на своей начальной позиции. Если да, то она может прыгнуть и на два поля вперёд. Надо проверить ещё одно поле перед проверенным. Этим и занимаемся в блоке кода в строках 22 - 28.
В этом блоке всё аналогично коду выше. К индексу горизонтали прибавляем ещё одно "направление" ($direction). Опять вычисляем индекс поля, берём фигуру,которая находится на этом поле, и если это "пустое поле", то пешка может пойти и туда - добавляем индекс этого поля в массив ходов - кандидатов (строка 27).
Как видите, я не боюсь небольших "копипастов". Две строки полностью повторяются (17, 18 и 24, 25), а одна строка перед ними - очень "похожа". Можно было организовать код и без "копипаста", но мне нравится именно так. Так нагляднее. Нет потерь на лишние проверки при организации кода без копипаста.
Взятия
Теперь проверим возможность "взятия". В строке 32 получаем опять, так-же как и ранее, индекс горизонтали перед пешкой. Сначала проверим, может ли пешка "срубить" что-то слева (перед собой и слева). В строке 33 уменьшаем индекс вертикали. В следующей строке проверяем - не вышли ли мы за край доски.
Если нет, то в строках 35, 36 опять, по привычной схеме, получаем индекс поля, которое "бьёт" пешка, и фигуру, которая стоит на этом поле. А вот дальше, на строке 37 интересное условие того, может ли пешка ходить на поле $to_index.
В условии - два логических выражения, объединённых операцией ИЛИ. В первом выражении мы берём цвет фигуры на "битом поле", и сравниваем с цветом фигур оппонента, $this->enemy_color. Если в поле своя фигура - условие не выполнится. Если поле пустое, то тоже условие не выполнится. Вспомните кодировку цвета, и белый и чёрный цвет - не нули. А FG_NONE==0 и метод получения цвета от FG_NONE вернёт 0.
Вторая часть условия - сравниваем на равенство индекс "битого поля" и "пересечённого поля" ($this->game_state->crossed_field). Это поле в "состоянии игры" было специально заведено для обслуживания правила "взятия на проходе". В нём в начале партии, и после каждого хода записывается null … кроме случая, когда пешка ходит на два поля вперёд. После такого хода, индекс поля, которое пешка перепрыгнула, будем записывать как раз в поле crossed_field состояния игры. И уже после следующего хода это поле будет перезаписано. Мы его уже перезаписываем! Посмотрите - в классе фигуры Figure в методе makeMove есть строка
$this->game_state->crossed_field = null;
Вернёмся к условию возможности взятия. Итак, во второй части условия проверяется возможность взятия "на проходе". В итоге, если на "битом поле" (впереди и слева) будет фигура цвета фигур оппонента, или если битое поле - это "пересечённое поле", то пешка может пойти на это поле. Записываем это поле в массив ходов - кандидатов (строка 38).
Мы проверили поле "наискосок" - впереди и слева от пешки. Теперь аналогично проверяем поле впереди и справа - см. код в строках 41 - 48. Как видите, опять, я не боюсь "копипаста" (в разумных пределах). Разница тут только в проверке выхода за пределы доски (строка 42).
Всё! Ходы - кандидаты получены. Надо ещё реализовать метод получения допустимых ходов и метод, делающий ход.
Получение допустимых ходов
Напомню, что мы называем "ходами - кандидатами" и "допустимыми ходами". Ходы - кандидаты содержат поля, куда фигура (пешка в данном случае) может переместиться по правилам своего хода. Но после такого перемещения может образоваться недопустимая позиция, например свой король оказался (или остался) под атакой. Метод получения допустимых ходов берёт ходы - кандидаты, для каждого хода строит позицию, получившуюся после хода, и проверяет её на допустимость.
У нас уже написан метод получения допустимых ходов - getAvailableMoves в классе Figure, от которого наследуются классы конкреных фигур. Этот метод рабочий и использовался нами при ходе коня. Он-же будет использоваться и при ходе слона, ладьи, ферзя. Там логика перемещения простая ушёл с одного поля, встал на другое. А вот с пешкой сложнее. При взятии на проходе она уходит с одного поля, встаёт на другое, а чужая пешка при этом исчезает с третьего поля.
Логику пешки конечно можно было реализовать и в общем классе фигуры, добавить проверки - если это пешка и … Но зачем лишние проверки. Они будут занимать дополнительное время, хотя и малое, но это всё будет выполнятся очень - очень много раз при поиске хода копмпьютером. Перекроем метод в классе пешки. Добавляем в engine/figure/pawn.php:
engine/figures/pawn.phppublic function getAvailableMoves() {
$moves = array();
$board_position = new BoardPosition();
$our_king_positions = $this->game_state->figures[FG_KING + $this->color];
if (count($our_king_positions) === 0) {
return $moves;
}
$our_king_position = $our_king_positions[0];
$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; // перемещаем фигуру на выбранное поле. Если на том поле что-то стояло - оно "убирается"
if ($to_index === $this->game_state->crossed_field) {
// это "взятие на проходе". Надо убрать с позиции проскочившую пешку.
$beat_col = Functions::positionToCol($to_index);
$beat_cell_index = Functions::colRowToPositionIndex($beat_col, $this->row);
$to_position[$beat_cell_index] = FG_NONE;
}
$board_position->setPosition($to_position);
if ($board_position->isFieldUnderAttack($our_king_position, $this->enemy_color)) {
// ход недопустим, т.к. после него наш король оказывается под атакой
continue;
}
$moves[] = $to_index;
}
return $moves;
}
Опять секретный приём индусских программистов - "копи-паст" . Метод почти такой-же, как в родительском классе. Разница - в строках 14 - 19. В них проверяем - если поле "куда" (куда встаёт пешка) равно "пересечённому" полю ($this->game_state->crossed_field), то рассматриваемое перемещение - это взятие "на проходе". Ранее в коде (строки 12, 13) мы уже убрали пешку с поля "откуда", переместили в поле "куда". А теперь надо убрать с доски пешку, которую мы взяли этим ходом. Где она находится?

Она находится на той-же вертикали, что и "пересечённое поле". На картинке красная стрелка показывает на "пересечённое поле". В строке 16 мы получаем вертикаль (колонку) "проскочившей" пешки. А горизонталь совпадает с горизонталью начальной позиции нашей (белой на картинке) пешки. В строке 17 получем индекс поля, на которой находится проскочившая пешка. И в строке 18 мы убираем эту пешку из позиции $to_position.
Остальной код - такой-же, как в перекрытом методе родительского класса.
Делаем ход
Метод makeMove делает ход, т.е. реальное изменение состояния игры. Он тоже уже есть в родительском классе. И тоже он полностью подходит для хода коня, слона, ладьи и ферзя. А вот для пешки его тоже (как и метод получения допустимых ходов) надо перекрыть. В данном случае не будет копи-паста. Существующий родительский метод нам вполне подходит, надо только подкорректировать "пару деталей".
Помимо взятия "на проходе", нам надо ещё учесть превращение пешки при достижении последней горизонтали. При генерации ходов-кандидатов и получении допусимых ходов, было неважно во что превратится пешка. От этого не зависела валидность (допустимость) полученной позиции. А вот при совершении хода очень даже важно - в какую фигуру превратится пешка.
Пока не будем реализовывать в интерфейсе выбор фигуры для превращения. Пока будем считать что пешка по умолчанию превращаетсяя в ферзя. Добавим к параметрам метода makeMove третий параметр, в котором будем указывать фигуру, в которую нужно превратиться при достижении последней горизонтали. Добавим значение по умолчанию - ферзь (FG_QUEEN). Итак, добавляем ещё один метод в класс пешки:
engine/figures/pawn.phppublic function makeMove($to_cell_index, $validate_move=true, $to_figure=FG_QUEEN) {
$row = $this->row;
$crossed_field = $this->game_state->crossed_field;
if (!parent::makeMove($to_cell_index, $validate_move)) {
return false;
}
$this->game_state->non_action_semimove_counter = 0;
$direction = $this->color === COLOR_WHITE ? -1 : 1;
$to_row = Functions::positionToRow($to_cell_index);
if ($row + 2*$direction == $to_row) {
// пешка перескочила поле, пометим его как проходное, т.е. доступное для взятия пешкой оппонента
$this->game_state->crossed_field = Functions::colRowToPositionIndex($this->col, $row + $direction);
} elseif ($crossed_field == $to_cell_index) {
// этот ход - взятие "на проходе", надо убрать взятую пешку
$beat_col = Functions::positionToCol($to_cell_index);
$beat_cell_index = Functions::colRowToPositionIndex($beat_col, $row);
$this->game_state->position[$beat_cell_index] = FG_NONE;
} elseif (($direction == 1 && $to_row == BOARD_SIZE-1) || ($direction == -1 && $to_row == 0)) {
// пешка достигла последней горизонтали - она превращается в другую фигуру
$to_figure = Functions::figureType($to_figure) + $this->color;
$this->game_state->position[$to_cell_index] = $to_figure;
}
return true;
}
Перед вызовом родительского метода, сохраним в локальных переменных то, что нам понадобится, и что метод родительского класса изменяет (может изменить). В строке 2 сохраняем индекс горизонтаи, на которой находится пешка перед ходом, и в строке 3 - "пересекаемое поле".
В строке 4 вызываем родительский метод, и в случае неудачи (метод вернул false), возвращаем false и из перекрытого метода. В родительском методе уже проделана основная работа по совешению хода, и нам остаётся только учесть особенности пешки.
В строке 7 обнуляем счётчик "non-action полу-ходов", т.е. счётчик полуходов, за которые не было взятия фигуры или движения пешек. Напомню, этот счётчик нужен для проверки выполнения условий правил 50 и 75 ходов. Так как в методе мы делаем ход пешкой, то счётчик надо обнулить.
Далее, в строке 8 вычисляем "направление" изменения горизонтали, так-же как и в методе getCandidateMoves. В строке 9 получаем индекс горизонтали, на которой окажется пешка после своего хода - $to_row.
Дальше у нас идёт цепочка if - elseif для обработки трёх взаимоисключающих особых случая для хода пешки. В строке 10 проверяем - если "горизональ назначения" $to_row отстоит от начальной позиции пешки на два шага "направления", т.е. если пешка шагнула на два поля вперёд, то надо записать "пересечённое" (перепрыгнутое) поле в состояние игры по ключу crossed_field. Это делается в сроке 12. Теперь на следующем полу-ходе будет информация о пересечённом поле, и пешку можно будет взять "на проходе".
Следующая проверка - на строке 13. Там мы проверяем, если поле назначения пешки совпадает с "пересечённым полем", значит пешка совершает "взятие на проходе". Обратите внимание - к этому моменту "пересечённое поле" в "состоянии игры" (в $this->game_state) уже обнулено, точнее, там записан null. Это сделано было в родительском методе, который мы вызывали в начале. А сравнение "поля назначения" происходит со значением, хранящимся в локальной переменной $crossed_field, в которую мы сохранили значение из состояния игры перед вызовом родительского метода.
Итак, если наш ход - это взятие на проходе, то надо убрать фигуру (пешку), которая перепрыгнула поле на предыдущем ходу. Это делается в строках 14 - 17. Делаем так-же, как и в методе getAvailableMoves - вычисляем положение перепрыгнувшей фигуры, и в её позицию записываем FG_NONE.
И последняя проверка "особых случаев" - в строке 18. Там мы проверяем, что пешка достигла "последней горизонтали". И, если достигла, то надо превратитьеё в фигуру, переданную во входном параметре $to_figure. А туда может прийти как фигура "без цвета", (например значение по умолчанию и есть "ферзь без цвета" - FG_QUEEN), так и с флагами цвета. Я хочу чтобы метод мог принимать оба этих варианта. Поэтому в строке 20 я сначала вычисляю "тип фигуры", а потом прибавляю цвет, записанный в поле класса.
Ну и в строке 21 записываем в "поле назначение" получившийся код фигуры - пешка "превращена". Всё.
Итоги
Ура! Самая сложная фигура описана. Можно уже играть с компьютером не только конями, но и пешками. Правда пока короли не боятся атак, и их можно даже съесть. Но терпение, не так далеко реализация всех правил шахмат. На скриншоте ниже - пример игры. Там я провёл свою пешку в ферзи, и дал компьютеру провести свою пешку.

Исходные коды этой части на github.
Демо №9 игры - игра пешек и коней.