A deterministic fantasy world-map generator written in Go. One command writes a PNG with continents, oceans, coasts, mountains, climate bands, deserts, forests, ice, lakes, rivers, streams, islands and a stable land target.
go run ./cmd/worldmapThe default run creates world.png from the built-in seed. The project has no
third-party Go dependencies.
One world, built one stage at a time. Eight steps, ending on the finished world.
The same eight steps side by side, for comparing one against the next. Reading left to right and top to bottom; the bottom right panel is the finished world. The key is in One World, Stage By Stage.
go run ./cmd/worldmapUseful variants:
go run ./cmd/worldmap -seed 42
go run ./cmd/worldmap -width 1024 -height 1024 -out small.png
go run ./cmd/worldmap -land 0.7
go run ./cmd/worldmap -render elevation -out elevation.png
go run ./cmd/worldmap -render temperature -out temperature.png
go run ./cmd/worldmap -render water -out water.png
go run ./cmd/worldmap -climate-axis ns -climate-bands chc
go run ./cmd/worldmap -without archipelagos,islets
go run ./cmd/worldmap -without hydrology -out dry.pngThe same world can be drawn as letters instead of pixels, into a file, into a pipe, or onto the terminal you can walk through:
go run ./cmd/worldmap -format ascii # into world.txt
go run ./cmd/worldmap -format ascii -out - # into a pipe
go run ./cmd/worldmap -view interactive -format ascii # onto the terminalFor very large worlds, render a crop instead of the whole map:
go run ./cmd/worldmap \
-width 1000000 -height 1000000 \
-view radius -at 500000,500000 -radius 512 \
-out crop.png- The same seed and options produce the same PNG, and the same letters.
-jobs 1and higher worker counts produce the same output.- Radius views are exact crops of the same world, not zoomed or resampled.
- The target land fraction is solved from the world configuration, so separate views agree on the same sea level.
- Logs go to stderr; the command report goes to stdout. A map sent to stdout by
-out -takes that stream, and the report joins the logs on stderr. - Every render layer draws the same in both formats: the letters and the colours come from one reading of a cell.
| Flag | Default | Description |
|---|---|---|
-seed |
0x5EEDCAFEF00D |
Selects the world. |
-width |
4096 |
Whole-world width in units. |
-height |
4096 |
Whole-world height in units. |
-land |
0.45 |
Fraction of the world that should be land. Use 0 to keep the fixed sea level. |
-continents |
0 |
Number of continents. 0 scales the count with world size, up to 6. |
-render |
biome |
Output layer: biome, elevation, moisture, temperature, water, density or landmask. |
-format |
png |
Output format: png for an image, ascii for a map of letters. |
-aspect |
2 |
Rows of the world one row of letters covers. A character is about twice as tall as it is wide, so 2 keeps the world in proportion; 1 draws every row. |
-view |
all |
Draw all, a square radius view using -at and -radius, or interactive to move through the world on the terminal. |
-at |
0,0 |
Center point for a radius view, as x,y. An interactive view given no -at opens on the largest body of land instead. |
-radius |
512 |
Half the side length of a radius view. |
-max-bytes |
8GiB |
Maximum field allocation for the selected view. In an interactive view, how much memory the kept squares may hold. |
-without |
empty | Comma-separated generation stages to disable, including hydrology. |
-jobs |
0 |
Worker goroutines. 0 uses one per processor. |
-out |
world.png |
Output path, or - for stdout. Defaults to world.txt under -format ascii. |
-keys |
jikl |
Which letters move an interactive view: jikl puts j left and k down, vi puts j down and k up. |
-step |
8 |
Characters one press moves an interactive view. |
-tile |
128 |
Side in units of one square that an interactive view keeps built. |
-mono |
false |
Draw an interactive view in letters and no colour. |
-climate-axis |
auto |
Direction of the temperature bands: auto, ns or ew. auto asks the seed. |
-climate-bands |
auto |
Order of the bands: auto, chc (cold-hot-cold) or hch. auto asks the seed. |
-log-level |
info |
debug, info, warn or error. |
-log-format |
text |
text or json. |
-log-file |
empty | Append the log records to a path instead of stderr. An interactive view writes none at all unless this is given. |
A flag that the chosen view cannot use is refused rather than ignored, so
-radius beside -view interactive, or -mono beside a PNG, stops the run and
says why.
The generator builds one density field. Positive density becomes land; negative density becomes sea. Ordered stages add structure at different scales:
| Stage | Role |
|---|---|
warp |
Bends sample space so features avoid a stamped-grid look. |
continents |
Places large land masses. |
regions |
Adds hills, highlands, plateaus, ridges, ranges, peaks, bays, inland seas and peninsulas. |
archipelagos |
Adds island chains. |
islets |
Adds small islands and lagoons. |
coastline |
Roughens the shore. |
borders |
Pushes the outer map edges toward sea. |
derived |
Computes elevation, moisture and temperature for rendering. |
hydrology |
Traces highland streams downhill into rivers, oceans and lakes. |
This is the key to the grid at the top of this file. Each panel switches off every stage that comes after it, so the grid reads left to right and top to bottom as the world being built.
| Stage | What it adds | |
|---|---|---|
| 1 | continents |
Bare elliptical land masses. Nothing has bent them yet, so the coast runs as a smooth curve — this is the panel that shows why warp exists. |
| 2 | warp |
The same continents through bent sample space. One change, and the coast gains bays and headlands. |
| 3 | regions |
Landforms inside the continents. The snow peaks and the ranges appear here. |
| 4 | archipelagos |
Island chains out in the open sea. |
| 5 | islets |
Single small islands and lagoons. Deliberately subtle: an islet is a few units across. |
| 6 | coastline |
Noise along the shore, so no coast reads as an arc of a circle. |
| 7 | borders |
The map edges pulled down to sea, so the world ends in open water rather than at a cut. |
| 8 | hydrology |
Rivers, their confluences, and the lakes they end in. The finished world. |
Panels 4 and 5 change least, which is the honest result: those stages add the smallest features the generator has. Panels 1 to 2 and 7 to 8 change most.
Every panel shares one sea level — the one the whole pipeline solved for. A panel that solved its own would move its coast, and the grid would then show that movement instead of the stage that was added. Regenerate the grid with:
go run ./cmd/stagegridA finished world on its own. This is the last panel of the grid above, drawn on its own so the detail is easier to read.
The built-in seed at -width 512 -height 512, which is the world every image
above draws.
Size is not a zoom. A world of 512 units and a world of 1024 units are different
worlds, however the seed matches, because the scaling law derives every length
from the size: the two hold their continents on grids of 296 and 593 units and
carry their detail at 0.71 and 1.0 of the reference. Change -width and you get
another world, not a closer look at this one.
After the density field is built, the pipeline calibrates sea level to satisfy the requested land fraction. It then derives elevation, moisture and temperature.
Temperature forms broad bands running either north-to-south or side-to-side,
laid out cold-hot-cold or hot-cold-hot. -climate-axis and -climate-bands
choose both; either one left at auto is chosen by the seed, which is the
default, so every seed keeps the climate it has always had. Low-frequency noise
bends the bands so they are never ruler-straight.
The hydrology pass then traces the rivers. It walks every source that can reach the region downhill, adds up how many sources drain through each place, and paints the water from that total: a stream where one or two arrive, a river at two, a big river at five. Because the walk is snapped to a world-anchored lattice, two paths that meet become one and their water adds, so the networks branch and merge on their own rather than being placed. A walk that cannot leave a hollow ends in a lake, sized by the water that reached it.
Renderers then read the finished terrain and write an image; they do not modify the generated data.
Two details are worth knowing if you change this code. The descent follows a smoothed height, not the raw one: the coastline noise is steeper than the land that carries it, so a walk down the raw field falls into a pit of noise within one or two steps and no river ever forms. And the choice between the ways down is weighted per place, because the steepest way down an even slope is a straight line, and a ruler-straight river reads as a pencil mark.
The seed is the whole of what separates one world from another. Same generator, same settings, same size; twelve seeds.
Reading left to right and top to bottom:
0x5eedcafef00d |
0x1 |
0x2 |
0x7 |
0xbeef |
0xc0ffee |
0xd00d |
0x1234 |
0xa11ce |
0xfacade |
0x5a1ad |
0xb0a7 |
The first is the built-in seed, which every other image here draws. Rebuild the sheet with:
go run ./cmd/seedsheet-view all renders the full world into memory. That is practical for normal
world sizes, but a million-by-million world would need far more memory than a
single machine should allocate. When the selected view exceeds -max-bytes, the
command exits with a clear error instead of attempting the allocation.
Use -view radius for large worlds. A radius view is a square crop with side
length 2 * radius, centered at -at. Views can be generated independently and
assembled later because every point is a pure function of coordinates, seed and
configuration.
If a radius view extends beyond the world boundary, the out-of-bounds pixels are marked with a magenta-and-black checker pattern that is not used by the terrain palette.
-format ascii draws the same world as letters. Nothing about the generator
changes: the letters come from the same biome rules the colours do, so a coast in
a PNG is a coast here.
go run ./cmd/worldmap -format ascii -width 512 -height 512 \
-view radius -at 150,303 -radius 30 -out -ttttttttttttTTTttt,,,t,,,,,,tt,,,,,,,,,,,,,,,,tt~tt,,,,,,,,,
ttttttttttttttttttt,,,,tttt,t,,,,,,,,,,,,,,,,,tt~tt,,,,,,,,,
ttttttttttttttttttttt,ttttt,,,,,,,,,,,,,,,,,,,tt~ttt,,,,,,,,
tttttttttttttttttttttttttt,,,,,,,,,,,,,,,,,,,,,,tt~~ttt,,,,,
tttttttttttttttttttttttt,ttttttt,,,,,,,,,,,,,,,,,,ttt~ttt,,,
tttttttttttttttttttttttt,,,ttt,,,,,,,,,,,,,,,,,,,,,,ttt~ttt,
ttttttttttttttttttttttttttt,,,,,,,,,,,,,,,,,,,,,,,,,,ttt~ttt
tttttttttttttttttttttttttt,t,,,,,,,,,,,,,,,,,,,,,,,,,,,tt~tt
ttttttttttttttttttttt,,t,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,t...
tttttttttttttttttttt,tttttt...,,t,......,,,,,,,,,,,,,,,..~~-
tttttttttttttttttt,,,,,,tttt,.............,,,,,,,,,,..~.~---
.........tt.........,,,,,,,,,...~~~~~~~-~~~.,,,,,,,,.--~----
..............~~...............~~---------~~..........~-----
~~--~........~~~.............~---------------~.........~~---
~-----~..~~~~----~....~--~.~-------------------~~-~-~~~.~---
------~~~~~-------~-----------------------------------------
------------------------------------------------------------
------------------------------------------------------------
------------------------------------------------------------
----------------------------------==--------=---------------
----------------------------------=-------------------------
--------------------------------------------------------===-
-------------------------------------------------------===--
-------------------------------------------------------====-
-------------------------------------------------------=====
-----------------------------------------------===----======
-----------------------------------------------==----=======
------------------------------------------==-----------=====
----------------------------------------==------------==-===
---------------------------------===--=====-------==--======
Rain forest at the top left, a river winding down through the plains, the coast, and the sea getting deeper away from it.
| Letters | Ground |
|---|---|
- = |
The open sea, getting deeper. |
~ |
Moving water: the shallows at the shore, and every stream and river inland. |
. |
Beach. |
_ |
Desert. |
, ; |
Grassland, rising. |
t T |
Rain forest, rising. |
f F |
Pine forest, rising. |
o 0 |
Lake, and a lake that has frozen. |
` ' |
Polar ice. |
n ^ |
Bare rock, drier then greener. |
* |
Snow. |
# and space |
Outside the world, in a checker. |
-view interactive opens the world on the terminal and lets you walk it. The
terminal is the size of the view, and the map is drawn in the same colours the
PNG carries unless -mono is given.
go run ./cmd/worldmap -view interactive -format asciiWith no -at, the view opens on the middle of the largest body of land. The
corner of a world is open sea — the border stage sinks the edge of every world —
so a view that started there would show nothing and leave a long walk to find
anything. Give -at to open somewhere else.
Colour is held to a few steps per biome, so ground of one kind draws as one block rather than as a gradient that changes at every character. A character is a large pixel: the smooth ramp an image wants reads as noise at this size, and it costs an escape code at nearly every letter.
| Keys | What they do |
|---|---|
Arrows, w a s d |
Move one step. |
i j k l (default) |
Move one step: i up, j left, k down, l right. |
h j k l (-keys vi) |
Move one step, with j down and k up. |
| Any of those as a capital | Jump a whole window. |
q, Ctrl-C |
Leave, and give the terminal back. |
Ground that has been shown once comes back instantly, because the view reads
through the same worldgen.Atlas that caches squares of the world, and asks for
the ring around the window while you are looking at what is already in it.
Larger worlds are not just smaller worlds stretched out. The scaling rules add more terrain variety as the world grows:
| Scale | Controls |
|---|---|
size |
Whole-world dimensions. |
size^0.75 |
Continent size, major warp and climate scale. |
size^0.5 |
Regional landforms, island chains, islets, coastline noise and hydrology detail. |
The default continent count grows with world size and caps at 6. Landforms grow more slowly than continents, so larger worlds contain more features per continent without changing the overall land coverage.
The command writes a short run report to stdout:
wrote world.png: 4096x4096 at (0,0) in 11s
world 4096x4096, seed 0x5eedcafef00d, land 45% at sea level ...
steps [warp continents regions archipelagos islets coastline borders hydrology]
Operational logs go to stderr:
go run ./cmd/worldmap -log-level debug
go run ./cmd/worldmap -log-level warn
go run ./cmd/worldmap -log-format json
go run ./cmd/worldmap -log-level info 2>/dev/nullUse debug to inspect stage parameters and timings. Use warn or error when
embedding the command in scripts.
| Path | Purpose |
|---|---|
cmd/worldmap |
CLI entry point, flag parsing, logging, and the wiring of the formats and the views. |
cmd/stagegrid |
Draws the stage-by-stage animation and grid in this README. Docs only; the generator is here so the images can be rebuilt rather than trusted. |
internal/worldgen |
Deterministic terrain generation, scaling, views and calibration. |
internal/worldrender |
Shared rendering contracts, palettes and channels. |
internal/worldrender/pngmap |
PNG renderers for biome and data-channel output. |
internal/worldrender/asciimap |
The same two renderers, drawn as letters. |
internal/worldwalk |
The interactive view: viewport, panning and the Atlas. |
internal/term |
Raw mode, keys and frames. Knows terminals and nothing about worlds. |
cmd/seedsheet |
Draws the twelve-seed sheet in this README. Docs only, for the same reason as cmd/stagegrid. |
docs/sample.png |
A single finished world at 512 units a side, which is the world every image here draws. |
docs/stages.gif |
The stage-by-stage animation, rebuilt by go run ./cmd/stagegrid. |
docs/stages.png |
The same stages as a grid of panels, rebuilt by the same command. |
docs/seeds.png |
Twelve worlds from twelve seeds, rebuilt by go run ./cmd/seedsheet. |
.github/workflows |
The test and release pipelines. |
UNLICENSE |
The public-domain dedication this project is released under. |
Run the test suite:
go test ./...The tests cover deterministic terrain generation, climate and biome rules, freshwater placement, golden PNG output, land fraction calibration, view stitching, logging behavior and scaling behavior.
Two workflows, using only actions/* and the gh command that every runner
already carries. No action from outside GitHub, for the same reason go.mod
requires nothing.
| Workflow | Runs when | Does |
|---|---|---|
.github/workflows/test.yml |
push to main, and any pull request to main |
go vet, a gofmt check and the whole suite, on arm64 and on amd64; then builds every platform |
.github/workflows/release.yml |
a release is published | checks, then builds worldmap for six platforms and attaches them to the release |
The Go version is read from go.mod by setup-go, so the toolchain of a run and
the toolchain of the module cannot drift apart.
The generator is bit-exact, and bit-exactness is not the same everywhere. A Go compiler fuses a multiply and an add into one instruction on arm64 and not on amd64, which rounds once instead of twice and moves the last bit of a height.
No pixel moves. goldenWorldPNG passes on both architectures, so the promise
above — the same seed and options produce the same PNG — holds wherever you run
it. Only goldenCellHash, which pins the raw bits of a float64, is
architecture-bound, and it is recorded per architecture in
internal/worldgen/determinism_test.go. An architecture with no recorded value
skips that one test and reports the hash it saw, so the value can be recorded
from real hardware rather than guessed.
The arm64 run is therefore the one that holds the terrain to its golden. The amd64 run holds everything else.
Publishing a release builds worldmap for Linux, macOS and Windows on both
amd64 and arm64, named worldmap-{os}-{arch}-{version}, with a SHA256SUMS
file beside them. The binaries are built with -trimpath -ldflags="-s -w", which
takes about a third off the size.
Nothing stamps a version into the binary, because the toolchain already does:
go version -m worldmap-linux-amd64-v1.0.0 | grep vcsnames the commit the binary was cut from, the time of it, and whether the tree was clean.
Public domain, under the Unlicense. Take it, ship it, sell it, no attribution needed.
The dedication is clean: go.mod requires nothing, so every line here is either
this project's or the Go standard library's.



