Морской бой - одна из лучших игр для скучных уроков или лекций. И, если в прошлом она требовала лишь два листа бумаги, то теперь понадобится лишь выход в интернет. Давайте его сделаем.
Краткие сведения
Мы сделаем клиент-серверную версию, где будет лёгкий клиент - веб страница, которую, при желании, можно перенести в какое-нибудь приложения, вроде 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 откроется приложения.
Исходники можно скачать или посмотреть тут.