mapwright 0.2.0__tar.gz → 0.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mapwright-0.2.0 → mapwright-0.11.0}/.github/workflows/ci.yml +2 -2
- {mapwright-0.2.0 → mapwright-0.11.0}/.github/workflows/publish.yml +4 -4
- {mapwright-0.2.0 → mapwright-0.11.0}/.gitignore +3 -0
- mapwright-0.11.0/CHANGELOG.md +197 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/NOTICE +7 -0
- mapwright-0.11.0/PKG-INFO +244 -0
- mapwright-0.11.0/README.md +220 -0
- mapwright-0.11.0/docs/gallery/archipelago.png +0 -0
- mapwright-0.11.0/docs/gallery/archipelago.svg +1 -0
- mapwright-0.11.0/docs/gallery/arctic.png +0 -0
- mapwright-0.11.0/docs/gallery/arctic.svg +1 -0
- mapwright-0.11.0/docs/gallery/citadel.png +0 -0
- mapwright-0.11.0/docs/gallery/citadel.svg +1 -0
- mapwright-0.11.0/docs/gallery/continent.png +0 -0
- mapwright-0.11.0/docs/gallery/continent.svg +1 -0
- mapwright-0.11.0/docs/gallery/desert.png +0 -0
- mapwright-0.11.0/docs/gallery/desert.svg +1 -0
- mapwright-0.11.0/docs/gallery/dungeon.png +0 -0
- mapwright-0.11.0/docs/gallery/dungeon.svg +1 -0
- mapwright-0.11.0/docs/gallery/highlands.png +0 -0
- mapwright-0.11.0/docs/gallery/highlands.svg +1 -0
- mapwright-0.11.0/docs/gallery/islands.png +0 -0
- mapwright-0.11.0/docs/gallery/islands.svg +1 -0
- mapwright-0.11.0/docs/gallery/pangaea.png +0 -0
- mapwright-0.11.0/docs/gallery/pangaea.svg +1 -0
- mapwright-0.11.0/docs/gallery/port.png +0 -0
- mapwright-0.11.0/docs/gallery/port.svg +1 -0
- mapwright-0.11.0/docs/gallery/regions.png +0 -0
- mapwright-0.11.0/docs/gallery/regions.svg +1 -0
- mapwright-0.11.0/docs/gallery/roads.png +0 -0
- mapwright-0.11.0/docs/gallery/roads.svg +1 -0
- mapwright-0.11.0/docs/gallery/town.png +0 -0
- mapwright-0.11.0/docs/gallery/town.svg +1 -0
- mapwright-0.11.0/docs/gallery/tropical.png +0 -0
- mapwright-0.11.0/docs/gallery/tropical.svg +1 -0
- mapwright-0.11.0/examples/benchmark.py +79 -0
- mapwright-0.11.0/examples/gallery.py +134 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/pyproject.toml +2 -2
- {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/__init__.py +29 -1
- mapwright-0.11.0/src/mapwright/_geometry.py +280 -0
- mapwright-0.11.0/src/mapwright/_graph.py +78 -0
- mapwright-0.11.0/src/mapwright/_serde.py +41 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/config.py +5 -1
- {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/dungeon.py +44 -15
- mapwright-0.11.0/src/mapwright/dungeon_renderer.py +162 -0
- mapwright-0.11.0/src/mapwright/regions.py +130 -0
- mapwright-0.11.0/src/mapwright/roads.py +109 -0
- mapwright-0.11.0/src/mapwright/settlement.py +758 -0
- mapwright-0.11.0/src/mapwright/settlement_renderer.py +270 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/svg_renderer.py +115 -3
- {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/terrain.py +148 -120
- {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_api_contract.py +57 -0
- mapwright-0.11.0/tests/test_dungeon_renderer.py +84 -0
- mapwright-0.11.0/tests/test_geometry.py +176 -0
- mapwright-0.11.0/tests/test_graph.py +99 -0
- mapwright-0.11.0/tests/test_properties.py +191 -0
- mapwright-0.11.0/tests/test_regions.py +104 -0
- mapwright-0.11.0/tests/test_roads.py +80 -0
- mapwright-0.11.0/tests/test_serialize.py +143 -0
- mapwright-0.11.0/tests/test_settlement.py +396 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_terrain.py +49 -1
- mapwright-0.2.0/CHANGELOG.md +0 -42
- mapwright-0.2.0/PKG-INFO +0 -136
- mapwright-0.2.0/README.md +0 -113
- {mapwright-0.2.0 → mapwright-0.11.0}/LICENSE +0 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/names.py +0 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/rng.py +0 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_config.py +0 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_dungeon.py +0 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_names.py +0 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_rng.py +0 -0
- {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_svg_renderer.py +0 -0
|
@@ -13,8 +13,8 @@ jobs:
|
|
|
13
13
|
matrix:
|
|
14
14
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
17
|
-
- uses: actions/setup-python@
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
18
18
|
with:
|
|
19
19
|
python-version: ${{ matrix.python-version }}
|
|
20
20
|
- name: Install
|
|
@@ -14,15 +14,15 @@ jobs:
|
|
|
14
14
|
build:
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
18
|
-
- uses: actions/setup-python@
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
- uses: actions/setup-python@v6
|
|
19
19
|
with:
|
|
20
20
|
python-version: "3.12"
|
|
21
21
|
- name: Build sdist + wheel
|
|
22
22
|
run: |
|
|
23
23
|
python -m pip install --upgrade build
|
|
24
24
|
python -m build
|
|
25
|
-
- uses: actions/upload-artifact@
|
|
25
|
+
- uses: actions/upload-artifact@v7
|
|
26
26
|
with:
|
|
27
27
|
name: dist
|
|
28
28
|
path: dist/
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
id-token: write # REQUIRED for Trusted Publishing (OIDC)
|
|
36
36
|
contents: read
|
|
37
37
|
steps:
|
|
38
|
-
- uses: actions/download-artifact@
|
|
38
|
+
- uses: actions/download-artifact@v8
|
|
39
39
|
with:
|
|
40
40
|
name: dist
|
|
41
41
|
path: dist/
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to mapwright are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and the project aims to follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
**Public API** = the names exported in `mapwright.__all__` (pinned by
|
|
8
|
+
`tests/test_api_contract.py`). While the version is `0.x`, minor versions may
|
|
9
|
+
make breaking changes; these will always be noted here.
|
|
10
|
+
|
|
11
|
+
## [0.11.0] — 2026-06-02
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Settlement footprints are no longer circular.** Town outlines are now
|
|
15
|
+
elongated along a random axis, lopsided via low-frequency radial lobes, and
|
|
16
|
+
rotated — a clearly organic (but still convex, so lots/streets/walls stay valid)
|
|
17
|
+
shape instead of a near-perfect disk. Regenerated the town/port/citadel gallery.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Property-based tests (Hypothesis, a new dev dependency) over config clamping,
|
|
21
|
+
the geometry/graph primitives, and generation round-trips. Test-only — no
|
|
22
|
+
change to the public API.
|
|
23
|
+
- `examples/benchmark.py` — micro-benchmarks for the generators, and a
|
|
24
|
+
**Performance** section in the README documenting the 1500-cell terrain cap,
|
|
25
|
+
the per-pixel rasterisation cost on large maps, and the dungeon MST scaling.
|
|
26
|
+
|
|
27
|
+
## [0.10.0] — 2026-06-02
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **Region / faction assignment** — `RegionGenerator.generate(terrain, count=…)`
|
|
31
|
+
partitions the land into named territories: well-spread capital cells
|
|
32
|
+
(farthest-point sampling) seed a multi-source flood fill over the land-cell
|
|
33
|
+
graph, so each reachable land cell joins its nearest capital's region (the sea
|
|
34
|
+
divides them). Returns `Region` objects (name, capital cell, member cells);
|
|
35
|
+
region names come from the Markov `NameGenerator`. `RegionalSVGRenderer.render(
|
|
36
|
+
..., regions=…)` draws political borders and italic region labels.
|
|
37
|
+
|
|
38
|
+
## [0.9.0] — 2026-06-02
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- **Regional roads / trade routes** — `RegionalRoadGenerator.generate(terrain, sites)`
|
|
42
|
+
connects settlement sites with a road network: a minimum spanning tree over the
|
|
43
|
+
sites whose edges are A*-routed over the terrain cell graph, preferring flat land
|
|
44
|
+
and paying penalties for sea, lakes, rivers (bridges), and uphill slope. Returns
|
|
45
|
+
`Road` objects (lists of cell ids); `RegionalSVGRenderer.render(..., roads=…)`
|
|
46
|
+
draws them as dashed routes with a pale casing.
|
|
47
|
+
- **Generic A* (`_graph.astar`)** — an internal shortest-path primitive over an
|
|
48
|
+
arbitrary graph (neighbour + cost + heuristic callbacks), alongside `prim_mst`.
|
|
49
|
+
|
|
50
|
+
## [0.8.0] — 2026-06-01
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- **Inland lakes** — `RegionalTerrainGenerator` now flags interior hollows (land
|
|
54
|
+
cells lower than most neighbours that collect real flux) as lakes: a new
|
|
55
|
+
`Biome.LAKE`, an `is_lake` flag on `TerrainCell`, and a bounded
|
|
56
|
+
`WorldMapConfig.lake_density` knob. Lakes seed the moisture model and rivers
|
|
57
|
+
terminate into them; `RegionalSVGRenderer` draws them as flat freshwater.
|
|
58
|
+
- **Rain-shadow climate** — moisture is now attenuated on the lee of high terrain.
|
|
59
|
+
A prevailing wind is swept upwind→downwind over the cell graph; air rising over
|
|
60
|
+
mountains precipitates (wet windward slopes) and arrives dry on the far side, so
|
|
61
|
+
windward/leeward biome contrast emerges naturally.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- `TerrainCell` gains an `is_lake` field and `TerrainResult` serialisation bumps to
|
|
65
|
+
`mapwright/terrain@2`; older `@1` payloads still load (`is_lake` defaults False).
|
|
66
|
+
`WorldMapConfig` gains `lake_density` (old payloads use its default). `Biome` adds
|
|
67
|
+
`LAKE = 12`. The `desert` preset sets a low `lake_density` for aridity.
|
|
68
|
+
|
|
69
|
+
## [0.7.0] — 2026-06-01
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
- **Settlement walls** — a defensive wall when `walled=True`, completing the
|
|
73
|
+
settlement tier. `Wall` (ring, closed, gates) is added to the public surface
|
|
74
|
+
and `Settlement.wall` holds it. The wall follows the footprint perimeter with a
|
|
75
|
+
tower at each corner and gate gaps where the main roads exit; on a coastal town
|
|
76
|
+
the ring is opened along the coast (a harbour, no wall over water).
|
|
77
|
+
`SettlementSVGRenderer` draws the wall, round corner towers, and square
|
|
78
|
+
gatehouses at the gates (replacing the previous heavier-boundary placeholder).
|
|
79
|
+
Added a walled `citadel` to the gallery.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- `Settlement` serialisation tag bumped to `mapwright/settlement@4` (adds
|
|
83
|
+
`wall`); older payloads without it still load (`wall` defaults to `None`).
|
|
84
|
+
|
|
85
|
+
## [0.6.0] — 2026-06-01
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- **Settlement streets** — a road network over the wards. `Street` (path, kind)
|
|
89
|
+
is added to the public surface; `Settlement` gains `streets` and `gates`.
|
|
90
|
+
Wards are connected by a minimum-spanning-tree (the shared `prim_mst`) over
|
|
91
|
+
ward adjacency — detected from shared polygon edges — plus a few loop roads;
|
|
92
|
+
each minor street runs through the two ward centres via their shared-edge
|
|
93
|
+
midpoint. A few gates are placed around the footprint (plus a harbour gate on
|
|
94
|
+
the coast when `coastal=True`), and `"main"` roads connect each gate to the
|
|
95
|
+
market. `SettlementSVGRenderer` overlays the network (casing + pale surface,
|
|
96
|
+
main roads wider) with a `show_streets` toggle.
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
- `Settlement` serialisation tag bumped to `mapwright/settlement@3` (adds
|
|
100
|
+
`streets` and `gates`); older payloads without those keys still load (they
|
|
101
|
+
default to empty).
|
|
102
|
+
|
|
103
|
+
## [0.5.0] — 2026-06-01
|
|
104
|
+
|
|
105
|
+
### Added
|
|
106
|
+
- **Settlement lots** — wards are now subdivided into building plots. `Lot`
|
|
107
|
+
(id, polygon, ward) is added to the public surface and `Settlement.lots` lists
|
|
108
|
+
them; `SettlementSVGRenderer` draws the building footprints (toggle with
|
|
109
|
+
`show_lots`). Each buildable ward is recursively bisected across its longest
|
|
110
|
+
axis down to a target plot area — a new bounded `SettlementConfig.lot_size`
|
|
111
|
+
knob — with per-kind sizing (noble = large estates, slums = cramped, market =
|
|
112
|
+
an open square with no buildings); each plot is inset so gaps read as alleys.
|
|
113
|
+
New shared geometry primitives: `polygon_area`, `inset_convex`.
|
|
114
|
+
|
|
115
|
+
### Changed
|
|
116
|
+
- `Settlement` serialisation tag bumped to `mapwright/settlement@2` (adds
|
|
117
|
+
`lots`); older `@1` payloads without a `lots` key still load (lots default to
|
|
118
|
+
empty), and `SettlementConfig` gains `lot_size` (old payloads use its default).
|
|
119
|
+
|
|
120
|
+
## [0.4.0] — 2026-06-01
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
- **Settlement tier (wards)** — `SettlementGenerator` → `Settlement` (+ `SettlementConfig`,
|
|
124
|
+
`Ward`, `SETTLEMENT_PRESETS`, and `SettlementSVGRenderer`). Self-contained town
|
|
125
|
+
*layout*: an organic convex footprint divided into named Voronoi wards (a central
|
|
126
|
+
market, residential/craftsmen/temple/noble/garrison/slums mix, plus a dockside ward
|
|
127
|
+
and synthetic coastline when `coastal=True`); `walled` is recorded for the upcoming
|
|
128
|
+
wall layer. Config follows the `WorldMapConfig` discipline (bounded knobs + boolean
|
|
129
|
+
flags, `from_dict` clamping, `json_schema()`, presets) and the result round-trips
|
|
130
|
+
via `to_dict`/`from_dict`/`to_json`/`from_json`. Clean-room from the *ideas* of
|
|
131
|
+
Watabou's TownGeneratorOS (GPLv3) — concept only, no code; see NOTICE.
|
|
132
|
+
_This is the first slice of the tier; lots, streets, and walls follow in later
|
|
133
|
+
versions, and the `Settlement`/`Ward` shapes will grow accordingly._
|
|
134
|
+
|
|
135
|
+
### Changed
|
|
136
|
+
- Internal refactor: extracted the shared Voronoi/Lloyd, half-plane polygon
|
|
137
|
+
clipping, and Prim-MST primitives into `_geometry.py` / `_graph.py`; the terrain
|
|
138
|
+
and dungeon tiers now delegate to them (no behaviour change — same seeds produce
|
|
139
|
+
byte-identical output). New tiers build on these instead of re-implementing them.
|
|
140
|
+
|
|
141
|
+
## [0.3.0] — 2026-06-01
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
- **Dungeon SVG renderer** — `DungeonSVGRenderer.render(dungeon, …)` turns a
|
|
145
|
+
`Dungeon` into a scalable SVG (dark walls, carved floor from the walkable grid,
|
|
146
|
+
room outlines, optional faint tile grid via `show_grid`, optional per-room
|
|
147
|
+
labels via `labels=True`/a sequence). Mirrors `RegionalSVGRenderer`: pure
|
|
148
|
+
string-building, no new dependency. Closes the previously ascii-only gap for
|
|
149
|
+
dungeons.
|
|
150
|
+
- **Serialisation / JSON round-trip** — `to_dict`/`from_dict` and
|
|
151
|
+
`to_json`/`from_json` on `TerrainResult`, `Dungeon`, and `Marker` (plus
|
|
152
|
+
`to_dict`/`from_dict` on the nested `TerrainCell`, `River`, and `Rect`). A saved
|
|
153
|
+
world or dungeon reloads bit-identically — numpy rasters (`cell_of`, dungeon
|
|
154
|
+
`grid`) and full-precision floats are preserved, so a reloaded world renders to
|
|
155
|
+
byte-identical SVG. Payloads carry a `schema` tag and ignore unknown keys on
|
|
156
|
+
load (forward-compatible). No new dependency; pure-JSON builtins only.
|
|
157
|
+
|
|
158
|
+
## [0.2.0] — 2026-06-01
|
|
159
|
+
|
|
160
|
+
### Added
|
|
161
|
+
- **Dungeon generation** — `DungeonGenerator` → `Dungeon` (+ `DungeonConfig`, `Rect`):
|
|
162
|
+
BSP space-partitioning rooms (no overlap) connected by a Prim minimum-spanning-tree
|
|
163
|
+
of L-corridors, plus optional loop corridors. Returns rooms, carved corridor cells,
|
|
164
|
+
and a boolean walkable grid; `Dungeon.ascii()` for quick previews. Clean-room from
|
|
165
|
+
Dungeon-Generator (MIT, BSP) and donjuan (CC0, MST connectivity).
|
|
166
|
+
|
|
167
|
+
## [0.1.0] — 2026-06-01
|
|
168
|
+
|
|
169
|
+
Initial release. Domain-neutral procedural fantasy map & world generation.
|
|
170
|
+
|
|
171
|
+
### Added
|
|
172
|
+
- `SeededRNG` — one-seed determinism with `.derive(label)` sub-streams; unifies
|
|
173
|
+
the stdlib and numpy generators. Reproducible across processes.
|
|
174
|
+
- `NameGenerator` / `MarkovNameGenerator` — order-k Markov place/person names
|
|
175
|
+
over hand-authored culture namebases (`NAMEBASES`); hash-seed independent.
|
|
176
|
+
- `RegionalTerrainGenerator` → `TerrainResult` — Voronoi cells (Lloyd-relaxed),
|
|
177
|
+
Planchon–Darboux depression fill, hydraulic + creep erosion, river tracing,
|
|
178
|
+
latitude/elevation climate, and a Whittaker `Biome` matrix.
|
|
179
|
+
- `WorldMapConfig` — bounded, documented world parameters (sea level, continents,
|
|
180
|
+
climate, mountains, rivers) with `from_dict` clamping, named `PRESETS`, and a
|
|
181
|
+
`json_schema()` contract for host/LLM population.
|
|
182
|
+
- `RegionalSVGRenderer` + `Marker` — shaded-relief (hillshade) SVG: biome
|
|
183
|
+
polygons, coastline, rivers, labelled markers. `compute_cell_polygons` rebuilds
|
|
184
|
+
convex Voronoi polygons via half-plane clipping.
|
|
185
|
+
|
|
186
|
+
[Unreleased]: https://github.com/sligara7/mapwright/compare/v0.11.0...HEAD
|
|
187
|
+
[0.11.0]: https://github.com/sligara7/mapwright/compare/v0.10.0...v0.11.0
|
|
188
|
+
[0.10.0]: https://github.com/sligara7/mapwright/compare/v0.9.0...v0.10.0
|
|
189
|
+
[0.9.0]: https://github.com/sligara7/mapwright/compare/v0.8.0...v0.9.0
|
|
190
|
+
[0.8.0]: https://github.com/sligara7/mapwright/compare/v0.7.0...v0.8.0
|
|
191
|
+
[0.7.0]: https://github.com/sligara7/mapwright/compare/v0.6.0...v0.7.0
|
|
192
|
+
[0.6.0]: https://github.com/sligara7/mapwright/compare/v0.5.0...v0.6.0
|
|
193
|
+
[0.5.0]: https://github.com/sligara7/mapwright/compare/v0.4.0...v0.5.0
|
|
194
|
+
[0.4.0]: https://github.com/sligara7/mapwright/compare/v0.3.0...v0.4.0
|
|
195
|
+
[0.3.0]: https://github.com/sligara7/mapwright/compare/v0.2.0...v0.3.0
|
|
196
|
+
[0.2.0]: https://github.com/sligara7/mapwright/compare/v0.1.0...v0.2.0
|
|
197
|
+
[0.1.0]: https://github.com/sligara7/mapwright/releases/tag/v0.1.0
|
|
@@ -24,5 +24,12 @@ copied from them; this NOTICE credits the lineage of the techniques.
|
|
|
24
24
|
slope^2), Planchon-Darboux depression filling for guaranteed drainage,
|
|
25
25
|
slope-normal hillshade ("shaded relief") rendering.
|
|
26
26
|
|
|
27
|
+
* Watabou's TownGeneratorOS (GPLv3)
|
|
28
|
+
https://github.com/watabou/TownGeneratorOS
|
|
29
|
+
Concept only (NO code derived — GPLv3 is incompatible with this MIT
|
|
30
|
+
project): the high-level idea of generating a town as an organic footprint
|
|
31
|
+
subdivided into Voronoi wards. mapwright's settlement code is an independent
|
|
32
|
+
clean-room implementation built on its own geometry primitives.
|
|
33
|
+
|
|
27
34
|
The name lists ("namebases") bundled with mapwright are original, hand-authored
|
|
28
35
|
data, not derived from any third-party generator.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mapwright
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: Domain-neutral procedural fantasy map & world generation: Voronoi terrain, hydraulic erosion, biomes, rivers, Markov place-names, and shaded-relief SVG.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sligara7/mapwright
|
|
6
|
+
Project-URL: Repository, https://github.com/sligara7/mapwright
|
|
7
|
+
Author: Anthony Sligar
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: NOTICE
|
|
11
|
+
Keywords: biomes,erosion,fantasy-map,procedural-generation,svg,terrain,ttrpg,voronoi,worldgen
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Games/Entertainment :: Role-Playing
|
|
16
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: numpy>=1.26
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# mapwright
|
|
26
|
+
|
|
27
|
+
> ⚠️ **Early development (v0.x, alpha).** The API is still moving and may change without
|
|
28
|
+
> notice between versions. Usable today, but pin a version (e.g. `mapwright==0.10.0`) if
|
|
29
|
+
> you depend on it.
|
|
30
|
+
|
|
31
|
+
**Domain-neutral procedural fantasy map & world generation** — Voronoi terrain with
|
|
32
|
+
hydraulic erosion, climate-driven biomes, rivers, Markov place-names, and shaded-relief
|
|
33
|
+
SVG rendering. Pure Python, `numpy`-only, fully seed-deterministic.
|
|
34
|
+
|
|
35
|
+
mapwright produces *neutral data* (cells, biomes, rivers, polygons) and a self-contained
|
|
36
|
+
SVG renderer. It has no opinion about your application's models — map its output onto your
|
|
37
|
+
own tiles/entities however you like.
|
|
38
|
+
|
|
39
|
+
## Gallery
|
|
40
|
+
|
|
41
|
+
Every image below is a deterministic render of a built-in preset (or a dungeon),
|
|
42
|
+
produced by [`examples/gallery.py`](examples/gallery.py):
|
|
43
|
+
|
|
44
|
+
<table>
|
|
45
|
+
<tr>
|
|
46
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/continent.png" alt="continent preset"><br><sub><code>continent</code></sub></td>
|
|
47
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/archipelago.png" alt="archipelago preset"><br><sub><code>archipelago</code></sub></td>
|
|
48
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/islands.png" alt="islands preset"><br><sub><code>islands</code></sub></td>
|
|
49
|
+
</tr>
|
|
50
|
+
<tr>
|
|
51
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/highlands.png" alt="highlands preset"><br><sub><code>highlands</code></sub></td>
|
|
52
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/desert.png" alt="desert preset"><br><sub><code>desert</code></sub></td>
|
|
53
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/arctic.png" alt="arctic preset"><br><sub><code>arctic</code></sub></td>
|
|
54
|
+
</tr>
|
|
55
|
+
<tr>
|
|
56
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/pangaea.png" alt="pangaea preset"><br><sub><code>pangaea</code></sub></td>
|
|
57
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/tropical.png" alt="tropical preset"><br><sub><code>tropical</code></sub></td>
|
|
58
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/dungeon.png" alt="generated dungeon"><br><sub><code>DungeonGenerator</code></sub></td>
|
|
59
|
+
</tr>
|
|
60
|
+
<tr>
|
|
61
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/town.png" alt="generated town"><br><sub><code>SettlementGenerator</code></sub></td>
|
|
62
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/port.png" alt="generated coastal port"><br><sub><code>Settlement (port)</code></sub></td>
|
|
63
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/citadel.png" alt="generated walled citadel"><br><sub><code>Settlement (citadel)</code></sub></td>
|
|
64
|
+
</tr>
|
|
65
|
+
<tr>
|
|
66
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/roads.png" alt="settlements linked by terrain-routed roads"><br><sub><code>RegionalRoadGenerator</code></sub></td>
|
|
67
|
+
<td align="center"><img width="240" src="https://raw.githubusercontent.com/sligara7/mapwright/main/docs/gallery/regions.png" alt="land partitioned into named territories"><br><sub><code>RegionGenerator</code></sub></td>
|
|
68
|
+
<td></td>
|
|
69
|
+
</tr>
|
|
70
|
+
</table>
|
|
71
|
+
|
|
72
|
+
Regenerate them with `python examples/gallery.py` (SVGs always; PNGs when
|
|
73
|
+
`cairosvg` is installed).
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install mapwright
|
|
79
|
+
# latest from git:
|
|
80
|
+
pip install git+https://github.com/sligara7/mapwright.git
|
|
81
|
+
# or, for local development:
|
|
82
|
+
pip install -e ".[dev]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quickstart
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from mapwright import SeededRNG, RegionalTerrainGenerator, RegionalSVGRenderer, Marker
|
|
89
|
+
|
|
90
|
+
# Same seed -> same world, every time.
|
|
91
|
+
terrain = RegionalTerrainGenerator(SeededRNG(7)).generate(width=60, height=40)
|
|
92
|
+
|
|
93
|
+
markers = [Marker(name="Eldmoor", x=30, y=18, kind="settlement_city")]
|
|
94
|
+
svg = RegionalSVGRenderer().render(terrain, markers)
|
|
95
|
+
open("world.svg", "w").write(svg)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Shape the world with `WorldMapConfig` — or describe it and let an LLM fill the config:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from mapwright import WorldMapConfig, RegionalTerrainGenerator, SeededRNG
|
|
102
|
+
|
|
103
|
+
desert = WorldMapConfig.preset("desert") # ready-made worlds...
|
|
104
|
+
custom = WorldMapConfig(continents=7, sea_level=0.55, temperature=-0.8) # ...or tune
|
|
105
|
+
world = RegionalTerrainGenerator(SeededRNG(1)).generate(60, 40, config=desert)
|
|
106
|
+
|
|
107
|
+
# Every field is a bounded scalar with a clear meaning, so it doubles as a schema
|
|
108
|
+
# a host app (or an LLM) can populate. from_dict clamps junk to valid ranges:
|
|
109
|
+
WorldMapConfig.from_dict({"temperature": 5, "continents": -3}) # -> safe, clamped
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Presets: `continent`, `pangaea`, `archipelago`, `islands`, `highlands`, `desert`,
|
|
113
|
+
`arctic`, `tropical`.
|
|
114
|
+
|
|
115
|
+
Save and reload worlds (and dungeons) — JSON round-trips losslessly, so a reloaded
|
|
116
|
+
world renders byte-identically:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from mapwright import RegionalTerrainGenerator, SeededRNG, TerrainResult
|
|
120
|
+
|
|
121
|
+
terrain = RegionalTerrainGenerator(SeededRNG(7)).generate(60, 40)
|
|
122
|
+
open("world.json", "w").write(terrain.to_json()) # ...later...
|
|
123
|
+
same = TerrainResult.from_json(open("world.json").read()) # bit-identical
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`to_dict`/`from_dict` (and `to_json`/`from_json`) are available on `TerrainResult`,
|
|
127
|
+
`Dungeon`, and `Marker`. Numpy rasters and full-precision floats are preserved.
|
|
128
|
+
|
|
129
|
+
Procedural place-names in several culture styles:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from mapwright import SeededRNG, NameGenerator
|
|
133
|
+
|
|
134
|
+
namer = NameGenerator(SeededRNG(7))
|
|
135
|
+
namer.settlement("nordic") # -> 'Eirmundheim'
|
|
136
|
+
namer.settlement("elvish") # -> 'Faelynnwood'
|
|
137
|
+
namer.region("dwarvish") # -> 'The Korvald Reach'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Generate a dungeon and render it:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from mapwright import SeededRNG, DungeonGenerator, DungeonSVGRenderer
|
|
144
|
+
|
|
145
|
+
dungeon = DungeonGenerator(SeededRNG(3)).generate(48, 32)
|
|
146
|
+
svg = DungeonSVGRenderer().render(dungeon, labels=True) # number the rooms
|
|
147
|
+
open("dungeon.svg", "w").write(svg)
|
|
148
|
+
print(dungeon.ascii()) # or eyeball it as text
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Generate a town — an organic footprint split into named wards, each subdivided
|
|
152
|
+
into building lots, threaded with streets, and optionally walled (try the
|
|
153
|
+
`port` and `citadel` presets):
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from mapwright import SeededRNG, SettlementGenerator, SettlementConfig, SettlementSVGRenderer
|
|
157
|
+
|
|
158
|
+
town = SettlementGenerator(SeededRNG(7)).generate(90, 90)
|
|
159
|
+
port = SettlementGenerator(SeededRNG(5)).generate(90, 90, SettlementConfig.preset("port"))
|
|
160
|
+
citadel = SettlementGenerator(SeededRNG(3)).generate(90, 90, SettlementConfig.preset("citadel"))
|
|
161
|
+
open("town.svg", "w").write(SettlementSVGRenderer().render(town))
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Settlement presets: `hamlet`, `village`, `town`, `city`, `port`, `citadel`.
|
|
165
|
+
|
|
166
|
+
## What's inside
|
|
167
|
+
|
|
168
|
+
| Component | What it does |
|
|
169
|
+
|-----------|--------------|
|
|
170
|
+
| `SeededRNG` | One seed drives everything; `.derive(label)` yields independent, reproducible sub-streams (unifies stdlib + numpy). |
|
|
171
|
+
| `NameGenerator` | Order-k character Markov names over hand-authored culture namebases; reproducible across processes. |
|
|
172
|
+
| `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. |
|
|
173
|
+
| `compute_cell_polygons` | Reconstructs convex Voronoi polygons (half-plane clipping) for vector rendering. |
|
|
174
|
+
| `RegionalSVGRenderer` | Shaded-relief (hillshade) SVG: biome polygons, coastline, rivers, roads, labelled markers. |
|
|
175
|
+
| `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). |
|
|
176
|
+
| `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. |
|
|
177
|
+
| `DungeonGenerator` | BSP-partitioned rooms + minimum-spanning-tree corridors → rooms, corridor cells, and a walkable grid (with `Dungeon.ascii()`). |
|
|
178
|
+
| `DungeonSVGRenderer` | Renders a `Dungeon` to SVG: walls, carved floor, room outlines, optional tile grid and per-room labels. |
|
|
179
|
+
| `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. |
|
|
180
|
+
| `SettlementSVGRenderer` | Renders a `Settlement` to SVG: sea, footprint, kind-coloured wards, building lots, streets, wall with towers/gatehouses, labels. |
|
|
181
|
+
|
|
182
|
+
Everything is neutral: `RegionalTerrainGenerator` returns a `TerrainResult` of `TerrainCell`s
|
|
183
|
+
(each with a `Biome`), and you decide how a `Biome` maps to your world.
|
|
184
|
+
|
|
185
|
+
## Determinism
|
|
186
|
+
|
|
187
|
+
Every generator draws from a `SeededRNG`. The same seed (and parameters) reproduces an
|
|
188
|
+
identical world — terrain, names, rivers, and SVG — across runs *and across processes*
|
|
189
|
+
(the Markov chains are built in sorted order, so output never depends on `PYTHONHASHSEED`).
|
|
190
|
+
|
|
191
|
+
## Performance
|
|
192
|
+
|
|
193
|
+
Pure Python + numpy, single-threaded. Typical map/town sizes generate in well under a
|
|
194
|
+
second; `examples/benchmark.py` prints a table for your machine. Rough figures (numbers
|
|
195
|
+
are machine-dependent):
|
|
196
|
+
|
|
197
|
+
| Generator | Size | Time |
|
|
198
|
+
|-----------|------|------|
|
|
199
|
+
| Terrain | 64×44 (≈470 cells) | ~150 ms |
|
|
200
|
+
| Terrain | 120×90 (1500 cells, capped) | ~1.8 s |
|
|
201
|
+
| Dungeon | 80×60 (≈50 rooms) | ~9 ms |
|
|
202
|
+
| Settlement | pop 9000 (50 wards, ~1100 lots) | ~65 ms |
|
|
203
|
+
| Roads / regions | on a 120×90 map | a few ms |
|
|
204
|
+
|
|
205
|
+
Two things worth knowing:
|
|
206
|
+
|
|
207
|
+
- **Terrain cell count is capped at 1500** (`cell_area` clamp in `generate`), which bounds
|
|
208
|
+
the hydrology/climate/graph work — but the initial Voronoi *rasterisation* is per-pixel,
|
|
209
|
+
so total time still grows roughly linearly with `width × height` on large maps. Raise
|
|
210
|
+
`cell_area` (fewer, coarser cells) to trade detail for speed, e.g.
|
|
211
|
+
`generate(w, h, cell_area=12)`.
|
|
212
|
+
- **Dungeon corridor connection is a dense MST (~O(rooms³))**, so dungeons with hundreds of
|
|
213
|
+
rooms get slow — keep them modest or raise `DungeonConfig.min_leaf` for fewer, larger rooms.
|
|
214
|
+
|
|
215
|
+
## API stability & contract
|
|
216
|
+
|
|
217
|
+
The **public API is exactly the names exported in `mapwright.__all__`** — that's
|
|
218
|
+
the contract. It's pinned by `tests/test_api_contract.py` (public surface, key
|
|
219
|
+
signatures), so an accidental breaking change fails CI.
|
|
220
|
+
|
|
221
|
+
For the world parameters specifically, `WorldMapConfig.json_schema()` returns a
|
|
222
|
+
JSON Schema (draft 2020-12) — the machine-readable contract a host app or LLM can
|
|
223
|
+
validate/generate against, then feed through `WorldMapConfig.from_dict()` (which
|
|
224
|
+
clamps to valid ranges). Schema and runtime clamping are generated from the same
|
|
225
|
+
field spec, so they can't drift.
|
|
226
|
+
|
|
227
|
+
Versioning follows [SemVer](https://semver.org/). While at `0.x` the API may still
|
|
228
|
+
change between minor versions; every change is recorded in `CHANGELOG.md`. Pin a
|
|
229
|
+
tag or commit if you depend on it.
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
python -m venv .venv && . .venv/bin/activate
|
|
235
|
+
pip install -e ".[dev]"
|
|
236
|
+
pytest
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Credits & license
|
|
240
|
+
|
|
241
|
+
MIT licensed (see `LICENSE`). Algorithms were implemented clean-room from the publicly
|
|
242
|
+
described techniques of **Azgaar's Fantasy-Map-Generator** (MIT) and **Martin O'Leary /
|
|
243
|
+
Ryan L. Guy's FantasyMapGenerator** (Zlib); see `NOTICE` for details. The bundled name
|
|
244
|
+
lists are original.
|