Найти в Дзене
Сделай игру

Делаем "Морской бой" на Go и JS

Морской бой - одна из лучших игр для скучных уроков или лекций. И, если в прошлом она требовала лишь два листа бумаги, то теперь понадобится лишь выход в интернет. Давайте его сделаем. Мы сделаем клиент-серверную версию, где будет лёгкий клиент - веб страница, которую, при желании, можно перенести в какое-нибудь приложения, вроде telegram, а сервер будет написан на Go. Хотя данную задачу можно решить и средствами одного лишь html+js, мы, всё же, пойдём по пути клиент-серверного решения: кто знает, может быть следующим шагом мы захотим сделать сетевую версию. Уже после того, как я начал делать игру, появился ряд вопросов, которые даже обсуждал с друзьями (у них, как ни странно, они тоже вызвали интерес). Фокус в том, что игра Морской боя совсем не так проста, как кажется, особенно если играть с компьютером. И если расстановка кораблей случайным способом задача тривиальная, то целенаправленная расстановка кораблей, минимизирующая шанс на попадание - уже другая задача. Также есть различн
Оглавление

Морской бой - одна из лучших игр для скучных уроков или лекций. И, если в прошлом она требовала лишь два листа бумаги, то теперь понадобится лишь выход в интернет. Давайте его сделаем.

Краткие сведения

Мы сделаем клиент-серверную версию, где будет лёгкий клиент - веб страница, которую, при желании, можно перенести в какое-нибудь приложения, вроде telegram, а сервер будет написан на Go. Хотя данную задачу можно решить и средствами одного лишь html+js, мы, всё же, пойдём по пути клиент-серверного решения: кто знает, может быть следующим шагом мы захотим сделать сетевую версию.

Морской бой
Морской бой

В чём вызов?

Уже после того, как я начал делать игру, появился ряд вопросов, которые даже обсуждал с друзьями (у них, как ни странно, они тоже вызвали интерес). Фокус в том, что игра Морской боя совсем не так проста, как кажется, особенно если играть с компьютером.

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

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

Это буквально теория вероятностей в действии.

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

Алгоритмы и структуры

В ходе реализации игры мне пришлось внедрить несколько алгоритмов:

  • Работа с игровыми сессиями
  • Синхронизация вебсокет-потока чтение/запись
  • Случайная расстановка кораблей
  • Случайная уникальная атака
  • Добивание корабля

Также потребовались структуры данных обмена клиента и сервера и API для начала новой сессии и управляемого ws-обмена данными.

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

Как видно на рисунке выше, структура игровой сессии хранит разноплановые данные.

Порядок реализации проекта

Как я уже писал, было принято решение делать клиент серверную версию. Однако т.к. клиент создаётся уже после того, как готов сервер, надо предусмотреть возможность отладки. А она, будьте уверены, понадобится. По этой причине, нам понадобятся инструменты. В моём случае будет достаточно двух: curl и wscat. Первый позволит получить новую сессию, второй - обмениваться данными с сервером.

Скрипт инициализации новой игры
Скрипт инициализации новой игры

Такой скрипт позволит относительно быстро начать новую игру.

Формат обмена данными

Изначально, я хотел минимизировать обмен данными, но, в какой-то момент, пришёл к мнению, что не стоит экономить на спичках. Со стороны клиента отправляется обёрнутый в JSON массив, не более 4 значений длиной, где первые два значения x и y атаки, третий (если равен 1) - запрос синхронизации поля (уточнение игровой доски) и четвёртый (если равен 1) - досрочное завершение партии. Вторые два значения не слишком важны, но предусмотреть такие возможности - никогда не будет лишним.

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

  • пустая ячейка = 0b0
  • буферная зона вокруг корабля = 0b001
  • клетка с кораблём = 0b010
  • атакованная клетка = 0b100

Как нетрудно заметить, признаки маски можно комбинировать, позволяя сделать клетку одновременно пустой, но поражённой. Например, если <значени> & <признак атаки> == <признак атаки> - клетку следует перечеркнуть.

Также, предусмотрены ошибки: например, несвоевременный ход или атака ранее поражённой ячейки.

Клиент

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

Он уметь должен не так уж и много:

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

Существующая модель серверного сообщения содержит некоторое количество сведений, но не все: кое-что придётся обрабатывать или выбирать на клиенте (например, координаты ударов).

Сборка и запуск

Go поддерживает возможность развёртывания сервера статики, им и воспользуемся для распространения клиента. Такой подход, обычно, не используется для публикации приложения; используется какой-нибудь веб сервер (например, nginx), который обслуживает статику и перенаправляет запросы.

Были и небольшие трудности

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

Первой трудностью было правильно реализовать алгоритмы "добивания" и определение "уничтожения корабля". Несмотря на их простую работу, где-то то и дело забывался индекс -1, либо же использовалось == вместо >=. На момент написания они все работали, но в процессе проверки появлялись аномалии в поведении. Пришлось написать тесты, чтобы выяснить, где же ошибки.

Второй трудностью оказалась оптимизация отрисовки: стандартный контекст для canvas (который 2d) имеет неприятную особенность притормаживать при частых вызовах stroke(). Да и fill() тоже часто вызывать не следует, хотя он и работает быстрее. Также есть некоторые особенности, связанные с цветами и толщиной линий. В общем, пришлось повозиться в поисках баланса.

Первая версия

Первая версия, как я писал ранее, была исключительно серверной. Для обращения использовались обычные http запросы. Выглядело это так:

Морской бой в терминале
Морской бой в терминале

Не очень удобно, но для отладки сойдёт.

Вторая версия

Данная версия уже содержала полноценный веб-интерфейс, который позволял полноценно играть.

Пользовательский интерфейс
Пользовательский интерфейс

Для удобства были реализованы некоторые особенности: текстовые подсказки хода противника, подсветки последних ходов (свой и противника), автоматическая обводка потопленных кораблей.

Заключение

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

У меня, пока что, нет выделенного сервера, так что поиграться вы можете скачав и запустив приложение у себя. Для запуска понадобится установленный go, желательно, последней версии. Запустить сервер можно так:

go run main.go

И на http://localhost:8080 откроется приложения.

Исходники можно скачать или посмотреть тут.