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://ouwss://selon la page). - Trames texte contenant un objet JSON. Les autres trames sont ignorées, une trame
Closetermine la connexion. - Chaque message porte un champ
typeégal au nom de la variante (CreateGame,StateUpdate…). - Les sous-objets
BeamOutcome,ViolationetHistoryEntrysont discriminés par un champkind. Les énumérations simples (Phase,GemColor,ResultColor,ProbeOutcome) sont ensnake_case. - Un JSON illisible ou un
typeinconnu renvoieError { code: "bad_message" }. La connexion reste ouverte.
Cycle de vie d'une connexion¶
- Non liée : seuls
CreateGame,JoinGame,ReconnectetStartSolosont acceptés. Tout autre message renvoieError not_bound. - 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 donneError already_bound. - Fermeture : le joueur passe
connected: false, unStateUpdateest 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} |
actorest l'index du joueur qui a fait l'action (sur la grille de son adversaire).- Les index de
Violationrenvoient aux positions dans le tableaupiecessoumis. - Les valeurs de
ResultColorsont 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, ounullpour une égalité (duel) ou un échec (solo, plus de vie).boards[i]: disposition secrète défendue par le joueuri. En solo,boards[0]est vide etboards[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 repasseconnected: trueet diffuse unStateUpdate. GameCreated/Joinedne sont pas renvoyés : le client ne récupère donc pas le code de partie, absent deStateUpdate.your_placementpermet 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, dansyour_placement(pour son propre auteur) et dansSolutionRevealed(solo uniquement). - L'identité d'un joueur repose uniquement sur son jeton UUID v4, conservé dans le
localStoragedu 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.