Files
Claude-Code-Game-Studios/.claude/docs/run-and-observe.md
T
DonchitosandClaude Opus 5.5 be8993bbc5 Release 1.1.3 — long skills keep their later steps
Fix release for 1.1.2: the 14 longest skills keep their later phases
after a compaction, a question nobody answered is never taken as an
answer, settings read the same from a subfolder, the commit check works
for a project in a subfolder of a larger repository, every skill opens
with a title, and the shipped allow rules no longer approve deleting a
branch or writing files without asking. Details in CHANGELOG.md; upgrade
steps in UPGRADING.md (v1.1.2 → v1.1.3).

Refs #83
Refs #133

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185en4t76fPn7315q4c3nko
2026-10-08 17:52:48 +11:00

11 KiB
Raw Blame History

Run and Observe

Shared procedure for any skill that closes a story which changes something a player can see. Used by /dev-story (Phase 6) and /story-done (Phase 3). A parse check is not a run, and a story nobody looked at does not close. This file explains how to look.

.claude/docs/coding-standards.md is the authority for what evidence a story needs. This file is the procedure for producing the visual half of it.

The rule

Every story that changes something player-observable is launched and observed before it closes, and the observation is a retained screenshot in production/qa/evidence/. Compiling proves the code loads. Tests prove the logic. Neither proves the text fits in the box.

The agent can see images: Claude Code's Read tool renders a PNG. The whole problem is getting a picture out of the engine unattended, and every supported engine has a native way.

The procedure

  1. Launch windowed at a fixed resolution — 1280x720 unless the project says otherwise — using commands.run from project.yaml. Never the headless / batch / null-RHI flag: those exist to skip rendering.
  2. Launch straight into the scene or map the story touched. A feature that is only reachable through the main menu and three clicks is a feature the run will not reach. Stories should be written so their surface is reachable from a launch argument (a scene path, a map URL, a --scene flag).
  3. Wait, capture, quit. Let the engine render real frames — 60 is a safe floor — capture one, and exit. Each engine's mechanism is below.
  4. Look. Read the PNG. Compare what is on screen to the acceptance criteria. Clipped text, an overflowing panel, a missing element, the wrong colour — these are defects, and this is the only step that finds them.
  5. Retain. Copy the image to production/qa/evidence/[story-slug]/[NN]-[what-it-shows].png and reference it from the evidence doc. A screenshot that was taken and discarded is an assertion, not evidence.
  6. Report one Run result: line in the implementation summary — vocabulary below.

Per engine

engine.name Launch Capture Output lands in Never use Scaffold needed
Godot 4 godot --path . --windowed --resolution 1280x720 <scene>.tscn --write-movie <path>.png --quit-after <N> beside the given path, <name>00000000.png … --headless none
Unity 6 built player .exe -screen-width 1280 -screen-height 720 -screen-fullscreen 0 --scene <Name> ScreenCapture.CaptureScreenshot(<abs path>) from a bootstrap script the absolute path you pass -batchmode, -nographics one MonoBehaviour
Unreal 5 `MSYS_NO_PATHCONV=1 "/Engine/Binaries/Win64/UnrealEditor.exe" "$(pwd -W 2>/dev/null pwd)/.uproject" -game -windowed -ResX=1280 -ResY=720` HighResShot 1280x720 filename=<abs path> console command the path you pass (else Saved/Screenshots/WindowsEditor/)

Godot 4 — zero scaffold

godot --path . --windowed --resolution 1280x720 --write-movie shots/shop.png --quit-after 60 res://scenes/shop.tscn

--write-movie is Movie Maker mode: with a .png target it writes a frame sequence and --quit-after sets the frame count. Filenames carry 8 zero-padded digits — shop00000000.png through shop00000059.png — so take the last. --fixed-fps is applied automatically. The scene path is positional; any .tscn can be launched directly, which is how step 2 is satisfied.

Capture-on-trigger alternative, for a state a scene launch cannot reach: an autoload that on the wanted condition does await RenderingServer.frame_post_draw, then get_viewport().get_texture().get_image().save_png(path), then get_tree().quit().

Unity 6 — one MonoBehaviour

-batchmode displays nothing, so the run is either the built player or the editor in play mode. The player is the robust path:

Builds/Win64/Game.exe -screen-width 1280 -screen-height 720 -screen-fullscreen 0 --scene Shop --screenshot C:/abs/path/shop.png --after 60

-screen-* are Unity's own player arguments. --scene, --screenshot and --after are yours, read by the script below. The template does not ship it: the first time a Unity story needs a screenshot, /dev-story asks to write it, verbatim, to Assets/Scripts/ScreenshotOnArg.cs. It needs no scene edit — it boots itself, and does nothing unless the player was launched with --screenshot:

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

// Run-and-observe capture: Game.exe --scene Shop --screenshot C:/abs/shop.png --after 60
public class ScreenshotOnArg : MonoBehaviour
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    static void Boot()
    {
        if (Arg("--screenshot") == null) return;
        var go = new GameObject(nameof(ScreenshotOnArg));
        DontDestroyOnLoad(go);
        go.AddComponent<ScreenshotOnArg>();
    }

    static string Arg(string name)
    {
        var args = System.Environment.GetCommandLineArgs();
        for (int i = 0; i < args.Length - 1; i++)
            if (args[i] == name) return args[i + 1];
        return null;
    }

    IEnumerator Start()
    {
        var scene = Arg("--scene");
        if (!string.IsNullOrEmpty(scene)) yield return SceneManager.LoadSceneAsync(scene);
        int after = int.TryParse(Arg("--after"), out var n) ? n : 60;
        for (int i = 0; i < after; i++) yield return null;
        yield return new WaitForEndOfFrame();
        ScreenCapture.CaptureScreenshot(Arg("--screenshot"));
        for (int i = 0; i < 3; i++) yield return null;   // the file is written after the frame
        Application.Quit();
    }
}

