how Civilization & Colonization ran a planet in real-mode DOS

CHAPTER 6 · PROCEDURAL WORLDS

Growing a World

Every Civilization map is grown, not drawn: a drunkard's-walk blob stamper, a latitude formula, two rainfall sweeps, an erosion pass and up to 256 river attempts — four menu knobs and a 16-bit seed away from every Earth that never was. The whole machine is reimplemented on this page. Press grow.

Four questions, a flicker of drive light

Start a new game of Civilization on a random world and the setup menu asks four questions — land mass, temperature, climate, age — each with three answers, defaults in the middle. Then the drive light flickers and 4,000 tiles of planet exist: continents with mountain spines, a desert belt at the waist, jungle near the equator, tundra fraying into polar ice, rivers that reliably reach the sea. On an 80×50 grid, that flicker of work has to manufacture geography that reads as geography — and it does it with roughly a dozen constants and the C runtime's stock random number generator.

None of this was documented in 1991. What follows is the algorithm as the community disassembled it from CIV.EXE over the following decades — chiefly darkpanda's "Civ1 map generation explained" thread (2013–2023, in his words "fully deciphered … fully ported to JCivED"), cross-checked against CivOne's open-source port of the same code. The figure below runs our reimplementation of that record, stage by stage, using the same linear congruential generator Civ itself used — x·214,013 + 2,531,011, Microsoft C's rand() — so a given seed and knob setting always regrows the same world.

