Пятничный JS: игра в 0 строк JS и CSS

Возможно, многие из старожилов помнят эпидемию статей с заголовками вида »%something% в 30 строк JS». А также последовавшую за ней эпичный пост «Игра в 0 строк кода на чистом JS», после которого эпидемия резко сошла на нет. Полностью осознавая, что этот шедевр мне никогда не превзойти, я всё же спустя пять лет решил докинуть свои пять копеек.

Дамы и господа, вашему вниманию предлагается игра «Крестики-нолики» в 0 строк JS, а также, в отличие от игры, упомянутой выше, в 0 строк CSS. Только статический HTML, только хардкор.

41imhjnqxqozjtphb0vpbtyylx0.png


→ Ссылка на игру

Под катом я расскажу, почему игра без JS оказалась в рубрике «Пятничный JS», а также другие грязные подробности. Впрочем, Америку я никому не открою, если вы опытный кодер, можете под кат даже не заходить
Собственно, всё устроено очень тупо. Игра состоит из почти 6 тысяч страниц «голого» HTML, ссылающихся друг на друга. При тыке на клетку игрового поля происходит переход на страницу, где ход в эту клетку уже сделан. Очевидно, писать 6к страниц руками — удовольствие ниже среднего. Поэтому (сюрприз!) страницы генерируются JS-скриптами с помощью NodeJS.

Лирическое отступление

Написав предыдущую строчку, я вдруг задумался, не является ли выражение «JS-скрипт» тавтологией, как «CD-диск» или «VIP-персона». С одной стороны, вроде как является. С другой, JS — это всё-таки не аббревиатура, а сокращение несколько иной природы. Однако пост всё-таки не о филологии, потому лирическое отступление заканчивается и начинается лирическая атака.


Сначала мы строим так называемое дерево игры — совокупность всех возможных игровых состояний и переходов между ними. Начальное состояние игры в моём коде представлено следующим образом:

const initialState = {
        player: PLAYER_X,
        field: Array.from(Array(9)).map(() => EMPTY_CELL),
        moves: {}
}


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

Дальше мы начинаем, прошу прощения за тавтологию, с начального состояния и делаем следующее:

  1. Проверяем, не является ли состояние терминальным (победа крестиков, победа ноликов, ничья).
  2. Если да, добавляем информацию об этом в объект состояния и заканчиваем.
  3. Если нет, проходимся по всем клеткам поля.
  4. Для каждой пустой клетки поля создаём новое состояние игры, в котором текущий игрок сделал ход в эту клетку, а ход перешёл к следующему игроку.
  5. В поле moves текущего состояния добавляем запись о возможном ходе. Ключом в этой записи служит индекс клетки, а значением — ссылка на новое состояние.
  6. Повторяем этот алгоритм рекурсивно для всех вновь появившихся состояний.


На самом деле мой код чуть сложнее, я по привычке развернул рекурсию в цикл, а вместо ссылок на другие состояния в moves хранятся их строковые ключи в некоем ассоциативном массиве. Но это всё детали.

Затем из каждого объекта игрового состояния мы генерируем HTML-страницу. Пройдясь по объекту moves, мы заполняем пустые клетки поля ссылками на страницы, соответствующие ходам, сделанным в эти клетки. Затем превращаем одномерный массив поля в двумерную HTML-таблицу. Добавляем всякие приятные мелочи вроде указания, который игрок ходит, и ссылки на начальную страницу — и вуаля!

Кроме режима, когда и крестики, и нолики ставятся человеком, в моём мега-инди-хите есть также возможность играть против железного мозга. Достигается это следующим образом:

  1. Сначала рекурсивно (на самом деле нет) для каждого игрового состояния вычисляется ожидаемый результат игры — тот, который будет достигнут, если обе стороны будут играть идеально.
  2. Затем дерево игры модифицируется следующим образом: вместо хода игрока мы теперь делаем сразу два хода. Второй ход — ход искусственного интеллекта. При этом из всех возможных ответов на ход игрока выбирается тот, у которого самый лучший ожидаемый результат. Таким образом, тыкнув на пустую клетку, игрок сразу переходит на позицию, где в этой клетке появился крестик (или нолик), а в какой-то другой появился нолик (или крестик).
  3. Все игровые позиции, соответствующие ходам, которые ИИ не делает, безжалостно отбрасываются.
  4. Затем из оставшихся позиций в отдельную папочку генерится HTML по принципам, описанным выше.


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

Кстати о гитхабе: можете посмотреть там код целиком (ссылка присутствует на заглавной странице игры). На этом, в общем-то, всё. До свидания, девочки и мальчики. До новых встреч.

P.S. Замена переносов строки с Windows-style на Unix-style — это очень долго, когда речь идёт о 6 тысячах файлов. Я пожалел, что не позаботился об этом на этапе написания кода, но всё-таки мужественно дотерпел до конца git add.

© Habrahabr.ru