ded7bf96d5
ci / verify (push) Successful in 47s
The roadmap was actively misleading: an earlier stage renumbering left Stage 2's completed work sitting under a "Stage 3 -- todo" heading, and the inventory table orphaned with no heading at all. Since that file is the primary handoff document, a fresh session would have started by re-implementing accounts and characters. Rewritten. Captured in full, from the original brief rather than from memory, the two stages not yet built: - Stage 3 (inventory and loot): 4 slots, potions rare from trash and guaranteed from bosses, world-shared loot, and the player-instanced food item -- with a note that two loot visibilities must exist from the start, because proving the instanced path works is the food item's entire purpose. - Stage 4 (upgrades): every upgrade with its exact stated effect, plus the two constraints it will collide with -- sniper's 2x bullet speed against the tunnelling threshold, and per-player bullet travel against the interest radius that test_interest.gd currently derives from static content. Ten open questions are listed as explicitly do-not-guess, seven of them blocking Stage 4. ARCHITECTURE.md still claimed "the arena is a rectangle" and "no tilemap collision", both untrue since Stage 1. Rewritten around MapGrid, with what the grid costs (axis-aligned, 32px-quantised, bullets under a tile per tick) rather than only what it buys. Added a "verification traps" section to WORKFLOW.md recording six mistakes made during this work, each of which cost a round trip of reporting something fixed that was not: verifying the artefact rather than the behaviour, dumping the wrong channel, measuring a configuration where the bug cannot exist, a test whose setup silently invalidated it, git checkout reverting real work alongside a probe, and a pattern edit matching in two files. They are specific enough to be actionable. Also documented the diagnostics in CLAUDE.md -- they were undiscoverable -- and recorded the current verification surface so "everything passes" has a stated meaning. 206 tests, 15 smoke assertions, both diagnostics pass.
238 lines
12 KiB
Markdown
238 lines
12 KiB
Markdown
# Agent-assisted Godot development
|
|
|
|
Research notes and the resulting setup for this repo. The short version: Godot
|
|
is an editor-centric engine and a coding agent is a text-centric tool, so the
|
|
whole workflow is built around making the game fully runnable, inspectable and
|
|
verifiable from the command line — with the editor as an optional viewer rather
|
|
than a required step.
|
|
|
|
## The core problem
|
|
|
|
An agent editing a Godot project cannot see the editor. It cannot click play,
|
|
cannot look at the scene dock, and cannot tell whether a `.tscn` it wrote is
|
|
valid. Left alone it will produce code that looks right and does not run.
|
|
|
|
Everything below exists to close that gap. Three properties matter:
|
|
|
|
1. **A fast, honest failure signal.** Parse errors must surface in seconds, from
|
|
the CLI, with the engine's own words.
|
|
2. **A way to actually play the game without a human.** Not "the tests pass" —
|
|
the real binary, the real socket, the real loop.
|
|
3. **Source of truth in text.** Binary scenes and inspector-tuned resources are
|
|
invisible to review and produce meaningless diffs.
|
|
|
|
## Godot's CLI, as an agent uses it
|
|
|
|
| Command | Use |
|
|
| --- | --- |
|
|
| `godot --headless --path . --import` | Rebuild the import + **global class cache**. Required after adding any `class_name`. ~2s. |
|
|
| `godot --headless --path . res://scene.tscn` | Run a scene headless with autoloads alive. |
|
|
| `godot --headless --path . --script tools/x.gd` | Run a `SceneTree` script. **No autoloads.** |
|
|
| `godot --headless --check-only -s file.gd` | Parse one file. |
|
|
| `godot --headless --path . -s addons/gut/gut_cmdln.gd -gdir=res://tests -gexit` | Run the test suite. |
|
|
| `godot --headless --path . --export-release <preset> <out>` | Build. |
|
|
|
|
Four things cost real time to discover, so they are worth stating plainly:
|
|
|
|
- **The global class cache is not built by running the game.** It lives in
|
|
`.godot/global_script_class_cache.cfg` and is written by the editor or by
|
|
`--import`. Add a `class_name`, run your code, and every reference to it fails
|
|
with `Identifier "Foo" not declared in the current scope` — a message that
|
|
looks exactly like a typo and is not one. `tools/check.sh` runs `--import`
|
|
first for this reason.
|
|
- **Autoload singletons do not exist under `--script`.** `--script` replaces the
|
|
main loop, so autoload names are unresolved identifiers at compile time. Any
|
|
tool that needs an autoload has to run as a scene instead. The practical
|
|
consequence for design: keep autoloads to the minimum that genuinely needs to
|
|
be a node. This repo has exactly one (`Net`, because RPC routing is by node
|
|
path); logging and CLI options are static classes, which makes them usable
|
|
from tests and tools alike.
|
|
- **`ResourceLoader.load()` on a script with a parse error can return a non-null
|
|
object.** Detecting failures by null-checking the return value silently passes.
|
|
Read the engine's stderr instead — that is what `tools/check.sh` does.
|
|
- **Never call `Script.reload()` on the script currently executing.** It hangs
|
|
the engine with no output.
|
|
|
|
## What this repo does about it
|
|
|
|
### `tools/check.sh` — the inner loop
|
|
|
|
Refreshes the class cache, loads every `.gd` in the project, greps the engine's
|
|
stderr for `Parse Error` / `SCRIPT ERROR` / `Failed to load script`, and exits
|
|
non-zero on any hit. About five seconds. This is the command to run after every
|
|
edit; it catches the entire class of "looks right, does not compile" mistakes
|
|
that an agent produces most often.
|
|
|
|
### `tools/test.sh` — GUT, headless
|
|
|
|
[GUT 9.7.1](https://github.com/bitwes/Gut) is vendored into `addons/gut/`.
|
|
Chosen over [gdUnit4](https://github.com/godot-gdunit-labs/gdUnit4) because this
|
|
project is GDScript-only, where GUT is the lighter and simpler of the two;
|
|
gdUnit4 is the better pick when C# is in play or when you want its scene-runner
|
|
utilities. Either runs headless in CI.
|
|
|
|
The suite runs in about two seconds because of an architectural choice, not a
|
|
testing trick: **the entire simulation is plain `RefCounted` objects** — no
|
|
nodes, no physics server, no rendering. A test constructs a `SimWorld`, drives
|
|
a thousand ticks, and asserts. Nothing to await, nothing to instantiate, no
|
|
frame timing. If the simulation had been built out of `CharacterBody2D` and
|
|
`Area2D`, every one of these tests would need a live SceneTree and would be
|
|
slower and flakier by an order of magnitude.
|
|
|
|
That is the single highest-leverage decision in this repo for agent-assisted
|
|
work, and it is worth stating as a general rule: **keep game logic out of nodes.**
|
|
Nodes are for presentation and input. Logic in plain objects is testable,
|
|
diffable, reviewable, and runs on a headless server for free.
|
|
|
|
### `tools/smoke.sh` — the thing tests cannot do
|
|
|
|
Boots the real dedicated server, connects two scripted bot clients over a real
|
|
ENet socket, and asserts on the server and client logs that the whole loop
|
|
happened: handshake, lobby, portal into a dungeon, emergency escape back out.
|
|
|
|
Unit tests structurally cannot cover RPC wiring, codec round-trips over the
|
|
wire, instance transfers or client reconciliation. This does, in ~35 seconds,
|
|
with no display. The bot input lives in `ClientRuntime._bot_input()` behind
|
|
`--bot`, so the "player" driving it is the same code path a human uses.
|
|
|
|
This is the pattern to reach for whenever an agent needs to verify something
|
|
interactive: give the program a scripted-input mode and a `--autoquit`, run it
|
|
headless, and assert on structured log lines. Log markers like `SERVER_READY`
|
|
exist specifically to be grepped.
|
|
|
|
### `tools/bench.gd` — performance as a number
|
|
|
|
Runs the simulation with no transport and no rendering and prints milliseconds
|
|
per tick. Current numbers, 4 players, 60s of play:
|
|
|
|
```
|
|
boss fight 0.26 ms/tick peak ~300 bullets headroom x64
|
|
trash wave 0.16 ms/tick peak ~82 bullets headroom x103
|
|
```
|
|
|
|
Note what this does *not* cover: it measures the simulation only, with no
|
|
transport. Per-peer snapshot encoding was measured separately (95.6us for four
|
|
filtered snapshots against 24.9us for one shared, or 0.032 ms/tick amortised).
|
|
Quoting a bench number for something the bench does not exercise is its own
|
|
version of the trap below.
|
|
|
|
A 60 Hz tick has a 16.6 ms budget, so one instance uses ~1.4% of one core. That
|
|
is the measurement that says a single server process can host dozens of
|
|
concurrent dungeons, and it took ten seconds to get because the simulation has
|
|
no engine dependencies.
|
|
|
|
## Content as code, not as `.tres`
|
|
|
|
Enemies and bosses are built by GDScript functions in `src/content/content.gd`,
|
|
not authored as `.tres` files. For agent-assisted work this is the right default:
|
|
|
|
- A boss is a readable diff. A `.tres` full of `SubResource` ids is not.
|
|
- No resource UID churn in version control.
|
|
- A test can build content inline without touching the filesystem.
|
|
- The agent can write a boss without an editor.
|
|
|
|
`tools/export_content.gd` writes `.tres` copies into `resources/` for anyone who
|
|
wants to tune numbers in the inspector, with the direction of truth documented:
|
|
code wins, port inspector changes back.
|
|
|
|
The same reasoning applies to scenes. This project has three `.tscn` files, each
|
|
a handful of nodes. Anything dynamic — the HUD, the menu — is built in code,
|
|
because a scene file for it would only be one more thing to keep in sync.
|
|
|
|
## Godot MCP servers
|
|
|
|
An MCP server lets an agent talk to a live Godot editor: inspect the scene tree,
|
|
read node properties, create nodes, run the project and read its output. The
|
|
most established option is
|
|
[Coding-Solo/godot-mcp](https://github.com/Coding-Solo/godot-mcp) (~5.5k stars,
|
|
Node ≥18):
|
|
|
|
```bash
|
|
claude mcp add godot -- npx @coding-solo/godot-mcp
|
|
```
|
|
|
|
It exposes `launch_editor`, `run_project`, `stop_project`, `get_debug_output`,
|
|
`get_godot_version`, `get_project_info`, `create_scene`, `add_node`,
|
|
`load_sprite`, `save_scene`, `export_mesh_library`, and UID helpers for 4.4+.
|
|
Set `GODOT_PATH` if `godot` is not on `PATH`.
|
|
|
|
**Where it earns its place:** projects with large hand-authored scene trees,
|
|
where the agent otherwise has to guess node names and paths. Being able to read
|
|
the real tree instead of inferring it is the biggest single quality jump in that
|
|
situation.
|
|
|
|
**Where it does not:** this repo. `run_project` and `get_debug_output` are what
|
|
`tools/server.sh` and `tools/smoke.sh` already do, more precisely and with
|
|
assertions attached; and the scene-authoring tools matter little when there are
|
|
three small scenes. It also adds a live-editor dependency, and editor
|
|
auto-reload racing against agent file writes is a known source of stale state.
|
|
|
|
The honest recommendation: **start with the headless CLI loop, add the MCP when
|
|
scene inspection becomes the bottleneck.** It is one command to add later.
|
|
|
|
## Verification traps, learned the hard way
|
|
|
|
Every entry below cost at least one round trip of reporting something as fixed
|
|
when it was not. They are specific to this setup, and they generalise.
|
|
|
|
### Verify the behaviour, not the artefact you produced
|
|
|
|
Bullet sprites were reported fixed three times before they were. Each time the
|
|
*atlas* was checked and found correct. The actual fault was one layer down: a
|
|
`MultiMeshInstance2D` cannot display a sub-region of a texture, so the renderer
|
|
was structurally incapable of showing one cell no matter what the atlas held.
|
|
Checking the input to a broken stage will confirm the input forever.
|
|
|
|
Ask what the *player* would see, and find a way to check that.
|
|
|
|
### An ASCII dump only proves what you dumped
|
|
|
|
The same sprites were "verified" by rendering their alpha channel. The pack
|
|
animates as a colour shimmer over a fixed silhouette, so identical-looking
|
|
frames proved nothing at all. The check was real; it was measuring the wrong
|
|
channel.
|
|
|
|
### Measure the configuration that has the bug
|
|
|
|
The client-prediction gap was measured at 0.1px on a listen server and reported
|
|
fixed. A listen server runs both halves in one process on one physics tick, so
|
|
the quantity that had drifted — two independent clocks — was constant *by
|
|
construction*. The bug needed two processes to exist at all.
|
|
|
|
If a bug is described as appearing "after a while", ask what accumulates, and
|
|
make sure your harness lets it accumulate.
|
|
|
|
### Have the test tell you the setup was valid
|
|
|
|
`test_firing_is_reported_even_when_the_bullet_dies_instantly` asserts that no
|
|
`BULLET_SPAWN` event occurred, purely to prove the interesting condition was
|
|
actually reached. The first version placed a turret as the victim — and turrets
|
|
shoot, so the assertion passed on the turret's own bullets while testing
|
|
nothing. A setup check inside the test caught it.
|
|
|
|
### `git checkout` to clean up a probe reverts real work too
|
|
|
|
A one-file revert to remove a temporary debug hook also discarded a fix made to
|
|
the same file earlier in the session, and it had already been verified and
|
|
reported. Only `git status` showing the file *missing* from the staged set
|
|
caught it. Prefer editing the probe back out, or stash.
|
|
|
|
### A pattern-based edit can match twice
|
|
|
|
A `replace()` intended for the snapshot decoder also matched inside the
|
|
character decoder, which then read a field its encoder never wrote. Both are
|
|
codecs and both had the same trailing lines. Check the match count when patching
|
|
by pattern, not by line.
|
|
|
|
## CI
|
|
|
|
`.github/workflows/ci.yml` runs check → test → smoke on a container image with
|
|
Godot preinstalled. All three are headless, so nothing extra is needed.
|
|
|
|
## Sources
|
|
|
|
- [Coding-Solo/godot-mcp](https://github.com/Coding-Solo/godot-mcp)
|
|
- [GUT](https://github.com/bitwes/Gut) · [gdUnit4](https://github.com/godot-gdunit-labs/gdUnit4)
|
|
- [Godot: high-level multiplayer](https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html)
|
|
- [Godot: command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html)
|