В предыдущих постах (1, 2, 3, 4) я описал создание шахматного поля на веб-странице с помощью HTML и CSS. Я добавил на это поле шахматные фигуры и с помощью скрипта на языке JavaScript реализовал возможность перемещения этих фигур мышью. В этом посте я добавлю в проект возможность сохранения позиции в файл и загрузку позиции из файла.

Работа с файлами из браузера ограничена

Поскольку мы используем интерпретатор (движок) JavaScript, встроенный в браузер, необходимо понимать, как устроено его взаимодействие с файловой системой операционной системы пользователя нашей веб-страницы. Это взаимодействие ограничено в целях безопасности.

Если бы браузер позволял любому скрипту на языке JavaScript с загружаемых веб-страниц манипулировать бесконтрольно любыми файлами в операционной системе пользователя браузера, то это было бы равносильно передаче ключей от своей квартиры гопникам на районе.

Подбор способа работы с файлами

Однако, это ограничение не значит, что мы вообще не можем сохранять данные в файл или загружать данные из файла. Просто это делается не так, как вы привыкли это делать в программах на разных языках программирования для настольных компьютеров. (Привычность инструментов в программировании зависит от порядка обучения. Я обучался сначала программированию на настольных компьютерах, затем изучал веб-разработку.)

Работа с файлами из JavaScript в браузере хорошо рассмотрена в разделе «Бинарные данные и файлы» третьей части учебника learn.javascript.ru. Способ сохранения данных в файл из скрипта на языке JavaScript в браузере мы взяли из статьи «Blob» этого раздела. Способ загрузки данных из файла в скрипте на языке JavaScript в браузере мы взяли из статьи «File и FileReader» вышеупомянутого раздела.

Данные решили хранить в текстовом файле. Текстовый файл можно легко просмотреть хоть в консоли, хоть в любом редакторе, умеющем работать с текстовыми файлами. Легкость просмотра файла помогает быстро обнаружить возможные ошибки при написании кода сохранения и загрузки данных.

Формат файла

Существует множество форматов файла для хранения шахматных партий. Один из самых популярных — PGN (Portable Game Notation). Но в этих форматах обычно хранят ход партии (ходы), а не конкретную позицию. Мы же на данном этапе развития нашего проекта хотели сохранить в файл или загрузить из файла именно текующую позицию, а не ходы, приведшие к этой позиции. Поэтому мы придумали свой формат файла.

Пример (позиция взята из статьи на сайте XChess.ru):

....bR......bK..
..bQ....bBbPbPbP
bP....bP........
..bP..wRbP......
........wP......
..wP............
..wPwP..wQwPwPwP
........wN..wK..

Названия фигур здесь обозначены по тому же принципу, который был применен в названиях файлов с изображениями фигур. То есть названия фигур совпадают с названиями соответствующих файлов без расширений, что удобно.

Напомню, каждая фигура обозначена двумя буквами. Первая из них может быть буквой b или w (цвет фигуры — черный или белый соответственно, black или white по-английски). Вторая из них может быть одной из следующих букв английского алфавита: K, Q, N, B, R, P (король [king], ферзь [queen], конь [knight], слон [bishop], ладья [rook] и пешка [pawn] соответственно).

Для свободных полей на доске тоже должно быть отведено по два символа, чтобы при использовании моноширинного шрифта можно было рассмотреть позицию, просто взглянув на содержимое текстового файла с этой позицией. Сначала мы для этого использовали по два пробела, но это было неудобно, так как не всегда видны края доски. Поэтому для обозначения свободного поля на доске используем по две точки.

Все используемые в формате символы входят в таблицу ASCII, а это уменьшает возможные проблемы с кодировками. Один символ занимает в памяти 1 байт, поэтому файл с любой позицией будет всегда иметь одинаковый размер в 128 байтов (на самом деле, чуть больше, так как еще немного байтов уйдет на символы новой строки).

Изменения в архитектуре проекта

Код на языке JavaScript для сохранения позиции в файл вынесли в отдельный файл saveFile.js. Код для загрузки позиции из файла вынесли в отдельный файл loadFile.js. Файлы трех скриптов проекта поместили в новую подпапку scripts проекта. Подключение этих скриптов на веб-страницу:

  <head>
    <!-- ... -->
    <script src="scripts/saveFile.js" defer></script>
    <script src="scripts/loadFile.js" defer></script>
  </head>

