Realistic, geology-based procedural world generation for games

Companion file: world_generation_bench.js in this folder, a single-thread plain-JS micro-benchmark of the kernels below (node world_generation_bench.js 256).

Dwarf Fortress world generation, stage by stage

Takeaway

Dwarf Fortress does not paint biomes. It generates six coarse scalar fields (elevation, rainfall, temperature, drainage, volcanism, savagery) on a tiny grid (17 to 257 squares a side) by seeding a coarse mesh with weighted random values and filling it fractally (midpoint-displacement style, with per-axis variance), then derives vegetation and biomes from field thresholds, carves rivers by running many “fake” downhill rivers that dig when stuck, grows lakes along rivers, smooths, then recomputes rain (rain shadow), temperature and vegetation from the final terrain, and finally checks the result against many count thresholds, throwing the whole world away and rerolling if it fails. The world map is small and coarse; detail comes from each square expanding to 16x16 local blocks of 48x48 tiles when you embark.

Cited Findings

Primary description, Tarn Adams (Gamasutra Q&A “Dissecting the development of Dwarf Fortress”, quoted in full on the DF wiki):

Earlier (2008) description, Adams to John Harris:

Parameters (DF wiki, Advanced world generation; DF2014 tokens and defaults/examples):

The rejection checks:

Sites and civilizations: placed at the start of history (“Civilizations and caves are placed”); civilizations need biomes (dwarves need mountains; elves will not spawn if max temperature is 9; humans need at least 0), so the rejection checks are partly there to guarantee civ placement is possible. — DF wiki: World generation; DF wiki: Advanced world generation

Second-hand (Polygon, 2014, paraphrased by Isegoria): rainfall layer, a separate fractal for mineral strata, temperature fractal, biomes as contiguous regions, then drainage/erosion with rivers carving valleys toward oceans “only after biomes exist”, a salinity pass for deltas and mangroves, region names from four cultures. — Isegoria, 14 Aug 2014, quoting Polygon 23 Jul 2014

On the requested book chapter: “Procedural Generation in Game Design” (2017) lists Tarn Adams as co-editor, not a chapter author; its world chapter is Ch. 10 “Worlds” by Mark R. Johnson (Ultima Ratio Regum). Adams’ own chapter is Ch. 13 “Emergent Narrative in Dwarf Fortress” in the 2019 “Procedural Storytelling in Game Design”, plus “Simulation Principles from Dwarf Fortress” in Game AI Pro 2. — Routledge TOC, Procedural Generation in Game Design; Routledge, Procedural Storytelling in Game Design; O’Reilly, Game AI Pro 2 ch. 41

Inferences

Gaps

Other games and tools, and what each does differently

Takeaway

There are two families. Field-and-threshold generators (Minecraft 1.18+, Terrafirmacraft’s layers, Vintage Story’s strata) evaluate noise and splines per point with no global pass, so they stream forever but cannot carve real drainage: Minecraft’s “rivers” are just the valleys band of a noise. Whole-map simulators (Dwarf Fortress, O’Leary, Azgaar, mapgen4, WorldEngine, Undiscovered Worlds) build a bounded map and then run global passes: depression filling, flow accumulation, wind sweeps. Only those give rivers that join and reach the sea. A bounded phone world (a quest’s island) belongs in the second family. Almost all of them fake erosion cheaply (O’Leary: 5 rounds of a flux-and-slope rule; Azgaar: a one-pass downcut of at most 5 height units; mapgen4: none, just lowering river beds to their tributaries). The physically based tools (WorldEngine’s plate sim) are slow and offline.

Cited Findings

Minecraft 1.18+ (Java), from the Minecraft Wiki and Henrik Kniberg:

Martin O’Leary, “Generating fantasy maps” (mewo2/terrain source):

Red Blob Games mapgen4 (Amit Patel, 2018; source on GitHub):

Azgaar’s Fantasy Map Generator (source, 2026):

WorldEngine (Mindwerks, Python + C plate library):

Undiscovered Worlds (Jonathan Hill, GPL):

Plate tectonics done cheaply (Frozen Fractal, “Around the World” 2023): plates grown from continent Voronoi regions by simultaneous flood fill (about 15 plates for 6 continents), random velocities, boundaries classed only as converging or diverging (transforms dropped), and boundary type × crust type picks a noisy profile curve: mountains (continent-continent converge, taller with speed), rift valleys, mid-ocean ridges, island arcs, trenches with volcanic arcs. — Frozen Fractal, “Around The World, Part 2: Plate tectonics”, 13 Nov 2023

Rock strata:

Inferences

Gaps

Algorithms, with costs

Takeaway

The backbone that every serious generator converges on is cheap: priority-flood (Barnes 2014) fills depressions and gives each cell a receiver and a topological order in O(n log n); flow accumulation is then one O(n) reverse pass; and the stream-power law solved implicitly in that order (Braun & Willett 2013, as used by Cordonnier 2016) erodes river valleys with a timestep that does not depend on resolution, converging in about 200 steps. On a CPU this is about 15 ms per step at 256x256 in plain JS (measured). Droplet erosion (Beyer, Lague) is the detail pass: gullies and sediment fans. It costs a few microseconds per droplet with a precomputed brush. Thermal erosion is a few ms per iteration. Climate is one sorted sweep along the wind. Caves are either 3D-noise thresholds (Minecraft) or a few cellular-automaton passes (sub-millisecond at 50x50). Schott 2023’s local, heap-free stream-power update is built for the GPU (a compute shader). On a CPU it is slower per step and needs more steps, so it fits Tycho only if the work moves onto Tycho.gpu.

Cited Findings

Stream-power law, tectonic uplift (Cordonnier et al. 2016):

Stream power for authoring (Schott, Paris, Fournier, Guérin, Galin, ACM TOG 2023):

Priority-flood (Barnes, Lehman, Mulla 2014):

Droplet (particle) hydraulic erosion (Beyer 2015; Lague 2019):

Thermal erosion and talus:

Lakes and endorheic basins:

Coastlines and continental shelves:

Wind, moisture and rain shadow: (see mapgen4 and Azgaar above) one sorted pass along the wind: humidity carried from upwind neighbors, gained over water, capped by (1 - elevation) or lost on climbing slopes (orographic rain), so the lee side is dry. DF applies the same correction once after final elevations and only (per a forum report) where drainage >= 50. — mapgen4 map.ts; Azgaar precipitation-generator.ts; DF wiki: Advanced world generation

Biomes: Whittaker and Köppen:

Caves:

Local measurements, plain JS (Node v26, Intel i9-9880H 2.3 GHz laptop, one thread), from world_generation_bench.js:

Inferences

Gaps

What fits a 256x256 or 512x512 grid on a phone in 1-3 s of JS, and what must be precomputed

Takeaway

On a 256x256 grid, everything except long erosion runs costs tens of milliseconds in plain JS: base noise, priority-flood, flow accumulation, rivers, lakes, wind and rain shadow, biomes, CA caves. The whole pipeline fits well under 1 s on a laptop, including about 20 implicit stream-power steps and a light droplet pass, which leaves room for a phone’s slower CPU (unmeasured here). At 512x512 the same pipeline is about 4x the cost: 1-3 s with erosion trimmed. Things to precompute and ship rather than run per seed: a Poisson point set or mesh, as mapgen4 does; spline and biome tables; and, for any “realistic” steady-state mountain range, either a coarse-grid stream-power run (128²) upsampled or a stock of eroded tiles. Full stream-power equilibrium (about 200 steps) is the one stage that does not fit at 512² on a phone.

Cited Findings

Inferences

Gaps