Consolidate plans into docs/PLAN.md and document dev loop

Delete autonomous_plan.md (fully shipped), PLAN_playtest.md (all P1-P7
done), PLAN_missions.md and PLAN_leveldesign.md (partial — engine done,
UI polish + 3 final missions + recurring demands + transformer visuals
remain). The surviving TODO list lives in docs/PLAN.md.

Also add the 6-step dev loop to CLAUDE.md (take next topic → implement →
tests → UI test → docs → commit).
This commit is contained in:
Samuel Bouchet 2026-04-17 22:09:53 +02:00
parent eba81400a8
commit bd1763f372
7 changed files with 106 additions and 721 deletions

View file

@ -29,7 +29,25 @@ Tout `Control` (ColorRect, Label…) a `MouseFilter = Stop` par defaut. Quand un
### Plans ### Plans
Les fichiers de plan doivent etre rediges a la racine du workspace (ex: `/workspace/PLAN_juice.md`), **pas** dans `.claude/plans/` car ce dossier a une taille limitee. Les plans vivants (travail restant, features a faire) vont dans
[`docs/PLAN.md`](docs/PLAN.md). Pour un brouillon ad-hoc, ecrire a la racine
du workspace (ex: `/workspace/PLAN_<sujet>.md`), **pas** dans
`.claude/plans/` (taille limitee). Une fois implemente, supprimer le
fichier ; si partiel, consolider le restant dans `docs/PLAN.md`.
### Boucle de developpement
Pour chaque sujet pris dans `docs/PLAN.md` :
1. **Prendre le sujet suivant** dans le plan (ordre de priorite).
2. **Implementer** (moteur + presentation selon le cas).
3. **Ajouter des tests unitaires** si applicable (`chessistics-tests/`).
4. **Tester l'UI/UX** de la fonctionnalite dans le jeu si applicable
(harness + quick save/load pour reprendre un checkpoint).
5. **Mettre a jour la documentation** (README, CLAUDE.md, GDD) si
necessaire et **retirer le sujet du plan** (ou annoter ce qui reste).
6. **Commit** (un commit par sujet, message en anglais, sans co-author
Claude).
## Harnais d'automatisation (Claude peut jouer tout seul) ## Harnais d'automatisation (Claude peut jouer tout seul)

View file