Вставка кнопок для вызова этих двух скриптов с веб-страницы:

    <div class="chess-field">
      <div class="header">
        <button type="button" class="saveButton">Сохранить</button>
        · Загрузить: <input type="file" class="loadButton">
      </div>
      <!-- ... -->
    </div>

Долго думали, куда вставить эти кнопки. Решили пока что над шахматным полем. Выглядит это так:

Кнопка для сохранения позиции реализована с помощью элемента button. Кнопка для загрузки позиции реализована с помощью элемента input типа file. Вообще элемент input имеет множество разных типов, поэтому может выглядеть на веб-странице очень по-разному.

Сохранение позиции в файл

Содержимое файла saveFile.js:

// назначить кнопке «Сохранить» функцию-обработчик нажатия
let button = document.querySelector(".saveButton");
button.onclick = saveFile;

// функция, выполняющая сохранение позиции в файл
function saveFile()
{
    // получить все элементы веб-страницы, представляющие шахматные поля
    let chessFields = document.querySelectorAll(".square");

    // сбор информации о позиции и запись ее в строку data
    let data = "";
    for (let i = 0; i < chessFields.length; i++) {
        let field = chessFields[i];  // очередное поле:

        if (field.firstElementChild) // это поле с фигурой
        {
            let src = field.firstElementChild.src;
            let pieceName = "" + src.at(-6) + src.at(-5);
            data += pieceName;
        }
        else                         // это пустое поле
        {
            data += "..";
        }

        // вставка символа новой строки для следующего шахматного ряда
        if ((i + 1) % 8 == 0)
        {
            data += "\n";
        }
    }

    // создать ссылку для загрузки файла
    let link = document.createElement("a");
    link.download = "chess.txt"; // название файла по умолчанию

    // превратить строку data в кусок двоичных данных blob
    let blob = new Blob([data], {type: "text/plain"});

    // использовать blob как URL для созданной ранее ссылки
    link.href = URL.createObjectURL(blob);

    // эмулировать клик мышью по ссылке
    link.click();

    // удаление ссылки на объект для освобождения памяти
    URL.revokeObjectURL(link.href);
}

В вышеописанном скрипте привязываем функцию saveFile к событию нажатия на кнопку «Сохранить» на веб-странице.

Внутри этой функции перебираем все поля шахматной доски и записываем информацию о них в строку data. Если на очередном поле есть фигура (элемент img), то извлекаем из ее свойства src ее обозначение, которое, напомню, состоит из двух букв, совпадающих с названием файла с изображением фигуры, и записываем это обозначение в строку data. Если поле пустое, в строку data записываем две точки.

В конец каждого ряда шахматной доски в строке data вставляем символ новой строки.

Как уже было описано выше, мы не можем напрямую записать данные в файл, который находится на устройстве пользователя, без разрешения пользователя. Поэтому мы создаем невидимую ссылку для загрузки нашего файла (по умолчанию его имя chess.txt) и эмулируем нажатие на эту ссылку.

Дальнейшее зависит от настроек браузера у пользователя.

Обычно, по умолчанию браузер настроен так, что загружаемый файл автоматически записывается в папку, предназначенную для хранения загружаемых из интернета файлов. Я тестировал этот скрипт на компьютере с системой Windows 10; мой браузер Google Chrome записал файл chess.txt в папку Загрузки внутри папки текущего пользователя; браузер выдал сообщение об этом. Если в папке Загрузки уже есть файл с таким именем, то браузер изменит имя файла на chess (1).txt, chess (2).txt и так далее.

Большинство браузеров можно настроить так, чтобы браузер спрашивал у пользователя, куда сохранить загруженный файл и как его назвать. При такой настройке браузера у пользователя при загрузке файла откроется стандартное окно для сохранения файла, в котором пользователь сможет выбрать нужную папку и/или изменить название файла.

Загрузка позиции из файла

Содержимое файла loadFile.js:

// назначить кнопке «Выберите файл» функцию-обработчик нажатия
let inputFile = document.querySelector(".loadButton");
inputFile.onchange = loadFile;

