Skip to content

Latest commit

 

History

28 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hello-world

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/worldmap

The 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: bare continents, then warp, regions, archipelagos, islets, coastline, borders and hydrology, each stage adding to the last

One world, built one stage at a time. Eight steps, ending on the finished world.

The same eight stages as still panels, reading left to right and top to bottom

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.

Quick Start

go run ./cmd/worldmap

Useful 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.png

The 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 terminal

For 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

What It Guarantees

  • The same seed and options produce the same PNG, and the same letters.
  • -jobs 1 and 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.

Command Options

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.

Generation Model

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.

One World, Stage By Stage

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/stagegrid

A 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 finished world: continents, oceans, climate bands, rivers, lakes, mountains, deserts, forests and island chains

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.

One Generator, Twelve Worlds

The seed is the whole of what separates one world from another. Same generator, same settings, same size; twelve seeds.

Twelve worlds from twelve seeds: continents, archipelagos, inland seas, ice caps and river networks, all from the same generator

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

Large Worlds And Views

-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.

The World In A Terminal

-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.

Moving Through It

-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 ascii

With 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.

Scaling

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.

Logging

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/null

Use debug to inspect stage parameters and timings. Use warn or error when embedding the command in scripts.

Project Layout

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.

Development

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.

Continuous Integration

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.

Why the tests run on two architectures

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.

Releases

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 vcs

names the commit the binary was cut from, the time of it, and whether the tree was clean.

License

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.

About

A deterministic fantasy world-map generator in Go. Continents, climate bands, rivers and lakes from one seed — zero dependencies, byte-identical output, and crops that stitch back into the whole world.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages