Domain-neutral procedural fantasy map & world generation: Voronoi terrain, hydraulic erosion, biomes, rivers, Markov place-names, and shaded-relief SVG.
Project description
mapwright
⚠️ Early development (v0.x, alpha). The API is still moving and may change without notice between versions. Extracted from a working application; usable today, but pin a commit if you depend on it.
Domain-neutral procedural fantasy map & world generation — Voronoi terrain with
hydraulic erosion, climate-driven biomes, rivers, Markov place-names, and shaded-relief
SVG rendering. Pure Python, numpy-only, fully seed-deterministic.
mapwright produces neutral data (cells, biomes, rivers, polygons) and a self-contained SVG renderer. It has no opinion about your application's models — map its output onto your own tiles/entities however you like.
Gallery
Every image below is a deterministic render of a built-in preset (or a dungeon),
produced by examples/gallery.py:
continent |
archipelago |
islands |
highlands |
desert |
arctic |
pangaea |
tropical |
DungeonGenerator |
SettlementGenerator |
Settlement (port) |
Settlement (citadel) |
RegionalRoadGenerator |
RegionGenerator |
Regenerate them with python examples/gallery.py (SVGs always; PNGs when
cairosvg is installed).
Install
pip install git+https://github.com/sligara7/mapwright.git
# or, for local development:
pip install -e ".[dev]"
Quickstart
from mapwright import SeededRNG, RegionalTerrainGenerator, RegionalSVGRenderer, Marker
# Same seed -> same world, every time.
terrain = RegionalTerrainGenerator(SeededRNG(7)).generate(width=60, height=40)
markers = [Marker(name="Eldmoor", x=30, y=18, kind="settlement_city")]
svg = RegionalSVGRenderer().render(terrain, markers)
open("world.svg", "w").write(svg)
Shape the world with WorldMapConfig — or describe it and let an LLM fill the config:
from mapwright import WorldMapConfig, RegionalTerrainGenerator, SeededRNG
desert = WorldMapConfig.preset("desert") # ready-made worlds...
custom = WorldMapConfig(continents=7, sea_level=0.55, temperature=-0.8) # ...or tune
world = RegionalTerrainGenerator(SeededRNG(1)).generate(60, 40, config=desert)
# Every field is a bounded scalar with a clear meaning, so it doubles as a schema
# a host app (or an LLM) can populate. from_dict clamps junk to valid ranges:
WorldMapConfig.from_dict({"temperature": 5, "continents": -3}) # -> safe, clamped
Presets: continent, pangaea, archipelago, islands, highlands, desert,
arctic, tropical.
Save and reload worlds (and dungeons) — JSON round-trips losslessly, so a reloaded world renders byte-identically:
from mapwright import RegionalTerrainGenerator, SeededRNG, TerrainResult
terrain = RegionalTerrainGenerator(SeededRNG(7)).generate(60, 40)
open("world.json", "w").write(terrain.to_json()) # ...later...
same = TerrainResult.from_json(open("world.json").read()) # bit-identical
to_dict/from_dict (and to_json/from_json) are available on TerrainResult,
Dungeon, and Marker. Numpy rasters and full-precision floats are preserved.
Procedural place-names in several culture styles:
from mapwright import SeededRNG, NameGenerator
namer = NameGenerator(SeededRNG(7))
namer.settlement("nordic") # -> 'Eirmundheim'
namer.settlement("elvish") # -> 'Faelynnwood'
namer.region("dwarvish") # -> 'The Korvald Reach'
Generate a dungeon and render it:
from mapwright import SeededRNG, DungeonGenerator, DungeonSVGRenderer
dungeon = DungeonGenerator(SeededRNG(3)).generate(48, 32)
svg = DungeonSVGRenderer().render(dungeon, labels=True) # number the rooms
open("dungeon.svg", "w").write(svg)
print(dungeon.ascii()) # or eyeball it as text
Generate a town — an organic footprint split into named wards, each subdivided
into building lots, threaded with streets, and optionally walled (try the
port and citadel presets):
from mapwright import SeededRNG, SettlementGenerator, SettlementConfig, SettlementSVGRenderer
town = SettlementGenerator(SeededRNG(7)).generate(90, 90)
port = SettlementGenerator(SeededRNG(5)).generate(90, 90, SettlementConfig.preset("port"))
citadel = SettlementGenerator(SeededRNG(3)).generate(90, 90, SettlementConfig.preset("citadel"))
open("town.svg", "w").write(SettlementSVGRenderer().render(town))
Settlement presets: hamlet, village, town, city, port, citadel.
What's inside
| Component | What it does |
|---|---|
SeededRNG |
One seed drives everything; .derive(label) yields independent, reproducible sub-streams (unifies stdlib + numpy). |
NameGenerator |
Order-k character Markov names over hand-authored culture namebases; reproducible across processes. |
RegionalTerrainGenerator |
Voronoi cells (Lloyd-relaxed) → heightmap → Planchon–Darboux depression fill → flux + hydraulic/creep erosion → rivers + inland lakes → latitude/elevation climate with rain-shadow → Whittaker biomes. |
compute_cell_polygons |
Reconstructs convex Voronoi polygons (half-plane clipping) for vector rendering. |
RegionalSVGRenderer |
Shaded-relief (hillshade) SVG: biome polygons, coastline, rivers, roads, labelled markers. |
RegionalRoadGenerator |
Connects settlement sites with trade routes — an MST whose edges are A*-routed over the terrain (avoids sea, climbs/crosses rivers at a cost). |
RegionGenerator |
Partitions land into named factions/territories: spread capitals seed a flood fill over the land graph (sea divides them); each Region is Markov-named. |
DungeonGenerator |
BSP-partitioned rooms + minimum-spanning-tree corridors → rooms, corridor cells, and a walkable grid (with Dungeon.ascii()). |
DungeonSVGRenderer |
Renders a Dungeon to SVG: walls, carved floor, room outlines, optional tile grid and per-room labels. |
SettlementGenerator |
Self-contained town layout: an organic footprint divided into named Voronoi wards (market, docks, …), each subdivided into building lots, a street network (MST over ward adjacency + main roads from gates to the market), an optional defensive wall (towers + gate gaps, opened at the harbour when coastal), and optional coastline. |
SettlementSVGRenderer |
Renders a Settlement to SVG: sea, footprint, kind-coloured wards, building lots, streets, wall with towers/gatehouses, labels. |
Everything is neutral: RegionalTerrainGenerator returns a TerrainResult of TerrainCells
(each with a Biome), and you decide how a Biome maps to your world.
Determinism
Every generator draws from a SeededRNG. The same seed (and parameters) reproduces an
identical world — terrain, names, rivers, and SVG — across runs and across processes
(the Markov chains are built in sorted order, so output never depends on PYTHONHASHSEED).
Performance
Pure Python + numpy, single-threaded. Typical map/town sizes generate in well under a
second; examples/benchmark.py prints a table for your machine. Rough figures (numbers
are machine-dependent):
| Generator | Size | Time |
|---|---|---|
| Terrain | 64×44 (≈470 cells) | ~150 ms |
| Terrain | 120×90 (1500 cells, capped) | ~1.8 s |
| Dungeon | 80×60 (≈50 rooms) | ~9 ms |
| Settlement | pop 9000 (50 wards, ~1100 lots) | ~65 ms |
| Roads / regions | on a 120×90 map | a few ms |
Two things worth knowing:
- Terrain cell count is capped at 1500 (
cell_areaclamp ingenerate), which bounds the hydrology/climate/graph work — but the initial Voronoi rasterisation is per-pixel, so total time still grows roughly linearly withwidth × heighton large maps. Raisecell_area(fewer, coarser cells) to trade detail for speed, e.g.generate(w, h, cell_area=12). - Dungeon corridor connection is a dense MST (~O(rooms³)), so dungeons with hundreds of
rooms get slow — keep them modest or raise
DungeonConfig.min_leaffor fewer, larger rooms.
API stability & contract
The public API is exactly the names exported in mapwright.__all__ — that's
the contract. It's pinned by tests/test_api_contract.py (public surface, key
signatures), so an accidental breaking change fails CI.
For the world parameters specifically, WorldMapConfig.json_schema() returns a
JSON Schema (draft 2020-12) — the machine-readable contract a host app or LLM can
validate/generate against, then feed through WorldMapConfig.from_dict() (which
clamps to valid ranges). Schema and runtime clamping are generated from the same
field spec, so they can't drift.
Versioning follows SemVer. While at 0.x the API may still
change between minor versions; every change is recorded in CHANGELOG.md. Pin a
tag or commit if you depend on it.
Development
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest
Credits & license
MIT licensed (see LICENSE). Algorithms were implemented clean-room from the publicly
described techniques of Azgaar's Fantasy-Map-Generator (MIT) and Martin O'Leary /
Ryan L. Guy's FantasyMapGenerator (Zlib); see NOTICE for details. The bundled name
lists are original.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mapwright-0.10.0.tar.gz.
File metadata
- Download URL: mapwright-0.10.0.tar.gz
- Upload date:
- Size: 1.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
38f4d06c9e690f65d70c0bd9b5ea252763c0f31ecd89efa3e1e0ebae26408ffc
|
|
| MD5 |
e6a84739e923b3d4a5ade64b8790f94e
|
|
| BLAKE2b-256 |
31b7e2d398c2b9ebd93ed8f28b2e67a246af7897257ee0cc02107ec8470d52fd
|
Provenance
The following attestation bundles were made for mapwright-0.10.0.tar.gz:
Publisher:
publish.yml on sligara7/mapwright
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mapwright-0.10.0.tar.gz -
Subject digest:
38f4d06c9e690f65d70c0bd9b5ea252763c0f31ecd89efa3e1e0ebae26408ffc - Sigstore transparency entry: 1703931813
- Sigstore integration time:
-
Permalink:
sligara7/mapwright@51a0f0a8fed209d76fb3d403cb31d5dbe60fce40 -
Branch / Tag:
refs/tags/v0.10.0 - Owner: https://github.com/sligara7
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@51a0f0a8fed209d76fb3d403cb31d5dbe60fce40 -
Trigger Event:
release
-
Statement type:
File details
Details for the file mapwright-0.10.0-py3-none-any.whl.
File metadata
- Download URL: mapwright-0.10.0-py3-none-any.whl
- Upload date:
- Size: 61.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7966265761cfa63b72d6877ccf6bded320d6e5206a9392d600465b131f75d78f
|
|
| MD5 |
8f60624b7613e3aee39be212626fa831
|
|
| BLAKE2b-256 |
f0ebbc4cad5e2d7df2b9cc33328375555e3e153250e0db64937f774137b6d087
|
Provenance
The following attestation bundles were made for mapwright-0.10.0-py3-none-any.whl:
Publisher:
publish.yml on sligara7/mapwright
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mapwright-0.10.0-py3-none-any.whl -
Subject digest:
7966265761cfa63b72d6877ccf6bded320d6e5206a9392d600465b131f75d78f - Sigstore transparency entry: 1703931827
- Sigstore integration time:
-
Permalink:
sligara7/mapwright@51a0f0a8fed209d76fb3d403cb31d5dbe60fce40 -
Branch / Tag:
refs/tags/v0.10.0 - Owner: https://github.com/sligara7
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@51a0f0a8fed209d76fb3d403cb31d5dbe60fce40 -
Trigger Event:
release
-
Statement type: