Чуть-чуть рефакторинга: превращаем змей в верблюдов
Посмотрел свежим взглядом на наш main.js и мысленно "схватился за голову". Имена некоторых функций записаны в "верблюжьей" нотации" (например initGame), некоторых - в "змеиной" (set_figure_to_cell). Никогда так не делайте! Разный стиль именования ухудшает читаемость кода, и может привести к путанице. Поддерживать такой код тяжелее, чем код следующий каким-то стандартам.
Будем придерживаться "верблюжей" нотации (CamelCase). Проходим файлу main.js, и заменяем определения функций и места их вызова на "правильные варианты". Т.е. делаем такие замены:
set_figure_to_cell | → | setFigureToCell |
deselect_cell | → | deselectCell |
select_cell | → | selectCell |
deselect_prev_move_cells | → | deselectPrevMoveCells |
make_move | → | makeMove |
send_move_to_server | → | sendMoveToServer |
set_game_state | → | setGameState |
И ещё одна мелочь. Игрался с полями, доступными для перемещения (available_moves_for_selected_cell), делал возможным взятие фигуры. Заметил, что зелёное выделение на чёрной фигуре смотрится как-то не очень заметно. Решил чуть увеличить непрозрачность c 0.5 до 0.8 в css-правиле для .available_for_move::after:
background: radial-gradient(circle, rgba(0, 153, 17, 0.8) 19%, rgba(0, 0, 0, 0) 20%);
Ещё заметил, что при перемещении фигуры на занятое поле (т.е. при взятии фигуры), на этом поле остаётся старая фигура. Получается, что на поле доски оказываются сразу две фигуры, и вторая, которая сюда пришла, просто скрывается за границей поля, и не видна. Чтобы это исправить, идём в функцию makeMove, и перед вставкой фигуры, добавляем одной строчкой очистку содержимого поля:
if (figure) {
cell_to.textContent = ''; // Вот эту строку надо вставить для очистки поля
cell_to.appendChild(figure);
}
Надо запоминать состояние игры.
Какая у нас схема работы игры? Человек делает ход. На сервер отсылается запрос: "Эй, компьютер, я сделал ход, твоя очередь!". PHP принимает запрос, и теперь ему надо ответить, сделать свой ход. Но как PHP поймёт, какая сейчас позиция? Мы же не шлём ему всё состояние игры. Да и от кого вообще пришёл запрос? Может сейчас играет 10 человек, от кого из них пришло сообщение?
Общение происходит по протоколу http(s), а этот протокол не сохраняет состояние. Нам надо сделать сохранение состояния игры! И тогда мы сможем закрыть страницу в любой момент, а когда вернёмся, сможем продолжить партию.
И надо менять схему работы.
Сейчас при загрузке страницы создаётся доска, и на ней сразу расставляются фигуры в начальном положении. И человек должен играть за белых. Сейчас мы изменим это поведение!
При загрузке страницы будем рисовать пустую доску. Потом будем слать запрос на сторону PHP: "Эй, сервер, дай-ка нам текущее состояние игры!". Получаем состояние, отрисовываем его и даём человеку возможность сделать ход. В самом начале, если мы ещё не играли, на стороне сервера не будет записано состояние игры, ведь её ещё не было. В этом случае доска останется пустой.
Но нам ведь надо как-то начать новую игру! Более того, хочется иметь возможность в любой момент начать новую игру, прервав текущую. И иногда хочется поиграть и за чёрных.
Добавляем кнопки начала игры
Сделаем две отдельные кнопки для начала новой игры - кнопка для игры за белых, икнопка для игры за чёрных. Ещё добавим место для отображения текущего текстового статуса игры. Открываем файл index.html, и сразу после открывающего тега body добавляем:
index.html<div class="control">
Начать новую игру:
<a href="#" class="new_game white">за белых</a>
<a href="#" class="new_game black">за чёрных</a>
</div>
<div class="game_status">
Статус игры: <span class="status">Игра не начата</span>
</div>
Добавим css-правила для наших новых элементов, дописав их в конец chess.css:
.control, .game_status {
border-bottom: 1px solid #000;
padding: 10px 0 16px;
margin-bottom: 20px;
}
.control a.new_game {
display: inline-block;
color: #000;
border: 1px solid #06f;
border-radius: 6px;
padding: 4px;
margin: 0 4px;
text-decoration: none;
font-weight: bold;
}
.game_status .status {
font-weight: bold;
font-size: 1.2em;
}
После этого, должнен получиться такой верх страницы:

Меняем инициализацию работы в javascript-коде
Обработчик для кнопок создания игры
Открываем файл chess.js. Удаляем функцию initPosition. Она нам уже не нужна. Теперь инициализацией позиции будет заниматься бекенд на php. Идём в функцию initGame. Удаляем в ней вызовы функций initPosition, showPosition. Вместо них вызываем пока не написанную функцию initButtons. Она назначит обработчики кликов по кнопкам начала игры.
initButtons();
На самом деле это не кнопки, а ссылки, просто мы сделали им внешний вид, как у кнопок.
Пишем новую функцию ниже, например после функции createBoard. Не принципиально, просто так у вас расположение функций будет как у меня, и возможно это облегчит дальнейшее понимание, если вы следуете за мной при написании кода. Итак, пишем функцию:
chess.jsfunction initButtons() {
// берём кнопку начала новой игры "за белых", назначаем обработчик
button = document.querySelector('.control .new_game.white');
button.addEventListener('click', (event) => createNewGame(event, 'w'));
// теперь аналогично для кнопки "за чёрных"
button = document.querySelector('.control .new_game.black');
button.addEventListener('click', (event) => createNewGame(event, 'b'));
}
Т.е. мы на наши "кнопки" начала игры назначили слушателей событий клика. При клике на кнопках начала игры будет вызываться функция createNewGame. Конечно её ещё нет, мы, как обычно, вызываем функцию, которую ещё только собираемся написать. В эту функцию передаём два параметра. Первый - само событие клика. Второй параметр - символ "w" или "b", который говорит, за какой цвет мы хотим играть - за белых (w - wtite) или за чёрных (b - black).
Функция создания новой игры на стороне javascript
Ну и сразу ниже пишем и эту новую функцию:
chess.jsfunction createNewGame(event, human_color) {
event.preventDefault();
if (selected_cell_index !== null) {
deselectCell(selected_cell_index);
selected_cell_index = null;
available_moves_for_selected_cell = [];
}
createRequest({
method: 'POST',
url: 'create_new_game.php',
data: { human_color: human_color },
callback: function(response) {
setGameState(response);
setBoardOriented(human_color);
if (human_color == 'b') {
sendWaitComputerMove();
}
}
});
}
Сначала мы отменяем действия браузера по умолчанию, связанные с кликом:
event.preventDefault();
Т.е. мы не хотим чтобы при клике произошёл переход по ссылке, мы хотим просто выполнить свой код.
Потом проверяем, если у нас есть выделенное поле, то снимаем выделение с него, указываем что теперь нет выделенного поля, и нет полей, доступных для перемещения (строки 3 - 7). Странно ведь будет, если мы создадим новую игру, а у нас есть какое-то выделенное поле, и стоят метки на полях, куда как будто-бы можно с него пойти.
Потом мы создаём POST-запрос на адрес текущий_домен/create_new_game.php. Да, опять используем то, что ещё не написали, и ниже ещё будут идти подряд вызовы ненаписанных функций. Но не волнуйтесь, мы их очень скоро прямо здесь и напишем.
В POST-запросе передаём один параметр - human_color - цвет фигур, за который будем играть.
Указываем (строки 13 - 19) функцию, которая будет обрабатывать ответ сервера. Сначала устанавливаем состояние игры:
setGameState(response);
Потом устанавливаем ориентацию доски (помните - мы иногда хотим играть за чёрных?):
setBoardOriented(human_color);
Потом, если мы хотим играть за чёрных, вызываем функцию, которая будет отсылать запрос на сервер - "эй, компьютер, твой ход, ходи давай!":
if (human_color == 'b') {
sendWaitComputerMove();
}
Есть другой вариант сценария действий, когда мы начинаем игру за чёрных. Мы так-же отсылаем на бекенд запрос на создание новой игры (create_new_game.php). Но на бекенде можно не только создать новую игру, но и, если компьютер играетза белых, сразу сделать ход. Но я решил разделить создание новой игры и первый ход компьютера. Почему? Компьютер может долго "думать", и мы будем смотреть в браузер, и не понимать почему ничего не меняется - компьютер рассчитывает свой ход, или у нас всё зависло, может запрос не ушёл.
А мы сначала создаём игру, отображаем позицию на доске, пишем статус игры, и потом уже ждём ход компьютера. Так по моему выглядит логичнее и "приятнее" в плане отзывчивости интерфейса.
Новая инициализация приложения
Чуть раньше в функции initGame мы убрали всё, кроме вызова функции содания доски, и добавили вызов новой функции "инициализация кнопок". С таким кодом, при открытии страницы, мы увидим пустую доску без фигур, и две кнопки над доской для начала новой игры.
Но нам надо продолжить игру, если она была. Представьте - вы сделали пару ходов, обновили страницу, и перед нами снова пустая доска, и надо начинать новую игру. Или вы просто хотите продолжить игру, которую начали вчера. Что делать? Надо при инициализации приложения запрашивать с бекенда состояние игры! А сохранять состояние на бекенде мы научимся совсем скоро, в этой статье.
Идём в функцию initGame, дописываем в конец запрос к бекенду "дай-ка мне текущее состояние игры", и обработчик ответа. Привожу полностью получившуюся функцию:
chess.jsfunction initGame() {
createBoard();
initButtons();
createRequest({
method: 'GET',
url: 'get_game_state.php',
callback: function(response) {
if (response) {
setGameState(response);
setBoardOriented(response.human_color);
if (!response.is_human_move) {
sendWaitComputerMove();
}
}
}
});
}
Т.е., что мы делаем при инициализации приложения? Создаём пустую доску. Инициализируем работу с кнопками. Шлём запрос по адресу текущий_домен/get_game_state.php. Обработчик ответа сервера (строки 14 - 18 почти дословно повторяет обработчик, который ми писали для запроса создании новой игры.
Разница - только в условии "а наш - ли сейчас ход?" (строка 11). При создании новой игры, мы сразу знали чей ход. Т.к. в шахматах первые ходят белые, то если мы играли за белых, то был наш ход, иначе - ход компьютера. А здесь, при загрузке сохранённого состояния игры, очередь хода мы определяем прямо из ответа сервера, из поля is_human_move. Когда займёмся php-кодом, мы это поле добавим.
Изменение функции отображения состояния
Изменим теперь функцию setGameState. Она принимает на вход game_state. Но теперь этот параметр может быть пустой, когда в начале работы приложения мы запрашиваем состояние игры, а игры никакой ещё нет. Так что в самом начале функции добавляем выход из функции, если переменная с состоянием игры пустая:
if (!game_state) {
return;
}
И в конце функции, удаляем этот кусок кода:
// передаём очередь хода человеку
is_our_move = game_state.is_our_move;
Вместо него пишем:
// показываем статус игры
setGameStatus(game_state.text_state);
// устанавливаем очередь хода
is_our_move = game_state.is_human_move;
Для определения, чья очередь хода теперь используем другое поле из структуры game_state. Решил переименовать его, потому что на стороне бекенда название is_our_move может сбить с толку. И для передачи текстового описания состояния игры, на стороне бекенда мы добавим поле text_state.
Функция setGameStatus будет просто устанавливать переданный ей текст в строку состояния, которую мы добавили в интерфейс:
function setGameStatus(text_state) {
document.querySelector('.game_status .status').textContent = text_state;
}
Изменение ориентации доски
Мы уже два раза использовали функцию для установки ориентации доски - setBoardOriented. Напишем её уже наконец:
function setBoardOriented(human_color) {
let board = document.querySelector('.board');
if (human_color == 'b') {
board.classList.add('reverse');
} else {
board.classList.remove('reverse');
}
}
Всё просто! Берём элемент, который представляет шахматную доску. Если человек играет за чёрных, то добавляем к "доске" css-класс reverse. Иначе - убираем его.
Что-же делает этот css-класс? Открываем файл chess.css и после класса .board добавляем:
.board.reverse {
flex-wrap: wrap-reverse;
flex-direction: row-reverse;
}
row-reverse меняет направление главной оси, она теперь идёт горизонтально справа налево. А wrap-reverse влияет на порядок рядов при переносе клеток - первый ряд будет в конце поперечной оси, а последний - в начале. В результате действий этих двух правил, доска расположится именно так, как нужно нам при игре за чёрных. И даже в вёрстке не пришлось ничего менять.
Ну и "для порядка", добавим к классу .board правило:
flex-direction: row
В принципе, делать это не обязательно, "row" является значением по умолчанию для свойства flex-direction. Но я предпочитаю здесь явно указать значение,
от которого зависит работа приложения.
В итоге, мы "крутим доску" переключая значения css-свойств flex-wrap, flex-direction
Ожидание хода компьютера
У нас на стороне javascript осталась одна не определённая функция - sendWaitComputerMove. Она нужна для отсылки сообщения: "компьютер, твой ход!" при начале новой партии за чёрных. Или при загрузке игры, которая прервалась на ходе компьютера. Пока напишем "заглушку" - определим эту функцию с пустым телом. Добавим в самый конец chess.js:
function sendWaitComputerMove() {
}
Идём в бекенд, определяем константы
Пора идти на бекенд, написать те скрипты, которые мы стали вызывать с фронтенда, ну и реализовать собственно сохранение состояния игры. Сначала определим константы, которые нам понадобятся. Создадим новый файл constants.php в папке engine:
constants.php<?php
define('COLOR_WHITE', 'w');
define('COLOR_BLACK', 'b');
define('FG_NONE', 0);
define('FG_KING_WHITE', 1);
define('FG_KING_BLACK', 2);
define('FG_QUEEN_WHITE', 3);
define('FG_QUEEN_BLACK', 4);
define('FG_ROOK_WHITE', 5);
define('FG_ROOK_BLACK', 6);
define('FG_BISHOP_WHITE', 7);
define('FG_BISHOP_BLACK', 8);
define('FG_KNIGHT_WHITE', 9);
define('FG_KNIGHT_BLACK', 10);
define('FG_PAWN_WHITE', 11);
define('FG_PAWN_BLACK', 12);
Тут всё понятно по именам констант. Определяем цвета фигур, потом определяем типы фигур. Префикс "FG_" означает "Fugure". Значения типов фигур соответствуют ключам хеша FIGURES из файла chess.js. Только здесь мы добавили ещё "фиктивную" фигуру - FG_NONE, означающую отсутствие фигуры, т.е. пустое поле.
Класс состояния игры, GameState
Создадим класс, который будет отвечать за состояние игры. В папке engine создадим новый файл game_state.php. Сразу приведу его полный текст:
game_state.php<?php
class GameState {
const PROPERTY_NAMES = array(
'position', 'current_player_color', 'enable_castling_white_king', 'enable_castling_black_king', 'enable_castling_white_queen', 'enable_castling_black_queen',
'crossed_field', 'non_action_semimove_counter', 'move_number', 'human_color', 'prev_move_from', 'prev_move_to', 'text_state'
);
public $position = null; // массив из 64 элементов - положение фигур на доске
public $current_player_color = null; // текущий игрок, чья очередь хода - белые или чёрные
public $enable_castling_white_king = null; // возможность рокировки белого короля на королевский фланг (короткая рокировка)
public $enable_castling_white_queen = null; // возможность рокировки белого короля на ферзевый фланг (длинная рокировка)
public $enable_castling_black_king = null; // возможность рокировки чёрного короля на королевский фланг (короткая рокировка)
public $enable_castling_black_queen = null; // возможность рокировки чёрного короля на ферзевый фланг (длинная рокировка)
public $crossed_field = null; // проходимое поле (поле которое пешка перескочила на предыдущем ходу) - для взятия "на проходе"
public $non_action_semimove_counter = null; // число полуходов, после последнего взятия фигуры или движения пешки (для правил 50 и 75 ходов)
public $move_number = null; // номер хода
public $human_color = null; // цвет фигур, которыми играет человек
public $prev_move_from = null; // индекс поля откуда был предыдущий ход
public $prev_move_to = null; // индекс поля куда пошли в предыдущий ход
public $text_state = null; // текстовое описание состояния
public function getHash() {
$result = array();
foreach (self::PROPERTY_NAMES as $key) {
$result[$key] = $this->$key;
}
return $result;
}
public function serializeState() {
return json_encode($this->getHash(), JSON_UNESCAPED_UNICODE);
}
public function unserializeState(string $serialized_state) {
try {
$data = json_decode($serialized_state, true);
foreach (self::PROPERTY_NAMES as $key) {
$this->$key = $data[$key];
}
return true;
} catch(Exception $e) {
return false;
}
}
}
Что означают поля класса, прокомментировано в коде, надеюсь достаточно понятно. В константе класса PROPERTY_NAMES в виде массива перечислены имена полей класса. Это сделано только для сокращения кода методов класса, чтобы не перечислять "вручную" все имена. Без этой константы вполне можно было обойтись.
Метод getHash
Приватный метод getHash возвращает ассоциативный массив, где просто перечислены поля класса с их значениями.
Методы serializeState и unserializeState
Метод serializeState берёт хеш с полями класса, который предоставляет вышеописанная функция getHash, и представляет этот хеш в виде строки в формате JSON. Т.е. метод "сериализует" состояние игры, набор полей запаковывает в строку, и возвращает её.
Метод unserializeState действует противоположно. Он берёт строку (входной параметр), и пытается декодировать её, предполагая, что она представляет собой json-формат. Эта расшифровка помещена в "защищённый блок" try - catch, ведь в качестве входного параметра может прийти строка, которая не представляет корректный json-формат. В этом случае метод возвратит false.
Если декодировать строку удалось, мы проходим (строки 39 - 41) по именам полей класса, и записываем в поля с этими именами значения, полученные из декодированной строки.
Класс для хранения состояний, SessionStorage
Сохранять состояние игры можно в хранилищах разного типа - в файле, в базах данных разного вида, в сессии. Сама сессия также может хранится в файлах, в памяти, в базах данных. Как видно по имени класса, я решил хранить состояние в сессии. По умолчанию, php хранит сессии в файлах. Сессии идентифицируются содержимым "сессионных кук", в которых хранится идентификатор сессии. Но эти подробности сейчас не имеет отношения к нашему проекту.
В папке engine создаём файл session_storage.php:
session_storage.php<?php
class SessionStorage {
const SESSION_KEY = 'chess_game_state';
public function __construct() {
session_start();
}
public function saveGameState(GameState $game_state) {
$_SESSION[self::SESSION_KEY] = $game_state->serializeState();
}
public function loadGameState() {
if (empty($_SESSION[self::SESSION_KEY])) {
return null;
}
$game_state = new GameState();
$game_state->unserializeState($_SESSION[self::SESSION_KEY]);
return $game_state;
}
}
В константе класса SESSION_KEY мы указываем ключ, по которому будем хранить данные игры. Сессионные данные с точки зрения программы представляют собой ассоциативный массив "Ключ => Значение". Вот здесь мы и определяем этот ключ - "chess_game_state".
Сам класс очень прост. В конструкторе мы стартуем сессию. И есть два метода - для сохранения (записи) состояния игры, и для загрузки (чтения).
Сохранение состояния, saveGameState
Во входном параметре $game_state мы получили состояние игры - объект класса GameState. Вызываем метод serializeState этого объекта, чтобы получить строку, содержащую состояние игры в формате JSON, и сохраняем эту строку в сессию с ключём self::SESSION_KEY = 'chess_game_state'
Загрузка состояния, loadGameState
В этом методе сначала проверяем, если в сессии состояние игры не записано (пусто), то возвращаем null. Если в сессии есть непустое состояние игры, то создаём объект класса GameState и вызываем его метод unserializeState для заполнения полей объекта. Потом возвращаем этот объект из функции.
Контроллеры (обработчики запросов)
Сейчас быстро набросаем контроллеры, т.е. те скрипты, которые обрабатывают запросы с фронтенда. Как обычно, будем писать код так, как мы хотим его видеть, вызывая функции, которых ещё нет.
Обработчик хода человека, make_move.php
Этот скрипт у нас уже есть, это файл make_move.php в веб-корне проекта. Что тут будем менять? Во первых, уберём строку с задержкой: sleep(2);. Эта задержка была нужна на предыдущем этапе просто для демонстрации выделения полей "откуда" и "куда" при ожидании хода компьютера.
Во вторых, мы получали данные для отдачи во фронтенд вот так:
$data = $game->getGameState();
Но в текущей статье мы ввели класс, представляющий состояние игры, и в неё определили много полей, которые не нужно передавать на сторону фронтенда.
На стороне фронтенда нужны: расположение фигур на доске, допустимые ходы в текущей позиции, поля откуда и куда был предыдущий ход. Ну ещё мы стали использовать поля с текстовым статусом игры, и флаг что сейчас ход человека. А в php-классе в состоянии игры нам надо хранить ещё возможности разного вида рокировок, проходное поле, счётчик ходов.
Т.е. состояние игры на бекенде, нужное для обеспечения игры, и состояние, которое мы передаём на фронтенд - это разный набор данных. Поэтому в контроллере переименуем вызываемый метод с getGameState на getClientJsonGameState. Этот метод напишем ниже.
В итоге, контроллер обработки хода человека должен выглядеть так:
make_move.php<?php
require_once('engine/chess_game.php');
$cell_index_from = $_POST['cell_index_from'];
$cell_index_to = $_POST['cell_index_to'];
$game = new ChessGame();
$game->makeMove($cell_index_from, $cell_index_to);
$data = $game->getClientJsonGameState();
header('Content-Type: application/json');
echo json_encode($data);
Получение состояния игры, get_game_state.php
В веб-корне проекта, т.е. рядом с index.html и make_move.php, создаём файл get_game_state.php:
get_game_state.php<?php
require_once('engine/chess_game.php');
$game = new ChessGame();
$game->loadGame();
$data = $game->getClientJsonGameState();
header('Content-Type: application/json');
echo json_encode($data);
Выглядит очень просто. Сначала подключаем "движок игры", т.е. файл с классом ChessGame:
require_once('engine/chess_game.php');
Создаём объект игры $game класса ChessGame, и загружаем состояние игры:
$game = new ChessGame();
$game->loadGame();
Метод loadGame напишем ниже. Там-то нам и понадобится рассмотренные выше классы с состоянием игры и хранилищем.
Ну и последние три строки контроллера get_game_state.php полностью повторяют последние три строки файла make_move.php. Т.е. получаем JSON-строку с состоянием игры, нужным для фронтенда. Отправляем заголовок, говорящий, что тип контента - json. И отдаём саму JSON-строку, представляющую состояние игры.
Создание новой игры, create_new_game.php
Помните, мы добавили две кнопки для создания новой игры? Сделали так, что при клике по ним, фронтенд шлёт запрос на create_new_game.php. И в запросе был параметр human_color, сообщающий за кого мы (человек) хотим играть - за белых ("w") или за чёрных ("b").
Создаём в веб-корне проекта ещё один файл, create_new_game.php:
create_new_game.php<?php
require_once('engine/chess_game.php');
$human_color = ($_POST['human_color'] == 'b' ? COLOR_BLACK : COLOR_WHITE);
$game = new ChessGame($human_color);
$game->createNewGame($human_color);
$data = $game->getClientJsonGameState();
header('Content-Type: application/json');
echo json_encode($data);
Почти всё знакомо, правда? Всё очень похоже на остальные контроллеры. В начале подключаем файл с "движком" игры. Последние три строки - читаем состояние игры, и отдаём в виде JSON-строки. В середине - создаём объект игры класса ChessGame
"Особенные" - всего две строчки. В строке 4 мы из POST-параметров запроса берём цвет, за который будет играть человек. Если это "b" - считаем что человек будет играть за чёрных, иначе - за белых.
И в строке 7 вызываем метод createNewGame объекта игры. Этот метод напишем ниже. Всё просто с контроллерами!
Изменения в основном классе игры
Сейчас будем править файл engine/chess_game.php В самом начале файла, сразу после открывающего php-тега, т.е., на второй строке, подключаем ранее созданные файлы с константами, классом хранилища и классом состояния игры:
require_once('constants.php');
require_once('session_storage.php');
require_once('game_state.php');
В самом начале класса объявляем приватные поля, где будем хранить объекты "хранилища" и состояния игры:
private $storage;
private $game_state;
Ниже пишем конструктор, создаём в нём объект класса SessionStorage, и сохраняем его в свойстве storage класса. Здесь у нас будет хранится "хранилище состояния игры".
public function __construct() {
$this->storage = new SessionStorage();
}
Далее, ниже пишем два метода - загрузка состояния игры из хранилища, и запись состояния игры в хранилище. Всё очень просто:
public function loadGame() {
$this->game_state = $this->storage->loadGameState();
}
public function saveGame() {
$this->storage->saveGameState($this->game_state);
}
Создание нвой игры, createNewGame
Ниже, сразу за saveGame размещаем метод создания новой игры, createNewGame. Мы его использовали в контроллере создания новой игры (create_new_game.php).
public function createNewGame($human_color) {
$this->game_state = new GameState();
$this->game_state->position = $this->getInitPosition();
$this->game_state->current_player_color = COLOR_WHITE; // белые ходят
$this->game_state->enable_castling_white_king = true; // возможна короткая рокировка у белых
$this->game_state->enable_castling_white_queen = true; // возможна длинная рокировка у белых
$this->game_state->enable_castling_black_king = true; // возможна короткая рокировка у чёрных
$this->game_state->enable_castling_black_queen = true; // возможна длинная рокировка у чёрных
$this->game_state->crossed_field = null;
$this->game_state->non_action_semimove_counter = 0;
$this->game_state->move_number = 1; // номер хода - 1
$this->game_state->human_color = ($human_color == COLOR_BLACK ? COLOR_BLACK : COLOR_WHITE);
$this->game_state->prev_move_from = null;
$this->game_state->prev_move_to = null;
$this->game_state->text_state = 'Новая партия создана. '.($this->game_state->human_color == $this->game_state->current_player_color ? 'Ваш ход' : 'Ждите мой ход');
$this->storage->saveGameState($this->game_state);
}
Что делаем в этом методе? Создаём новый объект класса GameState, и записываем его в поле класса, где у нас хранится состояние игры:
$this->game_state = new GameState();
Дальше заполняем поля состояния игры ($this->game_state), приводим состояние в "начальный вид", т.е. делаем всё как в начале игры. По комментариям здесь, и по комментариям в коде класса GameState всё должно быть понятно.
Единственное что наверное стоит пояснить - заполнение начальной позиции (расстановка фигур):
$this->game_state->position = $this->getInitPosition();
Здесь для получения начальной позиции мы используем отдельную новую функцию.
Получение начальной позиции, getInitPosition
Сразу ниже и запишем новый метод:
private function getInitPosition() {
return array(
FG_ROOK_BLACK, FG_KNIGHT_BLACK, FG_BISHOP_BLACK, FG_QUEEN_BLACK, FG_KING_BLACK, FG_BISHOP_BLACK, FG_KNIGHT_BLACK, FG_ROOK_BLACK,
FG_PAWN_BLACK, FG_PAWN_BLACK, FG_PAWN_BLACK, FG_PAWN_BLACK, FG_PAWN_BLACK, FG_PAWN_BLACK, FG_PAWN_BLACK, FG_PAWN_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_WHITE, FG_PAWN_WHITE, FG_PAWN_WHITE, FG_PAWN_WHITE, FG_PAWN_WHITE, FG_PAWN_WHITE, FG_PAWN_WHITE, FG_PAWN_WHITE,
FG_ROOK_WHITE, FG_KNIGHT_WHITE, FG_BISHOP_WHITE, FG_QUEEN_WHITE, FG_KING_WHITE, FG_BISHOP_WHITE, FG_KNIGHT_WHITE, FG_ROOK_WHITE
);
}
Этот метод возвращает массив с начальной расстановкой фигур. Это то-же самое, что и наш старый код в методе getGameState:
'position' => [
6, 0, 8, 4, 2, 8, 10, 6,
12, 12, 12, 12, 12, 12, 12, 12,
0, 0, 10, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
11, 11, 11, 11, 11, 11, 11, 11,
5, 9, 7, 3, 1, 7, 9, 5
],
Только теперь вместо цифр мы используем соответствующе константы. И обратите внимание на модификатор private - мы сделали наш метод приватным.
Удаляем ненужный код
Нам теперь не нужен метод getGameState. Удаляем его! Он "хардкодил" ответ бекенда, и нужен был просто для ускорения отладки работы фронтенда.
Отдача состояния игры для клиента (фронтенда)
В контроллере get_game_state.php мы использовали метод getClientJsonGameState для получения данных, которые потом отдавали на фронтенд, преобразовав в JSON-строку. Напишем этот метод:
public function getClientJsonGameState() {
if (!$this->game_state) {
return null;
}
return array(
'position' =>$this->game_state->position,
'is_human_move' => $this->game_state->current_player_color == $this->game_state->human_color,
'human_color' => $this->game_state->human_color,
'move_number' => $this->game_state->move_number,
'prev_move_from' => $this->game_state->prev_move_from,
'prev_move_to' => $this->game_state->prev_move_to,
'text_state' => $this->game_state->text_state,
'available_moves' => $this->generateAvailableMoves()
);
}
В основном все данные берём из поля, хранящего состояние игры, из $this->game_state. Флаг is_human_move, сигнализирующий, что сейчас очередь хода у человека, мы получаем как результат логического выражения. Мы сравниваем на равенство два поля из состояния игры - "цвет фигур текущего игрока равен цвету фигур человека?":
'is_human_move' => $this->game_state->current_player_color == $this->game_state->human_color
И ещё одно поле, которое мы берём не напрямую из состояния игры - available_moves, массив возможных ходов из данной позиции для человека. Добавим этот метод ниже. Пока поставим "заглушку" - просто возвратим пустой массив:
public function generateAvailableMoves() {
return array();
}
Итоги
При загрузке страницы, если мы до этого ещё не создавали игры, видим пустую доску, кнопки для создания игры, и фразу "Игра не начата" в строке статуса:

Можем начать новую игру, кликнув по одной из кнопок "за белых" или "за чёрных". Кликнем например по кнопке "за чёрных". Получим такую картинку:

Можем в любой момент снова создать игру. И главное, чего мы добились в этой части - состояние игры сохраняется. Можем обновлять страницу, закрывать, приходить позже, игра будет показана в том виде, как мы её оставили.
Ходить правда пока мы не можем (потому что стали передавать пустой массив допустимых ходов). И компьютер тоже не ходит. Но в следующей части мы как раз этим и займёмся.
А пока, как обычно, можно посмотреть полный исходный код этой части на github.
Ну и конечно можно посмотреть как это всё работает на демо - странице №5