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
- Outillage (ADR 0009, ADR 0004) : une image
docker/web/(Nodenode:22.11.0-alpine3.20déjà présent, versions npm figées et vérifiées) avec TypeScript, Biome,@cucumber/cucumberetdependency-cruiser. Ciblesmake web(compileapp/versbuild/web/avectsc), et intégration àmake lint(Biome ettsc --noEmit) etmake test. Étiquette d'image dans leMakefile,make setupla construit.CREDITS.mdà jour. - Arborescence (à ajuster si plus simple) :
app/src/domain/: le domaine pur, sous-dossiergame/;app/src/engine/: l'adaptateur qui implémenteEngineavec le moteur WebAssembly (réutiliserweb/engine-wasm.mjsetbuild/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.
- Interface
Enginedé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éterengine.hetengine-clisi 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. - 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). - 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.
- Glossaire ↔ code (ADR 0008) : chaque type du domaine qui correspond à
un terme porte
Glossaire : docs/glossaire/<fiche>.mden commentaire, et la fiche reçoitcode: <Type>. Ajouter les fiches manquantes. Étendretests/test_glossary.shpour vérifier le lien dans les deux sens. - 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. - Garde-fous d'architecture (dependency-cruiser) : le domaine n'importe
ni
engine/, niui/, ni rien de spécifique au navigateur ;ui/n'importe pasengine/; pas de dépendance circulaire. Vérifier qu'une violation volontaire fait échouermake 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 lintetmake testdoivent passer ; commits petits, en français, avec la ligneCo-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 testexé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 lintetmake testpassent.
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 deGenerateMovesde 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, etengine_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 ; cubedonneavailable(droit de doubler),double(décision DOUBLE_TAKE, DOUBLE_PASS, DOUBLE_BEAVER, REDOUBLE_TAKE ou REDOUBLE_PASS) ettake(double_take <= double_pass). La sortie dereferenceest 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/cucumber12.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.