Пятничный JS: игра в 0 строк JS и CSS
Возможно, многие из старожилов помнят эпидемию статей с заголовками вида »%something% в 30 строк JS». А также последовавшую за ней эпичный пост «Игра в 0 строк кода на чистом JS», после которого эпидемия резко сошла на нет. Полностью осознавая, что этот шедевр мне никогда не превзойти, я всё же спустя пять лет решил докинуть свои пять копеек.
Дамы и господа, вашему вниманию предлагается игра «Крестики-нолики» в 0 строк JS, а также, в отличие от игры, упомянутой выше, в 0 строк CSS. Только статический HTML, только хардкор.
→ Ссылка на игру
Под катом я расскажу, почему игра без 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: {}
}
В нём содержится информация о том, чей сейчас ход и каково состояние игрового поля. В будущем в нём также явится информация о том, какие ходы могут быть сделаны и к каким состояниям они приведут, а также некоторые другие приятные вещи.
Дальше мы начинаем, прошу прощения за тавтологию, с начального состояния и делаем следующее:
- Проверяем, не является ли состояние терминальным (победа крестиков, победа ноликов, ничья).
- Если да, добавляем информацию об этом в объект состояния и заканчиваем.
- Если нет, проходимся по всем клеткам поля.
- Для каждой пустой клетки поля создаём новое состояние игры, в котором текущий игрок сделал ход в эту клетку, а ход перешёл к следующему игроку.
- В поле
moves
текущего состояния добавляем запись о возможном ходе. Ключом в этой записи служит индекс клетки, а значением — ссылка на новое состояние. - Повторяем этот алгоритм рекурсивно для всех вновь появившихся состояний.
На самом деле мой код чуть сложнее, я по привычке развернул рекурсию в цикл, а вместо ссылок на другие состояния в moves
хранятся их строковые ключи в некоем ассоциативном массиве. Но это всё детали.
Затем из каждого объекта игрового состояния мы генерируем HTML-страницу. Пройдясь по объекту moves
, мы заполняем пустые клетки поля ссылками на страницы, соответствующие ходам, сделанным в эти клетки. Затем превращаем одномерный массив поля в двумерную HTML-таблицу. Добавляем всякие приятные мелочи вроде указания, который игрок ходит, и ссылки на начальную страницу — и вуаля!
Кроме режима, когда и крестики, и нолики ставятся человеком, в моём мега-инди-хите есть также возможность играть против железного мозга. Достигается это следующим образом:
- Сначала рекурсивно (на самом деле нет) для каждого игрового состояния вычисляется ожидаемый результат игры — тот, который будет достигнут, если обе стороны будут играть идеально.
- Затем дерево игры модифицируется следующим образом: вместо хода игрока мы теперь делаем сразу два хода. Второй ход — ход искусственного интеллекта. При этом из всех возможных ответов на ход игрока выбирается тот, у которого самый лучший ожидаемый результат. Таким образом, тыкнув на пустую клетку, игрок сразу переходит на позицию, где в этой клетке появился крестик (или нолик), а в какой-то другой появился нолик (или крестик).
- Все игровые позиции, соответствующие ходам, которые ИИ не делает, безжалостно отбрасываются.
- Затем из оставшихся позиций в отдельную папочку генерится HTML по принципам, описанным выше.
По аналогичным принципам можно реализовать любую игру с не очень большим деревом. Впрочем, если я захочу подобным образом сделать шахматы, мне кажется, гитхаб откажется это хостить =)
Кстати о гитхабе: можете посмотреть там код целиком (ссылка присутствует на заглавной странице игры). На этом, в общем-то, всё. До свидания, девочки и мальчики. До новых встреч.
P.S. Замена переносов строки с Windows-style на Unix-style — это очень долго, когда речь идёт о 6 тысячах файлов. Я пожалел, что не позаботился об этом на этапе написания кода, но всё-таки мужественно дотерпел до конца git add
.