Шахматы на php и javascript: Компьютер отвечает. Битва коней

Случайный искусственный интеллект

Научим теперь компьютер отвечать нам. Создадим класс для выбора хода компьютером. Своего рода "искуственный интеллект". Сначала сделаем просто случайный выбор из массива всех доступных ходов. Как понимаете, заголовок про случайный ИИ - это типа шутка.

В папке engine создадим файл computer_ai.php:

engine/computer_ai.php<?php

class ComputerAI {
    protected $game_state;

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

    public function getGenerateMove() {
        $move_generator = new MoveGenerator($this->game_state);
        $available_moves = $move_generator->generateAllMoves();
        if (count($available_moves) === 0) {
            return false;
        }
        $cell_from = array_rand($available_moves);
        $figure_moves = $available_moves[$cell_from];
        $cell_to = $figure_moves[array_rand($figure_moves)];
        return array('from' => $cell_from, 'to' => $cell_to);
    }
}

В файле создаём новый класс - ComputerAI. "AI" - это аббревиатура от "Artificial Intelligence" (искуственный интеллект). Как обычно, в классе создаём поле $game_state для состояния игры, а в конструкторе класса заполняем это поле.

Метод для генерации хода компьютера назвал getGenerateMove. В строках 11, 12 получаем все возможные ходы из текущей позиции. Точно так-же мы делали в методе generateAvailableMoves класса ChessGame. Если доступных ходов вообще нет, то возвращаем из метода false (строки 13 - 15).

В строке 16 мы берём случайное поле "откуда". Напомню, как выглядит структура данных, хранящих допустимые ходы. Это ассоциативный массив, где ключи это индексы полей, откуда возможны ходы. А значения - это массивы с индексами полей, куда можно переместиться. Php-функция array_rand возвращает случайный ключ из переданного массива. Т.е., в нашем случае - случайное поле "откуда", точнее индекс поля, откуда возможны ходы. И это выбранное поле "откуда" мы сохраняем в переменой $cell_from.

В строке 17 мы в переменную $figure_moves записываем все возможные перемещения из выбранного поля "откуда". И в следующей строке, опять с помощью функции array_rand берём случайное поле "куда" из массива $figure_moves.

В 19-ой строке возвращаем выбранный ход - ассоциативный массив с ключами "откуда", и "куда".

Идём теперь в engine/chess_game.php и там, где мы подключаем разные файлы, добавляем подключение нового:

require_once('computer_ai.php');

Отправка запроса "компьютер, делай ход"

Возвратимся ненадолго в наш фронтенд - файл chess.js. Помните, у нас там есть (пустая пока) функция sendWaitComputerMove. Она нужна чтобы послать запрос к бекенду - "компьютер, делай ход". Эта функция у нас вызывается в двух местах:

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

Итак, в файле chess.js идём в функцию sendMoveToServer, и в callback-функции, после строки

setGameState(response);
добавляем:

if (response && response.is_human_move == false) {
    sendWaitComputerMove();
}

Ну и напишем наконец тело функции sendWaitComputerMove. Делаем её такой:

function sendWaitComputerMove() {
    createRequest({
        method: 'POST',
        url: 'make_computer_move.php',
        callback: function(response) {
            setGameState(response);
        }
    });
}

Всё просто: отсылаем на сервер запрос /make_computer_move.php (этот скрипт сейчас напишем). И при приходе ответа, передаём его в функцию setGameState - как и в других местах, где обрабатывается ответ сервера.

Скрипт make_computer_move.php

В веб-корне проекта созадём файл make_computer_move.php:

make_computer_move.php<?php
require_once('engine/chess_game.php');
$game = new ChessGame();
$game->loadGame();
$game->makeComputerMove();
$data = $game->getClientJsonGameState();
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_UNICODE);

Здесь всё, кроме одной строки, уже должно быть знакомо. Подключаем файл с основным классом игры. Создаём экземпляр класса игры, загружаем состояние игры. Дальше, на строке 5, новенькое - вызываем метод makeComputerMove на объекте игры. Этот метод напишем чуть ниже. Ну а дальше - опять знакомаясхема - получаем состояние игры для клиента (фронтенда) и отдаём его в виде JSON-строки.

Метод makeComputerMove класса игры

Идём в класс ChessGame (файл engine/chess_game.php) и дописываем новый метод в конец класса:

public function makeComputerMove() {
    $ai = new ComputerAI($this->game_state);
    $move = $ai->getGenerateMove();
    if (!$move) {
        $this->game_state->text_state = 'Компьютер не смог найти ход';
        return;
    }
    if (!isset($move['from']) || !isset($move['to'])) {
        $this->game_state->text_state = 'Ошибка! Компьютер возвратил ход в неверном формате';
        return;
    }
    $this->makeMove($move['from'], $move['to'], false);
}

