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