SMERESKI
  1. PROJECTS
  2. SKILLS
  3. RESUME
  4. BLOG
  5. GAMES
  6. CONTACT

POST · 2026-09-14

2026-09-14 · 8 MIN READ

No river runs uphill

The planet under my survival game is one JSON entry. Everything wet on it — rivers, lakes, the coast, the mill — falls out of a single rule: water goes downhill, and only downhill.

gamesgodotalgorithmverification
A river winding through a green valley below terraced mesa cliffs and snow-capped peaks, pine forest on the shelf, big clouds overhead, on Spaceframe's home planet
AI narration — synthetic voice (Kokoro), not a human recordingAI-assisted text — drafted with Claude, edited and approved by a humanAI-built software — the planet code was pair-built with Claude Code against a probe suite; the screenshots are frames written by that suite's capture rig
AI narration — synthetic voice (Kokoro), not a human recording · listen, or click any word to play from there

Earlier this month I followed one of my rivers to the sea and it refused to stop. The course ran off the beach and kept going down the seabed, and when I asked the drainage network how many of its river points were underwater, the answer was 2,083 the deepest of them 143 m below the surface, still draining, still looking for somewhere lower to go.

Spaceframe is the survival game I have been building since July: a sphere 10 km in radius that you land on, walk across, dig into and build on, in Godot 4.6 with the godot_voxel add-on doing the meshing. Most of the code was written by my coding agent against probes I insisted on; my job was the demands and the verdicts. The rivers came with one demand attached. They had to be real hydrology, not decoration, because a painted river looks right in a screenshot and wrong the moment you follow one. So the whole water stack rests on one rule: every point on the planet either drains to a strictly lower neighbor or is a sink, where a lake forms or the sea already is. Two thousand river points under the sea did not break that rule. Each of them had a lower neighbor. What was wrong was my picture of where the rule stopped.

The ground is a small graph of noise fields compiled inside the voxel add-on a slow continental field that decides where high ground exists at all; a ridge field made from one minus the absolute value of a noise, which folds every valley into a sharp crease, then cubed so the creases stand up as narrow spines; and a mesa mask that suppresses the ridges into flat-topped plateaus with cliff bands. That graph answers exactly one question, how far out the ground sits in a given direction, and nothing in it knows about water.

The same valley at golden hour: terraced cliff bands in amber light, the river below, snow on the ridge behind
Plateau country at golden hour, where the mesa mask wins

The ridge field suppressed into strata, forest on the shelves, snow on the ridge behind. Every picture in this post is a frame written by the probe suite's capture rig, which checks only that a frame was drawn; the pictures are not press shots.

The sea is not in that graph at all. It is a separate sphere the renderer draws 90 m below the datum, the nominal radius every height is measured from: one node, exact everywhere, and underneath it the seabed is just more ground, sloping down.

A curving shoreline seen from a bluff: turquoise shallows, a surf line, a boulder-strewn meadow and dunes
The sea, a sphere drawn 90 m below the datum

The nearest coast to the spawn, 2.2 km away. The surf line and the shallows are the ocean shader reading the same depth the swimmer reads.

The drainage solve only ever reads the ground. 90,000 points are scattered over the sphere along a Fibonacci spiral equal-area everywhere, no seam at the poles so neighbors sit about 118 m apart, roughly one reach of river. Each point finds its neighbors through a spatial hash whose cell edge is the point spacing, then drains to the lowest one. A spike proved the idea before anything was built on it: the whole planet solved in under a second, zero points draining to anything not lower than themselves, and the largest catchment gathering 1,700 cells behind one point, which means flow concentrates into channels worth carving instead of a puddle at every point. So the underwater rivers were never a bug in the solver. To the solver, a river reaching the coast had simply found more downhill, and any submarine hollow with enough catchment behind it was a lake every one of them perfectly legal.

A river bending through meadow below terraced cliffs and pine forest, seen from high above at golden hour, sun glinting on the water
A river the solver drew

From above: the course bends where the catchment says, the banks follow the carve, and the village stands where the siting pass found dry ground beside the water.

The fix was to let the sea into the solve. Sea level now enters the drainage as a number in the same units as the heights. Nodes under the waterline drop out of the river and lake sets, the last land node still drains into a sea node so the mouth stays intact, the water table clamps every level to just under the sea surface so the two shaders never fight over a pixel, and a lake has to stand clear of the sea's highest storm crest or it is a bay. The probe counted the planet before and after.

Before the sea entered the solve, and after
River nodes under the waterline2,083 → 0
River mouths ending under the sea surface271 of 272 → 0
Lakes under the waterline107 of 224 → 0

Counts from the home world's drainage network before sea level was a fact the solver knew. The deepest of those underwater nodes sat 143 m below the surface.

The coast still looked wrong after that, and it took two wrong fixes to notice I was staring at the wrong object. A pale slab hung out over the sea at river mouth after river mouth, and both fixes went at the river ribbon on a perfectly plausible story. Neither moved a pixel. What worked was elimination: shoot the same frame three times, hiding one layer more each time, until the slab vanished along with the waterfall curtain. The fall test asked the raw heightfield for an 8 m drop, and at a coast the answer is always yes, because the next node is out on the seabed, so a river mouth was being drawn as a cataract wherever the shore fell away. The repair was one line, a drop into the sea is a shoreline and not a fall, and the first thing I do with a wrong frame now is hide layers until it changes.

A river cutting through a sandy beach into a turquoise sea, seen from above, with a long curving shoreline and hills beyond
Where a river ends now

A mouth after the fix: the last land node drains into the sea and the beach collar is the carve's.

