Salta ai contenuti

Architettura Multiplayer

Il multiplayer di En Parlant~ utilizza un server relay WebSocket per connettere due giocatori in tempo reale. Non c’è peer-to-peer: tutta la comunicazione passa attraverso il relay. Questo mantiene la rete semplice e funziona in modo affidabile attraverso firewall e 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 ----------->|

Il relay è un sottile livello di inoltro. Mantiene le stanze in memoria, instrada gli eventi tra i due giocatori in ciascuna stanza e gestisce la pulizia. Non c’è logica di gioco né validazione delle mosse sul server: i client fanno fede.

  • Frontend (client): socket.io-client — il client JavaScript standard di Socket.IO
  • Backend (relay): socketioxide — un server Socket.IO in Rust basato su Axum
  • Protocollo: Socket.IO su WebSocket (con fallback automatico a HTTP long-polling se necessario)

Socket.IO è stato scelto rispetto ai WebSocket puri perché fornisce riconnessione automatica, gestione di stanze/namespace e gestione strutturata degli eventi già inclusi.

L’host clicca su Multiplayer e inserisce il proprio nome visualizzato. Il client emette un evento create_game con il nome. Il server:

  1. Genera un codice stanza univoco di 6 caratteri
  2. Crea una stanza e aggiunge l’host come primo giocatore
  3. Risponde con game_created(code) in modo che l’host possa condividere il codice

Il giocatore che si unisce inserisce il codice stanza e il proprio nome visualizzato. Il client emette join_game(code, name). Il server:

  1. Cerca la stanza tramite il codice
  2. Aggiunge il giocatore come secondo partecipante
  3. Invia game_joined(name) a entrambi i giocatori: ciascuno riceve il nome visualizzato dell’altro

Una volta che entrambi i giocatori sono nella stanza:

  • L’host gioca sempre con il Bianco, chi si unisce gioca sempre con il Nero. Questo è determinato dall’ordine di accesso, non da una negoziazione.
  • Le mosse vengono inviate in formato UCI (ad es. e2e4) insieme ai tempi dell’orologio per entrambi i giocatori.
  • Il relay inoltra ogni evento game_move all’avversario. Entrambi i giocatori ricevono anche le proprie mosse come conferma.

Diversi eventi gestiscono gli scenari di fine partita:

  • Resa — inoltrata all’avversario per attivare la finestra di resa
  • Offerta di patta — inoltrata all’avversario, che può accettare o ignorare
  • Accettazione della patta — inoltrata a entrambi i giocatori per terminare la partita in parità

Al termine di una partita, ciascun giocatore può inviare un evento ready per segnalare la volontà di giocare una rivincita. Quando entrambi i giocatori hanno inviato ready, i client resettano la scacchiera e scambiano i colori (o li mantengono — determinato lato client).

I codici stanza sono di 6 caratteri, formattati come XX-XX-XX per facilitare la lettura quando vengono condivisi a voce. Il set di caratteri esclude quelli visivamente ambigui:

  • Nessun 0 o O (zero vs. lettera O)
  • Nessun 1, I o L (uno vs. I maiuscola vs. L maiuscola)

Questo evita il problema del “è uno zero o una O?” quando qualcuno legge un codice in chat vocale o lo digita dal messaggio di un amico.

Le connessioni multiplayer devono rilevare quando un giocatore si disconnette, che sia per un’interruzione di rete, la chiusura dell’app o un laptop che va in sospensione. En Parlant~ utilizza un sistema di heartbeat per questo scopo:

  1. Ogni client invia un evento heartbeat al server ogni 5 secondi
  2. Il server conferma l’heartbeat e lo inoltra all’avversario come peer_heartbeat
  3. Il client tiene traccia dell’ultimo peer_heartbeat ricevuto dall’avversario
  4. La funzione isPeerAlive(timeoutMs) verifica se l’ultimo heartbeat dell’avversario rientra nella soglia accettabile

Questo alimenta l’indicatore di stato della connessione nell’interfaccia utente. Se gli heartbeat smettono di arrivare, il giocatore vede che l’avversario potrebbe essersi disconnesso e può scegliere se attendere o abbandonare la partita.

Il server relay rimuove automaticamente le stanze inattive per prevenire perdite di memoria:

  • Una stanza è considerata inattiva dopo 30 minuti senza attività
  • Un task di pulizia viene eseguito ogni 60 secondi, eliminando le stanze che hanno superato la soglia di inattività
  • Quando una stanza viene eliminata, le eventuali connessioni rimanenti vengono interrotte

Non è presente alcuno storage persistente. Se il server si riavvia, tutte le stanze vengono perse. Questo è intenzionale: il relay è stateless ed effimero. Le partite in corso dovrebbero essere ricominciate, ma nella pratica questo accade raramente.

Il relay predefinito è ospitato su Fly.io, fornendo connessioni WebSocket a bassa latenza con TLS automatico. Consulta la guida alla configurazione del server Multiplayer per le istruzioni su come eseguire il proprio relay.

Per testare il multiplayer durante lo sviluppo:

  1. Clona e avvia il relay:

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

    Il server si avvia sulla porta 3210.

  2. In En Parlant~, cambia l’URL del server relay in ws://localhost:3210.

  3. Apri due istanze dell’app (o un’app e una in modalità sviluppo) per simulare entrambi i giocatori.

Mosse, heartbeat e tutti gli eventi funzionano in modo identico al relay di produzione: l’unica differenza è l’URL di connessione.