Aller au contenu

Architecture

Vue d'ensemble

Un seul processus serveur (orapa-server) sert à la fois le client compilé (fichiers statiques) et le WebSocket de jeu. Le moteur orapa-core est compilé deux fois : en natif pour le serveur, en WebAssembly pour le navigateur.

flowchart LR
  subgraph NAV["Navigateur"]
    UI["Composants React<br/>web/src/components"] --> STORE["Store zustand<br/>store/gameStore.ts"]
    STORE --> SOCK["GameSocket<br/>net/ws.ts"]
    UI --> WC["wasmClient/index.ts"]
    WC --> WASM["orapa_wasm.wasm"]
  end

  subgraph SRV["orapa-server (processus unique)"]
    ROUTER["Routeur axum<br/>lib.rs"] --> WSH["ws.rs<br/>connexion et liaison"]
    WSH --> LOGIC["logic.rs<br/>actions de jeu"]
    LOGIC --> STATE[("AppState<br/>parties en mémoire")]
    ROUTER --> STATIC["ServeDir web/dist<br/>repli SPA index.html"]
  end

  SOCK <-->|"WebSocket JSON /ws"| ROUTER
  LOGIC --> CORE["orapa-core"]
  WASM -.->|"compilé depuis"| CORE
  CLI["orapa-cli"] --> CORE
  CORE --> PIECES["data/pieces.json<br/>embarqué via include_str!"]

Workspace Rust

Cargo.toml à la racine déclare un workspace (resolver 2, version 0.1.0, édition 2021) avec 4 crates. Profil release : opt-level = "s", lto = true.

Crate Type Dépendances clés Rôle
orapa-core lib serde, serde_json, rand Moteur pur, sans UI ni réseau
orapa-server bin + lib orapa-core, axum 0.7 (ws), tokio, tower-http (fs, trace), dashmap, uuid, tracing Serveur de jeu
orapa-wasm cdylib + rlib orapa-core, wasm-bindgen, getrandom (feature js sur wasm32) Bindings navigateur
orapa-cli bin orapa-core, clap, rand Démo ASCII

orapa-core