@ -1,157 +0,0 @@
# Plan : Level Design - Batiments de Transformation
## Concept
Les batiments de **transformation** consomment une ressource en entree et produisent une ressource differente en sortie. Le joueur doit construire des chaines logistiques multi-etapes : extraction → transformation → livraison.
Exemple : Scierie (produit bois) → Forge (consomme bois, produit outils) → Caserne (demande outils).
## Modele Engine
### Nouveau type de cellule : Transformer
```
CellType.Transformer
```
Un `TransformerDef` combine une demande (input) et une production (output) :
```csharp
public record TransformerDef(
Coords Position,
string Name,
CargoType InputCargo, // ce qu'il consomme
int InputRequired, // nb d'unites avant conversion
CargoType OutputCargo, // ce qu'il produit
int OutputAmount // nb d'unites produites par cycle
);
```
### Logique de production
Chaque tour, le `TransferResolver` livre du cargo au transformateur comme a une demande normale. Quand le buffer d'entree atteint `InputRequired`, le transformateur :
1. Vide son buffer d'entree
2. Remplit son buffer de sortie avec `OutputAmount` unites de `OutputCargo`
3. Le buffer de sortie est distribue aux pieces adjacentes (comme une production classique)
Cela cree un rythme : accumulation → conversion → distribution.
### Changements au TerrainPatch
Ajouter le type `"transformer"` dans le JSON :
```json
{
"col": 3, "row": 2,
"type": "transformer",
"transformer": {
"name": "Forge",
"inputCargo": "wood",
"inputRequired": 2,
"outputCargo": "tools",
"outputAmount": 1
}
}
```
### Nouveaux types de cargo
Ajouter au `CargoType` enum :
- `Tools` (outils) — bois transforme
- `Arms` (armes) — pierre transformee
- `Gold` (or) — ressource rare de fin de campagne
## Evolution de la campagne (10 missions)
### Mission 1 : Premier Convoi (4x4)
- **Deverrouille** : Pion
- **Batiments** : Scierie → Depot Royal
- **Objectif** : livrer 3 bois — apprendre le placement de base
### Mission 2 : Forger les Tours (6x6)
- **Deverrouille** : Tour
- **Batiments** : +Carriere, +Caserne (bois), +Forge (pierre)
- **Objectif** : livrer bois a la caserne ET pierre a la forge
- **Defi** : gerer deux routes independantes
### Mission 3 : Le Col (6x6)
- **Deverrouille** : Cavalier
- **Batiments** : mur bloquant + Depot (bois)
- **Objectif** : traverser les murs avec des cavaliers
- **Les demands precedentes restent actives**
### Mission 4 : Le Carrefour (8x8)
- **Deverrouille** : Fou
- **Batiments** : +Chateau (bois), +Forge Royale (pierre), murs
- **Objectif** : routes diagonales avec les fous
### Mission 5 : La Forge (8x8) -- TRANSFORMATION
- **Deverrouille** : aucun (nouveau type de cargo : Outils)
- **Batiments** : Forge *transformee* en transformateur (bois → outils)
- **Objectif** : livrer des outils a un nouveau batiment (Armurerie)
- **Defi** : la route bois existante doit continuer, PLUS une branche vers la forge qui produit des outils
### Mission 6 : La Dame Blanche (10x10)
- **Deverrouille** : Dame
- **Batiments** : +Scierie Nord, +Grand Chantier (bois), +Arsenal (pierre)
- **Objectif** : routes longue distance avec la dame
### Mission 7 : L'Armurerie (10x10) -- TRANSFORMATION
- **Nouveau cargo** : Armes
- **Batiments** : Armurerie (transforme pierre → armes)
- **Objectif** : livrer des armes a une Garnison
### Mission 8 : Le Comptoir (12x12)
- **Batiments** : Comptoir (transforme outils → or)
- **Objectif** : livrer de l'or au Tresor Royal
- **Defi** : chaine a 3 etapes : bois → outils → or
### Mission 9 : L'Expansion Finale (14x14)
- **Batiments** : multiples transformateurs, murs complexes
- **Objectif** : maintenir toutes les chaines tout en s'etendant
- **Defi** : gestion de la congestion (risque de collisions)
### Mission 10 : Le Couronnement (14x14)
- **Batiments** : Cathedrale (demande or + armes + outils)
- **Objectif** : livrer les 3 types de cargo transforme
- **Defi** : orchestrer l'ensemble des chaines logistiques simultanement
## Demands recurrentes (futur)
Pour que le joueur doive "preserver ses automatisations", les demands pourraient devenir recurrentes :
- Un batiment de demande **consomme** N unites par tour
- S'il n'est plus approvisionne, il passe en etat "en penurie"
- Condition de mission : **aucun** batiment en penurie pendant X tours consecutifs
Cela force le joueur a maintenir ses routes existantes quand le terrain s'agrandit, au lieu de tout reconstruire.
## Implementation par phases
### Phase 1 : Modele Transformer
- Ajouter `CellType.Transformer` et `TransformerDef`
- Ajouter `TransformerState` avec buffers input/output
- Integrer dans `BoardState`
### Phase 2 : Logique de conversion
- Modifier `TurnExecutor` : sous-phase "transformation" entre production et transferts
- Le transformateur agit comme une demande (recoit) ET une production (emet)
### Phase 3 : Nouveaux cargos
- Ajouter `Tools`, `Arms`, `Gold` a `CargoType`
- Couleurs visuelles pour chaque cargo
- Mise a jour du `CampaignLoader` pour parser les transformateurs
### Phase 4 : Visuels
- Couleur de cellule pour les transformateurs (ex: orange cuivre)
- Animation de conversion (flash input → flash output)
- Icones de cargo dans les pieces
### Phase 5 : Missions 5-10
- Ecrire les donnees JSON des missions 5 a 10
- Tester la solvabilite de chaque mission
- Equilibrer les quantites (input/output ratios)
### Phase 6 (optionnel) : Demands recurrentes
- Modifier `DemandState` pour tracker la consommation par tour
- Ajouter un flag "en penurie"
- Condition de victoire : pas de penurie pendant N tours

View file