GROW A WORLD — the 1991 algorithm, live set the knobs, pick a seed, press grow — or freeze the map at any stage; hover any tile
Our reimplementation of the algorithm the community disassembled from CIV.EXE — darkpanda's "Civ1 map generation explained" (CivFanatics, 2013–2023), cross-checked against CivOne's Map.Generate.cs. Dice: the Microsoft C LCG (×214,013 + 2,531,011) that Civ itself used, so seed + knobs is fully reproducible. Close, not guaranteed byte-exact: the blob start box follows darkpanda ([4,71]×[8,33]; CivOne uses a wider one), and the overlap→mountains/hills reading and the river direction rose are the documented mainline — both flagged "probable" in the record, and the river meander term already carries one community correction (Renergy's ×2). The specials/huts overlays run the Chapter 4 formulas with this seed as the TerrainMasterWord; huts are drawn on land squares only (in-game they are further suppressed at cities and barbarian-visible squares).
The four knobs — every constant they feed
KnobMenu options (0 · 1 · 2)Constant it drivesWhere it bites
Land masssmall · normal · largetarget = (n+2)×320 → 640 / 960 / 1,280 squaresstage A; also the river cap
Temperaturecool · temperate · warm1−n added to latitudestage B
Climatearid · normal · wetocean yield +4n / +n; rainfall draw rnd(0..7−2n)stage C; also the river cap
Age3 · 4 · 5 billion yearspasses = 800×(1+n) → 800 / 1,600 / 2,400stage D

Stage A — a drunkard with a rubber stamp

Land is made by stamping. The generator keeps two 80×50 work buffers — the geography layer and a temporary stencil — and spawns land in chunks. Each chunk clears the stencil, picks a random start inside the box x ∈ [4,71], y ∈ [8,33], draws a random path length between 1 and 64, and then walks: stamp a three-tile L-shape — (x,y), (x+1,y), (x,y+1) — step to a random orthogonal neighbour, repeat. The walk ends when the length runs out or the walker leaves the cage [3,3]–[76,45], which keeps land clear of the map edge.

The stencil then merges into the geography, and the merge is where relief comes from: a stencil cell landing on empty sea becomes land; landing on existing land, it increments it. Overlap depth is elevation. In the community's reading of the merge counter, a square stamped twice becomes mountains and a square stamped three or more times becomes hills — so mountain chains are literally the places where drunken walks crossed. Chunks keep spawning until total land reaches (landmass+2)×320 — 640, 960 or 1,280 squares. On this page's reference seed 1991 with default knobs that takes 43 chunks. A final pass squares off any 2×2 cell whose diagonals alternate land and ocean, removing impossible diagonal straits that neither ships nor land units could legally cross.

Stage B — climate from a subtraction

Terrain starts as arithmetic on the row number. For every flat land square:

lat  = |y − 29 + rnd(0..7)|      ; distance from the equator, dithered
lat += 1 − temperature           ; the knob: shift one band either way
lat  = lat/6 + 1                 ; integer divide into bands
0–1 → desert   2–3 → plains   4–5 → tundra   6–7 → arctic

That is the whole climate model: hot at the middle, cold at the ends, an eight-way random dither so the band edges shred instead of ruling straight lines across the map. Mountain and hill squares from stage A keep their elevation and skip the formula. One genuine oddity, visible below: this stage measures latitude from row 29, but the rainfall stage that follows measures it from row 25. The original code carries two different equators, and the +rnd(0..7) — which only ever pushes south — appears to split the difference. That reading is our observation from the record, not a community-stated fact.

Where the bands fall — the latitude dice hover a row; switch the temperature knob
Formula verbatim from darkpanda's thread; CivOne implements it identically. Each row's split is the exact enumeration over the eight equally likely rnd(0..7) draws, for flat squares only — stage A's mountains and hills override the band, and stage C's rain then rewrites much of what remains. The two-equator quirk (bands from y=29, rain from y=25) is our analysis, flagged as such.

Stage C — rain from two directions

Rain is a per-row budget, swept across the map twice — west→east, then east→west — with latitude = |25 − y|. On the westerly sweep, each ocean square raises the row's wetness by one, up to a ceiling of |latitude − 12| + 4×climate; each land square, while the budget is positive, spends rnd(0..7−2×climate) of it and transforms: plains green into grassland, deserts soften into plains, hills sprout forest, tundra freezes to arctic. Mountains transform nothing but quietly eat three extra points of wetness — a one-line rain shadow. The easterly sweep runs the same budget with a different ceiling (latitude/2 + climate) and a warmer table: grassland within ten rows of the rain equator becomes jungle, elsewhere swamp; even mountains manage forest.

Notice what the climate knob actually does. A wetter setting raises the ocean ceilings and shrinks the random spend — the budget fills faster and drains slower, so the transforms reach further inland. "Arid" doesn't make deserts directly; it makes the wetness budget run out sooner, and the untransformed latitude bands are what deserts are. Coastlines facing the sweep get the rain; interiors keep the geometry lesson every schoolchild eventually learns about continental climates — emergent, here, from about fifteen lines of code.

Stage D — age, backwards

The age knob runs the erosion pass: 800×(1+age) iterations — 800, 1,600 or 2,400. Even iterations strike a random square; odd iterations strike a random neighbour of the previous one, so the edits arrive in correlated little pairs and clump. Each strike advances the square one step through a fixed table:

grassland → forest      forest → jungle       jungle → swamp
swamp     → grassland   plains → hills        tundra → hills
desert    → plains      hills  → mountains    arctic → mountains
mountains → ocean       ; only if no diagonal neighbour is ocean

The loop count is solid; what it means is disputed in the source thread itself. darkpanda's original write-up read the knob as "young worlds are mountainous", but later measurements posted in the same thread (tupi/GPR) found 3 billion years gives less mountain than 5 billion — which fits the table: it is net-uphill (plains→hills→mountains, with the mountain-sink guarded by its diagonal-ocean test), so more passes mean more relief. We implement the loop count as documented and let you test the dispute with the knob.

Stage E — 256 chances at a river

Rivers are the only stage that is allowed to fail, and it fails safely: before each attempt the generator backs up the entire map, lets a river wander, and if the result disappoints, restores the backup wholesale. Transactional map editing, in 1991.

Each attempt starts at a random hills square, sets it to river, and meanders with a direction accumulator over an eight-point rose:

A = rnd(0..3) × 2                     ; start on one of four headings
each step:  C = rnd(0..1)
            A = ((C − len % 2) × 2 + A) & 7

The len % 2 term alternates the turn's parity, so each step either holds course or turns 90° — with the permitted turn direction alternating step by step — and the river zig-zags downhill-ish instead of ballooning into a random blob. (That ×2 factor is itself a community correction — posted by user Renergy against darkpanda's first write-up; the fine arithmetic here is the most-corrected part of the record.) The walk ends when it hits ocean, an existing river, mountains, or steps beyond a square that touched the sea. The attempt is kept only if the river ran at least five squares and ended at water — the sea by adjacency, or head-on into an earlier river. Accepted mouths get a 7×7 splash of forest→jungle; everything else is rolled back. The generator stops after 256 attempts or when the count reaches (climate + landmass)×2 + 6 — between 6 and 14 rivers, 10 at defaults. It is not an efficient process: our reference seed burns all 256 attempts and banks only 6 of its permitted 10. It doesn't matter. Failed rivers cost nothing but time, and the map that survives is the one where every river reaches the sea.

