Chessistics/tools/automation
Samuel Bouchet c451a50349 Headless Linux dev container: Godot + .NET + Xvfb for autonomous testing
Claude Code running inside the project's dev container can now build the
game, launch a real Godot instance under Xvfb, and drive the automation
harness end-to-end — no Windows dependency.

Dockerfile adds (as root, before USER node):
- X11 / Mesa software GL / audio runtime deps + python3
- .NET SDK 9.0 via upstream dot.net install script -> /usr/local/dotnet
- Godot 4.6.2-stable mono Linux x86_64 -> /opt/godot/godot
- /usr/local/bin/godot-xvfb wrapper: auto-wraps invocations in
  xvfb-run -a --server-args="-screen 0 1280x720x24 ..."

harness.py picks GODOT_BIN from env, defaults to /opt/godot/godot on
Linux, and auto-wraps the subprocess in xvfb-run when DISPLAY is unset.
Windows code path unchanged.

init-firewall.sh adds api.nuget.org to the allowlist so dotnet restore
works post-boot. Godot + .NET SDK are fetched at image build time, before
the firewall exists.

New docs:
- autonomous_plan.md: design rationale, alternatives considered
- README.md: launch instructions for Windows terminal / Docker Desktop /
  VS Code Dev Containers / WSL2 natif
- CLAUDE.md already documents the harness (done in previous commit)

Validation: docker build succeeds; inside the container, dotnet --version
=9.0.313, godot --version=4.6.2.stable.mono, dotnet test=102/102,
python3 tools/automation/smoke.py passes end-to-end with 14 non-black
1280x720 PNGs. Mission 1 screenshot is visually identical to the Windows
build, and Xvfb determinism is a bonus (det_a.png ≡ det_b.png bytewise).
2026-04-17 16:57:56 +02:00
..
harness.py Headless Linux dev container: Godot + .NET + Xvfb for autonomous testing 2026-04-17 16:57:56 +02:00
README.md Add file-IPC automation harness for autonomous game testing 2026-04-16 22:34:56 +02:00
run_game.py Add file-IPC automation harness for autonomous game testing 2026-04-16 22:34:56 +02:00
smoke.py Add file-IPC automation harness for autonomous game testing 2026-04-16 22:34:56 +02:00

Chessistics automation harness

Drive a running Chessistics build via file-based IPC, so an AI agent (or a scripted test) can take screenshots, inject inputs, and read game state without a human in the loop.

How it works

  • Launch Godot with --automation=<dir>. The game activates an AutomationHarness node that polls <dir>/inbox/ each frame.
  • Send commands as JSON files; the game writes results to <dir>/outbox/ and screenshots to <dir>/screens/.
  • A handshake file <dir>/ready.json is written on startup.

Without the flag, the harness is not instantiated — zero runtime overhead and zero behavior change for normal play.

Quick start

# From repo root
python tools/automation/smoke.py     # end-to-end smoke test
python tools/automation/run_game.py  # interactive REPL

Requirements:

  • Python 3.10+ (stdlib only)
  • Godot 4.6 mono build at C:\Apps\godot\Godot_v4.6.2-stable_mono_win64_console.exe (override with Harness(godot_exe=...) or edit the default in harness.py).
  • A compiled build: run dotnet build Chessistics.csproj first.

Python API

from tools.automation.harness import Harness

with Harness.launch() as h:
    h.load_mission("campaign_01", 0)
    state = h.state()                       # full snapshot as dict
    h.screenshot("before")                  # → .automation_runs/<ts>/screens/before.png
    h.place("Rook", (0, 0), (0, 3))         # place a piece
    h.step()                                # one simulation tick (auto-waits for animation)
    h.screenshot("after")
    h.set_speed(0.1); h.play()              # auto-run fast

Methods on Harness:

  • screenshot(name) -> Path
  • state() -> dict — full snapshot + animating flag
  • select(kind) — e.g. "Rook", "Knight"
  • place(kind, start, end, level=1) — returns {placed, pieceId, reason}
  • click_cell(col, row, button="left"|"right")
  • key(name)"Space" (play/pause), "Escape" (cancel)
  • play() / pause() / step() / wait_idle()
  • set_speed(interval_seconds) — auto-step interval
  • load_mission(campaign, missionIndex=0)
  • back_to_menu() / quit()

Every non-query command auto-waits for EventAnimator.IsAnimating == false before returning, so consecutive calls see a fully-settled state.

JSON protocol

Inbox: { "id": "<uuid>", "cmd": "...", "args": {...} } Outbox: { "id": "<uuid>", "ok": true, "result": {...} } or { "id": "<uuid>", "ok": false, "error": "..." }

See the cmd table in Scripts/Automation/CommandDispatcher.cs for the complete list.

Output locations

  • .automation_runs/<name>/ready.json — handshake
  • .automation_runs/<name>/inbox/, outbox/ — command queue
  • .automation_runs/<name>/screens/*.png — 1280x720 PNGs

.automation_runs/ is a good candidate for .gitignore (not added here automatically — add it manually if needed).

Troubleshooting

  • Godot exits before ready.json — build probably didn't compile, or the --path arg is wrong. Run dotnet build Chessistics.csproj and check stderr from the harness.
  • Screenshot is all black--headless was passed; the harness needs a real rendering context. Don't use --headless with automation.
  • Command times out — an animation may be stuck; check the Godot console log for errors.

Architecture notes

The harness sits behind a thin facade (AutomationFacade) that the dispatcher uses. It never reaches into engine internals — only calls existing public surfaces on GameSim, InputMapper, EventAnimator, ControlBar, PieceStockPanel. The black-box simulation separation stays intact.