@ -1,371 +0,0 @@
# Plan : Puzzle → Logistique incrémentale (Missions)
## Vision
Remplacer la série de niveaux indépendants par un plateau persistant où les **missions** s'enchaînent. Chaque mission complétée débloque une extension du terrain (nouvelles cases, productions, demandes, murs) sans casser la solution en place. Le jeu devient un jeu de **logistique** et non de **puzzle**.
### Fil narratif (lore GDD §12)
Les pions manquent d'un roi. Mission après mission, ils construisent des pièces de plus en plus puissantes pour atteindre des ressources plus lointaines, jusqu'à fabriquer le roi — qui exécute tout le monde.
Chaque mission débloque de nouveaux **types de pièces** : on commence avec les Pions seuls, puis les Tours, les Fous, les Cavaliers, et enfin la Dame. Le lore justifie la progression mécanique.
---
## Concepts clés
| Ancien | Nouveau |
|--------|---------|
| Level (plateau isolé) | **Campaign** : un plateau persistant avec N missions |
| Victoire → charger le niveau suivant | Victoire → le plateau **s'étend**, la mission suivante se débloque |
| Stock fixe par niveau | Stock cumulé : chaque mission donne un budget additionnel |
| Reset complet entre niveaux | Les pièces posées et les productions **restent actives** |
| Stop (retour en Edit) | **Supprimé** — la simulation tourne en continu, le joueur édite en temps réel |
| Toutes pièces disponibles dès le début | Pièces **débloquées progressivement** par mission (lore) |
| Pièces à niveau fixe | **Niveaux de pièces** (I, II, III) débloqués par la progression |
### Règle d'or : pas de régression
Les murs ne peuvent apparaître que sur les **nouvelles cases** débloquées par la mission. Le terrain existant ne change jamais de façon bloquante.
---
## Changements de game design
### Suppression du Stop — édition en temps réel
Il n'y a plus de phase Edit séparée. La simulation tourne en continu. Le joueur peut :
- **Pause** (barre espace) : l'animation termine le tour en cours puis se fige.
- **Placer/retirer des pièces** à tout moment (en pause ou pendant que ça tourne).
- **Réoptimiser** les missions précédentes — essentiel pour un jeu de logistique.
Conséquence engine : `PlacePieceCommand` et `RemovePieceCommand` fonctionnent quel que soit le `SimPhase` (plus de guard `phase == Edit`).
### Auto-pause pendant le placement
Sélectionner un type de pièce à placer met automatiquement la simulation en pause. La pause est levée quand le placement est confirmé (clic arrivée) ou annulé (Échap). Le joueur ne pense pas "pause" — il pense "placement". C'est transparent.
### Retirer une pièce — touche Suppr
Le clic droit est réservé au pan de caméra. Pour retirer une pièce :
- Clic gauche sur la pièce → sélection + panneau de détail
- **Touche Suppr** ou **bouton [Retirer]** dans le panneau de détail → retourne au stock
### Collisions → retour au stock + pause auto
Quand une pièce est détruite par collision, elle retourne dans le stock du joueur (au lieu d'être perdue). Cela évite un soft-lock où le joueur n'a plus assez de pièces pour résoudre la mission.
La simulation se met en **pause automatique** sur collision :
- La caméra effectue un **pan + zoom** vers la zone de collision.
- Une **notification** apparaît dans un coin de l'écran pour expliciter ce qui s'est passé (ex: "Tour II détruite par Dame — retournée au stock").
- Le joueur peut reprendre la simulation (Espace) après avoir pris connaissance de la situation.
### Undo (Ctrl+Z)
Annule le dernier placement ou retrait de pièce. L'architecture event-sourcing de l'engine rend l'implémentation naturelle : on conserve un historique de commandes et on les rejoue sans la dernière. Essentiel pour l'itération rapide sur un réseau en temps réel.
### Productions — pas d'intervalle, production variable
Chaque bâtiment de production a un champ `amount` (1 à 4) : il produit ce nombre de cargaisons **à chaque tour**. Le buffer max = `amount`. La surproduction non récupérée est écrasée au tour suivant (perdue). Pas de champ `interval`.
### Niveaux de pièces (I, II, III)
Chaque type de pièce a un niveau qui affecte :
- **Portée** : Tour I = 1 case, Tour II = 2 cases, Tour III = 3 cases
- **Priorité de transfert** : à statut social égal, le niveau départage
- **Collisions** : à statut égal, le niveau supérieur survit
Les missions débloquent des niveaux supérieurs progressivement.
### Drag & drop de pièces
Le joueur peut glisser une pièce déjà placée pour déplacer son point de départ (les arrivées possibles se recalculent). Quality-of-life essentiel quand on itère sur un réseau en temps réel.
---
## Architecture (Black-Box Sim)
### Nouveaux modèles (engine)
```
CampaignDef # remplace la liste de LevelDef
├── name: string
├── initialWidth / initialHeight
├── missions: MissionDef[]
MissionDef
├── id: int
├── name: string
├── description: string
├── terrainPatch: TerrainPatch # extension du plateau
├── stock: PieceStock[] # stock additionnel offert
├── demands: DemandDef[] # nouveaux objectifs
├── unlockedPieces: PieceKind[] # types de pièces débloqués par cette mission
├── unlockedLevels: PieceUpgrade[] # niveaux de pièces débloqués
PieceUpgrade
├── kind: PieceKind
├── level: int # ex: Tour niveau 2
TerrainPatch
├── newWidth / newHeight # nouvelle taille du plateau (>= précédente)
├── cells: PatchCell[] # cases ajoutées/modifiées
│ ├── col, row
│ └── type: Empty | Wall | Production(def) | Demand(def)
```
### Nouveau state (engine)
```
CampaignState
├── campaignDef: CampaignDef
├── currentMissionIndex: int
├── completedMissions: int[]
├── boardState: BoardState # persistant entre missions
├── availablePieces: Set<(PieceKind, int level)> # pièces débloquées
```
`BoardState` évolue :
- Ajout d'un champ `missionIndex` sur chaque `DemandState` pour savoir à quelle mission appartient une demande.
- Les demandes complétées restent dans le state (marquées `Satisfied`), les nouvelles s'ajoutent.
- `ProductionDef` reçoit un champ `amount` (1-4) au lieu de `interval`.
### Modification de SimPhase
```
Running ←→ Paused
↓ ↓
MissionComplete (la sim continue mais un overlay félicite)
AdvanceMission → Running (terrain étendu, nouvelles demandes)
```
Phases supprimées : `Edit`, `Victory`, `Defeat`.
- Plus de `Edit` : le joueur place des pièces à tout moment.
- Plus de `Victory` séparé : `MissionComplete` pour chaque mission, la dernière affiche un écran de fin.
- Plus de `Defeat` : pas de deadline punitive (jeu de logistique, pas de puzzle).
> **Note** : la suppression des deadlines est une conséquence de la suppression du Stop. Sans pouvoir reset, un deadline qui expire sans recours serait frustrant. Les demandes ont un `amount` cible mais pas de date limite. Les métriques (tours pour compléter) servent de score optionnel.
### Nouvelles Commands
| Command | Effet |
|---------|-------|
| `LoadCampaignCommand` | Charge la campagne, initialise le plateau avec la mission 0, démarre Running |
| `AdvanceMissionCommand` | Applique le `TerrainPatch` de la mission suivante, ajoute stock/demandes/pièces |
| `MovePieceCommand` | Drag & drop : déplace une pièce existante (nouveau start + end) |
Commands modifiées :
| Command | Changement |
|---------|------------|
| `PlacePieceCommand` | Fonctionne en Running et Paused (plus de guard Edit) |
| `RemovePieceCommand` | Idem |
| `PauseSimulationCommand` | Termine le tour en cours avant de figer |
Commands supprimées :
| Command | Raison |
|---------|--------|
| `StopSimulationCommand` | Plus de Stop |
| `StartSimulationCommand` | La sim démarre automatiquement au load |
| `ResetLevelCommand` | Plus de reset complet |
### Nouveaux Events
| Event | Données |
|-------|---------|
| `MissionCompleteEvent` | missionIndex |
| `MissionStartedEvent` | missionIndex, terrainPatch appliqué |
| `TerrainExpandedEvent` | nouvelles cases, nouvelle taille |
| `PieceUnlockedEvent` | kind, level |
| `PieceMovedByPlayerEvent` | pieceId, oldStart, oldEnd, newStart, newEnd |
| `PieceReturnedToStockEvent` | pieceId, kind, reason (collision) |
### Modifications du flow de victoire
1. `VictoryChecker` → renommé `MissionChecker`.
2. Vérifie les demandes **de la mission courante** uniquement.
3. Si toutes satisfaites → `MissionCompleteEvent`.
4. La sim continue de tourner (les pièces continuent de produire/livrer).
5. Le joueur déclenche `AdvanceMissionCommand` quand il est prêt.
6. Dernière mission complétée → écran de fin de campagne.
### Collisions — retour au stock + pause auto + caméra
`CollisionResolver` modifié :
- La pièce détruite émet `PieceReturnedToStockEvent` au lieu de `PieceDestroyedEvent`.
- Le stock dans `BoardState` est incrémenté.
- La pièce est retirée du plateau mais le joueur peut la replacer.
Côté présentation (Godot) :
- La simulation se met en **pause automatique** à la détection d'une collision.
- La caméra effectue un **pan + zoom** vers la zone de collision.
- Une **notification** apparaît dans un coin de l'écran (ex: "Tour II détruite par Dame — retournée au stock").
- Le joueur reprend avec Espace après avoir pris connaissance.
---
## Format JSON de campagne
```json
{
"name": "La Quête du Roi",
"initialWidth": 4,
"initialHeight": 4,
"missions": [
{
"id": 1,
"name": "Premier Convoi",
"description": "Les pions découvrent une scierie. Il faut acheminer le bois.",
"terrainPatch": {
"newWidth": 4,
"newHeight": 4,
"cells": [
{ "col": 0, "row": 0, "type": "production", "production": { "name": "Scierie", "cargo": "wood", "amount": 1 } },
{ "col": 3, "row": 0, "type": "demand", "demand": { "name": "Dépôt", "cargo": "wood", "amount": 3 } }
]
},
"unlockedPieces": ["pawn"],
"unlockedLevels": [{ "kind": "pawn", "level": 1 }],
"stock": [
{ "kind": "pawn", "count": 6 }
]
},
{
"id": 2,
"name": "Forger les Tours",
"description": "Les pions ont forgé des Tours. De nouveaux territoires s'ouvrent à l'est.",
"terrainPatch": {
"newWidth": 6,
"newHeight": 4,
"cells": [
{ "col": 4, "row": 0, "type": "empty" },
{ "col": 4, "row": 1, "type": "wall" },
{ "col": 4, "row": 2, "type": "empty" },
{ "col": 4, "row": 3, "type": "empty" },
{ "col": 5, "row": 0, "type": "empty" },
{ "col": 5, "row": 1, "type": "empty" },
{ "col": 5, "row": 2, "type": "production", "production": { "name": "Carrière", "cargo": "stone", "amount": 1 } },
{ "col": 5, "row": 3, "type": "demand", "demand": { "name": "Chantier", "cargo": "stone", "amount": 5 } }
]
},
"unlockedPieces": ["rook"],
"unlockedLevels": [{ "kind": "rook", "level": 1 }],
"stock": [
{ "kind": "pawn", "count": 2 },
{ "kind": "rook", "count": 3 }
]
}
]
}
```
---
## Phases d'implémentation
### Phase 1 — Modèles engine (pas de Godot)
1. Créer `CampaignDef`, `MissionDef`, `TerrainPatch`, `PatchCell`, `PieceUpgrade` dans `chessistics-engine/Model/`.
2. Créer `CampaignState` dans `chessistics-engine/Model/`.
3. Modifier `SimPhase` : supprimer `Edit`, `Victory`, `Defeat`. Ajouter `MissionComplete`.
4. Ajouter `missionIndex` à `DemandState`.
5. Ajouter `amount` à `ProductionDef` (remplace le concept d'intervalle). Buffer max = amount.
6. Ajouter `level` à `PieceStock` et aux modèles de pièces pour supporter les niveaux I/II/III.
### Phase 2 — Refactor SimPhase et édition en temps réel
1. Supprimer les guards `phase == Edit` dans `PlacePieceCommand` et `RemovePieceCommand`.
2. Supprimer `StopSimulationCommand`, `StartSimulationCommand`, `ResetLevelCommand`.
3. Modifier `PauseSimulationCommand` pour terminer le tour en cours avant de figer.
4. La sim démarre automatiquement au chargement (état initial = `Running` en pause).
### Phase 3 — Collisions → retour au stock + pause auto
1. Modifier `CollisionResolver` : la pièce détruite retourne au stock.
2. Créer `PieceReturnedToStockEvent` (avec coordonnées de la collision pour le pan caméra).
3. Incrémenter le stock dans `BoardState` quand une pièce est détruite.
4. L'engine émet un event de pause automatique après une collision.
5. Tests : vérifier qu'après collision la pièce réapparaît dans le stock et la sim est en pause.
### Phase 4 — Commands & Events campagne
1. Créer `LoadCampaignCommand` : initialise `BoardState` depuis mission 0, applique `unlockedPieces`.
2. Créer `AdvanceMissionCommand` : applique `TerrainPatch`, ajoute stock/demandes/pièces débloquées.
3. Créer `MissionCompleteEvent`, `MissionStartedEvent`, `TerrainExpandedEvent`, `PieceUnlockedEvent`.
4. Renommer `VictoryChecker``MissionChecker` : ne vérifie que les demandes de la mission courante.
### Phase 5 — Drag & drop engine
1. Créer `MovePieceCommand` : valide le nouveau placement, met à jour start/end.
2. Créer `PieceMovedByPlayerEvent`.
3. Tests : déplacer une pièce en Running, vérifier qu'elle reprend sa trajectoire.
### Phase 6 — Production variable
1. Modifier la production pour utiliser `amount` (1-4) au lieu de toujours produire 1.
2. Le buffer max = `amount`. La surproduction écrase le buffer.
3. Tests : production amount=3 remplit 3 slots, non récupérés → écrasés au tour suivant.
### Phase 7 — CampaignLoader
1. Créer `CampaignLoader` dans `chessistics-engine/Loading/` : parse le JSON campagne.
2. Valider la cohérence : pas de régression de taille, `unlockedPieces` cohérent.
3. Les anciens JSON de niveaux restent compatibles (mode puzzle legacy optionnel).
### Phase 8 — Tests engine intégration
1. Test : charger une campagne, compléter mission 1, avancer, vérifier terrain étendu.
2. Test : les pièces de la mission 1 restent en place après `AdvanceMissionCommand`.
3. Test : `MissionCompleteEvent` émis au bon moment.
4. Test : placer une pièce pendant Running fonctionne.
5. Test : collision retourne la pièce au stock.
6. Test : pièces non débloquées ne sont pas plaçables.
7. Test : niveaux de pièces affectent portée et priorité.
### Phase 9 — GameSim facade
1. `GameSim` accepte un `CampaignDef` (mode principal) ou un `LevelDef` (legacy).
2. Exposer `CampaignSnapshot` avec `currentMissionIndex`, `completedMissions`, `availablePieces`.
3. Plus de `StopSimulationCommand` dans la facade.
### Phase 10 — Présentation Godot
1. `Main.cs` : charger une campagne, supprimer le flow Stop/Start.
2. Contrôles : Espace = pause/resume, plus de bouton Stop. Barre de contrôle : `[⏸/▶] [x1] [x2] [x4] Tour: N Mission: M/T`.
3. **Auto-pause placement** : sélectionner un type de pièce dans le stock met la sim en pause. La pause est levée quand le placement est confirmé ou annulé (Échap).
4. **Retrait de pièce** : supprimer le clic droit pour retirer. Clic gauche sélectionne → touche Suppr ou bouton [Retirer] dans le panneau de détail.
5. **Collision — pause + caméra + notification** :
- Sur `PieceReturnedToStockEvent` : pause auto de la simulation.
- Pan + zoom caméra vers la zone de collision (tween animé).
- Notification dans un coin de l'écran (ex: "Tour II détruite par Dame — retournée au stock").
- Le joueur reprend avec Espace.
6. **Transition de mission** (cinématique) :
- Titre "Nouvelle mission" en plein écran, fade-in.
- Lock du pan/zoom joueur — la caméra se déplace pour montrer la zone de la prochaine mission.
- Les nouvelles cases apparaissent sur le plateau avec animation d'expansion.
- Le titre de mission se déplace vers la zone d'objectif du panneau latéral avant de disparaître (guide l'œil).
- Unlock pan/zoom, la simulation reprend.
7. `ObjectivePanel` : objectifs de la mission courante + badge ✓ pour les missions complétées.
8. `PieceStockPanel` : stock cumulatif, n'affiche que les pièces débloquées.
9. Notification `PieceUnlockedEvent` : animation de déblocage d'une nouvelle pièce.
10. Drag & drop visuel : le joueur glisse une pièce, les destinations légales s'affichent.
11. **Undo (Ctrl+Z)** : annule le dernier placement ou retrait. Conserver un historique de commandes côté présentation, rejouer l'état sans la dernière.
### Phase 11 — Données de campagne
1. Créer `Data/campaigns/campaign_01.json` — "La Quête du Roi".
2. Concevoir 6-8 missions avec progression de pièces (Pion → Tour → Fou → Cavalier → Dame).
3. Chaque mission étend le terrain sans casser les solutions précédentes.
4. Équilibrer les stocks cumulatifs et les `amount` de production.
---
## Risques et mitigations
| Risque | Mitigation |
|--------|------------|
| Un patch de terrain casse une solution existante | Validation loader : les patches ne peuvent que **ajouter** des cases ou modifier des cases hors de la zone initiale |
| Le stock cumulé rend les missions triviales | Playtesting + stock additionnel calibré mission par mission |
| Placer des pièces en Running cause des bugs de timing | La commande s'applique **entre deux tours** (fin du tour courant → placement → tour suivant) |
| Collision = perte de pièce frustrante | Retour au stock — le joueur peut replacer immédiatement |
| Drag & drop complexe pendant la simulation | Le drag ne modifie la pièce qu'au **relâchement** (atomique), appliqué entre deux tours |
| Niveaux de pièces explosent la complexité | Introduction graduelle : mission 1-3 = niveau I seul, niveaux II/III arrivent plus tard |

View file

@ -1,41 +0,0 @@
# Plan Playtest Fixes
## Problèmes identifiés
### P1 — Superposition production/demand (Bug moteur)
**Cause racine**: `ApplyTerrainPatch` ne nettoie pas les bâtiments existants avant d'ajouter un nouveau (Production, Demand, Transformer). Mission 3 place un demand sur (5,5) où Mission 2 avait une production → les deux coexistent dans les dictionnaires.
**Fix**: Appeler `ClearBuildingAt(coords)` pour TOUS les types de cellules dans `ApplyTerrainPatch`.
### P2 — Murs sur cases existantes / pièces traversent les murs
**Cause racine**: Mission 3 ajoute des murs sur des cases déjà jouables (2,2), (2,3), etc. Si des pièces y sont placées, elles restent et traversent le mur.
**Fix moteur**: Quand un mur apparaît via terrain patch, retirer les pièces dont StartCell ou EndCell est sur ce mur (retour au stock).
**Fix level design**: Redessiner mission 3 pour que les murs soient sur des cases nouvellement révélées (agrandir le plateau).
### P3 — Noms de bâtiments dupliqués
**Cause**: Deux "Dépôt Royal" (mission 1 et mission 3).
**Fix**: Renommer dans campaign_01.json. Mission 3 demand → "Avant-Poste du Col".
### P4 — Compteurs d'objectifs qui montent à l'infini
**Cause**: `ObjectivePanel.UpdateProgress` affiche le current réel même quand il dépasse le required.
**Fix**: Afficher `min(current, required)/required` et marquer visuellement les objectifs complétés. Les objectifs des missions précédentes complétées: afficher "✓" et ne plus mettre à jour.
### P5 — Espace sélectionne un pion au lieu de lancer la simulation
**Cause**: Les boutons Godot ont `FocusMode = All` par défaut et capturent la touche Espace.
**Fix**: Mettre `FocusMode = None` sur les boutons du PieceStockPanel. Ajouter un handler Espace dans Main pour toggle play/pause.
### P6 — Centrage du plateau
**Cause**: Le calcul de centrage ne prend pas en compte la barre de titre (~36px en haut).
**Fix**: Ajouter `TitleBarHeight` au calcul de l'offset caméra.
### P7 — Pas de narration/lore
**Fix**: Ajouter un champ `flavor` dans MissionDef + campaign JSON. Afficher un encart narratif au démarrage de chaque mission. Un personnage parle en une phrase, ton léger et enjoué.
## Ordre d'implémentation
1. P1 + P2: Fixes moteur (ApplyTerrainPatch) + tests
2. P3: Redesign campaign_01.json (missions 2-7, noms uniques, pas de superposition, murs sur nouvelles cases)
3. P4: Test automatisé de validation du campaign (pas de superposition, niveaux finissables)
4. P5: Spacebar play/pause
5. P6: Centrage caméra
6. P4: Objectifs capped
7. P7: Narration

View file

@ -62,8 +62,6 @@ Claude Code, lancé à l'intérieur, peut donc compiler le projet, exécuter
les tests, **démarrer une vraie instance de Godot en headless** et lire les les tests, **démarrer une vraie instance de Godot en headless** et lire les
captures PNG produites — sans dépendance Windows. captures PNG produites — sans dépendance Windows.
Voir [`autonomous_plan.md`](autonomous_plan.md) pour le design détaillé.
### Option 1 — Docker Desktop + terminal Windows (le plus simple) ### Option 1 — Docker Desktop + terminal Windows (le plus simple)
1. Installer [Docker Desktop pour Windows](https://www.docker.com/products/docker-desktop/). 1. Installer [Docker Desktop pour Windows](https://www.docker.com/products/docker-desktop/).

View file

@ -1,149 +0,0 @@
# Headless Linux dev container for autonomous Chessistics testing
## Why
Today the automation harness only runs on Windows because the Godot binary is
hardcoded to `C:\Apps\godot\Godot_v4.6.2-stable_mono_win64_console.exe`. When
Claude Code runs inside the project's dev container (Linux / `node:20`), it
can read source code and run `dotnet test`, but it **cannot launch the actual
game** — there's no Godot binary, no .NET SDK, and no display server for the
renderer.
The goal: make the dev container a self-contained environment where Claude
Code can build the project, launch a real Godot instance in headless Linux
mode, drive it via the automation harness, and read back 1280×720 PNG
screenshots — all without any Windows dependency.
## Design
### Pieces required
1. **Godot 4.6.2-stable Mono for Linux** — matches the Windows editor the
project already uses. Installed once at image build time to `/opt/godot/`
with a symlink `/opt/godot/godot`.
2. **.NET SDK 9.0** — the project targets `net9.0`. Installed via the
upstream `dot.net` install script to `/usr/local/dotnet/`.
3. **Xvfb + Mesa software GL** — a virtual framebuffer at `:99` so Godot's
GL-compatibility renderer has somewhere to draw. `xvfb-run` wraps any
command transparently.
4. **Python 3** — the automation harness is stdlib-only Python.
5. **Minimal X / audio runtime deps**`libx11`, `libxcursor`, `libxrandr`,
`libxi`, `libgl1`, `libgles2`, `libasound2`, `libxkbcommon0`, etc.
Without these, Godot exits on startup with `libXext not found`-style errors.
### How Godot reaches the framebuffer
Two options considered:
- **(A)** Run `Xvfb :99 -screen 0 1280x720x24 &` as a background process,
export `DISPLAY=:99`, launch Godot normally. Persistent display, shared by
many Godot runs.
- **(B)** Use `xvfb-run -a --server-args="-screen 0 1280x720x24"` as a prefix
on every Godot invocation. A fresh display per launch; cleans up
automatically on exit.
**Chosen: (B)**, because the automation harness already spawns Godot once per
`Harness.launch()` and cleans up on context exit — matches the per-launch
lifecycle naturally, no daemon to keep alive, no race on the display number.
A tiny wrapper `/usr/local/bin/godot-xvfb` wraps `xvfb-run … $GODOT_BIN
"$@"`, so the harness (or a human) only has to invoke one path.
### Integration with the existing harness
`tools/automation/harness.py` currently hardcodes the Windows Godot path. We
teach it two things:
1. Read `GODOT_BIN` from the environment first; fall back to the platform
default (Windows path on Windows, `/opt/godot/godot` on Linux).
2. On Linux, auto-prepend `["xvfb-run", "-a", "--server-args=-screen 0
1280x720x24"]` to the Godot launch command unless `DISPLAY` is already
set (someone has a real display, skip the wrap).
With those two tweaks, every existing Python helper (`smoke.py`,
`run_game.py`, `solve_*.py`) works unchanged inside the container.
### Firewall considerations
The container's `init-firewall.sh` runs at `postStartCommand`, **after** the
image is built, and drops all outbound traffic except to a small allowlist
(GitHub, npmjs, Anthropic, Sentry, Statsig). Impact on our pieces:
- **Godot binary + .NET SDK**: downloaded during `docker build`, which runs
_before_ the firewall exists → works unconditionally.
- **`dotnet restore`** (runtime, e.g. after a `git pull`): needs
`api.nuget.org`. Added to the allowlist.
- **Godot runtime**: no outbound traffic required — the engine runs fully
offline once installed.
### Build sequence inside the Dockerfile
As `root`, before the existing `USER node` switch:
```
# 1. X/GL/audio runtime + python + xvfb
apt-get install xvfb xauth libx11-6 libxcursor1 libxinerama1 libxrandr2 \
libxi6 libxext6 libgl1 libglx-mesa0 libgl1-mesa-dri libglu1-mesa \
libasound2 libxkbcommon0 libxkbcommon-x11-0 libfontconfig1 libdbus-1-3 \
python3 python3-pip
# 2. .NET SDK 9.0 via upstream install script
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- \
--channel 9.0 --install-dir /usr/local/dotnet
ln -s /usr/local/dotnet/dotnet /usr/local/bin/dotnet
# 3. Godot 4.6.2-stable mono, Linux x86_64
wget https://github.com/godotengine/godot/releases/download/${VERSION}/\
Godot_v${VERSION}_mono_linux_x86_64.zip
unzip … -d /opt/godot
ln -s /opt/godot/Godot_v…/Godot_v…_mono_linux.x86_64 /opt/godot/godot
```
Then:
- `ENV GODOT_BIN=/opt/godot/godot`
- `ENV PATH=$PATH:/opt/godot:/usr/local/dotnet`
- Drop in `/usr/local/bin/godot-xvfb` wrapper
## File-by-file change list
| File | Change |
|------|--------|
| `.devcontainer/Dockerfile` | Add Godot / dotnet / xvfb installs before `USER node` |
| `.devcontainer/init-firewall.sh` | Append `api.nuget.org` to the domain allowlist |
| `.devcontainer/godot-xvfb.sh` *(new)* | `exec xvfb-run -a … "$GODOT_BIN" "$@"` |
| `tools/automation/harness.py` | Env-aware Godot path + Linux xvfb auto-wrap |
| `README.md` *(new)* | Windows / WSL2 launch instructions for the dev container |
Nothing inside `Scripts/` or `chessistics-engine/` changes. The harness
contract (inbox/outbox/screens) is platform-agnostic already.
## Verification
After rebuild:
1. `docker build .devcontainer -t chessistics-dev` succeeds.
2. `devcontainer up --workspace-folder .` starts the container and
post-start firewall passes.
3. Inside: `dotnet --version``9.0.x`, `godot --version`
`4.6.2.stable.mono.official.*`.
4. `dotnet build Chessistics.csproj` → green.
5. `dotnet test chessistics-tests/` → 102 / 102.
6. `python3 tools/automation/smoke.py` → loads mission 1, takes PNG
screenshots that are non-black, quits cleanly.
7. `Read` one of the PNGs — it should show the same mission 1 UI as the
Windows run (title bar, board, objectives, stock panel).
## Out of scope (explicit non-goals)
- **GPU acceleration**: we use Mesa software rendering. Xvfb + llvmpipe is
enough for 1280×720 at a few FPS, which is what the harness needs.
- **Real display forwarding** (X11 forwarding, VNC, noVNC): doable but
unnecessary — Claude reads PNGs, not a live video feed.
- **Multi-arch images**: we ship x86_64 only. ARM (Apple Silicon via
Docker Desktop emulation) would need `Godot_v…_mono_linux_arm64.zip`
straightforward to add if needed, not done here.
- **Shrinking the image**: Godot + .NET SDK adds ~500 MB. Worth it;
multi-stage builds could trim later.
- **Keeping Xvfb warm across launches**: the single-launch pattern is clean
enough. If someone ever scripts dozens of rapid Godot starts and the
xvfb-run startup cost shows up, revisit approach (A).

87
docs/PLAN.md Normal file
View file

@ -0,0 +1,87 @@
# Chessistics — Plan de travail (restant)
Consolidation des sections non implementees des anciens `PLAN_missions.md` et
`PLAN_leveldesign.md`. Le moteur (black-box sim, campagne, transformateurs,
missions 1-7) est en place. Ce qui suit concerne la finition UX, les visuels
et l'extension de la campagne.
---
## 1. UX / Presentation — gaps Godot
Le moteur expose deja les commandes et events requis ; cote Godot il manque
les surfaces d'interaction et d'animation.
### 1.1 Undo (Ctrl+Z)
Annule le dernier placement ou retrait. L'architecture event-sourcing rend
l'implementation naturelle : conserver un historique de commandes cote
presentation, rejouer l'etat sans la derniere. Essentiel pour l'iteration
rapide sur un reseau en temps reel.
### 1.2 Drag & drop des pieces placees
`MovePieceCommand` + `PieceMovedByPlayerEvent` existent cote engine.
Cote Godot : permettre de glisser une piece placee pour deplacer son point de
depart. Les arrivees legales se recalculent pendant le drag. Application
atomique au relachement (entre deux tours).
### 1.3 Collision — camera pan/zoom + notification
L'engine emet deja `PieceReturnedToStockEvent` + auto-pause. Il reste a :
- Animer un pan + zoom de la camera vers la case de collision.
- Afficher une notification dans un coin de l'ecran
(ex : "Tour II detruite par Dame — retournee au stock").
- Reprise a Espace apres lecture.
### 1.4 Touche Suppr pour retirer une piece
Le bouton `[Retirer]` du `DetailPanel` existe. Ajouter en complement :
selection + `Delete` → meme effet que le bouton.
### 1.5 Cinematique de transition de mission
Sur `MissionStartedEvent` (hors mission 0) :
- Titre "Nouvelle mission" plein ecran en fade-in.
- Lock pan/zoom ; la camera se deplace vers la nouvelle zone.
- Animation d'expansion pour les cases debloquees par le `TerrainPatch`.
- Le titre glisse ensuite vers le panneau d'objectifs avant de disparaitre
(guide l'oeil).
- Unlock pan/zoom, la simulation reprend.
### 1.6 Visualisation des trajets
`TrajectView` existe. Manque :
- Fleches directionnelles sur le trait.
- Pulsation legere du trait quand la piece est en mouvement.
- Couleur unique par piece pour distinguer les chaines.
---
## 2. Extension de la campagne
`campaign_01.json` compte actuellement 7 missions (Pion → Tour → Cavalier →
Fou → Dame + 2 transformateurs). La vision GDD/plan prevoit une campagne
plus longue et un final orchestrant toutes les chaines.
### 2.1 Missions supplementaires
- **"L'Expansion Finale"** (14x14) : multiples transformateurs, murs
complexes. Defi : maintenir toutes les chaines en s'etendant ; gestion de
la congestion / collisions.
- **"Le Couronnement final"** (14x14) : Cathedrale qui demande or + armes +
outils simultanement. Orchestrer l'ensemble des chaines logistiques.
Actuel mission 7 "Le Couronnement" = transformation outils→or seule.
La renommer ou l'inserer comme mission intermediaire et ajouter les deux
missions ci-dessus pour retrouver la progression 10 missions de la vision
initiale.
### 2.2 Demandes recurrentes (post-campagne 1)
Pour forcer la preservation des automatisations entre missions :
- Une demande consomme N unites par tour.
- Si plus approvisionnee → etat "en penurie".
- Condition : aucune demande en penurie pendant X tours consecutifs.
Implique : nouveau champ sur `DemandState`, flag shortage, regle de victoire
parametrable par mission. A valider via playtest avant d'ajouter au scope.
---
## 3. Polish visuel des transformateurs
Couleur de cellule dediee (orange cuivre), animation flash input → flash
output sur `CargoConvertedEvent`, icones de cargo sur les pieces porteuses.