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 :
orientationest un index dans la listePieceDef.orientationsgénérée par le moteur.anchor_x/anchor_ydé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(sansIniO), 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://ouwss://selon la page, même hôte, chemin/ws. - Le jeton reçu dans
GameCreated/Joinedest stocké danslocalStorage(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) |