Stage F — freezing the poles

Last, cartographic housekeeping: rows 0 and 49 are set entirely to arctic — whatever ocean or terrain was there — and then 20 times over, a random square in the four polar rows {0, 1, 48, 49} is flipped to tundra, so the ice edge looks weathered rather than machined. That's the whole world: on our reference seed, 1,118 land squares and 6 rivers, grown from 6,001 draws of a 16-bit-seeded LCG.

One more pass runs invisibly: the thread records that the generator also derives continent and ocean IDs, per-square land values in [8..15] and per-continent counts of good city sites — the data the AI uses to choose where civilisation starts. The IDs and the land values go into the save's companion .MAP file, and Chapter 4 takes it apart.

What the generator never stores

Some of the world's furniture is not generated at all — not here, not anywhere. Special resources, goody huts and grassland shields never touch the map buffers and are never written to disk. They are pure functions, recomputed on demand — specials and huts from the tile coordinates and the 16-bit map seed, shielded grassland from the coordinates alone:

special(x,y) ⇔ 1 < y < 48
             ∧ (x mod 4)·4 + (y mod 4) = ((x÷4)·13 + (y÷4)·11 + seed) mod 16
hut(x,y)     ⇔ 1 < y < 48   ∧  no city, not seen by barbarians
             ∧ (x mod 4)·4 + (y mod 4) = ((x÷4)·13 + (y÷4)·11 + seed + 8) mod 32
shielded grassland ⇔ (x + y) mod 4 ∈ {0, 3}

The left-hand side indexes the sixteen cells of the 4×4 block a tile sits in; the right-hand side hashes the block's coordinates with the primes 13 and 11 plus the seed. Exactly one cell per block matches mod 16 — one special per 4×4 block — and the hut variant's mod 32 fires in only half the blocks. The polar exclusion rows {0, 1, 48, 49} are precisely the rows stage F just froze. Flip the ▣ specials and ▣ huts overlays on the figure above and change the seed: the whole world regrows, and the riches rearrange on a lattice all their own, because the riches were never part of the map. The seed itself — the community calls it the TerrainMasterWord — is saved in two bytes at offset 0x6 of the .SVE file and lives at ds:6E00 while the game runs. Chapter 4 puts these same formulas under a seed slider and covers what the trick buys: 4,000 tiles of resource placement for zero bytes of state.

And here is what the real machine grows — same grid, same bands, same logic, running in the shipped binary:

Civilization's in-game world map: procedurally generated continents with desert bands, tundra fringes and mountain chains, in the 1991 DOS release.
The production algorithm at work: Civilization's world map in the shipped DOS binary, captured running under mddosem. Compare the texture with the figure above — the desert waist, the shredded band edges, mountain chains where blobs overlapped, rivers that all reach the sea.

Sources for this chapter — community record: darkpanda's "Civ1 map generation explained" and RNG threads (CivFanatics) and JCivED; CivOne's Map.Generate.cs (credits JCivED r23); Gowron & Dack's EXE tables for the specials/huts arithmetic; LCG constants cross-checked against Microsoft C's rand(). Runtime: comparison screenshot captured under mddosem. The generator on this page is our reimplementation of that record; disputed details are flagged in the figure notes.