diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 8b48f6a..08c7b00 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -25,8 +25,80 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
jq \
nano \
vim \
+ ca-certificates \
+ curl \
+ wget \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
+# -----------------------------------------------------------------------------
+# Chessistics: headless Godot + .NET SDK
+# -----------------------------------------------------------------------------
+
+# 1. Xvfb + Mesa software GL + X/audio runtime deps for Godot's GL-compatibility renderer
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ xvfb \
+ xauth \
+ x11-utils \
+ libx11-6 \
+ libxcursor1 \
+ libxinerama1 \
+ libxrandr2 \
+ libxi6 \
+ libxext6 \
+ libxrender1 \
+ libxfixes3 \
+ libxss1 \
+ libxkbcommon0 \
+ libxkbcommon-x11-0 \
+ libgl1 \
+ libglx-mesa0 \
+ libgl1-mesa-dri \
+ libglu1-mesa \
+ libegl1 \
+ libgles2 \
+ libasound2 \
+ libpulse0 \
+ libfontconfig1 \
+ libfreetype6 \
+ libdbus-1-3 \
+ libudev1 \
+ fonts-dejavu-core \
+ python3 \
+ python3-pip \
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
+
+# 2. .NET SDK 9.0 via the upstream install script (arch-agnostic, no apt repo needed)
+RUN curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \
+ && bash /tmp/dotnet-install.sh --channel 9.0 --install-dir /usr/local/dotnet \
+ && ln -s /usr/local/dotnet/dotnet /usr/local/bin/dotnet \
+ && rm /tmp/dotnet-install.sh
+ENV DOTNET_ROOT=/usr/local/dotnet
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+ENV DOTNET_NOLOGO=1
+
+# 3. Godot 4.6.2-stable Mono for Linux x86_64
+# The zip contains a directory like "Godot_v..._mono_linux_x86_64/" with
+# an executable whose exact filename has varied across releases
+# ("Godot_v..._mono_linux.x86_64" on 4.x). Locate it dynamically.
+ARG GODOT_VERSION=4.6.2-stable
+RUN mkdir -p /opt/godot \
+ && cd /tmp \
+ && wget -q "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}/Godot_v${GODOT_VERSION}_mono_linux_x86_64.zip" \
+ && unzip -q "Godot_v${GODOT_VERSION}_mono_linux_x86_64.zip" -d /opt/godot \
+ && GODOT_EXE="$(find /opt/godot -maxdepth 3 -type f \( -name 'Godot_v*mono_linux*x86_64' -o -name 'Godot_v*mono_linux*x86_64' \) | head -1)" \
+ && if [ -z "$GODOT_EXE" ]; then echo "Godot executable not found in zip" && ls -R /opt/godot && exit 1; fi \
+ && chmod +x "$GODOT_EXE" \
+ && ln -sf "$GODOT_EXE" /opt/godot/godot \
+ && rm "Godot_v${GODOT_VERSION}_mono_linux_x86_64.zip"
+ENV GODOT_BIN=/opt/godot/godot
+ENV PATH=$PATH:/opt/godot:/usr/local/dotnet
+
+# 4. xvfb wrapper — any Godot invocation gets its own virtual 1280x720x24 display.
+# Usage: `godot-xvfb --path /workspace ...` or let tools/automation/harness.py
+# invoke it automatically on Linux.
+COPY godot-xvfb.sh /usr/local/bin/godot-xvfb
+RUN chmod +x /usr/local/bin/godot-xvfb
+
# Ensure default node user has access to /usr/local/share
RUN mkdir -p /usr/local/share/npm-global && \
chown -R node:node /usr/local/share
diff --git a/.devcontainer/godot-xvfb.sh b/.devcontainer/godot-xvfb.sh
new file mode 100644
index 0000000..32a2d5c
--- /dev/null
+++ b/.devcontainer/godot-xvfb.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Wrap a Godot invocation in a fresh Xvfb 1280x720x24 display so the GL
+# renderer has something to draw into. If DISPLAY is already set (real
+# display / nested X server), skip xvfb-run and exec Godot directly.
+set -euo pipefail
+
+: "${GODOT_BIN:=/opt/godot/godot}"
+
+if [[ -n "${DISPLAY:-}" ]]; then
+ exec "$GODOT_BIN" "$@"
+fi
+
+exec xvfb-run -a \
+ --server-args="-screen 0 1280x720x24 -ac +extension GLX +render -noreset" \
+ "$GODOT_BIN" "$@"
diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh
index ad4e589..49b2726 100644
--- a/.devcontainer/init-firewall.sh
+++ b/.devcontainer/init-firewall.sh
@@ -69,7 +69,8 @@ for domain in \
"api.anthropic.com" \
"sentry.io" \
"statsig.anthropic.com" \
- "statsig.com"; do
+ "statsig.com" \
+ "api.nuget.org"; do
echo "Resolving $domain..."
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
if [ -z "$ips" ]; then
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3670609
--- /dev/null
+++ b/README.md
@@ -0,0 +1,174 @@
+# Chessistics
+
+Jeu de logistique sur échiquier en Godot 4 / C#. Le joueur place des pièces
+d'échecs sur un plateau ; elles se déplacent automatiquement et transportent
+des ressources entre des productions et des demandes.
+
+Voir [`CLAUDE.md`](CLAUDE.md) pour l'architecture (black-box simulation) et
+les conventions internes.
+
+## Arborescence
+
+```
+Chessistics/
+├─ Scripts/ # Code Godot (présentation)
+│ ├─ Automation/ # Harness autonome (CLI --automation=
)
+│ ├─ Board/ Input/ UI/ Pieces/ Presentation/
+│ └─ Main.cs
+├─ chessistics-engine/ # Moteur pur .NET, sans Godot
+├─ chessistics-tests/ # Tests unitaires xUnit (sans Godot)
+├─ Data/
+│ ├─ campaigns/ # campaign_01.json (7 missions)
+│ └─ levels/ # niveaux legacy
+├─ tools/automation/ # Harness Python stdlib pour piloter Godot
+├─ .devcontainer/ # Image Docker + firewall + Xvfb + Godot Linux
+├─ Scenes/ icon.svg project.godot …
+```
+
+## Lancer le jeu (poste Windows direct)
+
+Prérequis : Godot 4.6 mono + .NET 9 SDK installés localement.
+
+```powershell
+dotnet build Chessistics.csproj
+"C:\Apps\godot\Godot_v4.6.2-stable_mono_win64.exe" --path .
+```
+
+Tests headless du moteur :
+
+```powershell
+dotnet test chessistics-tests/
+```
+
+Smoke test du harness d'automatisation :
+
+```powershell
+python tools/automation/smoke.py
+```
+
+## Dev container autonome (recommandé pour Claude Code)
+
+Le dossier `.devcontainer/` contient une image Docker qui embarque :
+
+- **.NET SDK 9.0** (build + tests)
+- **Godot 4.6.2-stable mono Linux x86_64** sous `/opt/godot/godot`
+- **Xvfb** + Mesa software GL pour un framebuffer virtuel 1280×720
+- **Python 3** pour le harness d'automatisation
+- **Claude Code** installé automatiquement (`@anthropic-ai/claude-code`)
+- Un firewall `iptables` qui restreint les sorties réseau à une allow-list
+ (GitHub, npm, Anthropic, NuGet, Sentry, Statsig)
+
+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
+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)
+
+1. Installer [Docker Desktop pour Windows](https://www.docker.com/products/docker-desktop/).
+ Docker Desktop utilise WSL2 en coulisses ; aucune configuration WSL
+ manuelle n'est nécessaire.
+2. Installer la CLI devcontainers :
+
+ ```powershell
+ npm install -g @devcontainers/cli
+ ```
+
+3. Depuis PowerShell ou Terminal Windows, à la racine du repo :
+
+ ```powershell
+ cd C:\Projets\Chessistics
+ devcontainer up --workspace-folder .
+ devcontainer exec --workspace-folder . zsh
+ ```
+
+ La première commande construit l'image (long le premier coup : Godot +
+ .NET à télécharger). La seconde ouvre un shell interactif dans le
+ container.
+
+4. Dans le container :
+
+ ```bash
+ dotnet build Chessistics.csproj
+ python3 tools/automation/smoke.py
+ claude # lance Claude Code inside the container
+ ```
+
+### Option 2 — VS Code + extension Dev Containers
+
+1. Installer Docker Desktop + [VS Code](https://code.visualstudio.com/) +
+ l'extension **Dev Containers**.
+2. Ouvrir le dossier du repo dans VS Code.
+3. Palette de commandes → `Dev Containers: Reopen in Container`.
+4. Le terminal intégré est déjà dans le container. Lancer `claude` pour
+ démarrer Claude Code.
+
+### Option 3 — WSL2 natif (plus performant si Docker Desktop rame)
+
+Les bind-mounts Docker Desktop sur `C:\` sont plus lents qu'un fichier
+stocké directement dans le système de fichiers WSL2. Pour un gros projet
+c'est négligeable, mais si la lenteur devient visible :
+
+1. Installer WSL2 + Ubuntu depuis le Microsoft Store.
+2. Cloner le repo **à l'intérieur** de WSL2 (pas sur `/mnt/c/`) :
+
+ ```bash
+ cd ~ && git clone chessistics && cd chessistics
+ ```
+
+3. Installer Docker dans WSL2 (via `docker.io` apt ou Docker Desktop avec
+ intégration WSL2 activée).
+4. Installer la CLI devcontainers :
+
+ ```bash
+ npm install -g @devcontainers/cli
+ devcontainer up --workspace-folder .
+ devcontainer exec --workspace-folder . zsh
+ ```
+
+Avantage : I/O disque natif Linux. Inconvénient : pas d'accès Explorer
+direct aux fichiers (`\\wsl$\Ubuntu\home\…`).
+
+### Vérifier que tout fonctionne dans le container
+
+```bash
+dotnet --version # -> 9.0.x
+godot --version # -> 4.6.2.stable.mono.official.*
+dotnet test chessistics-tests/ # 102/102
+python3 tools/automation/smoke.py # Godot boots, PNG screenshots written
+ls .automation_runs/smoke/screens/ # 01_loaded.png, 02_placed.png, ...
+```
+
+Le harness Python détecte automatiquement Linux et enveloppe Godot dans
+`xvfb-run`. Aucune variable d'environnement à positionner.
+
+### Personnaliser la version de Godot
+
+L'image par défaut pose Godot 4.6.2-stable. Pour changer, modifier
+l'argument de build :
+
+```jsonc
+// .devcontainer/devcontainer.json
+"build": {
+ "args": {
+ "GODOT_VERSION": "4.7.0-stable"
+ }
+}
+```
+
+Puis `devcontainer up --workspace-folder . --build-no-cache`.
+
+## Dépannage
+
+| Symptôme | Cause probable | Fix |
+|----------|----------------|-----|
+| `godot --version` donne *no such file* | PATH non mis à jour | `source /etc/profile` ou relancer le shell |
+| Screenshot tout noir | Aucun DISPLAY + pas d'xvfb | Vérifier `which xvfb-run` ; utiliser `godot-xvfb` au lieu de `godot` directement |
+| `dotnet restore` bloque | Firewall bloque `api.nuget.org` | Vérifier que `init-firewall.sh` s'est bien exécuté avec les changements récents |
+| Build Docker échoue au download de Godot | Réseau restreint côté hôte | Retry, ou installer Godot manuellement et commenter les lignes correspondantes |
+| Claude Code demande `--dangerously-skip-permissions` | Comportement normal en container sandbox | Accepter si tu es conscient du modèle de confiance |
+
+## Licence
+
+Voir les fichiers du repo.
diff --git a/autonomous_plan.md b/autonomous_plan.md
new file mode 100644
index 0000000..ee214a7
--- /dev/null
+++ b/autonomous_plan.md
@@ -0,0 +1,149 @@
+# 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).
diff --git a/tools/automation/harness.py b/tools/automation/harness.py
index 59edd4e..636466b 100644
--- a/tools/automation/harness.py
+++ b/tools/automation/harness.py
@@ -36,10 +36,37 @@ from typing import Any
# Resolve defaults relative to the repo root (parent of tools/).
_REPO_ROOT = Path(__file__).resolve().parents[2]
-_DEFAULT_GODOT = Path(r"C:\Apps\godot\Godot_v4.6.2-stable_mono_win64_console.exe")
_DEFAULT_RUNS = _REPO_ROOT / ".automation_runs"
+def _default_godot_exe() -> Path:
+ """Locate a Godot binary. Env var wins; otherwise platform defaults."""
+ env = os.environ.get("GODOT_BIN")
+ if env:
+ return Path(env)
+ if sys.platform.startswith("linux"):
+ return Path("/opt/godot/godot")
+ return Path(r"C:\Apps\godot\Godot_v4.6.2-stable_mono_win64_console.exe")
+
+
+_DEFAULT_GODOT = _default_godot_exe()
+
+
+def _wrap_with_xvfb(argv: list[str]) -> list[str]:
+ """On Linux without an existing DISPLAY, wrap Godot in xvfb-run so the
+ GL-compatibility renderer has a framebuffer to target.
+ """
+ if not sys.platform.startswith("linux"):
+ return argv
+ if os.environ.get("DISPLAY"):
+ return argv
+ return [
+ "xvfb-run", "-a",
+ "--server-args=-screen 0 1280x720x24 -ac +extension GLX +render -noreset",
+ *argv,
+ ]
+
+
class HarnessError(RuntimeError):
"""Raised when a command fails or times out."""
@@ -98,11 +125,11 @@ class Harness:
if not self.project_path.exists():
raise HarnessError(f"Project path not found: {self.project_path}")
- args = [
+ args = _wrap_with_xvfb([
str(self.godot_exe),
"--path", str(self.project_path),
f"--automation={self.root}",
- ]
+ ])
print(f"[harness] launching: {' '.join(args)}", file=sys.stderr)
# Inherit stdout/stderr so GD.Print output is visible.
self._proc = subprocess.Popen(args)