Chessistics/CLAUDE.md
Samuel Bouchet 3077b2d669 Teach Claude the autonomous devcontainer test recipe in CLAUDE.md
Covers toolchain sanity check, the root-owned .godot permissions pitfall,
build -> smoke -> Harness loop, Python snippet, visual validation loop,
and non-obvious state shape details. Also fixes the stale Windows-only
Godot path mention.
2026-04-17 21:13:05 +02:00

8.2 KiB

Chessistics

Jeu de logistique sur echiquier en Godot 4 / C#. Le joueur place des pieces d'echecs sur un plateau ; elles se deplacent automatiquement et transportent des ressources entre des productions et des demandes.

Architecture : Black-Box Simulation

Ref: https://samuel-bouchet.fr/posts/2026-04-08-black-box-sim/

Le moteur de jeu (chessistics-engine/) est une boite noire sans aucune dependance vers Godot. Il recoit des Commands, mute son etat interne, et retourne des Events. Le code Godot (Scripts/) ne fait que traduire l'input en commands et les events en visuels/animations.

Input → Command → GameSim (state + rules) → Events → Presentation
  • Commands (PlacePieceCommand, StartSimulationCommand, …) : seul moyen de modifier l'etat.
  • Events (PiecePlacedEvent, CargoDeliveredEvent, …) : seul output du moteur. Le presenteur les consomme pour animer.
  • GameSim : point d'entree unique. ProcessCommand() retourne la liste d'events.
  • Tests : chessistics-tests/ teste le moteur en headless, sans Godot.

Pieges Godot a eviter

MouseFilter sur les Controls enfants de Node2D

Tout Control (ColorRect, Label…) a MouseFilter = Stop par defaut. Quand un Control est enfant d'un Node2D (ex: les ColorRect dans CellView, les Labels dans PieceView), il participe quand meme au systeme GUI et consomme les clics, empechant _UnhandledInput de recevoir l'evenement.

Regle : toujours mettre MouseFilter = Control.MouseFilterEnum.Ignore sur les Controls purement visuels enfants de Node2D.

Conventions Claude

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.

Harnais d'automatisation (Claude peut jouer tout seul)

Le jeu peut etre pilote de maniere autonome via le flag --automation=<dir>. Un AutomationHarness (Scripts/Automation/) s'active alors comme noeud au root de la scene, lit des commandes JSON dans <dir>/inbox/, ecrit les resultats dans <dir>/outbox/, et place les captures d'ecran dans <dir>/screens/. Sans le flag, comportement normal — overhead zero.

Cote agent, un wrapper Python stdlib (tools/automation/harness.py) expose une API simple. Le binaire Godot est detecte via GODOT_BIN (fallback Windows C:\Apps\godot\Godot_v4.6.2-stable_mono_win64_console.exe, Linux /opt/godot/godot). Sous Linux sans DISPLAY, la commande Godot est auto-wrappee dans xvfb-run (framebuffer virtuel 1280x720).

Build + utilisation

dotnet build Chessistics.csproj             # compiler avant tout lancement
python tools/automation/smoke.py            # smoke test end-to-end
python tools/automation/run_game.py         # REPL interactif

API Python

from tools.automation.harness import Harness

with Harness.launch() as h:
    h.load_mission("campaign_01", 0)        # charge la campagne + mission 0
    state = h.state()                       # snapshot complet (dict)
    h.screenshot("before")                  # -> .automation_runs/<ts>/screens/before.png
    h.place("Pawn", (0, 0), (0, 1))         # pose une piece
    h.step()                                # un tour (auto-wait animation)
    h.screenshot("after")
    h.set_speed(0.1); h.play()              # auto-play rapide

Methodes : screenshot, state, select, place, click_cell, key, play, pause, step, wait_idle, set_speed, load_mission, back_to_menu, quit.

Toutes les commandes non-query attendent EventAnimator.IsAnimating == false avant de retourner -> appels en serie toujours vus par le prochain state().

Validation visuelle par Claude

Les PNG 1280x720 ecrites dans .automation_runs/<run>/screens/ peuvent etre lues directement par l'outil Read de Claude. Workflow type pour valider l'UI :

  1. h.load_mission("campaign_01", N) + h.screenshot("mission_N_start")
  2. Lire le PNG -> verifier titre, flavor banner, board, panneau objectifs, stock
  3. Placer des pieces via h.place(...) et re-screenshot
  4. h.step() en boucle + screenshot a chaque etape
  5. Attendre phase == "MissionComplete" dans le snapshot

