Files
settled-reach/docs/sprints/sprint-9/server.md
T
jpmschweitzerandClaude Opus 4.6 85a3e87d36 docs(sprints): add Sprint 9 Gauntlet briefing files
Sprint 9 theme: Gauntlet — test infrastructure backbone with test
client binary, Gauntlet rooms 1-3, golden file suite, and client QA
harness. 23 tickets across server (9), client (6), CI (6), audio (2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:32:28 +01:00

9.3 KiB

Sprint 9: Gauntlet — Server Tasks

Goal: Build the test infrastructure backbone — test client binary, Gauntlet rooms 1-3, golden file suite, and client QA harness — so every future sprint can be validated end-to-end.

Branch: server Agents: Dudley (simulation dev), Tyre (architect), Hoshe (QA)

Carry-over from Sprint 8

# Title Status Notes
#480 Test client binary scaffolding + CLI backlog Linchpin — blocks #481, #482, #483, #484. Was Sprint 8 scope but did not start.
#481 Text renderer library backlog Blocked by #480. Needed for golden diff and WRONG button (F12).

New Tickets

# Title Blocked by
#482 Layer 3 subprocess test: server_subprocess_sends_snapshot_on_connect #480
#484 Test client golden file comparison (JSON diff) #480, #481
#485 Golden file test suite: gauntlet_tick_10_matches_golden #484
#487 Gauntlet first 3 rooms (Inventory Warehouse, Occlusion Corridor, Pause Chamber)
#488 Gauntlet room constants module (server/src/test_world/constants.rs) #487
#489 Content runtime validation: boot + tick 10 + snapshot
#490 Room reset trigger mechanism #487

Key Decisions

  • decisions/architecture.md — D-020 (subprocess IPC, ObserverSnapshot, PlayerInput), D-030 (testability architecture, Layer 3 integration tests), D-010 (deterministic simulation, injectable resources)
  • decisions/scope.md — D-065 (smuggler inventory, CarriedBy component — Inventory Warehouse room), D-053 (movement stances — Pause Chamber room)
  • decisions/perception.md — D-035 (symmetric shadowcasting — Occlusion Corridor room)

Notes

#480 — Test client binary scaffolding + CLI (tooling/test-client/)

What exists: server/ is a workspace crate (see server/Cargo.toml). The bridge module exports ObserverSnapshot, PlayerInput, read_framed, write_framed from server/src/bridge/. TcpBridge is in server/src/bridge/tcp.rs. The server binary (server/src/main.rs) already implements --test-mode --port 0 and emits LISTENING:{port} on stdout (Sprint 8, #459).

What to deliver: A new Cargo workspace crate at tooling/test-client/ that imports bridge types from the server crate. CLI flags: --connect <port>, --replay <file>, --text (text output), --json (JSON output), --quiet. This is the foundation for every subsequent test client ticket in this sprint — do it first.

Integration point: The server crate must expose read_framed and write_framed as pub from server/src/bridge/framing.rs (verify they are already public; the integration test at server/tests/game_loop.rs already uses them).

#481 — Text renderer library (server/src/bridge/text_renderer.rs)

What exists: ObserverSnapshot is fully defined in server/src/bridge/types.rs (v8, protocol version 8). Fields include entities (Vec), visible_tiles, tick, game_time, player_stance, player_inventory, etc.

What to deliver: format_snapshot_text(&ObserverSnapshot) -> String. Entity labels as kind:entity_id, sorted by distance from player position. Room name from room_at() helper (or "unknown" stub if room system not yet in place). Export as pub from the server crate for tooling/test-client/ consumption. This is also consumed by #495 (WRONG button F12 capture on the client).

#482 — Layer 3 subprocess test: server_subprocess_sends_snapshot_on_connect

What exists: server/tests/game_loop.rs already has a full IPC round-trip test using TcpBridge::accept_on(). The --test-mode --port 0 + LISTENING:{port} pattern is live in server/src/main.rs. The test for #459 already validated the handshake.

What to deliver: A new test at server/tests/layer3.rs. Spawns the server binary with --test-mode --port 0, reads LISTENING:{port} from stdout, connects via TCP (TcpBridge::connect()), sends one PlayerInput, reads one ObserverSnapshot, asserts version == PROTOCOL_VERSION, tick >= 0, entities.len() > 0. This is D-030 Layer 3 — the real subprocess, not a mock.

Gotcha: The server binary must be pre-built before this test runs. Use std::process::Command with the binary path from env!("CARGO_MANIFEST_DIR") or a build script. Look at how server/tests/bridge_tcp.rs handles spawning if there is a prior pattern.

#484 — Test client golden file comparison (JSON diff)

What exists: #480 (test client crate) must be done first. The ObserverSnapshot struct derives Serialize/Deserialize via serde.

What to deliver: A recursive diff_json_values() comparison function in tooling/test-client/src/golden.rs. Sorts object keys before compare (BTreeMap semantics). Pretty-prints field-level diffs on mismatch (path + expected + got). Exit code 1 on any mismatch. This is the comparison engine for #485.

#485 — Golden file test suite: gauntlet_tick_10_matches_golden

What exists: #484 must be done. The determinism regression test at server/tests/determinism.rs (Sprint 8, #466) already validates tick-level determinism. Gauntlet room #487 must be done (provides the map for the replay).

What to deliver: A test in server/tests/ (or via the test client) that runs a 10-tick deterministic replay against the Gauntlet map, serializes the resulting ObserverSnapshot to JSON, and compares against a committed golden file at tests/golden/gauntlet_tick_10.json. First run: generate and commit the golden file. Subsequent runs: fail on any deviation. This is the primary regression guard.

#487 — Gauntlet first 3 rooms (Inventory Warehouse, Occlusion Corridor, Pause Chamber)

What exists: Content loading system is live — server/src/content/ (ContentPlugin, ContentConfig, loader, spawn). The proof room setup in server/src/main.rs (setup_proof_room()) shows the ECS entity spawning pattern. WalkabilityMap is the tile collision resource. The room system needs a new server/src/test_world/ module (Gauntlet-specific, not production content).

What to deliver: Three rooms as hybrid YAML + Rust inject:

  • Inventory Warehouse (30x28, 11 entities): tests D-065 inventory system, CarriedBy component
  • Occlusion Corridor (42x22, 4 entities): tests D-035 LOS shadowcasting, wall placement critical
  • Pause Chamber (16x16, 1 entity): tests D-053 pause guard; --test-mode gate check

Room layout: embed in server/src/test_world/rooms/ as YAML, with a Rust loader in server/src/test_world/mod.rs that spawns them via WalkabilityMap + ECS entities. Each room gets a unique room_id string. This module is Gauntlet-only — do not pollute server/src/content/.

Blocks: #488 (constants module), #490 (room reset), #497 (checklist YAML), #496 (room timer), #498 (next 3-4 rooms).

#488 — Gauntlet room constants module (server/src/test_world/constants.rs)

What exists: #487 must be done first. Depends on room definitions from that ticket.

What to deliver: GauntletRoom struct (id, name, spawn_x, spawn_y, width, height), GauntletEntity struct (stable_id, kind, x, y), 8 room constants (one per planned Gauntlet room — stub the unimplemented ones), ROOMS: &[GauntletRoom] array, room_at(x, y) -> Option<&GauntletRoom> lookup. ~100 lines. These stable coordinate references are used by all Gauntlet tests to avoid magic numbers.

#489 — Content runtime validation: boot + tick 10 + snapshot

What exists: ContentPlugin loads from content/ at startup. If content schema is valid YAML but has runtime issues (e.g., NPC references broken entity IDs), the server may panic on tick rather than at load time.

What to deliver: A test at server/tests/content_loading.rs (or extend the existing file) that: starts the server with ContentPlugin, ticks 10 times, reads an ObserverSnapshot, asserts no panic and version == PROTOCOL_VERSION. This is a smoke test for production content. Standalone — does not depend on Gauntlet rooms.

#490 — Room reset trigger mechanism

What exists: #487 (Gauntlet rooms) must be done. ECS has WalkabilityMap, TilePosition, KnowledgeGraph components.

What to deliver:

  • RoomResetTrigger component: marks a tile as a reset plate
  • RoomSnapshots resource: stores tick-0 entity state per room (positions, KG entries, inventory items)
  • execute_room_reset system: on trigger activation, restores entity positions, player KG (room refs only), fog (room tiles), inventory items to tick-0 state

Resets: entity positions and KG, player KG (room refs only — not cross-room knowledge), fog (room tiles only), inventory items. Does NOT reset: game time, player stance, cross-room knowledge, the hub area.

Blocks: #495 (WRONG button F12 depends on reset plate context), #502 (client-side reset UX).

Dependency Chain

#480 (test client binary)
    -> #481 (text renderer)
        -> #484 (golden diff)
            -> #485 (golden suite)
    -> #482 (Layer 3 subprocess test)
    -> #484 (golden diff)

#487 (Gauntlet rooms 1-3)
    -> #488 (room constants)
    -> #490 (room reset trigger)

#489 (content runtime validation) — standalone, parallel track

PR Workflow

When ready to submit, create a PR with tea CLI. All flags are required to avoid TTY prompts (see CLAUDE.md "Gitea access" section):

tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(simulation): sprint 9 gauntlet — test client and gauntlet rooms" --description "body" --base main --head server