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.
Files changed (72) hide show
  1. {mapwright-0.2.0 → mapwright-0.11.0}/.github/workflows/ci.yml +2 -2
  2. {mapwright-0.2.0 → mapwright-0.11.0}/.github/workflows/publish.yml +4 -4
  3. {mapwright-0.2.0 → mapwright-0.11.0}/.gitignore +3 -0
  4. mapwright-0.11.0/CHANGELOG.md +197 -0
  5. {mapwright-0.2.0 → mapwright-0.11.0}/NOTICE +7 -0
  6. mapwright-0.11.0/PKG-INFO +244 -0
  7. mapwright-0.11.0/README.md +220 -0
  8. mapwright-0.11.0/docs/gallery/archipelago.png +0 -0
  9. mapwright-0.11.0/docs/gallery/archipelago.svg +1 -0
  10. mapwright-0.11.0/docs/gallery/arctic.png +0 -0
  11. mapwright-0.11.0/docs/gallery/arctic.svg +1 -0
  12. mapwright-0.11.0/docs/gallery/citadel.png +0 -0
  13. mapwright-0.11.0/docs/gallery/citadel.svg +1 -0
  14. mapwright-0.11.0/docs/gallery/continent.png +0 -0
  15. mapwright-0.11.0/docs/gallery/continent.svg +1 -0
  16. mapwright-0.11.0/docs/gallery/desert.png +0 -0
  17. mapwright-0.11.0/docs/gallery/desert.svg +1 -0
  18. mapwright-0.11.0/docs/gallery/dungeon.png +0 -0
  19. mapwright-0.11.0/docs/gallery/dungeon.svg +1 -0
  20. mapwright-0.11.0/docs/gallery/highlands.png +0 -0
  21. mapwright-0.11.0/docs/gallery/highlands.svg +1 -0
  22. mapwright-0.11.0/docs/gallery/islands.png +0 -0
  23. mapwright-0.11.0/docs/gallery/islands.svg +1 -0
  24. mapwright-0.11.0/docs/gallery/pangaea.png +0 -0
  25. mapwright-0.11.0/docs/gallery/pangaea.svg +1 -0
  26. mapwright-0.11.0/docs/gallery/port.png +0 -0
  27. mapwright-0.11.0/docs/gallery/port.svg +1 -0
  28. mapwright-0.11.0/docs/gallery/regions.png +0 -0
  29. mapwright-0.11.0/docs/gallery/regions.svg +1 -0
  30. mapwright-0.11.0/docs/gallery/roads.png +0 -0
  31. mapwright-0.11.0/docs/gallery/roads.svg +1 -0
  32. mapwright-0.11.0/docs/gallery/town.png +0 -0
  33. mapwright-0.11.0/docs/gallery/town.svg +1 -0
  34. mapwright-0.11.0/docs/gallery/tropical.png +0 -0
  35. mapwright-0.11.0/docs/gallery/tropical.svg +1 -0
  36. mapwright-0.11.0/examples/benchmark.py +79 -0
  37. mapwright-0.11.0/examples/gallery.py +134 -0
  38. {mapwright-0.2.0 → mapwright-0.11.0}/pyproject.toml +2 -2
  39. {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/__init__.py +29 -1
  40. mapwright-0.11.0/src/mapwright/_geometry.py +280 -0
  41. mapwright-0.11.0/src/mapwright/_graph.py +78 -0
  42. mapwright-0.11.0/src/mapwright/_serde.py +41 -0
  43. {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/config.py +5 -1
  44. {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/dungeon.py +44 -15
  45. mapwright-0.11.0/src/mapwright/dungeon_renderer.py +162 -0
  46. mapwright-0.11.0/src/mapwright/regions.py +130 -0
  47. mapwright-0.11.0/src/mapwright/roads.py +109 -0
  48. mapwright-0.11.0/src/mapwright/settlement.py +758 -0
  49. mapwright-0.11.0/src/mapwright/settlement_renderer.py +270 -0
  50. {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/svg_renderer.py +115 -3
  51. {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/terrain.py +148 -120
  52. {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_api_contract.py +57 -0
  53. mapwright-0.11.0/tests/test_dungeon_renderer.py +84 -0
  54. mapwright-0.11.0/tests/test_geometry.py +176 -0
  55. mapwright-0.11.0/tests/test_graph.py +99 -0
  56. mapwright-0.11.0/tests/test_properties.py +191 -0
  57. mapwright-0.11.0/tests/test_regions.py +104 -0
  58. mapwright-0.11.0/tests/test_roads.py +80 -0
  59. mapwright-0.11.0/tests/test_serialize.py +143 -0
  60. mapwright-0.11.0/tests/test_settlement.py +396 -0
  61. {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_terrain.py +49 -1
  62. mapwright-0.2.0/CHANGELOG.md +0 -42
  63. mapwright-0.2.0/PKG-INFO +0 -136
  64. mapwright-0.2.0/README.md +0 -113
  65. {mapwright-0.2.0 → mapwright-0.11.0}/LICENSE +0 -0
  66. {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/names.py +0 -0
  67. {mapwright-0.2.0 → mapwright-0.11.0}/src/mapwright/rng.py +0 -0
  68. {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_config.py +0 -0
  69. {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_dungeon.py +0 -0
  70. {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_names.py +0 -0
  71. {mapwright-0.2.0 → mapwright-0.11.0}/tests/test_rng.py +0 -0
  72. {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@v4
17
- - uses: actions/setup-python@v5
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@v4
18
- - uses: actions/setup-python@v5
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@v4
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@v4
38
+ - uses: actions/download-artifact@v8
39
39
  with:
40
40
  name: dist
41
41
  path: dist/
@@ -25,3 +25,6 @@ htmlcov/
25
25
  # Generated preview artifacts
26
26
  *.preview.svg
27
27
  *.preview.png
28
+
29
+ # Local working notes (roadmap, scratch)
30
+ *.local.md
@@ -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.