Трёхмерный «Морской бой» в браузере: бой против ИИ и полноценный онлайн 1 vs 1.
Клиент — PWA на Three.js. Сервер — чистый Node.js без единой внешней зависимости.
- Возможности
- Быстрый старт
- Как играть вдвоём
- Структура проекта
- Как устроен онлайн
- Античит
- Рейтинг и лидерборд
- Протокол WebSocket
- HTTP API
- Переменные окружения
- Тесты
- Деплой
- Лицензия
- Полноценная 3D-сцена на Three.js: море с многооктавными волнами, небо, модели кораблей, ракеты, взрывы, дым и осколки.
- Берег и горизонт: горная гряда вокруг акватории со снегом на вершинах и воздушной перспективой, прибой у подножия, маяк с красно-белыми поясами, галереей и вращающейся линзой, дальний конвой с дымом из труб, парусники, буйки и стаи чаек.
- Обстановка боя: четыре времени суток (рассвет, день, закат, ночь) × пять погод (ясно, облачно, дождь, шторм, туман). Меняются высота и азимут светила, палитра воды и неба, высота волны, видимость, освещение полей, тон гор и поведение птиц. На рассвете и закате солнце за бой заметно проходит по дуге; ночью поле освещают прожекторы на мачтах, а луч маяка виден в воздухе.
- Три ракурса — «Своё», «Оба», «Поле соперника» — переключаются кнопками (клавиши
1,2,3,V). Камера переезжает между ними плавным перелётом и никогда не перебрасывается сама. - Одно касание — одно действие: тап по клетке ставит корабль или бьёт по полю соперника, тянущий жест вращает камеру, долгое нажатие снимает корабль.
- PWA: устанавливается на телефон и рабочий стол. Движок и шрифты лежат в репозитории, поэтому игра поднимается полностью офлайн с первого запуска, без единого внешнего запроса.
- Адаптивный интерфейс: в книжной ориентации телефона поля встают одно за другим — своё ближе к игроку, — и клетка становится в два с половиной раза крупнее. Кадр строится по свободной от панелей области экрана, поэтому шторки и вкладки не накрывают поле ни в портрете, ни в альбоме.
- Звук и музыка целиком генерируются через Web Audio API — ни одного аудиофайла.
- Диалоги доступны с клавиатуры: фокус забирается, держится по Tab и возвращается на место.
- Четыре уровня сложности: Юнга, Мичман, Капитан, Адмирал.
- Охота по шахматному паттерну, точное добивание, карта вероятностей по остатку флота, а на «Адмирале» — перебор расстановок, совместимых со всеми известными выстрелами.
- Расчёт идёт целиком на устройстве игрока: сеть не нужна. Панель «Замысел соперника» показывает, чем движок руководствовался.
- Регистрация ника (сохраняется в
localStorageвместе с токеном владения — чужой ник занять нельзя). - Лобби со списком игроков онлайн и поиском соперника по нику.
- Приглашения: отправка, принятие, отклонение, отмена, тайм-аут.
- Бой полностью идентичен игре с ботом — только вместо ИИ живой человек.
- Доска соперника скрыта: сервер физически не отправляет клиенту чужой флот.
- Реванш в один клик, корректная обработка сдачи, выхода и обрыва связи.
- Ники, статистика и глобальный лидерборд.
Нужен Node.js 18+. Внешние зависимости не требуются.
cd seabattle/server
npm start # http://localhost:8080Сервер раздаёт статику игры из seabattle/game, держит WebSocket-лобби,
судит онлайн-матчи и ведёт лидерборд.
Игру с движком можно открыть и вообще без сервера — достаточно отдать
seabattle/game/index.html любым статическим хостингом. Онлайн-режим при
этом будет недоступен: ему нужен ws-эндпоинт.
| Параметр | Что делает |
|---|---|
?gfx=low · ?gfx=mid · ?gfx=high |
принудительный класс графики — посмотреть на игру глазами слабого устройства, не имея его под рукой |
- Запустите сервер.
- Оба игрока открывают его адрес (в одной сети —
http://IP-хоста:8080). - На первом экране выбираем «Против человека».
- Вводим ник. Он сохраняется в браузере и закрепляется за вами на сервере.
- В лобби вводим ник соперника или выбираем его в списке → «Вызвать на бой».
- Соперник принимает вызов, оба расставляют флот, начинается бой.
seabattle/
game/ клиент — одна страница, устанавливается как PWA
index.html разметка, оформление, сцена и вся игровая логика
vendor/ three.js и OrbitControls (не CDN — см. ниже)
fonts/ Oswald, Golos Text, JetBrains Mono (woff2)
icons/ manifest.webmanifest sw.js
server/ сервер без внешних зависимостей
server.js HTTP, статика, лидерборд
ws.js реализация WebSocket (RFC 6455) с нуля
rules.js правила и судейство, общие с клиентом
test/ протокол и сквозной тест двух браузеров
tools/
check-text.mjs проверка текстовой гигиены (см. «Тесты»)
Почему движок лежит в репозитории, а не берётся с CDN. Раньше three.js
подключался с unpkg.com, и под этот отказ в коде был написан отдельный экран
ошибки. Собственный файл убирает целый класс отказов, делает установленную
PWA работоспособной офлайн с первого запуска и позволяет автоматически
тестировать рендер.
Сервер — единственный источник правды. Клиент знает только свою доску и результаты своих выстрелов.
- Клиент подключается к
ws://host/ws, получаетhello. registerзакрепляет ник за токеном изlocalStorage.invite→inviteRespсоздают матч.readyс расстановкой флота — сервер её проверяет.fire— сервер судит выстрел и рассылает результат обеим сторонам.gameOver— только здесь сервер раскрывает флот проигравшего.
- Расстановку проверяет сервер: ровно 10 кораблей 4/3/3/2/2/2/1/1/1/1, без касаний бортами и углами, всё в пределах поля.
- Чужой флот не отправляется в браузер до конца боя, поэтому подглядеть его нельзя ни через инструменты разработчика, ни через камеру: соответствующих данных в клиенте просто нет. Это проверяется тестом, который читает и память клиента, и число видимых моделей в сцене.
- Выстрел вне своего хода, по обстрелянной клетке и до готовности флота отклоняется сервером.
- Ник закрепляется за токеном владения: занятый ник чужим игроком не берётся.
рейтинг = победы×100 − поражения×40 + точность×120 + потопленные корабли×2
Виден из лобби и доступен по HTTP.
| От клиента | Назначение |
|---|---|
register |
занять ник |
lobby |
запросить список игроков в сети |
invite · inviteResp · cancelInvite |
вызов на бой |
ready |
отправить расстановку флота |
fire |
выстрел |
leave · rematch |
покинуть бой, предложить реванш |
leaderboard |
запросить таблицу |
| От сервера | Назначение |
|---|---|
hello · registered · lobby |
подключение и лобби |
inviteFrom · inviteDeclined · inviteCancelled |
вызовы |
matchStart · readyOk · battleStart |
начало матча и боя |
shotResult · incoming |
результаты выстрелов |
gameOver · opponentLeft · rematchOffer |
финал |
error |
отказ с причиной |
| Метод | Назначение |
|---|---|
GET /api/leaderboard |
таблица рейтинга |
GET /api/health |
состояние сервера: игроков в сети, активных матчей |
| Переменная | Назначение | По умолчанию |
|---|---|---|
PORT |
порт HTTP и WebSocket | 8080 |
SB_DATA_DIR |
папка для players.json с рейтингами |
server/data |
SB_QUIET |
1 — не писать логи в консоль |
выкл. |
cd seabattle/server
npm test # текстовая гигиена + протокол (32 проверки)
npm run test:text # только проверка текста
npm run test:protocol # только протокол и правила
# сквозной тест: два браузера играют онлайн-матч целиком
PORT=8099 node server.js &
node test/test-e2e.js # нужен playwrighttools/check-text.mjs ловит то, что легко просачивается незаметно:
битые символы U+FFFD, файлы не в UTF-8, дробные размеры шрифта, приёмы,
размывающие текст, захардкоженные ключи доступа и эмодзи вместо иконок.
Сквозной тест идёт на настоящем движке и настоящем WebGL (через программный OpenGL), а не на заглушке: подделка, которую надо держать в курсе всех изменений API, однажды уже оставила проверку утечки чужой доски бессильной.
Любая площадка, где можно запустить Node.js 18+ и держать открытым WebSocket. Внешних зависимостей нет, сборка не нужна.
PORT=8080 SB_DATA_DIR=/var/lib/seabattle node seabattle/server/server.jsЗа обратным прокси нужно пропустить Upgrade и Connection для /ws.
Рейтинги хранятся в players.json в SB_DATA_DIR — эту папку стоит сделать
постоянным томом.
MIT.