Files
Ustagammon/docs/plans/2026-11-02-domaine-partie.md
T
2026-11-02 20:45:00 +01:00

9.9 KiB

Domaine « partie » en TypeScript, scénarios exécutés, garde-fous

  • Date : 2026-11-02
  • Étape : 0d.1
  • Statut : terminé (branche plateau-domaine-travail)
  • Exécutant : sous-agent general-purpose, dans un worktree isolé

Objectif

Le domaine d'une partie contre l'ordinateur existe en TypeScript, sous forme de journal d'événements (ADR 0007), branché au moteur WebAssembly par une interface Engine. Le scénario docs/scenarios/jouer-une-partie.feature s'exécute sous Node, sur le vrai moteur, dans make test. Les règles d'architecture et le lien glossaire ↔ code sont vérifiés par les tests. L'interface graphique et la PWA sont l'étape suivante (0d.2), pas celle-ci.

Consignes

  1. Outillage (ADR 0009, ADR 0004) : une image docker/web/ (Node node:22.11.0-alpine3.20 déjà présent, versions npm figées et vérifiées) avec TypeScript, Biome, @cucumber/cucumber et dependency-cruiser. Cibles make web (compile app/ vers build/web/ avec tsc), et intégration à make lint (Biome et tsc --noEmit) et make test. Étiquette d'image dans le Makefile, make setup la construit. CREDITS.md à jour.
  2. Arborescence (à ajuster si plus simple) :
    • app/src/domain/ : le domaine pur, sous-dossier game/ ;
    • app/src/engine/ : l'adaptateur qui implémente Engine avec le moteur WebAssembly (réutiliser web/engine-wasm.mjs et build/wasm/) ;
    • app/src/ui/ : vide pour l'instant (étape 0d.2) ;
    • app/tests/ : tests unitaires (runner intégré de Node, node:test) et définitions des étapes Cucumber. Les pages de prototype existantes (web/index.html, web/bench.html) doivent continuer de fonctionner.
  3. Interface Engine définie par le domaine (coups légaux avec position résultante, meilleur coup pour un niveau, décision de videau, décodage d'une position en plateau pour l'affichage). Compléter engine.h et engine-cli si une information manque (par exemple décoder un Position ID en nombre de pions par case, savoir si la partie est finie et combien elle rapporte), en restant simple, avec tests natifs.
  4. Journal d'événements (ADR 0007, point 3) : événements immuables avec identifiant, parent et version de format (GameStarted, DiceRolled, MovePlayed, CubeOffered, CubeTaken, CubeDropped, GameEnded). L'état se calcule en rejouant le journal. Une branche est un nouvel événement accroché à un ancien ; annuler revient à repartir du parent. Le hasard des dés est injecté (source de dés passée au domaine).
  5. Règles de la partie de 0d : partie libre contre l'ordinateur (Jacoby selon les réglages par défaut de gnubg) ; l'ordinateur joue le meilleur coup au niveau « standard » (choisir un niveau qui tienne les 2 s sur téléphone : 2 plis d'après les mesures 0b, à confirmer) ; il décide du videau avec le moteur ; le joueur peut prendre ou passer, et peut lui-même doubler (le moteur décide alors si l'ordinateur prend ou passe). Le joueur saisit son coup demi-coup par demi-coup (case de départ, case d'arrivée) ; le domaine n'accepte que ce qui mène à un coup légal complet. Aucune règle du jeu n'est réimplémentée : tout vient du moteur.
  6. Glossaire ↔ code (ADR 0008) : chaque type du domaine qui correspond à un terme porte Glossaire : docs/glossaire/<fiche>.md en commentaire, et la fiche reçoit code: <Type>. Ajouter les fiches manquantes. Étendre tests/test_glossary.sh pour vérifier le lien dans les deux sens.
  7. Scénarios exécutés : définitions des étapes en français pour docs/scenarios/jouer-une-partie.feature, branchées sur le domaine et le vrai moteur WebAssembly (sous Node), avec des dés imposés pour être reproductibles. Si une étape du scénario est impossible à exécuter telle quelle, ajuster le scénario en gardant son sens pour le joueur, et le dire.
  8. Garde-fous d'architecture (dependency-cruiser) : le domaine n'importe ni engine/, ni ui/, ni rien de spécifique au navigateur ; ui/ n'importe pas engine/ ; pas de dépendance circulaire. Vérifier qu'une violation volontaire fait échouer make test.

Contraintes :

  • CLAUDE.md, ADR 0004, 0007, 0008, 0009 : simplicité avant tout, pas de patron de conception sans besoin, identifiants en anglais, commentaires et docs en français, tout outil dans Docker, -j2 ;
  • ne pas modifier engine/gnubg/ ni les références ;
  • make lint et make test doivent passer ; commits petits, en français, avec la ligne Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> ;
  • ne pas pousser ni créer de merge request ; ne rien simuler.

Rapport attendu : ce qui a été fait, l'API du domaine en quelques lignes, les fiches de glossaire ajoutées, le résultat des scénarios, les vérifications des garde-fous, les difficultés, les écarts au plan.

Critères de réussite

  • make test exécute le scénario « Jouer une partie contre l'ordinateur » sur le vrai moteur et passe.
  • Une partie complète (jusqu'à GameEnded) se joue dans un test, l'ordinateur contre une stratégie simple du joueur, avec des dés imposés.
  • Les garde-fous d'architecture et le lien glossaire ↔ code échouent sur une violation volontaire.
  • make lint et make test passent.

Résultat

Terminé. make lint et make test passent (9/9, dont le nouveau tests/test_web.sh). Les quatre critères de réussite sont tenus.

Moteur (engine.h, engine-cli, tests natifs dans tests/test_engine.sh) :

  • engine_check_half_moves / halfmoves POSITION_ID DÉS "8/5 6/5" : des demi-coups saisis un par un sont-ils le début d'un coup légal, le forment-ils en entier, et quelle position en résulte. La légalité de chaque demi-coup vient de GenerateMoves de gnubg appelé avec un seul dé ; celle du coup entier, de la liste des coups légaux (jouer le plus de dés possible). L'ordre des demi-coups est libre (6/5 puis 8/5 est accepté) ;
  • engine_decode_position / position : plateau (pions par case, barre, sortis, compte de points), même position vue de l'adversaire, et engine_game_result : partie finie, gagnant, simple/gammon/backgammon, points (videau et règle Jacoby compris) ;
  • engine_encode_match / matchid VIDEAU PROPRIÉTAIRE ;
  • les coups (legal, moves, bestmove) donnent leurs demi-coups ;
  • cube donne available (droit de doubler), double (décision DOUBLE_TAKE, DOUBLE_PASS, DOUBLE_BEAVER, REDOUBLE_TAKE ou REDOUBLE_PASS) et take (double_take <= double_pass). La sortie de reference est inchangée : les suites de contrat natives et WebAssembly passent toujours.

Domaine (app/src/domain/game/) : Game.start(engine, { dice, ply, setup }), puis roll(), playHalfMove({ from, to }) (false si refusé ; le coup est enregistré dès qu'il est complet), cancelHalfMoves(), double(), take(), drop(), playComputerTurn(), undo(), goBackTo(id) ; lecture par state (phase, main, position vue de celui qui a la main, jet, videau, résultat), events, journal, displayedPosition. Le journal est un arbre d'événements gelés (id, parent, version: 1) ; replay() recalcule l'état sans le moteur, chaque événement portant les positions calculées. Niveau standard : 2 plis (STANDARD_PLY) ; sous Node sur le PC, une partie entière à 2 plis prend 1,6 s, un tour de l'ordinateur quelques dizaines de ms.

Adaptateur (app/src/engine/wasm-engine.ts) : createWasmEngine(run), où run exécute une commande d'engine-cli ; sous Node, app/tests/support/node-engine.ts le branche sur web/engine-wasm.mjs et build/wasm/. Dans le navigateur (0d.2), run passera par le Web Worker existant (web/engine-client.mjs).

Scénarios : les 7 scénarios de jouer-une-partie.feature passent (25 étapes) sur le vrai moteur. Deux retouches : « vous essayez de jouer 8/4 » avec 3-1 devient « 13/12 » (8/4 se joue légalement en 8/5/4 ; la case 12 est tenue par l'ordinateur, expliqué dans le scénario) ; le constat « Et c'est au tour de l'ordinateur » devient « Et c'est ensuite au tour de l'ordinateur », Cucumber ne distinguant pas une situation de départ et un constat de même texte.

Garde-fous : dependency-cruiser (le domaine n'importe que le domaine, l'interface n'importe pas le moteur, pas de cycle) ; tests/test_web.sh génère des violations volontaires et vérifie que chacune est signalée. make lint compile aussi le domaine seul sans types Node ni DOM. Vérifié à la main : un import du moteur dans side.ts et une étape sans définition font échouer make test ; un code: faux, une référence vers une fiche absente ou un mauvais type font échouer tests/test_glossary.sh.

Glossaire : fiches ajoutées Demi-coup (HalfMove) et Plateau (Board), à relire ; code: ajouté à Coup (Move), Jet (Roll), Pli (Ply), Moteur (Engine), Identifiant de position (PositionId), Videau (Cube), Événement de partie (GameEvent), Partie (Game).

Écarts et limites :

  • @cucumber/cucumber 12.9.0 et non 13.x : la 13 exige Node 22.12 (require() de modules ES) et l'image est en 22.11. Nouvelle dépendance de types : @types/node (MIT), pour compiler les tests.
  • Ce que le domaine fait lui-même, faute de place dans le moteur : alterner les tours, le jet d'ouverture (le plus fort dé commence, égalité : on relance), doubler la valeur du videau et le donner à celui qui prend, la perte à la valeur d'avant le double quand on passe.
  • Un demi-coup utilise un seul dé : 8/4 avec 3-1 se saisit 8/5 puis 5/4.
  • Le coup est enregistré dès que les demi-coups forment un coup légal ; dans le cas théorique où il pourrait encore être prolongé en un autre coup légal, le joueur ne le pourrait pas.
  • L'ordinateur ne double pas sur un double « facultatif » (OPTIONAL_*), ne propose pas d'abandon, et les castors ne sont pas gérés.
  • app/src/ui/ ne contient qu'un .gitkeep.