Gå til innholdet

Flerspillerarkitektur

Flerspiller i En Parlant~ bruker en WebSocket-reléserver for å koble sammen to spillere i sanntid. Det er ingen peer-to-peer — all kommunikasjon går gjennom reléet. Dette holder nettverkslaget enkelt og fungerer pålitelig på tvers av brannmurer og NAT-er.

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 ----------->|

Reléet er et tynt videresendingslag. Det holder rom i minnet, ruter hendelser mellom de to spillerne i hvert rom, og håndterer opprydding. Det er ingen spillogikk eller trekkvalidering på serveren — klientene er autoritative.

  • Frontend (klient): socket.io-client — den standard Socket.IO JavaScript-klienten
  • Backend (relé): socketioxide — en Rust Socket.IO-server bygget på Axum
  • Protokoll: Socket.IO over WebSocket (med automatisk fallback til HTTP long-polling ved behov)

Socket.IO ble valgt fremfor rå WebSocket fordi det gir automatisk gjenoppkobling, rom-/navneromsadministrasjon og strukturert hendelseshåndtering rett ut av boksen.

Verten klikker Multiplayer og skriver inn visningsnavnet sitt. Klienten sender en create_game-hendelse med navnet. Serveren:

  1. Genererer en unik romkode på 6 tegn
  2. Oppretter et rom og legger til verten som den første spilleren
  3. Svarer med game_created(code) slik at verten kan dele koden

Den som blir med skriver inn romkoden og visningsnavnet sitt. Klienten sender join_game(code, name). Serveren:

  1. Slår opp rommet etter kode
  2. Legger til den andre spilleren som spiller nummer to
  3. Sender game_joined(name) til begge spillerne — hver mottar den andres visningsnavn

Når begge spillerne er i rommet:

  • Verten spiller alltid hvit, den som blir med spiller alltid svart. Dette bestemmes av rekkefølgen man ble med, ikke gjennom forhandling.
  • Trekk sendes i UCI-format (f.eks. e2e4) sammen med klokketider for begge spillerne.
  • Reléet videresender hver game_move-hendelse til motstanderen. Begge spillerne mottar også sine egne trekk tilbake som bekreftelse.

Flere hendelser håndterer scenarier ved spillslutt:

  • Gi opp — videresendes til motstanderen for å utløse oppgivelses-dialogen
  • Remistilbud — videresendes til motstanderen, som kan akseptere eller ignorere
  • Akseptere remis — videresendes til begge spillerne for å avslutte partiet som remis

Etter at et parti er over, kan begge spillerne sende en ready-hendelse for å signalisere at de ønsker revansje. Når begge spillerne har sendt ready, nullstiller klientene brettet og bytter farger (eller beholder dem — dette bestemmes på klientsiden).

Romkoder er på 6 tegn, formatert som XX-XX-XX for lesbarhet ved muntlig deling. Tegnsettet ekskluderer visuelt tvetydige tegn:

  • Ingen 0 eller O (null vs. bokstaven O)
  • Ingen 1, I eller L (én vs. stor I vs. stor L)

Dette unngår problemet med «er det en null eller en O?» når noen leser en kode over talchat eller skriver den inn fra en venns melding.

Flerspillertilkoblinger må oppdage når en spiller kobler fra — enten det skyldes nettverkstap, lukking av appen eller at laptopen går i dvale. En Parlant~ bruker et hjerteslag-system for dette:

  1. Hver klient sender en heartbeat-hendelse til serveren hvert 5. sekund
  2. Serveren bekrefter hjerteslaget og videresender det til motstanderen som peer_heartbeat
  3. Klienten holder styr på når den sist mottok et peer_heartbeat fra motstanderen
  4. Funksjonen isPeerAlive(timeoutMs) sjekker om motstanderens siste hjerteslag er innenfor akseptabel terskel

Dette driver tilkoblingsstatusindikatoren i brukergrensesnittet. Hvis hjerteslagene slutter å komme, ser spilleren at motstanderen kan ha koblet fra, og kan velge å vente eller forlate spillet.

Reléserveren fjerner automatisk inaktive rom for å forhindre minnelekkasjer:

  • Et rom anses som inaktivt etter 30 minutter uten aktivitet
  • En oppryddingsoppgave kjører hvert 60. sekund og rydder bort rom som har overskredet inaktivitetsterskelen
  • Når et rom ryddes bort, droppes eventuelle gjenværende tilkoblinger

Det er ingen persistent lagring. Hvis serveren starter på nytt, er alle rom borte. Dette er tilsiktet — reléet er tilstandsløst og flyktig. Pågående partier må startes på nytt, men dette skjer sjelden i praksis.

Standard-reléet kjører på Fly.io, som gir WebSocket-tilkoblinger med lav latens og automatisk TLS. Se oppsettguiden for flerspillerserveren for instruksjoner om å kjøre ditt eget relé.

For å teste flerspiller under utvikling:

  1. Klon og kjør reléet:

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

    Serveren starter på port 3210.

  2. I En Parlant~, endre reléserverens URL til ws://localhost:3210.

  3. Åpne to instanser av appen (eller én app og én i utviklingsmodus) for å simulere begge spillerne.

Trekk, hjerteslag og alle hendelser fungerer identisk med produksjonsreléet — den eneste forskjellen er tilkoblings-URL-en.