# Theriapolis — Procedural World Generation ### Addendum A: Border Organics & Linear Feature Tile Rules --- ## ISSUE 1: Organic Borders — No Straight Lines ### The Problem Biome-to-biome boundaries and coastline edges render as straight lines when biome assignment is done per-cell without edge treatment. A grid of cells, each tagged with a biome, produces staircase patterns at best and ruler-straight borders at worst. Neither looks natural. ### The Rule **No border between any two biome types, and no border between land and ocean, may contain a straight segment longer than 2 tiles.** "Straight segment" is defined as: 3 or more consecutive border-edge tiles that form a line along any cardinal or diagonal axis without deviation. This rule applies to: - Biome-to-biome transitions (forest ↔ grassland, grassland ↔ mountain, etc.) - Land-to-ocean transitions (coastline) - Land-to-lake transitions - Land-to-river transitions (riverbanks) --- ### Implementation: Border Noise Injection Biome borders are NOT the raw edges of macro-cell assignments. They are post-processed using a dedicated border-shaping pass. **Step 1 — Identify Raw Borders** After biome assignment (Layer 1), scan the tile grid for every tile that is adjacent (including diagonal) to a tile of a different biome type or a water tile. Tag these tiles as BORDER tiles. **Step 2 — Generate Border Distortion Map** Create a separate high-frequency Simplex noise layer (distinct from the terrain noise). This is the **Border Noise Layer**. Parameters: ``` frequency: high (4×–8× the base terrain noise frequency) octaves: 2–3 amplitude: scaled to 1–3 tile displacement seed: world_seed + BORDER_OFFSET (constant offset to decouple from terrain) ``` The purpose of this layer: it tells each BORDER tile how far and in what direction to "push" the border from its mathematically derived position. **Step 3 — Apply Distortion to Border Tiles** For each BORDER tile: 1. Sample the Border Noise Layer at the tile's coordinates. This yields a displacement vector (dx, dy), each component ranging from -3 to +3 tiles. 2. The tile's biome assignment is re-evaluated: instead of asking "what biome does THIS cell belong to?", ask "what biome does the cell at (x + dx, y + dy) belong to?" 3. This effectively warps the border, creating irregular, organic-looking transitions. The result: borders between biomes become wavy, irregular curves with peninsulas of one biome jutting into another, concavities, and natural-looking interlock patterns. **Step 4 — Coastline-Specific Treatment** Coastlines receive an ADDITIONAL distortion pass with higher amplitude (2–5 tile displacement) and lower frequency (producing broad bays and headlands rather than jagged noise). This is layered on top of the standard border distortion. Coastline distortion parameters: ``` frequency: 2×–4× base terrain noise octaves: 3–4 (more detail than biome borders) amplitude: 2–5 tile displacement persistence: 0.5 (each octave contributes less — smooth large shapes with fine detail) ``` This produces coastlines with large-scale features (bays, peninsulas, capes) and small-scale features (coves, rocky outcrops, sandbars) simultaneously. **Step 5 — Validation Pass** After distortion, run a straight-line detector across all borders: ``` for each BORDER tile B: look at the 6 consecutive border tiles in each of the 8 directions (N, NE, E, SE, S, SW, W, NW) if 3+ consecutive tiles form a collinear segment: flag for correction ``` Flagged tiles receive a forced 1-tile perpendicular displacement (randomly left or right of the border line). This is the safety net — it should rarely trigger if the noise parameters are tuned correctly, but it guarantees the rule is never violated. --- ### Implementation: Biome Transition Zones Beyond border shape, the visual and mechanical transition between biomes should never be a hard line. Implement a **transition band** of 2–4 tiles on each side of every biome border where tiles are assigned a MIXED biome type: - Forest ↔ Grassland border: transition tiles are FOREST_EDGE (scattered trees, tall grass, mixed flora). - Grassland ↔ Mountain border: transition tiles are FOOTHILLS (rocky grassland, increasing elevation, scrub). - Forest ↔ Wetland border: transition tiles are MARSH_EDGE (muddy ground, standing water between trees). - Any biome ↔ Coastline: transition tiles are BEACH, CLIFF, TIDAL_FLAT, or MANGROVE depending on biome type and elevation. Transition bands are also distorted by the Border Noise Layer, so their width varies organically (wider in some spots, narrower in others). **Visual Rule:** A player walking from forest into grassland should cross: dense forest → thinning forest → scattered trees with grass → tall grass → open grassland. Never a single-tile hard switch. --- ### Diagram: Border Distortion Visualization ``` RAW BIOME ASSIGNMENT (bad — straight edges): FFFFFFGGGGG FFFFFFGGGGG FFFFFFGGGGG FFFFFFGGGGG FFFFFFGGGGG AFTER BORDER NOISE (good — organic edges): FFFFfgGGGGG FFFFFfGGGGG FFFfggGGGGG FFFFfgGGGGG FFFFFfgGGGG F = Forest (core) G = Grassland (core) f = Forest Edge (transition) g = Grassland Edge (transition) COASTLINE EXAMPLE: Raw: After Distortion: LLLLLOOOOO LLLLllOOOO LLLLLOOOOO LLLLLlOOOO LLLLLOOOOO LLLllOOOOO LLLLLOOOOO LLLLlOOOOO LLLLLOOOOO LLLLLllOOO L = Land O = Ocean l = Coastal transition (beach/cliff/tidal) ``` --- ### Additional Organic Features **Peninsula and Bay Generation:** Beyond noise distortion, explicitly generate 2–4 large-scale coastal features per continental edge: 1. **Peninsulas:** Select a random coastline point. Extend a noise-distorted finger of land 10–30 tiles into the ocean at a random angle. Width tapers from 5–10 tiles at base to 1–3 tiles at tip. Peninsulas are high-value settlement locations (natural harbors on the flanks). 2. **Bays:** Select a random coastline point. Carve a noise-distorted concavity 10–20 tiles into the land. Width varies 5–15 tiles. Bays are natural harbor sites — settlement placement scores bay interiors highly. 3. **Islands:** 3–8 islands generated offshore. Each is a small noise-generated heightmap (10–30 tiles diameter), placed 5–20 tiles off the nearest coast. Islands can host Tier 4–5 settlements, PoIs, or remain wilderness. These features are placed BEFORE border distortion (they modify the raw land/ocean mask), ensuring the distortion pass treats them as natural coastline. **River Meander:** Rivers generated by drainage simulation (Layer 2) also receive meander treatment: 1. After the base river path is computed (gradient descent), apply a lateral sine-wave displacement along the river's length. 2. Amplitude: 1–3 tiles (wider in flat terrain, tighter in mountain valleys). 3. Frequency: varies by river length (longer rivers have broader, slower meanders). 4. In very flat terrain (grasslands, floodplains), meander amplitude increases and occasional oxbow lakes are generated (abandoned meander loops that became disconnected). --- --- ## ISSUE 2: Linear Feature Tile Exclusivity ### The Problem Rivers, roads, and railroads are all drawn as tile-based linear features on the world map. When two or more of these features occupy the same tile and run in the same direction (parallel), they become visually indistinguishable, cause rendering conflicts, and create gameplay ambiguity (is the player on the road or in the river?). ### The Rule **No tile may contain more than one linear feature running in the same direction.** Specifically: - A river and a road may NOT occupy the same tile running parallel (same or adjacent direction). - A river and a railroad may NOT occupy the same tile running parallel. - A road and a railroad may NOT occupy the same tile running parallel. - A river, road, and railroad may NEVER all occupy the same tile under any circumstances. **Crossings ARE permitted.** A road may cross a river (bridge tile). A railroad may cross a river (bridge tile). A road may cross a railroad (crossing tile). These are perpendicular or near-perpendicular intersections, not parallel co-occupation. **"Parallel" Definition:** Two linear features are considered parallel on a tile if their traversal directions through that tile differ by 45 degrees or less. ``` PERMITTED (crossing — ~90° intersection): Road →→→ ↓ River ↓↓↓ FORBIDDEN (parallel — same direction): Road →→→→→→ River →→→→→→ (both heading east through the same tiles) FORBIDDEN (near-parallel — ~45° on same tile): Road →→↗ River →→→ (road angling NE while river goes E — too close) PERMITTED (diverging after cross): Road →→→↗↗↗ River →→→→→→ (road crosses river then diverges — crossing tile is fine) ``` --- ### Implementation: Linear Feature Priority and Exclusion Zones Linear features are generated in a fixed priority order. Higher-priority features claim tiles first. Lower-priority features must route around claimed tiles. **Priority Order:** 1. **Rivers** (highest — water flows where gravity dictates; it was here first) 2. **Railroads** (second — expensive infrastructure, routes are surveyed and committed) 3. **Roads** (third — roads are the most flexible and can divert most easily) **Step 1 — River Generation (Layer 2, unchanged)** Rivers are generated first via drainage simulation. Each river tile is tagged with: - `feature: RIVER` - `direction: [N, NE, E, SE, S, SW, W, NW]` (flow direction through this tile) - `exclusion_zone: true` The **exclusion zone** for a river tile extends to all 8 adjacent tiles. These adjacent tiles are tagged: - `river_adjacent: true` - `river_direction: [inherited from the river tile's direction]` This means: no other linear feature may enter a river-adjacent tile traveling in the same direction (±45°) as the river. They CAN enter the tile traveling perpendicular (crossing). **Step 2 — Railroad Generation** Railroads are generated using A* pathfinding between rail-connected settlements (Layer 4). The pathfinding cost function is modified: ``` cost(tile) = base_terrain_cost + (if tile.feature == RIVER: INFINITY) // never on river + (if tile.river_adjacent AND direction_parallel: INFINITY) // never parallel adjacent + (if tile.river_adjacent AND direction_perpendicular: BRIDGE_COST) // cross OK, expensive ``` Where: - `INFINITY` = impassable (pathfinder routes around) - `BRIDGE_COST` = high but finite (bridges are expensive but buildable; the pathfinder will cross rivers when routing around is worse) - `direction_parallel` = the railroad's proposed direction through this tile is within 45° of `river_direction` - `direction_perpendicular` = the railroad's proposed direction is 60°+ different from `river_direction` When a railroad tile is placed, it receives the same treatment as rivers: - `feature: RAILROAD` - `direction: [direction]` - `exclusion_zone: true` - Adjacent tiles tagged `railroad_adjacent`, `railroad_direction` **Step 3 — Road Generation** Roads are generated last, using the same modified A* pathfinding but now respecting BOTH river and railroad exclusion zones: ``` cost(tile) = base_terrain_cost + (if tile.feature == RIVER: INFINITY) + (if tile.feature == RAILROAD: INFINITY) + (if tile.river_adjacent AND direction_parallel: INFINITY) + (if tile.railroad_adjacent AND direction_parallel: INFINITY) + (if tile.river_adjacent AND direction_perpendicular: BRIDGE_COST) + (if tile.railroad_adjacent AND direction_perpendicular: CROSSING_COST) ``` Where `CROSSING_COST` is lower than `BRIDGE_COST` (road-rail crossings are cheaper to build than bridges). --- ### Crossing Tile Types When a linear feature crosses another, the intersection tile receives a special type that handles both features: | Crossing | Tile Type | Visual | Gameplay | |----------|-----------|--------|----------| | Road crosses River | BRIDGE | Road surface over water, bridge supports visible | Traversable on road; river continues beneath. Bridge can be a chokepoint. | | Railroad crosses River | RAIL_BRIDGE | Rail tracks on bridge structure over water | Rail travel continues; river flows beneath. | | Road crosses Railroad | RAIL_CROSSING | Road surface with rail tracks crossing it, warning markers | Both traversable. Possible train-encounter event at crossing. | | Road crosses Road | CROSSROADS | Intersection | Standard crossroads — signpost, possible encounter point. | **Crossing Angle Constraint:** Crossings must occur at 60°–120° angles (near-perpendicular). If the pathfinder produces a crossing at a shallower angle, the crossing tile is nudged: the lower- priority feature's path is locally adjusted to approach the crossing more perpendicularly. ``` BAD CROSSING (too shallow, ~30°): River →→→→→→→ Road ↗↗↗↗ CORRECTED (road approaches at ~80°): River →→→→→→→ Road ↑↑↗ ↑ ``` This local adjustment affects only the 2–3 tiles immediately before and after the crossing point. The rest of the road's path is unchanged. --- ### River-Following Roads: The Setback Rule In real geography, roads often follow river valleys because valleys are flat and rivers indicate gentle terrain. The generation system should produce roads that travel NEAR rivers (in the same valley) without occupying the same tiles. **The Setback Rule:** When road pathfinding produces a path that would run parallel to a river (same general direction, within exclusion zone), the road is pushed to a **setback distance** of 3–5 tiles from the river centerline. The road follows the river valley's general direction but maintains visual and mechanical separation. Implementation: ``` // During road A* pathfinding, for tiles near a river: if (tile.river_adjacent AND proposed_direction is parallel to river): // Don't use INFINITY — we WANT the road nearby, just not ON the river // Instead, add a moderate cost that pushes it a few tiles away cost += SETBACK_COST × (1 / distance_to_river) // Closer to river = higher cost, pushing the path outward // But the valley terrain is still cheaper than mountains, // so the road stays in the valley, just offset ``` Visual result: ``` Terrain Cross-Section (valley): Mountains Road River Meadow Mountains /\/\/\ ==== ~~~~ ........ /\/\/\ ↑ ↑ 3-5 tiles separation ``` The player sees: a road running through a valley with a river visible 3–5 tiles to one side. Realistic, readable, no tile conflicts. --- ### Railroad-Following Roads: Same Principle Railroads and roads frequently follow the same corridors (both seek flat, direct routes). The same setback rule applies: - Railroad is placed first (higher priority). - Road pathfinding applies `SETBACK_COST` near railroad tiles, pushing the road 2–4 tiles to one side. - The road and railroad travel the same general corridor but on parallel paths with clear tile separation. Visual result: ``` Top-Down View: ==== Railroad .... (2-4 tile gap, terrain visible between) ---- Road ``` --- ### Edge Case: Convergence Points (Settlements) All three linear features may converge at settlement tiles. Settlements are special: - Settlement tiles are multi-layer. A town can have a river running through it, a rail station, and roads entering from multiple directions. - **Within settlement boundaries (the tile cluster that forms the town), the exclusion rules are RELAXED.** Rivers, roads, and rail can coexist within a town's footprint because the settlement's own infrastructure (bridges, crossings, canal walls, station platforms) handles the overlap. - **The exclusion rules resume at the settlement boundary.** As features exit the town, they must separate to compliant distances within 2 tiles. Implementation: ``` if (tile.is_settlement): // All linear feature costs revert to base terrain cost // No exclusion zones within settlement boundaries // Crossing types still apply (bridges, rail crossings) else: // Full exclusion rules in effect ``` This means: a river, road, and railroad can all enter the same town (because towns form at resource convergence points), but they must diverge onto separate tile paths once they leave. --- ### Validation Pass After all three linear feature layers are generated, run a validation sweep: ``` for each tile T on the map: features_on_tile = list of linear features present on T if len(features_on_tile) > 1: if T.is_settlement: PASS (settlements are exempt) else: for each pair (A, B) in features_on_tile: angle_diff = abs(A.direction - B.direction) if angle_diff < 60°: FLAG AS VIOLATION // Reroute the lower-priority feature around this tile // using local A* with the violating tile set to INFINITY ``` Rerouting is local (affects 3–5 tiles around the violation). The global path is not recomputed — only the immediate conflict area is adjusted. This keeps the validation pass cheap. **Violation Logging:** In development builds, log all violations with coordinates, feature types, and directions. Target: 0 violations after the validation pass. Any logged post- validation violation indicates a bug in the exclusion cost functions. --- ### Summary: Tile Content Rules (Quick Reference) | Tile State | River | Railroad | Road | Valid? | |------------|-------|----------|------|--------| | Empty | — | — | — | Yes | | River only | ✓ | — | — | Yes | | Railroad only | — | ✓ | — | Yes | | Road only | — | — | ✓ | Yes | | River + Road parallel | ✓ | — | ✓ | **NO** | | River + Road crossing | ✓ (beneath) | — | ✓ (bridge) | Yes | | River + Railroad parallel | ✓ | ✓ | — | **NO** | | River + Railroad crossing | ✓ (beneath) | ✓ (bridge) | — | Yes | | Road + Railroad parallel | — | ✓ | ✓ | **NO** | | Road + Railroad crossing | — | ✓ | ✓ (crossing) | Yes | | River + Road + Railroad | ✓ | ✓ | ✓ | **NO** (never, outside settlements) | | Any combination in settlement | ✓ | ✓ | ✓ | Yes (settlement exempt) | --- *Addendum A, Version 1.0 — Border Organics & Linear Feature Tile Rules* *Compiled by ENI for LO*