Devlog · Loop 1
Loop 1 — Procedural World & Exploration Prototype
Loop 1 built the first thing in DUST that can be played: one procedurally generated world, one player walking through it in real time, terrain that stops you, a world that is unknown until you see it and remembered once you have, and one ruin somewhere out there. It exists to answer a single design question, and this entry is honest about the part of the answer that a headless server cannot give.
Objective
One question: is exploring an unknown DUST world already interesting before survival, loot, crafting, industry, trade or PvP exist? The loop had to produce the smallest playable thing that can be judged on that question — spawn, wonder where you are, move, reveal terrain, recognize geography, suspect something, move toward it, find a place, remember it, want to keep going — and nothing else.
Starting state
Branch main at 00c8c53 (Loop 0b), clean; the Loop 0 foundation is commit bc11af2. client/ held only the Loop 0 bootstrap scene. Godot 4.7.2-stable, headless only on the dev host (1 vCPU, 2 GB). Pre-flight hygiene: the memory files the brief asked about (MEMORY.md, user-dust-owner.md, project-dust-git-access.md) are not in the repository — they live in the Claude Code memory directory outside it, contain no secret, and hold only the owner's GitHub handle, e-mail and how git access works; nothing to clean.
Team
Four roles, as the brief allowed, no more:
- Loop Owner (this agent): rediscovery, the architecture contract (
WorldDataandPlayerMovementwritten first as the interface both specialists built against), integration, the tests re-run under its own hands, ASCII multi-seed review, screenshots, documentation, this entry, commit. - Procedural World specialist:
world_generator.gd,discovery_model.gd,tests/run_tests.gd. Necessary because generation is the largest single piece and needed iteration against the geography thresholds. - Gameplay / Godot specialist: player, camera, renderers, HUD, main scene,
tests/test_scene_movement.gd, input map. Necessary because presentation and physics are a separate craft and could be built in parallel on disjoint files against the contract. - Independent reviewer / QA: read everything, re-ran the suites in its own processes, challenged the implementation against the brief and found a real blocker (below). Necessary because the two specialists and the Loop Owner had all read the same contract; someone had to try to break it.
The two specialists ran in parallel on disjoint file sets (no shared file, one Godot process each at a time); the Loop Owner merged nothing by hand because nothing overlapped. Twenty-five agents were not needed and were not used.
Procedural world
WorldGenerator.generate(seed, 160, 120) returns a WorldData: a 160 × 120 grid of 32 px cells (5120 × 3840 px — about four screens wide and five tall at 1280 × 720), a spawn cell and one ruin. Pipeline, in order:
- Two low-frequency fractal noise fields (elevation, moisture;
FastNoiseLite, features 20–30 cells wide), sampled once as 8-bit images. - Classification by per-seed quantiles: the lowest 6–20 % of cells become WATER, the highest 4–17 % ROCK, the moistest 10–34 % of the land FOREST, the rest PLAIN. The fractions are drawn from the seeded RNG, so worlds have a character (wet, rocky, wooded) but never degenerate into all-water or all-rock.
- Clean-up: 3 × 3 majority smoothing twice, then every region smaller than 12 cells is absorbed by its surroundings (a small traversable region always stays traversable).
- Guarantees: a lake of at least 60 connected cells (carved at the lowest point if the noise gave none — it never had to on 84 seeds), and one river traced downhill on the exact noise from the highest ground of each 32 × 24 block, scored by length and by how much open land it crosses, ending in water, at the edge or in a pond; 1–2 fords revert crossing cells to their original terrain so the river never cuts the world in two.
- Spawn: the PLAIN cell nearest a per-seed target drawn in the central third of the world, with at least 6 of 8 neighbours traversable, in the largest traversable component.
- Landmark: the ruin (below).
- Stats for the HUD, the tests and this entry.
All randomness comes from one RandomNumberGenerator seeded with the world seed; no global randi(), no Dictionary iteration order, no time.
Seed determinism
The world signature is SHA-256 over the terrain bytes, the spawn, the landmark rectangle and door, the dimensions, the seed and GENERATOR_VERSION (1). Never derived from pixels. Reference signatures, generator version 1:
seed=1 97f503fe1dcf8c0c…
seed=42 1fc565dc29e4a96b…
seed=12345 294986079456854f…
seed=987654 76e6dd4723804415…
Verified: each seed generated twice in one process gives the same signature, spawn and landmark; the same four signatures came out of three separate processes run by the World specialist and again in processes run by the Loop Owner and by the reviewer. A robustness sweep generated seeds 1–40, −5–4, 100–119 and 4294967290–4294967299 (negative, zero and beyond 232 included): 0 failures, 0 null worlds, every landmark placed under the strict distance bound, no lake ever carved. The seed is visible in the HUD; it can be typed, regenerated, replayed, or replaced by a random one whose value is then shown. Invalid seed text produces a HUD message, not a crash.
Variation: the six pairwise signatures differ; cell-by-cell, 55–68 % of the terrain differs between any two of the four seeds; spawns and ruins are all in different places. The terrain histograms differ by 12–43 % of cells — the brief asked for 15 % pairwise, and seeds 1 and 12345 legitimately share a composition (12.5 %), so the test keeps an 8 % histogram bar and adds the robust cell-wise layout check (≥ 40 %). Recorded as a deviation, not hidden.
Geography
Terrain vocabulary: WATER, PLAIN, FOREST, ROCK, plus WALL and FLOOR for the ruin. Water and rock block movement; rock and walls block sight; forest is traversable and transparent for now. The Loop Owner reviewed downsampled ASCII maps of the four seeds: each has forest massifs tens of cells wide, one or several lakes, rock ridges or massifs, and a thin river (208 cells long and winding on seed 1, 47–61 on the others); the ruin stands on open plain 39–66 cells from the spawn with its door toward the spawn. Compositions: water 11–16 %, rock 4–15 %, forest 11–28 %, plain 44–65 %. Sentences like "the great forest crosses the middle, the ruin is east of it beyond the river" (seed 12345) or "rocky north-west, big lake west, ruin in the south-east by the rocks" (seed 987654) are possible. Weak points are listed under Known problems.
Movement & camera
WASD and arrow keys through the input map, diagonals included. The rule is a pure function, PlayerMovement: input clamped to unit length (a diagonal is exactly as fast as a cardinal, 180.000 px/s both), exponential easing with a 0.08 s time constant (95 % speed after 0.24 s, stopping distance under half a cell), displacement = velocity × delta. A CharacterBody2D with a 12 px circle applies it and slides against the collision tiles. The camera is a child of the player with position smoothing (speed 8: about 22 px of lag at full speed, settled 0.4 s after a stop), limited to the world bounds; = and − zoom between 0.5 and 2.0 for debugging, and zooming out reveals nothing new. The "movement-feel pass" was analytical only — see Playtest.
Discovery
DiscoveryModel holds one state per cell — UNKNOWN, DISCOVERED, VISIBLE — as data, separate from rendering. On every change of the player's cell it recomputes the visible set within a 7-cell radius (about 150 cells, one short grid ray each) and returns the indices that changed; VISIBLE cells that dropped out become DISCOVERED and never go back. The renderer paints terrain only for cells that are no longer UNKNOWN (unknown cells are simply dark) and lays a 55 % black overlay on DISCOVERED cells, so a remembered area reads as subdued. Collision exists for every blocking cell from the start, seen or not: geography stops you before you see it. Memory lasts for the session; regenerating resets it.
Visibility
The occlusion proof: a cell is visible only if a straight grid line (Bresenham) from the player reaches it without crossing ROCK or WALL; the blocking cell itself is seen. Tests on the hand-built debug world assert that the far side of a rock ridge stays UNKNOWN while the ridge face is VISIBLE, that the ruin interior is invisible from outside its north wall while all five wall cells are visible, that from the doorway all 15 interior cells are visible, and that from inside the ruin the outside is not. The scene test asserts the same through the renderer: the ridge face is painted, the far side is not. On the real seed 42 a scripted walk ended against the ruin's north wall:
Limits: forest does not block or degrade sight; there is no darkness; the reveal edge is a cell-radius circle. All by design for Loop 1.
Landmark
One ruin per world: a rectangle of WALL cells 7–9 wide and 5–6 tall (sizes drawn from the seed), FLOOR inside, one door in the middle of the side facing the spawn. Placement: every top-left corner whose footprint plus a one-cell ring is PLAIN or FOREST and whose center lies 35–90 cells from the spawn is a candidate; one is drawn with the RNG and tried (walls built, local clean-up, traversable components relabelled); it is accepted only if the spawn is still PLAIN and the cell outside the door is in the spawn's component, i.e. the interior is reachable. Up to 12 candidates per tier, then the bounds relax to 25–120 and 12–1000 cells (recorded in stats.landmark_bound), then a loud error and a null world. On the 84 seeds generated so far the strict tier always succeeded at the first attempt. Distances on the test seeds: 48.4, 39.2, 41.2, 65.9 cells. In play mode nothing points at it — no marker, arrow, distance or quest; F3 debug mode alone draws its rectangle and door and prints its coordinates.
Architecture
Recorded in docs/ARCHITECTURE.md §8 and client/README.md; decisions D033–D036. The real boundary: client/world/ is data and rules with no node and no rendering (WorldData, WorldGenerator, DiscoveryModel); client/game/ is presentation and input (PlayerMovement as a pure rule, Player, WorldRenderer, DiscoveryRenderer, DebugHud, main.gd wiring). Sprites are not the game state; the grid is. Moving world/ into a headless server later changes where generate and update run, not what they compute. No networking abstraction was built.
Verification
Everything below was run on the dev host on 2026-09-13; only what ran is listed.
tests/run_tests.gd(headless, model only, no rendering):[DUST] tests done | passed=149 failed=0in the Loop Owner's final run, exit 0 — determinism (4 seeds × 2), variation (6 pairs: terrain bytes differ, histogram ≥ 8 %, cell-wise layout ≥ 40 %, spawn, landmark), spawn validity, landmark validity (border walls, single door, interior floor, open one-cell ring, reachability through the door, distance by tier), geography sanity (composition bands, no region under 12 cells, largest component ≥ 70 % — measured 99.0–100 %, a lake ≥ 60 cells, a river ≥ 12 cells, ≥ 1 ford), discovery on the debug world (ridge, ruin interior from outside / from the door / from in front of the door / from inside, remembered after leaving), movement rule (diagonal = cardinal, 30 vs 60 Hz 0.81 %, 60 vs 120 Hz 0.43 %, speed cap). Before the review's additions the same suite passed 132/132 in the World specialist's three processes, in the Loop Owner's and in the reviewer's runs.tests/test_scene_movement.gd(headless, real scene, physics on): 16/16 PASS — the input map drives the body (169.5 px in 1 s, analytic ≈ 165.6), no drift, the unseen rock ridge stops the body at the tile edge, the ridge gap is passable, the lake stops it, speed never above 180, the ridge face is painted and the far side is not, remembered cells get the overlay, the body stays inside the world at the west and north edges, and after a Regenerate the player stands exactly on the new spawn.- Main scene headless (
--quit-after) on seeds 1, 42, 12345, 987654 and −7: exit 0, no script error,[DUST] world ready | seed=… | 160x120 cells | spawn=… | landmark=yes | gen=142–188ms | source=generator; a 20-digit seed andabcare refused with a warning and fall back to seed 1. - Seed sweeps: 60 seeds by the World specialist, 20 more (100–119) by the Loop Owner, 60 more (100000–100059) plus the int64 extremes by the reviewer: 0 bad, generation 115–203 ms.
- Rendering path: real frames rendered under Xvfb with Mesa llvmpipe (OpenGL 4.5, GL Compatibility) and saved by
tests/screenshot_walk.gd; the tile atlas, the paint-on-discover layer, the overlay, the player disc and the HUD all draw. This validates that the renderer works, not how it feels. - Independent review: one reviewer agent read every file, re-ran both suites in its own processes (identical signatures), probed edge seeds and returned 15 findings. Fixed before the commit: the blocker — nothing stopped the player at the world border (the camera stopped and the disc walked off-screen into the void; now a clamp on the body, with a scene test); the silent relaxation of the variation threshold (now decision D037, and the vacuous signature check replaced by a terrain-bytes check); dev automation living in
main.gd(moved totests/screenshot_walk.gd); seeds beyond 64 bits (an engine error and a silently saturated seed; now refused with a message); a regenerate race between the new collision and the teleported player (collision updated synchronously, tested); a landmark fallback that could never succeed and a ruin ring that clean-up could block (both fixed, ring asserted); river / lake / ford guarantees that were printed but never asserted (asserted); the thin frame-rate margin (60 vs 120 Hz added); the ready line printing the requested seed for a fallback world (prints the world's seed and its source). Left as recorded caveats: per-platform determinism (documented indocs/ARCHITECTURE.md §8), editor-only GDScript warnings, some constants typed twice in tools. The reviewer's independent judgement: the load-bearing pieces are real and verified; the two honest weaknesses for the emotional target are design, not bugs — there is no long-range cue, and remembering a place rests on overlay contrast.
Performance
Measured on the dev host (Azure B1ms, 1 vCPU, 2 GB), Godot 4.7.2 headless unless stated. Not optimized; nothing is structurally expensive.
world 160 x 120 = 19 200 cells (5120 x 3840 px)
generation seed 1: 133–157 ms seed 42: 130–158 ms seed 12345: 136–158 ms seed 987654: 127–150 ms
~150 seeds swept across three sweeps: 115–203 ms, average ~140 ms
scene rebuild ~140 ms on 160x120 (generation + collision fill + discovery + first reveal)
per cell change discovery update: ~150 grid rays of ≤ 7 steps; renderers touch only changed cells
headless startup 0.57 s to world ready + 1 frame, 117 MB RSS
test suite run_tests.gd 2.1 s; scan of 20 seeds 3.4 s
software render Xvfb + llvmpipe: 26–37 fps while walking, ~300 MB RSS — no GPU figure exists
Playtest
No interactive playtest was possible. The dev host has no display and no GPU; nobody has held the keys. What did happen: scripted walks under Xvfb (the three screenshots above) showed the loop's mechanics working end to end — spawn in the unknown, reveal by walking, remember what was left behind, be stopped by terrain never seen, reach a wall and not see through it. That is evidence for the machinery, not for the feeling.
The nine subjective questions of the brief — wanting to move toward the unexplored, understanding unknown vs discovered, recognizing regions, remembering where something was, the landmark feeling discovered rather than delivered, curiosity, whether limited visibility helps or annoys, whether movement gets out of the way, whether seeds feel like different places — require a human playtest on a machine with a display: godot --path client -- --seed=42, then a few more seeds, a few minutes each. They are not answered here and not guessed.
Known problems
- Not human-validated (above). Colours, HUD width at 1280 × 720, camera feel, the 55 % dimming and the tile patterns were judged only from software-rendered frames.
- No long-range cue exists: from the spawn, "something over there" can only come from terrain shapes inside the 7-cell radius and from the memory of where one has been. The reviewer named this as the main design risk for the emotional target; only a playtest can say whether partial geography alone creates a plan.
- The world edge is a clamp on the player body (found as a blocker by the review), not a visible feature; at a border the disc simply stops.
- Terrain fragments of 12–20 cells (rock bits, small ponds) read as noise on the ASCII maps; raising the minimum region size is the first tweak to try.
- Landmark distances cluster at 35–50 cells because the spawn is central and the annulus is clipped by the edges; the ruin can stand two cells from the river (seed 1).
- The reveal disc has a jagged cell-radius edge; forest neither blocks nor degrades sight.
- Two generator fallbacks (lake carving, relaxed landmark tiers) have never triggered on real seeds, so they are exercised by parsing and the shared helpers only.
- Determinism is verified on this Linux x86_64 build only; float or noise differences on another platform (a future Web export) could change signatures for the same seed.
- The variation test's histogram threshold was lowered from the brief's 15 % to 8 % (with a stronger cell-wise layout check added) because two honest seeds shared a composition — recorded as decision D037 after the review flagged it as a silent relaxation.
- Player input is ignored while any Control has keyboard focus; today only the seed field can take it.
- The hidden collision layer still submits draw calls for every blocking cell at alpha 0; harmless at this size.
Intentionally out of scope
Not implemented, by the owner's instruction: survival (health, hunger, thirst, temperature, death), loot and inventory, crafting and industry, bases and building, market and trade, combat and PvP, multiplayer and networking of any kind, backend, persistence and save files, audio, art direction, a Web export (the export templates are a 1.3 GB download and were not worth derailing the loop), MMO infrastructure. The ruin holds nothing and rewards nothing except having been found.
Open questions
- Does the world make a human want to walk toward the dark? (The only question that matters; unanswered.)
- Is a 7-cell reveal radius right, and should forest shorten it?
- Should small terrain fragments be merged more aggressively, at the cost of some detail?
- Should the landmark be allowed farther away, or hinted at by geography (a path, a clearing)?
- Is 180 px/s with 0.08 s easing the right feel, and is the camera lag noticeable?
- Should discovery memory persist across sessions already, or wait for the server?
Next
Recommendation (not a decision): a human playtest before any new system — the owner runs the prototype on a machine with a display, plays seeds 1, 42, 12345, 987654 and a few random ones for several minutes each, and answers the nine questions. If exploration already pulls, Loop 2 can start adding pressure (survival or gathering). If it does not, Loop 2 tunes generation, visibility, landmarks and movement. Nothing has been promised either way.