И здесь тоже всё выглядит просто. В начале этой статьи мы написали класс ComputerAI который "играет за компьютер". В новом методе мы создаём экземпляр этого класса, и в строке 3 получаем "ход компьютера".

Дальше мы проверяем - удалось ли получить этот ход, и потом - а соответствует ли результат ожидаемому формату, т.е. если ли в полученном результате поля "откуда" и "куда" (ключи from, to). Если какая-то из проверок не прошла, устанавливаем соответствующее текстовое сообщение в поле text_state состояния игры, и возвращаем false.

Если проверки успешны, то вызываем метод makeMove. Мы уже пользовались этим методом, кода делали ход человека. Мы туда передавали индексы полей "откуда" и "куда". Но сейчас мы после этих параметров передаём ещё и третий параметр - false. Что это и зачем? В определении метода нет третьего параметра! Сейчас объясню.

Убираем лишние проверки

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

Но сейчас мы вызываем makeMove, передавая поля "откуда" и "куда", полученные от нашего-же кода. Эти данные точно должны быть верны, и проверки в начале метода makeMove не нужны. И я решил добавить третий параметр, который бы говорил - нужно ли делать начальные проверки или нет. Итак, в классе ChessGame идём в метод makeMove и добавляем третий параметр - $validate_move. И указываем ему значение по умолчанию = true, чтобы не нужно было изменять предыдущие места вызовов.

Начальные проверки вносим под условие if ($validate_move). И … в этом методе мы вызываем метод makeMove и на объекте фигуры. А в классе Figure в этом методе мы тоже делаем проверки - генерируем все возможные ходы фигуры, и проверяем, есть ли переданный индекс поля "куда" в списке возможных ходов. При ответе компьютера это тоже лишне действия. Так что в вызов метода makeMove тоже добавим параметр $validate_move. В итоге метод makeMove класса ChessGame должен выглядеть так:

engine/chess_game.phppublic function makeMove($cell_index_from, $cell_index_to, $validate_move=true) {
    if ($validate_move) {
        $color = $this->game_state->current_player_color;
        $figure_code = $this->game_state->position[$cell_index_from];
        if ($figure_code === FG_NONE || Functions::color($figure_code) != $color) {
            $this->game_state->text_state = 'Недопустимый ход: Вы можете ходить только фигурами своего цвета';
            return;
        }
    }
    $figure_factory = new FigureFactory();
    $figure = $figure_factory->create($this->game_state, $cell_index_from);
    if ($figure->makeMove($cell_index_to, $validate_move)) {
        $this->storage->saveGameState($this->game_state);
    }
}

Теперь идём в метод makeMove класса Figure. Добавляем параметр $validate_move=true, и вносим проверку на корректность хода под условие. В итоге, начало кода метода должно выглядеть так:

engine/figure.phppublic function makeMove($to_cell_index, $validate_move=true) {
    // проверяем - а корректный ли ход нам передали?
    $this->game_state->setFigures();
    if ($validate_move) {
        $available_moves = $this->getAvailableMoves();
        if (!in_array($to_cell_index, $available_moves)) {
            $this->game_state->text_state = 'Недопустимый ход. Сделайте корректный ход';
            return false;
        }
    }

    $to_figure = $this->game_state->position[$to_cell_index];

    …

Если честно, можно было и не убирать эти "лишние проверки". Они выполняются не в цикле, так что мы бы и не заметили их лишней работы.

А если съели короля?

К этому моменту компьютер уже отвечает, делает ходы. Уже можно играть с ним и за белых, и за чёрных. Есть только несколько "но". Ходить можно только конями. И можно съесть хоть короля. И вот если вы его съедите, то компьютер вдруг перестанет отвечать. Даже не напишет в строке состояния "Компьютер не смог найти ход". Когда вы съедите его коней, то он так и напишет. А вот если короля …

Причина - в классе фигуры, в методе getAvailableMoves. Там есть строка, в которой мы получаем положение короля:

$our_king_position = $this->game_state->figures[FG_KING + $this->color][0];

Т.е. берём массив положений королей, и берём его первый (нулевой) элемент. А когда короля нет, то в $this->game_state->figures[FG_KING + $this->color] будет не массив, а null. Берём "нулевой элемент" null и получаем ошибку.

Чтобы не было такого досадного недоразумения, и кони продолжали скакать и без короля (подумаешь - король! У нас демократия), добавим проверку, и вместо указанной строки напишем такое:

$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];

Итоги - в джазе только кони

Изменения в этой статье получились небольшими по коду, но существенными по "визуальному эффекту" - теперь можно играть с компьютером, и он будет отвечать. Правда ходить можно пока что только конями. На скриншоте ниже, компьютер, играя за белых, на 15-ом ходу съел моего слона:

Демо игры #8. Ходят только кони

Исходные коды этой части на github.

Демо №8 игры - битва коней.