Архитектура мультиплеера
Мультиплеер в En Parlant~ использует WebSocket-ретранслятор для соединения двух игроков в реальном времени. Прямого соединения между игроками нет — вся коммуникация проходит через ретранслятор. Это упрощает сетевое взаимодействие и обеспечивает надёжную работу за файрволами и NAT.
Обзор архитектуры
Заголовок раздела «Обзор архитектуры»Player A (host) Relay Server Player B (joiner) | | | |--- create_game(name) ----------->| | |<-- game_created(code) -----------| | | |<--- join_game(code, name) -----| |<-- game_joined(name) ------------|--- game_joined(name) -------->| | | | |--- game_move(uci, times) ------->|--- game_move(uci, times) --->| |<-- game_move(uci, times) --------|<-- game_move(uci, times) ----| | | | |--- heartbeat ------------------>|--- peer_heartbeat ----------->|Ретранслятор — это тонкий слой пересылки. Он хранит комнаты в памяти, маршрутизирует события между двумя игроками в каждой комнате и занимается очисткой. На сервере нет игровой логики или валидации ходов — авторитетными являются клиенты.
Технологический стек
Заголовок раздела «Технологический стек»- Фронтенд (клиент): socket.io-client — стандартный JavaScript-клиент Socket.IO
- Бэкенд (ретранслятор): socketioxide — Socket.IO сервер на Rust, построенный на Axum
- Протокол: Socket.IO поверх WebSocket (с автоматическим откатом на HTTP long-polling при необходимости)
Socket.IO был выбран вместо чистого WebSocket, поскольку из коробки предоставляет автоматическое переподключение, управление комнатами/пространствами имён и структурированную обработку событий.
Игровой процесс
Заголовок раздела «Игровой процесс»1. Создание игры
Заголовок раздела «1. Создание игры»Хост нажимает Multiplayer и вводит своё отображаемое имя. Клиент отправляет событие create_game с именем. Сервер:
- Генерирует уникальный 6-символьный код комнаты
- Создаёт комнату и добавляет хоста как первого игрока
- Отвечает событием
game_created(code), чтобы хост мог поделиться кодом
2. Присоединение к игре
Заголовок раздела «2. Присоединение к игре»Присоединяющийся игрок вводит код комнаты и своё отображаемое имя. Клиент отправляет join_game(code, name). Сервер:
- Находит комнату по коду
- Добавляет присоединяющегося как второго игрока
- Отправляет
game_joined(name)обоим игрокам — каждый получает отображаемое имя другого
3. Игра
Заголовок раздела «3. Игра»Когда оба игрока в комнате:
- Хост всегда играет белыми, присоединившийся — чёрными. Это определяется порядком подключения, а не согласованием.
- Ходы отправляются в формате UCI (например,
e2e4) вместе с показаниями часов обоих игроков. - Ретранслятор пересылает каждое событие
game_moveпротивнику. Оба игрока также получают свои собственные ходы обратно в качестве подтверждения.
4. События окончания игры
Заголовок раздела «4. События окончания игры»Несколько событий обрабатывают сценарии завершения партии:
- Сдача — пересылается противнику для отображения диалога о сдаче
- Предложение ничьей — пересылается противнику, который может принять или проигнорировать
- Принятие ничьей — пересылается обоим игрокам для завершения партии вничью
5. Реванш
Заголовок раздела «5. Реванш»После окончания партии любой игрок может отправить событие ready, чтобы сообщить о желании сыграть реванш. Когда оба игрока отправили ready, клиенты сбрасывают доску и меняют цвета (или сохраняют их — это определяется на стороне клиента).
Коды комнат
Заголовок раздела «Коды комнат»Коды комнат состоят из 6 символов и форматируются как XX-XX-XX для удобства при устном сообщении. Набор символов исключает визуально неоднозначные знаки:
- Нет
0иO(ноль и буква O) - Нет
1,IиL(единица, заглавная I и заглавная L)
Это позволяет избежать проблемы «это ноль или буква O?», когда кто-то диктует код в голосовом чате или набирает его из сообщения друга.
Система heartbeat
Заголовок раздела «Система heartbeat»Мультиплеерным соединениям необходимо обнаруживать отключение игрока — будь то из-за потери сети, закрытия приложения или перехода ноутбука в спящий режим. En Parlant~ использует для этого систему heartbeat:
- Каждый клиент отправляет событие
heartbeatна сервер каждые 5 секунд - Сервер подтверждает heartbeat и пересылает его противнику как
peer_heartbeat - Клиент отслеживает, когда последний раз получал
peer_heartbeatот противника - Функция
isPeerAlive(timeoutMs)проверяет, укладывается ли последний heartbeat противника в допустимый порог
Это управляет индикатором состояния подключения в интерфейсе. Если heartbeat перестают приходить, игрок видит, что противник, возможно, отключился, и может решить подождать или покинуть игру.
Очистка комнат
Заголовок раздела «Очистка комнат»Ретранслятор автоматически удаляет неактивные комнаты для предотвращения утечек памяти:
- Комната считается неактивной после 30 минут без какой-либо активности
- Задача очистки запускается каждые 60 секунд, удаляя все комнаты, превысившие порог неактивности
- При очистке комнаты все оставшиеся соединения разрываются
Постоянного хранилища нет. При перезапуске сервера все комнаты исчезают. Это сделано намеренно — ретранслятор не хранит состояния и является эфемерным. Незавершённые партии придётся начать заново, но на практике это случается редко.
Развёртывание
Заголовок раздела «Развёртывание»Ретранслятор по умолчанию работает на Fly.io, обеспечивая низкую задержку WebSocket-соединений с автоматическим TLS. Инструкции по запуску собственного ретранслятора смотрите в руководстве по настройке мультиплеерного сервера.
Локальное тестирование
Заголовок раздела «Локальное тестирование»Для тестирования мультиплеера в процессе разработки:
-
Клонируйте и запустите ретранслятор:
Окно терминала git clone https://github.com/DarrellThomas/en-parlant-relay.gitcd en-parlant-relaycargo runСервер запускается на порту 3210.
-
В En Parlant~ измените URL ретранслятора на
ws://localhost:3210. -
Откройте два экземпляра приложения (или один в обычном режиме и один в режиме разработки) для имитации обоих игроков.
Ходы, heartbeat и все события работают идентично продакшн-ретранслятору — единственное отличие — URL подключения.