// функция, выполняющая загрузку позиции из файла
function loadFile()
{
    // запуск чтения из выбранного пользователем файла
    let file = this.files[0];
    let reader = new FileReader();
    reader.readAsText(file);

    // при окончании чтения из файла запустить функцию-обработчик
    reader.onload = loadPieces;

    // функция-обработчик, расставляющая фигуры на доске по данным из файла
    function loadPieces()
    {
        let data = reader.result;             // данные из файла
        data = data.replace(/\r?\n|\r/g, ""); // удалить символы новой строки

        // получить все элементы веб-страницы, представляющие шахматные поля
        let chessFields = document.querySelectorAll(".square");

        // перебрать шахматные поля, расставить фигуры по данным из файла
        for (let i = 0; i < chessFields.length; i++) {
            // получить данные (два символа) для очередного поля из файла
            let fieldData = data[0] + data[1];
            data = data.slice(2);
            
            // рассмотреть очередное шахматное поле на веб-странице
            let field = chessFields[i];
            
            // удалить из поля на веб-странице фигуру, если она там есть
            if (field.firstElementChild)
            {
                field.firstElementChild.remove();
            }
            
            // если в файле в этом месте есть фигура
            if (fieldData != "..")
            {
                // то создать элемент, представляющий фигуру
                let newImg = document.createElement("img");
                newImg.src = "images/adventurer/" + fieldData + ".png";
                newImg.className = "chess-piece";

                // и вставить фигуру на поле на веб-странице
                field.append(newImg);
            }
        }
    }
}

В вышеописанном скрипте привязываем функцию loadFile к событию нажатия на кнопку «Выбрать файл» на веб-странице (на самом деле, к событию изменения содержимого элемента input типа file).

При нажатии на эту кнопку в браузере откроется стандартное окно для выбора одного или множества файлов. В нашем случае (по умолчанию) браузер позволит выбрать только один файл в этом окне. Нам этого достаточно. (Если нужно включить выбор сразу множества файлов, это можно сделать с помощью свойства multiple элемента input типа file веб-страницы.)

Внутри функции loadFile мы определили вложенную функцию loadPieces, которую привязали к событию окончания загрузки данных из выбранного пользователем файла.

Вообще, написание скриптов для браузера предполагает использование асинхронного программирования и событийно-ориентированного программирования. То есть мы пишем куски кода, которые выполняются не один после другого (синхронное программирование), а каждый кусок — в нужное время. При этом время выполнения этих кусков кода может перекрываться (часть времени несколько кусков кода могут выполняться одновременно).

Такой подход очень удобен пользователю: пользователь может заниматься другими делами, пока выполняется одна или несколько задач, требующих много времени для выполнения (например, загрузка файла/файлов, проигрывание видеоролика и/или аудиозаписи, загрузка веб-страниц, имеющих большой размер, и тому подобное; всё перечисленное может выполняться одновременно).

В целом в вышеприведенной функции всё более-менее должно быть понятно для человека, изучившего основы синтаксиса языка JavaScript. Единственное, что хотелось бы пояснить дополнительно — для поиска в загруженном тексте символов новой строки используется шаблон в виде регулярного выражения /\r?\n|\r/g. Этот шаблон найдет символы новой строки любого типа: CR, CRLF или LF (для тех, кто не в курсе: в разных программах символы новой строки могут представляться по-разному). Регулярные выражения — великолепный инструмент для поиска в тексте и манипуляций с текстом, но он не очень легкий в изучении. Начать их изучать можно с соответствующей главы в учебнике learn.javascript.ru.

Результат

Можно посмотреть онлайн: https://textpub.neocities.org/my/chess-board/v2.3/.

Развитие проекта

В ближайшее время я не планирую возвращаться к этому учебному проекту. Возможно, мы продолжим его через год с другой группой студентов.

В следующей версии шахматной доски, на мой взгляд, в первую очередь имеет смысл добавить «взятие фигуры» в случае, когда пользователь передвигает какую-либо фигуру на поле, в котором уже находится какая-либо фигура. Всё это пока что без учета правил шахмат. «Взятую фигуру» легче всего просто удалить, но мне хотелось бы не удалять ее, а перемещать в пространство слева или справа от шахматной доски (в зависимости от того, с какой стороны находится игрок, взявший фигуру).