7.8 KiB
0007. Architecture et conventions de code
- Statut : accepté
- Date : 2026-11-02
- Décideurs : Pierre (stockage des parties, nommage, garde-fous), sur proposition de Claude Code
Contexte
Le code va être écrit par des agents et Pierre ne le relit pas en détail. Il doit donc rester simple, lisible par un humain, et protégé par des tests contre les dérives. Le cadrage impose déjà que le moteur soit caché derrière une interface, et demande l'annulation et les branches de partie (« Rejouer d'ici »).
Décision
1. La simplicité d'abord. Le code se lit sans connaître de patron de conception. Pas d'abstraction sans un deuxième usage réel, pas de framework sans besoin avéré, pas de couche « au cas où ». Dans le doute, la solution la plus directe.
2. Trois parties, une seule règle de dépendance.
- Le domaine : les règles de l'application (partie, match, branches,
coach, analyse, parcours). Du code pur : ni écran, ni stockage, ni appel
direct au moteur. Il décrit l'interface
Enginedont il a besoin (coups légaux, classement, videau, évaluation). - Le moteur : gnubg en C derrière
engine/include/engine.h, et un adaptateur par plateforme (WebAssembly dans un Web Worker, ou JNI sur Android) qui implémente l'interfaceEnginedu domaine. - L'application : écrans, stockage, presse-papiers, assemblage.
Les dépendances vont vers le domaine, jamais l'inverse. Aucun écran n'appelle le moteur directement. Ces règles sont vérifiées par des tests ; l'outil qui les vérifie sera choisi avec le socle (PWA ou Android) et signalé avec sa licence.
3. Une partie est un journal d'événements. Une partie est enregistrée comme
la suite de ce qui s'est passé (GameStarted, DiceRolled, MovePlayed,
CubeOffered, CubeTaken, CubeDropped, GameEnded…), sans jamais modifier
un événement passé. Chaque événement pointe vers le précédent : une branche
n'est qu'un nouvel événement accroché à un ancien. L'état de la partie se
recalcule en rejouant les événements ; annuler revient à repartir de
l'événement précédent ; l'export SGF est un parcours de l'arbre. Chaque
événement porte un numéro de version de format. Le reste de l'application
(réglages, progression, leçons) est stocké simplement, sans journal.
4. Le hasard est injecté. Le domaine reçoit sa source de dés : les tests sont reproductibles, et l'historique des dés d'une partie est consultable.
5. Nommage. Tous les identifiants du code sont en anglais (fichiers, types,
fonctions, variables, clés JSON). Commentaires, documentation et messages de
commit sont en français. Les textes de l'interface sont en français, dans des
fichiers de textes séparés du code. En conséquence, l'interface C prévue sous le
nom moteur.h (ADR 0005) s'appelle engine.h, avec le préfixe engine_.
6. Le vocabulaire du jeu, pas la cérémonie. On reprend du DDD le langage
commun (un glossaire des termes du jeu, les mêmes dans le code, la doc et
l'interface) et le découpage par sous-domaines en dossiers (game,
analysis, learning, prompts). On n'en reprend pas les patrons tactiques
(dépôts, fabriques, objets-valeurs systématiques) tant qu'un besoin concret ne
les justifie pas.
7. Tests.
- Domaine : tests unitaires rapides, avec un faux moteur.
- Moteur : une même suite de tests de contrat, rejouée sur chaque adaptateur (ligne de commande, WebAssembly, Android), contre les positions de référence de gnubg de bureau.
- Bout en bout : quelques scénarios seulement.
8. Notre code C (hors copie de gnubg) : C11, compilé avec
-Wall -Wextra -Werror, sans état global ajouté par nous.
Alternatives écartées
- Event sourcing pour toute l'application, avec projections (CQRS) : très traçable, mais lourd pour une application hors ligne à un seul utilisateur.
- État classique pour les parties (position courante et historique à côté) : plus simple au départ, mais les branches et l'annulation deviennent du code au cas par cas.
- Identifiants en français : cohérents avec la doc, mais écart avec les conventions habituelles du code et des bibliothèques.
- Architecture hexagonale formelle, DDD complet : beaucoup d'interfaces et de vocabulaire pour une application de cette taille.
- Règles d'architecture seulement documentées : sans relecture humaine du code, rien ne garantirait qu'elles sont respectées.
Conséquences
- Le format des événements de partie est un contrat durable : le changer demande une migration et un ADR.
- La structure des dossiers du domaine et l'outil de vérification des dépendances seront fixés avec le socle, à la fin de l'étape 0.
- Les sous-agents reçoivent ces règles dans leurs consignes ;
CLAUDE.mdy renvoie.
Amendements
2026-11-03 — Le stockage des parties est un format durable
Avec les parties enregistrées (étape 1b), le schéma IndexedDB (base
« ustagammon », version 1 : magasins games, journals, settings) et les
clés localStorage de l'application (ustagammon.settings,
ustagammon.last-board, et le repli des parties) rejoignent le journal
d'événements parmi les formats durables : toute modification passe par une
montée de version avec migration testée, jamais par une réécriture qui
perdrait les parties des joueurs.
2026-11-09 — Version 2 des événements : niveau et match
Avec le choix du niveau et les matchs (étape 1c), le format des événements
passe à la version 2 (EVENT_VERSION, app/src/domain/game/events.ts) :
GameStartedporte le niveau de l'ordinateur (level, un niveau de gnubg) et, pour une partie de match, le match à son début (match: longueur, score, partie Crawford) ;GameEndedporte, dans un match, le match après la partie (score, fin du match, partie Crawford suivante), tel que le moteur l'a calculé.
Chaque partie d'un match est une partie à part entière, avec son journal et
son enregistrement ; la suivante reprend le match que la précédente a laissé.
La migration (migrateJournal, testée dans app/tests/game.test.ts) passe un
journal de la version 1 à la version 2 au moment de le reprendre : le niveau,
implicite, devient explicite (2 plis, le seul qui existait) ; rien d'autre ne
change, une partie de la version 1 étant une partie libre. Le schéma IndexedDB
ne change pas (version 1) : le résumé d'une partie gagne un champ facultatif
match, recalculé à chaque enregistrement, et les réglages
(ustagammon.settings) quatre champs (gameKind, matchLength, level,
masterAdvice) qui ont une valeur par défaut quand ils manquent.
2026-11-10 — Le conseil du maître rattaché au journal
Avec le conseil du maître (étape 1d), le journal d'une partie
(GameJournal, app/src/domain/game/events.ts) gagne un champ facultatif
advice : pour chaque décision du joueur analysée (coup, jet sans doubler,
double, prise ou refus d'un double), l'analyse du moteur à 2 plis, rangée
sous l'identifiant de l'événement de la décision
(app/src/domain/game/advice.ts). Ce n'est pas un événement : l'analyse
arrive après coup, d'un second moteur, et ne change pas la partie ; les
événements gardent leur version (2). Le bilan de fin de partie (1e) la
relira sans recalcul.
- Chaque conseil porte sa version (
ADVICE_VERSION, 1). À la reprise (migrateJournal), un conseil d'une autre version, ou rattaché à un événement absent, est oublié : ce n'est qu'une analyse, qui peut être refaite. Un journal enregistré avant l'étape 1d n'a pas de champadviceet se reprend tel quel. Testé dansapp/tests/advice.test.ts. - Ajouter un événement, revenir en arrière ou changer de branche garde les conseils ; un conseil reste attaché à son événement, même hors de la branche courante.
- Le schéma IndexedDB ne change pas : le journal est enregistré tel quel.