Aller au contenu

Protocole WebSocket

Sources : crates/orapa-server/src/protocol.rs (types), ws.rs (connexion), logic.rs (traitement), et le miroir TypeScript web/src/types/protocol.ts.

Deux définitions à synchroniser

protocol.ts est maintenu à la main. Toute modification de protocol.rs (ou des types sérialisés de orapa-core) doit y être reportée.

Transport

  • Point d'entrée : GET /ws (upgrade WebSocket), sur la même origine que le client (ws:// ou wss:// selon la page).
  • Trames texte contenant un objet JSON. Les autres trames sont ignorées, une trame Close termine la connexion.
  • Chaque message porte un champ type égal au nom de la variante (CreateGame, StateUpdate…).
  • Les sous-objets BeamOutcome, Violation et HistoryEntry sont discriminés par un champ kind. Les énumérations simples (Phase, GemColor, ResultColor, ProbeOutcome) sont en snake_case.
  • Un JSON illisible ou un type inconnu renvoie Error { code: "bad_message" }. La connexion reste ouverte.

Cycle de vie d'une connexion

  1. Non liée : seuls CreateGame, JoinGame, Reconnect et StartSolo sont acceptés. Tout autre message renvoie Error not_bound.
  2. Liée à un couple (partie, index joueur) : les messages de jeu sont routés vers logic.rs. Renvoyer l'un des 4 messages d'établissement donne Error already_bound.
  3. Fermeture : le joueur passe connected: false, un StateUpdate est diffusé. La partie reste en mémoire et le jeton permet d'y revenir.

États de partie

stateDiagram-v2
  [*] --> lobby: CreateGame
  [*] --> playing: StartSolo
  lobby --> placement: JoinGame du 2e joueur
  placement --> playing: les 2 joueurs prêts
  playing --> finished: proposition décisive
  finished --> placement: RequestRematch des 2 joueurs (duel)
  finished --> playing: RequestRematch (solo)
Phase Ce qui se passe current_turn first_player_index
lobby Le créateur attend un adversaire (duel uniquement) null null
placement Chacun soumet sa disposition puis se déclare prêt null null
playing Ondes, sondages, propositions index du joueur actif renseigné
finished Plateaux révélés (GameOver), revanche possible null renseigné

La mort subite n'est pas une phase : c'est le booléen sudden_death de StateUpdate, levé quand le premier joueur trouve la solution (voir Règles).

Types communs

RoomOptions

