Standardized YAML frontmatter on all 20 files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
24 KiB
title, description, type, status, workshop, agent, round, created
| title | description | type | status | workshop | agent | round | created |
|---|---|---|---|---|---|---|---|
| Ozzie — Round 3: Final Tester Workflow + UX Specs | Ozzie's final tester workflow and UX specifications for the Gauntlet test client | workshop | archived | test-architecture | ozzie | 3 | 2026-02-17 |
Ozzie — Round 3: Final Tester Workflow + UX Specs
Workshop: QA Strategy & Test Architecture Track: 1 (Test World Design) — Human Tester Experience Date: 2026-02-17 Round: 3 (Prioritization) Inputs: All Round 2 outputs, round-2-notes.md
1. Human Tester Walkthrough — Final
This is the definitive step-by-step workflow. A human tester picks this up and knows exactly what to do from zero to bug report.
Prerequisites
- Server binary built:
make build-server - Test client binary built:
make build-test-client(buildssettled-reach-test-client) - Gauntlet content pack exists at
content/gauntlet/ - Checklist generated:
make checklist(producesdocs/qa/gauntlet-checklist.md)
Session Start
Terminal 1 — Server:
make test-world-headless
# Equivalent to: settled-reach-server --test-mode --port 0 --seed 42
# Server prints: LISTENING:54321
# Server waits for client connection
Terminal 2 — Test Client:
make test-client
# Equivalent to: settled-reach-test-client --connect 127.0.0.1:54321 --text
# Test client connects, receives first ObserverSnapshot
# Live terminal display appears
What the tester sees on connect:
╔══════════════════════════════════════════════════════════════════════╗
║ GAUNTLET TEST CLIENT v0.1 Tick: 0 TickRate: Full ║
║ Room: Central Hub Seed: 42 Session: 00:00:01 ║
╠══════════════════════════════════════════════════════════════════════╣
║ PLAYER (50,50) → North | Walk | Inventory: 0/9 ║
╠══════════════════════════════════════════════════════════════════════╣
║ ENTITIES (0 visible) ║
╠══════════════════════════════════════════════════════════════════════╣
║ FOG Clear:24 | Periph:8 | Deep:0 | Map:0 | Dark:468 ║
╠══════════════════════════════════════════════════════════════════════╣
║ EXITS: [N]Fog Theater [E]Occlusion [S]Dialogue [W]Inventory ║
╠══════════════════════════════════════════════════════════════════════╣
║ MONOLOGUE: none | DIALOGUE: none ║
╠══════════════════════════════════════════════════════════════════════╣
║ CHECKLIST: Central Hub — N/A ║
║ [F12:WRONG] [Home:Hub] [R:Reset] ║
╚══════════════════════════════════════════════════════════════════════╝
Phase 1: Navigate to a Room
Tester sends movement inputs (arrow keys or WASD via --interactive mode, or via scripted replay file). They walk East toward the Occlusion Corridor.
As they enter the corridor connecting Hub to Occlusion Corridor:
- Room name changes:
Room: → Occlusion Corridor - Timer starts:
TIMER: 00:00 - Checklist loads:
CHECKLIST: Occlusion Corridor — 0/7 - Entities appear as they enter the player's vision cone
Phase 2: Execute the Checklist
The tester has a printed copy of docs/qa/gauntlet-checklist.md taped to their monitor (or the test client shows checklist items in the status bar).
Step 1 from checklist: "Stand at corridor entrance (45,3), face East"
Tester moves to position. Terminal updates:
╠══════════════════════════════════════════════════════════════════════╣
║ ENTITIES (3) ║
║ ● npc:100 (48,3) Fwd Neutral(#4a9ebb) VISIBLE d=3 ║
║ ● obj:200 (46,2) Fwd n/a VISIBLE d=1 ║
║ ✕ npc:101 (50,7) --- --- BLOCKED ║
║ └─ wall at (49,4) blocks LOS ║
╠══════════════════════════════════════════════════════════════════════╣
Tester checks against the checklist:
npc:100(guard-1) visible, Forward sector? YESnpc:101(hidden-1) NOT visible? YES — blocked by wall- Corridor tiles visible, room behind wall unexplored? Check FOG line
Three items checked. Progress bar updates: 3/7 ██████░░░░░
Step 2: "Walk south to (45,8), observe peripheral vision"
Tester moves. guard-1 shifts from Forward to Peripheral:
║ ◐ npc:100 (48,3) Periph Neutral(#4a9ebb) VISIBLE d=6 ║
- guard-1 in Peripheral sector? YES — symbol changed from
●to◐ - Fog visible_count decreased? Check FOG line: was 24, now 18
Step 3: "Switch to Sensor perception mode"
Tester sends TogglePerceptionMode input. If working correctly, hidden-1 should appear:
║ ⚡ npc:101 (50,7) --- Unknown(#4a9ebb) DETECTED d=7 ║
- hidden-1 appears in entity list with sensor data? YES
Checklist complete: 7/7. Timer shows 00:47. If personal best was 00:38, tester sees (PB: 00:38).
Phase 3: Encounter a Bug
What if step 3 fails? hidden-1 doesn't appear even in Sensor mode. The tester presses F12.
F12 Flow:
- Game pauses immediately (Pause sent to server)
- Terminal shows:
╔══════════════════════════════════════════════════════════════════╗
║ BUG REPORT — Tick 87 — Occlusion Corridor ║
╠══════════════════════════════════════════════════════════════════╣
║ What's wrong? (type, then Enter): ║
║ > Sensor mode doesn't detect NPC behind wall_ ║
╚══════════════════════════════════════════════════════════════════╝
- Tester types one sentence, presses Enter
- Terminal shows:
Bug report saved: tests/bug-reports/gauntlet-t87-20260217-084213/
report.md | snapshot.json | text_output.txt | description.txt
Resume testing. [Enter]
- Game unpauses. Tester continues.
Phase 4: Reset and Re-Test
Tester walks back to the room entrance and steps on the reset plate (2x2 amber-striped floor tile). Interaction list shows [Reset Room]. Tester activates it.
Reset: Occlusion Corridor → tick-0 state. Timer reset.
All entities return to starting positions. Fog reverts. Timer restarts. Checklist resets to 0/7. Tester can re-run the checklist from step 1.
Phase 5: Move to Another Room
Tester presses Home → teleports to Central Hub. Walks to next room. Repeat Phase 2-4.
Phase 6: End Session
Tester presses Ctrl+C. Test client prints:
══════════════════════════════════════════════════════
SESSION SUMMARY — 2026-02-17 08:55
══════════════════════════════════════════════════════
Rooms tested: 4 / 14
Total time: 12:34
Occlusion Corridor: 7/7 ███████████ 100% 00:47
Fog Theater: 5/8 ████████░░░ 63% 01:12
Inventory Warehouse: 7/7 ███████████ 100% 00:25 ★ PB
Crowd Plaza: 4/9 ██████░░░░░ 44% 01:35
Bug reports filed: 1
→ tests/bug-reports/gauntlet-t87-20260217-084213/
Stats saved: tests/gauntlet-stats.json
══════════════════════════════════════════════════════
Done. Developer picks up the bug report folder, has everything they need to reproduce.
2. Test Client UX Final Spec
Merges my Round 2 terminal layout with Tyre's kind:entity_id labels and the lead-confirmed architecture decisions.
Display Architecture
- Binary:
settled-reach-test-clientattooling/test-client/(separate crate, imports shared types from server) - Text renderer:
tooling/test-client/src/text_renderer.rs(or shared module within the crate) - Display mode: Live-updating terminal via crossterm ANSI escape codes. NOT a TUI framework. Fixed-layout sections that refresh every tick.
- Simultaneous logging: Every tick appended to
gauntlet-session-{timestamp}.logfor the WRONG button and post-session review.
Entity Label Format
Per Tyre's decision: kind:entity_id. Examples:
npc:100— NPC with wire entity_id 100obj:200— Object with wire entity_id 200player:0— Player entity
No display names on wire. Test assertions use Gauntlet constants: assert_entity_visible(&snapshot, GUARD_1.wire_id).
For human testers, the printed checklist maps IDs to names: "npc:100 = guard-1, npc:101 = hidden-1". This lives in the checklist header, not the terminal display.
Entity Visibility Symbols
| Symbol | State | Meaning |
|---|---|---|
● |
VISIBLE | In clear vision cone (Forward or Peripheral) |
◐ |
REMEMBERED | In fog, previously seen, entity persists in snapshot |
◌ |
FOGGED | Detected in fog, NOT yet recognized (grey blob state) |
✕ |
BLOCKED | Exists but LOS blocked by wall (debug info, requires blocked_entities field) |
⚡ |
RECOGNIZING | Mid-cognitive-delay (D-060), transitioning from blob to recognized |
Peripheral entities show ● but with the Periph sector label — the symbol indicates visibility state, the sector label indicates where in the cone.
Section Order (top to bottom)
- Header — tick, tick rate, room name, seed, session timer
- Player — position, facing, stance, inventory count
- Entities — one line per entity, sorted by distance (nearest first per Tyre's format)
- Fog — 5-layer tile counts (Clear/Periph/Deep/Map/Dark)
- Sound — events this tick (MVP: omit if no
SoundEventin snapshot yet) - Cognition — active cognitive delays (MVP: derive from
pending_recognitions) - Interactions — available verbs per entity
- Monologue/Dialogue — exact text content
- Inventory — slot map
- Status — checklist progress, timer, PB, hotkey reminders
MVP vs Full Display
Sprint 8 MVP (what ships first):
Sections 1, 2, 3, 4, 7, 8, 9, 10. These derive entirely from the existing ObserverSnapshot fields. No server changes needed.
Sprint 9+ Full (requires new snapshot fields):
Add sections 5 (Sound) and 6 (Cognition). These need:
Vec<SoundEvent>in ObserverSnapshot (source position, type, range)- Enhanced
pending_recognitionswith elapsed/total timing blocked_entitiesfor the✕ BLOCKEDdisplay
Refresh Behavior
- Per-tick refresh. Every ObserverSnapshot received triggers a full terminal redraw.
- Cursor positioning. Use ANSI escape
\x1b[H(cursor home) + section rewrites. No full clear (avoids flicker). - Paused display. When TickRate is Paused, display shows
[PAUSED]in header. Still refreshes on snapshot receipt (pause state changes are snapshots).
3. WRONG Button Final Spec
MVP — Sprint 8
The MINIMUM that makes bug reporting useful. Implementable without any server changes.
Hotkey: F12
What gets captured:
| Data | Source | Format | Size |
|---|---|---|---|
| Current ObserverSnapshot | Last received snapshot (already in memory) | JSON | ~2-5KB |
| Current text output | Last format_snapshot_text() result |
Text | ~1KB |
| Tick + room + seed | From snapshot + Gauntlet constants | Part of report.md | Trivial |
| Tester description | One-line text prompt | Text | ~100 bytes |
What does NOT ship in MVP:
- No snapshot history ring buffer (requires 60x snapshot storage)
- No input history (requires InputMapper ring buffer)
- No screenshot (test client is terminal-only)
- No world digest (requires server-side addition)
UX Flow (MVP):
- F12 pressed
- Test client sends Pause to server
- Terminal shows one-line prompt:
BUG: What's wrong? > _ - Tester types, presses Enter
- Test client writes files to
tests/bug-reports/gauntlet-t{tick}-{timestamp}/:
tests/bug-reports/gauntlet-t87-20260217-084213/
├── report.md # Human-readable summary (generated)
├── snapshot.json # ObserverSnapshot as pretty-printed JSON
├── text_output.txt # What the terminal was showing
└── description.txt # Tester's one-line description
- Test client sends Unpause
- Testing resumes
report.md Format (MVP):
# Bug Report — Gauntlet
- **Tick:** 87
- **Room:** Occlusion Corridor
- **Seed:** 42
- **Date:** 2026-02-17 08:42:13
- **Description:** Sensor mode doesn't detect NPC behind wall
## Player State
Position: (45,8) facing East | Stance: Walk | Inventory: 0/9
## Entities at Time of Report
| ID | Position | Sector | Relationship | Visibility | Distance |
|----|----------|--------|-------------|------------|----------|
| npc:100 | (48,3) | Periph | Neutral | VISIBLE | 6 |
| npc:101 | (50,7) | --- | --- | BLOCKED | 7 |
## Fog State
Clear: 18 | Peripheral: 6 | Deep: 12 | Map: 0 | Dark: 464
## Checklist State
Occlusion Corridor: 5/7 — item 'occ_hidden_sensor' FAILED
## Reproduction
1. `make test-world-headless` (seed 42)
2. `make test-client`
3. Walk to Occlusion Corridor (45,8), face East
4. Switch to Sensor perception mode
5. Expected: npc:101 appears in entity list
6. Actual: npc:101 still BLOCKED
The reproduction steps are templated from the current checklist step + room position. A developer reads this and can reproduce in under a minute.
Full — Sprint 9+
Everything in MVP plus:
| Addition | Source | Requires |
|---|---|---|
| Snapshot history (60 ticks) | Ring buffer in test client | ~300KB memory, client-side only |
| Input history (60 inputs) | Ring buffer in test client | ~12KB memory, client-side only |
| Replay seed + input file | Combine seed + full input log | Can reproduce entire session |
| Room metadata + expected state | From checklist YAML | Client loads checklist at startup |
Additional files in Full mode:
tests/bug-reports/gauntlet-t87-20260217-084213/
├── report.md
├── snapshot.json
├── snapshot_history.jsonl # Last 60 snapshots, one per line
├── input_history.jsonl # Last 60 inputs, one per line
├── text_output.txt
├── description.txt
└── room_metadata.json # Checklist items + expected state
The ring buffer is the key addition. 60 ticks = ~6 seconds at 10 tps. When the tester presses F12, the last 6 seconds of game state are preserved. A developer can replay those 6 seconds to see exactly what happened leading up to the bug.
Implementation Notes for Dudley (server) and Stig (client)
Server (Dudley): MVP requires ZERO server changes. The test client formats the existing ObserverSnapshot. Full mode also requires no server changes — the ring buffer and input history are client-side. The only server-side addition (deferred to Sprint 9+) is blocked_entities for the ✕ BLOCKED display.
Client (Stig): The Godot client gets its own F12 handler (bug_report.gd autoload per Stig's R2 spec). The Godot version captures a screenshot + scene tree dump that the test client can't. Both clients write to the same tests/bug-reports/ directory. Reports from either client are useful.
4. Quick-Test Developer Workflow
A developer just fixed the fog shader. They want to verify the Occlusion Corridor works. Total time target: under 2 minutes.
The Flow
# Step 1: Build (incremental, ~5-10s)
make build-server
# Step 2: Start server + client (split terminal or use tmux)
make test-world-headless &
# Wait for LISTENING:port output
make test-client
# Step 3: Teleport to target room (~2s)
# Press Home (if not already in Hub)
# Walk East to Occlusion Corridor (or type room shortcut if implemented)
# Step 4: Run the specific checklist items (~30-60s)
# Move to (45,3), face East — check entities
# Move to (45,8) — check peripheral
# Toggle Sensor mode — check hidden NPC
# Step 5: Done
# If all good: Ctrl+C, session summary confirms 7/7
# If bug found: F12, one sentence, resume or Ctrl+C
Total wall-clock: Build 10s + startup 3s + navigate 5s + test 45s + exit 2s = ~65 seconds.
Why This Is Fast
- Incremental build — only recompiles changed files (~5-10s for a shader fix)
- Hub-and-spoke layout — walk directly to target room, skip everything else
- No server restart for re-test — room reset plate lets them retry without killing the server
- No report overhead — F12 captures everything in one press, no manual note-taking
For Regression Verification After a Fix
The developer fixed the bug from the report. Now they want to verify:
# Option A: Manual verification (interactive)
make test-world-headless &
make test-client
# Navigate to the room, reproduce the steps from report.md
# Verify the bug is fixed
# Option B: Automated verification (replay file)
make test-world-headless &
settled-reach-test-client \
--connect 127.0.0.1:54321 \
--replay tests/bug-reports/gauntlet-t87-20260217-084213/input_history.jsonl \
--ticks 90 \
--text
# Watch the replay, verify the entity appears correctly at tick 87
Option B is available once the Full WRONG button ships (Sprint 9+) — it uses the captured input history as a replay file. The developer literally replays the exact sequence that triggered the bug and checks if it's fixed.
5. Anti-Tedium Priority for Sprint 8 MVP
Four features approved. Not all are equally important for Sprint 8. Here's the priority ranking.
Sprint 8: MUST SHIP (2 features)
Priority 1: Room Reset Triggers
Why first: Without room reset, a tester who wants to re-run a room's checklist must restart the server. That's 30+ seconds of downtime every retry. Over a sprint of testing, that's hours of wasted time. Room reset is the difference between "I'll re-test that" and "I'll skip it."
Implementation scope:
- Server:
RoomResetTriggercomponent,RoomSnapshotsresource,detect_room_reset+execute_room_resetsystems, 10-tick debounce. ~150 lines (Dudley's R2 spec). - Client:
reset_platetile type in TileRenderer, "Reset Room" interaction verb. ~20 lines (Stig's R2 spec). - Test mode only: systems only registered with
--test-mode.
Estimated effort: 1-1.5 days.
Priority 2: WRONG Button (MVP)
Why second: Without the WRONG button, a tester who finds a bug has to manually describe it — writing down the tick, room, entity positions, what they expected. That's slow, error-prone, and most testers won't bother. The WRONG button makes bug reporting a 5-second action.
Implementation scope:
- Test client: capture current snapshot + text output, one-line prompt, write to disk. ~100 lines.
- Godot client:
bug_report.gdautoload, F12 handler, modal prompt, snapshot + screenshot + scene tree dump. ~80 lines (Stig's R2 spec). - No server changes.
Estimated effort: 1 day.
Sprint 9: SHOULD SHIP (2 features)
Priority 3: Hub Teleport
Why deferred: Hub teleport saves walking time (~5-10 seconds per room transition). Useful, but the Gauntlet corridors are only ~10-15 tiles long. Walking is tolerable in Sprint 8. Hub teleport becomes more valuable as more rooms are added.
Implementation scope:
- Server:
TeleportToHubPlayerAction variant, process inprocess_player_input. ~20 lines. - Client:
Homekey mapping,TeleportHubwire action, camera snap + fade. ~30 lines.
Estimated effort: 0.5 days.
Priority 4: Room Timer + Auto-Checklist Progress
Why deferred: Timers and progress tracking are engagement features — they make testing more pleasant, not more possible. The Gauntlet works without them. They become valuable once the team is doing regular sprint-over-sprint testing.
Implementation scope:
- Test client: room detection from bounds, wall-clock timer, stats file, session summary. ~150 lines.
- Test client: checklist YAML loading, condition evaluation against snapshot, progress display. ~200 lines.
- Godot client:
GauntletProgressoverlay. ~60 lines (Stig's R2 spec). - Stats persistence:
tests/gauntlet-stats.json.
Estimated effort: 1.5-2 days.
Summary Table
| Priority | Feature | Sprint | Effort | Why |
|---|---|---|---|---|
| P1 | Room Reset | 8 | 1-1.5d | Can't re-test without it |
| P2 | WRONG Button (MVP) | 8 | 1d | Can't report bugs without it |
| P3 | Hub Teleport | 9 | 0.5d | Convenience, not necessity |
| P4 | Timer + Checklist | 9 | 1.5-2d | Engagement, not necessity |
Sprint 8 anti-tedium budget: ~2-2.5 days. This fits alongside the other Sprint 8 priorities (test client binary, determinism fixes, server flags).
What About F3 Debug Overlay?
Defer indefinitely. Stig is right — the WRONG button captures the same data on demand. F3 as a real-time overlay is a performance cost for a marginal convenience gain. If testers ask for it after using the WRONG button for a sprint, reconsider.
Summary of Round 3 Deliverables
| # | Deliverable | Status |
|---|---|---|
| 1 | Human tester walkthrough — final (start to finish, 6 phases) | Complete |
| 2 | Test client UX final spec (labels, symbols, sections, MVP vs full) | Complete |
| 3 | WRONG button final spec (MVP Sprint 8 + Full Sprint 9+) | Complete |
| 4 | Quick-test developer workflow (65-second flow) | Complete |
| 5 | Anti-tedium Sprint 8 priority (Room Reset + WRONG Button MVP) | Complete |
Build-Ready Specs
These specs are implementation-ready for the assigned developers:
- Dudley: Room reset server mechanism (Section 5, P1). Use your R2
RoomResetTrigger+RoomSnapshotsdesign. No server changes needed for WRONG button MVP. - Stig: Room reset tile type + WRONG button Godot client (Section 5, P1+P2). Use your R2
reset_platetile +bug_report.gdautoload designs. - Tyre: Test client binary text renderer + WRONG button integration (Section 2+3). Use your R2
test_client.rsdesign. Add the F12 capture flow from Section 3 MVP.
The human tester picks up docs/qa/gauntlet-checklist.md, opens two terminals, and goes. That's the goal. That's what we're shipping.