The water table that the fix leans on, the one place every water level is written down, exists because of an earlier mistake, and the mistake starts with what a carve costs. Cutting a river channel into the voxels is not free. One reach costs a few milliseconds and a thirtieth of a megabyte to bake, which sounds like nothing until you multiply by the planet.

What a carve costs, by how much of the planet you carve
One 120 m reach0.031 MB · 4.6 ms
Every reach inside a 640 m view0.8 MB · 124 ms
The whole planet at once813 MB · about two minutes

Measured by a spike before the streaming decision was made. Only channels above a catchment threshold are carved, which is how 90,000 drainage points become roughly 26,000 reaches; about 27 of them sit inside one view.

So reaches come and go around the player, exactly as trees do, and nothing is built for ground nobody is standing near. For a while the water level at each node was computed from whichever reaches happened to be streamed in, so a river's surface depended on where you were standing the same river, a different height, depending on which way you had walked up to it. A probe that teleports the player and re-measures the same points convicted the last version of it with a 40 cm shift over a 200 m walk, and the fix was the water table: every level computed once, globally, at load. Everything that touches water now reads that table: the swimmer, a crate dropped in the river, and the village mill. The crate is the interesting one, because the sea's four Gerstner wave trains are summed on the CPU on the same clock the shader uses, so it bobs on the crest your eye sees rather than on a second, slightly different ocean. The mill is the small proof I like best: its wheel turns at the rim speed of the river's flow at the paddle dip, seated so the lowest paddle rides 35 cm under the live surface.

A wooden water mill with a large wheel standing in a river, seen from the water looking upstream, forest behind
The mill earns its river

The wheel is driven from below by the current, with no channel lifting water onto it, because the river has no drop to spend here. The probe measured it at 0.528 rad/s against the rule's 0.527, then doubled the flow and checked that the rate doubled with it.

Each streamed reach is a prism baked into a signed distance field, and the bake produced the most convincing bug I have met, a hole that removes nothing. A correctly wound mesh can still bake an inverted interior: the add-on's sign pass flips the inside of a prism at particular grid sizes 48 cells across for the river prisms and for the windmill's cone, solid again at 64, and then a wider pad failed at 64, so the bad size moves with the shape and no single floor is safe. It is deterministic, invisible to the add-on's own debug check, and from the outside identical to a modifier that was never attached: no error, right size, right place, zero meters of ground moved. It passed its own inspection and quietly dammed rivers. I have not filed it upstream yet; the repro is a prism and a cell count. Every bake is now sampled at the prism's own center; one that reads hollow is retried one grid size up, then two, and a bake that still reads hollow is reported as an error instead of shipped as a dam.

By this month a planet had enough settings behind it to be described instead of coded, so a planet became one entry in a JSON file: 25 keys, of which eleven are colors and look knobs, six describe the shape (radius, relief, where the sea sits, how deep craters bite, how much land a river needs, where to guarantee a lake), six switch whole subsystems on or off, one biases the climate, and one is the name. A headless probe enforces two rules in about a second. Every key must be a known setting, so typing registor instead of register turns the build red, where it used to render a perfectly nice default and keep the typo to itself. And every setting must have a reader, because a key nothing reads is a lie told to whoever edits the file next; that rule caught two texture settings authored per world since the summer while the shader's own tuned defaults sat unused. Nine worlds live in the file, among them a cratered dry moon, an ocean world with the sea line lifted up among the hills, and three demo worlds Ashfall, Redmesa and Frostline that exist only to prove the look settings move.

Orange-red dunes under a low sun and a hazy sky, the red-desert demo world seen from the air
Redmesa, one entry in the file

The red-desert demo world at golden hour: the same solver, the same rivers and the same sky code as the home world, with a different look family and palette.

Last week I asked the question that decides whether any of this can leave the game: do the same settings produce the same planet, byte for byte, in two separate processes? The multiplayer plan assumes they do every peer builds its own planet from the settings and a seed, and no terrain crosses the wire. The ruler is a headless probe that evaluates the height field over 4,096 pseudo-random directions, runs the full drainage solve, and hashes four things separately: the direction set itself, which is the control and should never differ, then the heights, the entire drainage network, and the river and lake sets. Every value enters the digest as its own raw IEEE bytes, never as printed text, because a formatted float is an engine implementation detail. No file carries a hash between the runs; the verdict comes from diffing two independently written logs, each of which also prints its process id, the engine version and the SHA-256 of the voxel binary, which is what proves the two runs were different processes sharing one build. The answer was equal on all four digests, two processes, three evaluations each.

That proves the solve and only the solve. It does not yet prove two peers mesh the same ground, because the mesh reads a second copy of the height formula that is gated to a tolerance rather than a hash; it says nothing about where the trees land; and it was one machine. There is also no seed setting yet the seed is still a compile-time constant. Also not done, on purpose: the ocean is four wave trains rather than a spectrum, because a full-screen wave spectrum cost the one studio that published a number for it about 8 ms of a 20 ms frame, and a village frame here already costs 17 ms against the 16.7 that sixty frames a second allows. I want to lift the planet out of the game into an add-on with a real seed and put a second game on the same ground, which is what the determinism question was for.

What I keep coming back to is how much of the planet fell out of one rule. The rivers, the lakes, the coast, where the village stands, where you wake up, which way the mill turns: all of it is downstream, literally, of 90,000 points each agreeing to drain to a lower neighbor. The rule never changed. Every bug worth writing about was a fact the world had not been told that there was a sea, that a drop into it is a shoreline, where the water really stood, which side of a mesh was the inside. There are still no boats, though the water field was built so a hull can sample it at its corners. When there are, I expect the first one to sail straight through something nobody told the rule about, and I expect the rule to be right.

REFERENCES5 LINKS