Pass an absolute path — the relative base differs between editor and player. --scene must be in the build settings. Building the player first costs 1–3 minutes on a small project; that is the price of looking at the thing you ship rather than the editor.

Unreal 5 — one actor, or -ExecCmds

MSYS_NO_PATHCONV=1 timeout 60 "<UE root>/Engine/Binaries/Win64/UnrealEditor.exe" "$(pwd -W 2>/dev/null || pwd)/Game.uproject" /Game/Maps/Shop -game -windowed -ResX=1280 -ResY=720

That line is Windows'. On Linux the same arguments go to "<UE root>/Engine/Binaries/Linux/UnrealEditor" (no MSYS_NO_PATHCONV needed); on macOS Epic documents only the UnrealEditor.app bundle, so run the map from the editor or a command you have confirmed (see docs/engine-reference/unreal/current-best-practices.md, "Command Line").

-game runs standalone without the editor UI. The map is a positional URL immediately after the .uproject — not a console command. The MSYS_NO_PATHCONV=1 prefix is required on Windows: Claude Code runs commands in Git Bash, which rewrites any argument beginning with / as a Windows path, so /Game/Maps/Shop reaches the engine as C:/Program Files/Git/Game/Maps/Shop and the game stops on a "map not found" dialog nobody can see. Alternatively pass the map through -ExecCmds="open /Game/Maps/Shop", which is not converted. The project path must be absolute — with MSYS_NO_PATHCONV set, only $(pwd -W 2>/dev/null || pwd) gives one: a relative Game.uproject is not found, and the launch waits on an error dialog forever (verified on 5.7). There is no -unattended on this line, so every failure is a dialog nobody clicks: always run it under timeout. Capture is the HighResShot console command, fired either from a dev actor (Execute Console Command on BeginPlay after a short delay, then quit) or from the launch line:

-ExecCmds="HighResShot 1280x720 filename=C:/abs/path/shop.png"

The size (or a multiplier such as 2) comes before filename=: HighResShot filename=… alone is rejected as bad input and saves nothing. Without filename= the file lands in Saved/Screenshots/WindowsEditor/. Do not put Quit in the same -ExecCmds list — it runs on the same frame, before the file is written, and leaves nothing on disk. -ExecCmds has no delay, so let timeout end the launch (exit 124 is expected) and check the file exists; a dev actor can instead wait a second, then quit. The Blueprint node Take High Res Screenshot is editor-only and does not work in a -game launch.

Any engine — OS capture fallback

On Windows, PowerShell with System.Drawing can CopyFromScreen into a bitmap and save it. It needs a visible desktop and the game window on top, and it captures whatever is there — it cannot be told "the shop panel". Use it when the engine-native path is unavailable, and say so in the run result. Linux CI with no display needs xvfb-run in front of the launch; --headless is not a substitute.

The Run result: line

Exactly one of three shapes, in the implementation summary:

  • Run result: OBSERVED — <what was on screen, one line> with the retained path. "Shop panel renders, all 6 slots inside the frame, prices legible at 720p — production/qa/evidence/shop-panel/01-shop-open.png."
  • Run result: NOT VERIFIED — <reason>. Engine binary not found, commands.run unset, launch crashed, screenshot empty. This is a blocker at the default gate level for Visual/Feel and UI stories, not a note — a story nobody looked at does not close. Under an explicit testing.strict.visual/.ui: false it is recorded and the story may close with the gap in ## Completion Notes.
  • Run result: N/A — <reason>, only for a story with genuinely nothing observable — a pure data migration, a save-format change with no surface. Say why. "It's a Logic story" is not a reason: a damage formula has a number on screen somewhere.

The run is not waived at qa.level: minimal. Tests are waived there; the look is not. It is the cheapest verification in the pipeline and the one whose absence produces a well-tested game that looks wrong.

What a still can and cannot show

Can: layout, clipping, overflow, presence, alignment, colour, legibility, that the right screen appeared at all. This is where shipped visual defects actually live.

Cannot: timing, responsiveness, weight, animation curves, audio sync — the "feel" half of Visual/Feel. Godot's --write-movie gives a frame sequence; the other engines need OS video capture. Feel stays a human check in /team-qa. Say which half the screenshot covered.

Reaching the state

A screenshot of the title screen verifies nothing about depth 2. Either launch the scene/map directly (Godot scene arg, Unreal map URL, Unity --scene) or give the bootstrap a debug argument that sets state (--gold=500 --depth=2). Write stories so their surface is reachable one of those ways; a story that is not is a story that will be marked NOT VERIFIED every time.

Finding the engine

None of the three engines installs onto PATH on Windows. engine.path in project.yaml is where the editor lives; resolve it before concluding "no engine on this machine". Absence of a path is not absence of an engine.


Commands above were run on Windows with Godot 4.6.1 (gdUnit4 6.1.3), Unity 6000.3.23f1 and Unreal Engine 5.7; the Unreal Linux and macOS notes come from Epic's documentation. docs/engine-reference/<engine>/ is the project's pinned authority; check it before trusting a version-qualified claim here.