Перейти до вмісту

Архітектура мультиплеєра

Мультиплеєр En Parlant~ використовує WebSocket-реле-сервер для з’єднання двох гравців у реальному часі. Жодного peer-to-peer — уся комунікація проходить через реле. Це спрощує мережеву архітектуру та забезпечує надійну роботу через файрволи та 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 (з автоматичним fallback на HTTP long-polling за потреби)

Socket.IO було обрано замість чистого WebSocket, оскільки він надає автоматичне перепідключення, керування кімнатами/просторами імен та структуровану обробку подій із коробки.

Хост натискає Multiplayer і вводить своє ім’я для відображення. Клієнт надсилає подію create_game з іменем. Сервер:

  1. Генерує унікальний 6-символьний код кімнати
  2. Створює кімнату та додає хоста як першого гравця
  3. Відповідає game_created(code), щоб хост міг поділитися кодом

Гравець, що приєднується, вводить код кімнати та своє ім’я для відображення. Клієнт надсилає join_game(code, name). Сервер:

  1. Знаходить кімнату за кодом
  2. Додає гравця як другого учасника
  3. Надсилає game_joined(name) обом гравцям — кожен отримує ім’я суперника

Коли обидва гравці в кімнаті:

  • Хост завжди грає білими, гравець, що приєднався — чорними. Це визначається порядком приєднання, а не узгодженням.
  • Ходи надсилаються у форматі UCI (наприклад, e2e4) разом із часом на годиннику для обох гравців.
  • Реле пересилає кожну подію game_move супернику. Обидва гравці також отримують свої власні ходи назад як підтвердження.

Кілька подій обробляють сценарії завершення гри:

  • Здача — пересилається супернику для виклику діалогу капітуляції
  • Пропозиція нічиєї — пересилається супернику, який може прийняти або проігнорувати
  • Прийняття нічиєї — пересилається обом гравцям для завершення гри внічию

Після завершення гри будь-який гравець може надіслати подію ready, щоб сигналізувати про бажання реваншу. Коли обидва гравці надіслали ready, клієнти скидають дошку та міняють кольори (або залишають їх — визначається на стороні клієнта).

Коди кімнат складаються з 6 символів у форматі XX-XX-XX для зручності при усному повідомленні. Набір символів виключає візуально неоднозначні символи:

  • Без 0 або O (нуль проти літери O)
  • Без 1, I або L (одиниця проти великої I проти великої L)

Це дозволяє уникнути проблеми «це нуль чи O?», коли хтось диктує код через голосовий чат або набирає його з повідомлення друга.

Система серцебиття (Heartbeat)

Section titled “Система серцебиття (Heartbeat)”

Мультиплеєрні з’єднання повинні виявляти відключення гравця — чи то через обрив мережі, закриття додатку, чи перехід ноутбука в сплячий режим. En Parlant~ використовує для цього систему серцебиття:

  1. Кожен клієнт надсилає подію heartbeat на сервер кожні 5 секунд
  2. Сервер підтверджує серцебиття та пересилає його супернику як peer_heartbeat
  3. Клієнт відстежує, коли він востаннє отримав peer_heartbeat від суперника
  4. Функція isPeerAlive(timeoutMs) перевіряє, чи останнє серцебиття суперника знаходиться в межах допустимого порогу

Це забезпечує роботу індикатора стану з’єднання в інтерфейсі. Якщо серцебиття перестають надходити, гравець бачить, що суперник, можливо, відключився, і може вирішити почекати або покинути гру.

Реле-сервер автоматично видаляє неактивні кімнати для запобігання витокам пам’яті:

  • Кімната вважається неактивною після 30 хвилин без активності
  • Завдання очищення запускається кожні 60 секунд, видаляючи будь-які кімнати, що перевищили поріг неактивності
  • При очищенні кімнати всі активні з’єднання розриваються

Постійне сховище відсутнє. Якщо сервер перезапуститься, всі кімнати зникнуть. Це зроблено навмисно — реле є stateless та ефемерним. Ігри, що тривають, доведеться перезапустити, але на практиці це трапляється рідко.

Реле за замовчуванням працює на Fly.io, забезпечуючи WebSocket-з’єднання з низькою затримкою та автоматичним TLS. Див. посібник з налаштування мультиплеєрного сервера для інструкцій щодо запуску власного реле.

Для тестування мультиплеєра під час розробки:

  1. Клонуйте та запустіть реле:

    Terminal window
    git clone https://github.com/DarrellThomas/en-parlant-relay.git
    cd en-parlant-relay
    cargo run

    Сервер запускається на порту 3210.

  2. В En Parlant~ змініть URL реле-сервера на ws://localhost:3210.

  3. Відкрийте два екземпляри додатку (або один додаток і один у режимі розробки), щоб імітувати обох гравців.

Ходи, серцебиття та всі події працюють ідентично до продакшн-реле — єдина відмінність — це URL підключення.