Module Contenu principal
geometry.rs Corner, CellKind (square, tri_*), Cell, Direction, rotation 90° et miroir des formes
pieces.rs PieceCatalog chargé depuis data/pieces.json (include_str!), génération et dédoublonnage des orientations, GemColor, Special, Expansion
board.rs PlacedPiece, Board (grille remplie à partir d'un placement), BoardCell
beam.rs entry_points (36 points), PointIndex, fire_beam, fire_beam_traced, BeamOutcome
colors.rs mix_colors, ResultColor (16 résultats)
placement.rs validate_placement, Violation
solution.rs compare_solution (comparaison case par case)
generator.rs random_valid_placement (tirage avec rejet), GameOptions (diamond, black)

Une pièce posée (PlacedPiece) se sérialise ainsi :

{ "piece_id": "red", "orientation": 2, "anchor_x": 4, "anchor_y": 3 }
  • orientation est un index dans la liste PieceDef.orientations générée par le moteur.
  • anchor_x / anchor_y désignent le coin haut-gauche de la boîte englobante de cette orientation.

Index d'orientation

L'ordre des orientations dépend de la forme de base dans data/pieces.json. Client (WASM) et serveur doivent donc être compilés à partir du même catalogue. C'est automatique dans l'image Docker. En développement, il faut relancer npm run build:wasm après toute modification du catalogue.

orapa-server

Fichier Rôle
main.rs Lit PORT (défaut 8080) et ORAPA_STATIC_DIR (défaut web/dist), initialise tracing (filtre RUST_LOG, sinon info), écoute sur 0.0.0.0
lib.rs build_router : route /ws, repli sur ServeDir(static_dir). Tout chemin inconnu renvoie index.html (SPA).
protocol.rs Types ClientMsg / ServerMsg et structures sérialisées (voir Protocole)
state.rs AppState (registre des parties), Game, Player, Kind (Duel / Solo), génération des codes de partie
ws.rs Boucle par connexion : création, jonction, reconnexion, solo, puis routage vers logic.rs
logic.rs Actions de jeu, résolution des solutions et de la mort subite, minuteur de tour, revanche, mode aide serveur
tests/integration.rs Tests WebSocket bout en bout (vrai routeur axum et client tokio-tungstenite)

État en mémoire (AppState) :

  • games : DashMap<code, Arc<Mutex<Game>>>.
  • tokens : DashMap<Uuid, code>, qui permet la reconnexion avec le seul jeton.
  • catalog : catalogue partagé.
  • Codes de partie : 5 lettres tirées dans ABCDEFGHJKLMNPQRSTUVWXYZ (sans I ni O), uniques parmi les parties en mémoire.

Une partie (Game) contient notamment kind, options, players (jeton UUID v4, pseudo, connecté, prêt, vies), secrets[i] (disposition défendue par le joueur i), phase, current_turn, first_player_index, sudden_death, history, et un émetteur par joueur (senders).

Concurrence : chaque connexion WebSocket a une tâche d'écriture alimentée par un canal mpsc non borné. Les actions verrouillent le Mutex de la partie, mutent l'état, puis diffusent un StateUpdate personnalisé à chaque joueur.

Minuteur de tour : après ReadyToPlay, FireBeam, Probe et SubmitSolution, une tâche tokio dort turn_timer_secs secondes. Un compteur turn_generation, incrémenté à chaque changement de tour, rend les minuteurs obsolètes inopérants.

Pas de purge des parties

Aucune entrée de games ni de tokens n'est jamais supprimée : les parties terminées ou abandonnées restent en mémoire jusqu'au redémarrage du processus.

orapa-wasm

Fonctions exportées (toutes renvoient du JSON sous forme de chaîne) :

Fonction Retour Usage côté client
catalog_json() PieceCatalog (grille, règles, pièces avec toutes leurs orientations) Chargé au démarrage. L'app attend le WASM avant tout rendu.
validate_placement_json(pieces) enveloppe {ok, value: Violation[]} ou {ok: false, error} Validation en direct dans PlacementEditor
fire_beam_json(pieces, entry) enveloppe BeamOutcome Mode aide (rejeu des ondes)
probe_json(pieces, x, y) enveloppe {kind, color?}, avec kind parmi Empty, Color, OccupiedNoColor Mode aide. Converti au format serveur par localProbe
grid_width(), grid_height(), version() nombres / chaîne Non utilisés par wasmClient

Les bindings sont générés dans web/src/wasm/ (ignoré par git) par wasm-bindgen --target web --typescript. La version de wasm-bindgen-cli doit être exactement celle de la dépendance wasm-bindgen du Cargo.lock (0.2.127), sinon le module ne se charge pas dans le navigateur.

orapa-cli

Démo du moteur : tire un plateau avec une graine, l'affiche en ASCII et donne le résultat d'une onde ou des 36. Voir Développement.

Client web (web/)

React 19 + TypeScript, bundlé par Vite, état global avec zustand. Thème « pixel art » (polices Google Fonts Press Start 2P et VT323). Pas de StrictMode, pour éviter que le double montage des effets en dev n'ouvre deux WebSockets.

Chemin Rôle
src/App.tsx Initialise le WASM, restaure la session, ouvre la connexion, choisit l'écran selon la phase
src/store/gameStore.ts Store zustand : traduit les ServerMsg en état, expose une action par commande du protocole
src/net/ws.ts GameSocket : connexion, envoi typé, reconnexion automatique
src/types/protocol.ts Miroir TypeScript des types Rust du protocole, synchronisé à la main
src/wasmClient/index.ts Chargement unique du module WASM et désenveloppage des réponses {ok, value} / {ok: false, error}
src/lib/points.ts Réplique de la disposition des 36 points de tir
src/lib/colors.ts Palette, et RESULT_MIX (inverse de mix_colors) pour expliquer les résultats
src/lib/mixExplain.ts Phrases d'explication des mélanges (infobulles)
src/lib/randomPseudo.ts Générateur de pseudo aléatoire
src/i18n/fr.ts Tous les libellés (dictionnaire plat et fonction t())
src/components/ Lobby, PlacementScreen, PlacementEditor, PieceTray, GameScreen, Board (SVG), HistoryPanel, ColorLegend, ResultScreen, RulesModal, RuleDiagrams, Logo
src/styles/theme.css Thème global

Écran affiché selon la phase :

Phase (store) Écran
connecting, lobby Lobby (formulaire, ou salle d'attente avec le code)
placement PlacementScreen
playing GameScreen : grille adverse avec trajets des ondes, légende, historique, grille perso, grille d'hypothèses
finished ResultScreen : issue, plateaux révélés, historique, bouton Revanche (duel uniquement)

Connexion et reconnexion (GameSocket) :

  • URL : ws:// ou wss:// selon la page, même hôte, chemin /ws.
  • Le jeton reçu dans GameCreated / Joined est stocké dans localStorage (clé orapamine_token).
  • En cas de coupure, nouvelle tentative après 0,5 s, 1 s, 2 s, 4 s, puis toutes les 8 s. À l'ouverture, envoi de Reconnect { token }.
  • Retour au menu oublie le jeton, ferme la connexion et en rouvre une vierge.

Flux client / serveur

Tir d'une onde

sequenceDiagram
  participant A as Joueur actif (navigateur)
  participant S as orapa-server
  participant C as orapa-core
  participant B as Adversaire (navigateur)
  A->>S: FireBeam { entry: "C" }
  S->>S: ensure_turn, plateau secret adverse
  S->>C: fire_beam(plateau, points, "C")
  C-->>S: BeamOutcome
  S->>S: history.push(Beam), switch_turn
  S-->>A: BeamResult
  S-->>A: StateUpdate (historique, tour)
  S-->>B: StateUpdate (historique, tour)

Validation et vérifications locales

Calcul Où Données utilisées
Validation pendant le placement Navigateur (WASM) Placement du joueur
Validation à la soumission (SetPlacement) Serveur Placement du joueur
Tir d'onde, sondage Serveur Disposition secrète adverse
Vérification d'une proposition Serveur Disposition secrète adverse
Mode aide (cohérence des hypothèses) Navigateur (WASM) Grille d'hypothèses et historique public
Placement aléatoire Serveur Catalogue

Mode aide côté serveur

Le serveur implémente aussi CheckHypothesis (même rejeu, réponse HypothesisCheckResult). Le client actuel ne l'appelle que si le mode aide est désactivé, cas où le bouton n'est pas affiché et où le serveur refuserait la requête : en pratique, la vérification est toujours locale.

Logique dupliquée à garder synchronisée

Source Rust Copie côté client
orapa-server/src/protocol.rs web/src/types/protocol.ts
orapa-core/src/beam.rs (entry_points) web/src/lib/points.ts
orapa-core/src/colors.rs (mix_colors) web/src/lib/colors.ts (RESULT_MIX)
orapa-core/src/geometry.rs (convention des triangles) web/src/components/Board.tsx (TRIANGLE_POINTS)