Cette boucle permet de valider que :

  • Les demandes affichent les bons compteurs
  • Les pieces bougent comme prevu
  • Le stock se met a jour
  • L'ecran MissionComplete apparait quand attendu

Details importants

  • Place passe par le signal PlacementRequested (meme chemin qu'un vrai clic) -- ne pas appeler GameSim.ProcessCommand(PlacePieceCommand) directement dans le dispatcher, ca mute deux fois.
  • Les captures d'ecran sont prises apres RenderingServer.frame_post_draw -> le frame reflete l'etat final, animations incluses.
  • La facade (AutomationFacade) est la seule surface exposee au dispatcher. Elle ne touche que des methodes/signals publics de GameSim, InputMapper, EventAnimator, ControlBar, PieceStockPanel. La separation black-box tient.
  • Les fichiers IPC sont ecrits .tmp puis renommes (atomique sur Windows).
  • La campagne se charge via load_mission("campaign_01", 0). Passer a une mission

    0 n'est pas supporte directement (il faut passer par MissionComplete reel).

Mode autonome dans le devcontainer

Claude tourne dans un devcontainer Linux (.devcontainer/) qui embarque deja .NET 9 SDK, Godot 4.6.2 mono Linux, Xvfb, Python 3. Tout le workflow ci-dessous est executable sans sortir du container, sans display physique.

Sanity check toolchain (une fois par session)

dotnet --version        # 9.0.x
godot --version         # 4.6.2.stable.mono.official.*
which godot-xvfb        # /usr/local/bin/godot-xvfb (auto xvfb-run wrapper)

Piege permissions .godot/

Si une etape precedente (build Docker, editeur…) a laisse .godot/ detenu par root, le build dotnet echoue avec MSB3374: Access to the path '...Up2Date' is denied. Les perms sont a 777 -> supprimer le cache suffit, pas besoin de sudo :

rm -rf /workspace/.godot
dotnet build Chessistics.csproj

Recette pour verifier que Claude peut jouer tout seul

dotnet build Chessistics.csproj          # doit etre vert
python3 tools/automation/smoke.py        # charge mission 1, screenshots, determinisme
ls .automation_runs/smoke/screens/       # PNG non-noirs

Si smoke.py passe, tout le pipeline marche : Godot boot -> IPC inbox/outbox -> screenshots lisibles via l'outil Read.

Driver le jeu en Python depuis Claude

import sys, time
sys.path.insert(0, '/workspace')
from tools.automation.harness import Harness

with Harness.launch(run_name="claude_drive") as h:
    h.load_mission("campaign_01", 0)
    s = h.state()                       # dict: phase, turn, width, height,
                                        # grid, productions, demands,
                                        # transformers, pieces, remainingStock
    h.screenshot("01_loaded")
    h.place("Pawn", (0, 0), (0, 1))
    h.screenshot("02_placed")
    h.set_speed(0.05); h.play()
    time.sleep(2); h.pause()
    h.screenshot("03_after_play")

Les PNG atterrissent dans /workspace/.automation_runs/<run_name>/screens/. Claude les lit directement via Read (multimodal).

Boucle typique de validation visuelle

  1. h.load_mission("campaign_01", N) -> h.screenshot(f"m{N}_start") -> Read
  2. Verifier sur le PNG : titre, bandeau flavor, board, panneau OBJECTIFS, compteurs PIECES.
  3. Poser des pieces via h.place(...), relire l'etat (remainingStock diminue, pieces grandit).
  4. h.play() + sleep + h.pause() (ou h.step() en boucle), screenshot a chaque palier.
  5. Boucler jusqu'a state()["phase"] == "MissionComplete" ou un objectif demands[i].satisfied == True.

Details qui surprennent

  • Le snapshot state() expose width/height au niveau racine (pas board.width).
  • remainingStock est un dict {"Pawn": 4, ...} ; verifier qu'il decremente apres un place() confirme que la commande a bien ete appliquee.
  • Les logs Godot affichent des warnings ALSA (pas de carte son) et V-Sync — inoffensifs en headless, les filtrer avant d'afficher a l'utilisateur.
  • Le firewall du container bloque tout sauf l'allowlist ; le runtime Godot n'a besoin d'aucun reseau, donc aucun probleme.