Tous les champs ont une valeur par défaut côté serveur (#[serde(default)]) et peuvent être omis. L'objet options lui-même reste obligatoire dans CreateGame et StartSolo.

Champ Type Défaut serveur Défaut client (lobby) Choix proposés par le client
diamond booléen false false case à cocher
black booléen false false case à cocher
lives entier (u8) 1 1 1 ou 2
turn_timer_secs entier ou null null null aucune, 30, 60, 120
help_mode booléen false true case à cocher

Autres structures

Type Exemples JSON
PlacedPiece {"piece_id":"red","orientation":2,"anchor_x":4,"anchor_y":3}
BeamOutcome {"kind":"Exit","point":"14","color":"purple"}, {"kind":"Absorbed"}, {"kind":"Lost"}
ProbeOutcome "empty", "occupied_no_color", {"color":"red"}
Violation {"kind":"OutOfBounds","placement_index":0}, {"kind":"Overlap","placement_index":0,"other_index":1}, {"kind":"EdgeContact",…}, {"kind":"Unreachable","placement_index":2}, {"kind":"WhiteSymmetry"}
HistoryEntry {"kind":"Beam","actor":0,"entry":"C","outcome":{…}}, {"kind":"Probe","actor":1,"x":3,"y":4,"result":"empty"}, {"kind":"Solution","actor":0,"correct":false}, {"kind":"Timeout","actor":1}
PlayerPublicInfo {"pseudo":"Alice","connected":true,"ready":false,"has_placement":true,"lives":1}
  • actor est l'index du joueur qui a fait l'action (sur la grille de son adversaire).
  • Les index de Violation renvoient aux positions dans le tableau pieces soumis.
  • Les valeurs de ResultColor sont listées dans Règles › Mélange des couleurs.

Messages client → serveur

type Champs Conditions Réponse(s)
CreateGame pseudo, options connexion non liée GameCreated puis StateUpdate (phase lobby)
JoinGame code, pseudo non liée. Code insensible à la casse, partie duel avec moins de 2 joueurs. Joined, puis StateUpdate aux deux joueurs (phase placement). Sinon Error join_failed.
Reconnect token non liée StateUpdate diffusé à tous. Sinon Error reconnect_failed.
StartSolo pseudo, options non liée GameCreated puis StateUpdate (phase playing)
SetPlacement pieces: PlacedPiece[] phase placement Valide : enregistrée, StateUpdate diffusé. Sinon PlacementRejected ou Error wrong_phase / invalid_placement.
RequestRandomPlacement — aucune RandomPlacement (proposition non enregistrée : il faut ensuite un SetPlacement)
ReadyToPlay — phase placement et disposition enregistrée StateUpdate. Quand les deux sont prêts : phase playing, premier joueur tiré au sort. Sinon Error not_placed.
FireBeam entry (ex. "C", "14") phase playing, son tour (duel) BeamResult puis StateUpdate diffusé. Sinon Error wrong_turn / no_opponent_board / invalid_entry.
Probe x, y idem ProbeResult puis StateUpdate. Sinon Error out_of_bounds…
SubmitSolution pieces: PlacedPiece[] idem SolutionResult, StateUpdate, et GameOver si la partie se termine
CheckHypothesis pieces: PlacedPiece[] help_mode actif (pas de contrôle de phase ni de tour) HypothesisCheckResult. Sinon Error help_mode_disabled.
Reaction id (le client envoie gg, oops, think) aucune ReactionReceived diffusé à tous les joueurs, émetteur compris
RequestRematch — phase finished (ignoré sinon) StateUpdate. Quand tous l'ont demandé, la partie est réinitialisée.
RevealSolution — partie solo SolutionRevealed. Sinon Error not_solo.

En solo, FireBeam, Probe et SubmitSolution ne vérifient pas le tour.

Messages serveur → client

type Champs Destinataire Émis quand
GameCreated code, token, player_index créateur après CreateGame ou StartSolo
Joined token, player_index (toujours 1) arrivant après JoinGame
StateUpdate voir ci-dessous chaque joueur, version personnalisée après presque chaque changement d'état, connexion ou déconnexion
PlacementRejected violations: Violation[] auteur SetPlacement invalide
RandomPlacement pieces demandeur RequestRandomPlacement
BeamResult entry, outcome: BeamOutcome tireur FireBeam
ProbeResult x, y, result: ProbeOutcome auteur Probe
SolutionResult correct auteur SubmitSolution
HypothesisCheckResult consistent, contradicting_entries: string[] demandeur CheckHypothesis
GameOver winner (index ou null), boards: PlacedPiece[][], history tous fin de partie
ReactionReceived player_index, id tous Reaction
SolutionRevealed board: PlacedPiece[] demandeur RevealSolution (solo)
Error code, message (en français) auteur voir Codes d'erreur

StateUpdate

Champ Contenu
phase lobby, placement, playing ou finished
players PlayerPublicInfo[] (index 0 = créateur)
your_index index du destinataire
current_turn joueur actif, seulement en playing
first_player_index premier joueur, en playing et finished
sudden_death mort subite en cours
options RoomOptions de la partie
history historique complet des actions des deux joueurs
your_placement la disposition déjà soumise par le destinataire (ou null), pour la restaurer après un rechargement

GameOver

  • winner : index du gagnant, ou null pour une égalité (duel) ou un échec (solo, plus de vie).
  • boards[i] : disposition secrète défendue par le joueur i. En solo, boards[0] est vide et boards[1] contient la disposition générée.

Codes d'erreur

Le client affiche message dans un bandeau (un clic le ferme).

code Déclencheur
bad_message JSON illisible ou type inconnu (message = erreur serde)
not_bound Action de jeu avant création, jonction ou reconnexion
already_bound CreateGame / JoinGame / Reconnect / StartSolo sur une connexion déjà liée
join_failed Code inconnu, partie solo, ou partie complète
reconnect_failed Jeton mal formé, ou partie ou joueur introuvable (par exemple après un redémarrage du serveur)
wrong_phase SetPlacement hors phase placement
invalid_placement piece_id ou orientation inconnus dans SetPlacement
not_placed ReadyToPlay sans disposition enregistrée, ou hors phase placement
wrong_turn Partie pas en cours, ou pas son tour
no_opponent_board Disposition adverse absente
invalid_entry Point de tir inconnu
out_of_bounds Sondage hors grille
help_mode_disabled CheckHypothesis sans mode aide
not_solo RevealSolution en duel

Exemple : déroulé d'un duel

sequenceDiagram
  participant A as Alice (index 0)
  participant S as Serveur
  participant B as Bob (index 1)
  A->>S: CreateGame
  S-->>A: GameCreated (code, token) + StateUpdate lobby
  B->>S: JoinGame (code)
  S-->>B: Joined (token)
  S-->>A: StateUpdate placement
  S-->>B: StateUpdate placement
  A->>S: SetPlacement + ReadyToPlay
  B->>S: SetPlacement + ReadyToPlay
  S-->>A: StateUpdate playing (current_turn tiré au sort)
  S-->>B: StateUpdate playing
  loop Tours alternés
    A->>S: FireBeam / Probe / SubmitSolution
    S-->>A: BeamResult / ProbeResult / SolutionResult
    S-->>A: StateUpdate
    S-->>B: StateUpdate
  end
  S-->>A: GameOver (winner, boards, history)
  S-->>B: GameOver

Échange minimal (valeurs d'exemple) :

{"type":"CreateGame","pseudo":"Alice","options":{"diamond":false,"black":false,"lives":1,"turn_timer_secs":null,"help_mode":true}}
{"type":"GameCreated","code":"KQXRT","token":"<uuid v4>","player_index":0}
{"type":"FireBeam","entry":"C"}
{"type":"BeamResult","entry":"C","outcome":{"kind":"Exit","point":"14","color":"purple"}}

Reconnexion

  • Le client renvoie Reconnect { token } à chaque réouverture du socket (voir Architecture). Le serveur remplace l'émetteur du joueur, le repasse connected: true et diffuse un StateUpdate.
  • GameCreated / Joined ne sont pas renvoyés : le client ne récupère donc pas le code de partie, absent de StateUpdate.
  • your_placement permet de restaurer la grille du joueur, si aucune édition locale n'est en cours.

Conséquence côté client

Après un rechargement de page, code est vide dans le store. La salle d'attente du lobby (qui affiche le code) et le bouton Retour au menu (affiché seulement si le code est connu) ne réapparaissent pas.

Sécurité et confiance

  • Les dispositions secrètes ne quittent le serveur que dans GameOver, dans your_placement (pour son propre auteur) et dans SolutionRevealed (solo uniquement).
  • L'identité d'un joueur repose uniquement sur son jeton UUID v4, conservé dans le localStorage du navigateur.
  • Le serveur ne valide ni la longueur du pseudo ni la valeur de lives : c'est le client qui limite à 20 caractères et à 1 ou 2 vies.