diff --git a/server/src/atlas/body_params_reader.rs b/server/src/atlas/body_params_reader.rs index 828dd818b..62741faa4 100644 --- a/server/src/atlas/body_params_reader.rs +++ b/server/src/atlas/body_params_reader.rs @@ -1,9 +1,9 @@ //! Pre-dispatch reader for body physical parameters (T-1032, D-239 §1). //! -//! Reads the columns needed to construct a [`BodyParams`] for the RegionProfile +//! Reads the columns needed to construct a [`BodyParams`] for the DistrictProfile //! carrier layer from `systems.db` at `AnalyzeBody` enqueue time. This keeps //! the Rayon work item DB-free while supplying the climate/tectonic inputs -//! required by `derive_all_regions`. +//! required by `derive_all_districts`. //! //! **D-240:** orbit/star fields (`orbital_period_days`, `axial_tilt_deg`, //! `spectral_class`, `star_type`) are non-canonical placeholder data — they are @@ -23,8 +23,8 @@ //! //! `tectonic_activity` is **not** in the current schema; `BodyParams.tectonic_activity` //! is left `None` so the derivation falls back to `planet_class` as documented -//! on the struct. The per-region fields `region_latitude_deg` and `elevation_km` -//! are set by `derive_all_regions` / `derive_region_profile`, not here; they +//! on the struct. The per-district fields `district_latitude_deg` and `elevation_km` +//! are set by `derive_all_districts` / `derive_district_profile`, not here; they //! remain at their struct defaults (0.0) from this reader. //! //! Read-only `systems.db` access follows the same pattern as @@ -37,7 +37,7 @@ use std::sync::{Arc, Mutex}; use rusqlite::{Connection, OpenFlags}; use thiserror::Error; -use crate::atlas::region_profile::BodyParams; +use crate::atlas::district_profile::BodyParams; // --------------------------------------------------------------------------- // Error type @@ -119,9 +119,9 @@ impl BodyParamsReader { planet_class, // tectonic_activity not in schema — leave None. tectonic_activity: None, - // Per-region fields are set by derive_all_regions / derive_region_profile, + // Per-district fields are set by derive_all_districts / derive_district_profile, // not at the body level. Leave at struct defaults (0.0). - region_latitude_deg: 0.0, + district_latitude_deg: 0.0, elevation_km: 0.0, body_radius_km, }), @@ -185,7 +185,13 @@ mod tests { conn.execute( "INSERT INTO bodies (body_id, hydrosphere, atmosphere, planet_class, body_radius_km) VALUES (?1, ?2, ?3, ?4, ?5)", - rusqlite::params![body_id, hydrosphere, atmosphere, planet_class, body_radius_km], + rusqlite::params![ + body_id, + hydrosphere, + atmosphere, + planet_class, + body_radius_km + ], ) .expect("insert body"); @@ -209,8 +215,8 @@ mod tests { assert_eq!(params.atmosphere.as_deref(), Some("breathable")); assert_eq!(params.planet_class.as_deref(), Some("temperate")); assert_eq!(params.body_radius_km, Some(6371.0)); - // Per-region fields always start at 0.0 from the reader. - assert_eq!(params.region_latitude_deg, 0.0); + // Per-district fields always start at 0.0 from the reader. + assert_eq!(params.district_latitude_deg, 0.0); assert_eq!(params.elevation_km, 0.0); // tectonic_activity not in schema → None. assert!(params.tectonic_activity.is_none()); @@ -238,7 +244,13 @@ mod tests { #[test] fn read_is_deterministic() { - let db = make_test_db("GJ4d", Some("ice"), Some("thin"), Some("frozen"), Some(3389.5)); + let db = make_test_db( + "GJ4d", + Some("ice"), + Some("thin"), + Some("frozen"), + Some(3389.5), + ); let reader = BodyParamsReader::open(&db).expect("open"); let p1 = reader.read_body_params("GJ4d").expect("first read"); let p2 = reader.read_body_params("GJ4d").expect("second read"); diff --git a/server/src/atlas/body_world_state.rs b/server/src/atlas/body_world_state.rs index 092c000d9..a31e77077 100644 --- a/server/src/atlas/body_world_state.rs +++ b/server/src/atlas/body_world_state.rs @@ -14,7 +14,7 @@ use bevy_ecs::prelude::Resource; use serde::{Deserialize, Serialize}; use crate::atlas::attractor_matching::CityPlacement; -use crate::atlas::region_profile::{RegionPos, RegionProfile}; +use crate::atlas::district_profile::{DistrictPos, DistrictProfile}; use crate::atlas::road_graph::RoadGraph; use crate::simulation::generator::{ GeographicAttractor, QuarterId, QuarterWorldState, TerritorialStatus, @@ -94,12 +94,12 @@ pub struct BodyWorldState { /// Populated by `GenCompletion::SkeletonGenerated` after the plan phase /// completes for each city. `BTreeMap` for D-010 determinism. pub quarters: BTreeMap, - /// Per-region (~1 km) profiles derived from body params + terrain (T-1023, D-239 §1). + /// Per-district (~1 km) profiles derived from body params + terrain (T-1023, D-239 §1). /// /// Populated by the background cascade after Layer 1 completes. - /// `BTreeMap` keyed by `RegionPos` for D-010 determinism. - /// Empty until the RegionProfile layer has run. - pub regions: BTreeMap, + /// `BTreeMap` keyed by `DistrictPos` for D-010 determinism. + /// Empty until the DistrictProfile layer has run. + pub districts: BTreeMap, /// Last sim tick this entry was read. Used for LRU eviction. pub last_accessed: SimTick, } @@ -223,7 +223,7 @@ mod tests { placements: vec![], road_graph: RoadGraph::default(), quarters: BTreeMap::new(), - regions: BTreeMap::new(), + districts: BTreeMap::new(), last_accessed: tick, } } diff --git a/server/src/atlas/cascade.rs b/server/src/atlas/cascade.rs index 126512c07..f542938bc 100644 --- a/server/src/atlas/cascade.rs +++ b/server/src/atlas/cascade.rs @@ -24,10 +24,10 @@ use crate::atlas::attractor_matching::{ match_cities, territorial_status_from_faction, CityPlacement, CityRecord, }; use crate::atlas::body_world_state::{BodyWorldState, RiverNetwork}; +use crate::atlas::district_profile::{self, BodyParams, DistrictPos, DistrictProfile}; use crate::atlas::features::TerrainAnalysis; use crate::atlas::heightmap::{self, BodyHeightmap, HeightmapLoadError}; use crate::atlas::layer1::{self, Layer1Output}; -use crate::atlas::region_profile::{self, BodyParams, RegionPos, RegionProfile}; use crate::atlas::road_graph::{self, RoadGraph}; use crate::seed::SeedChain; use crate::simulation::generator::{CompatibilityMatrix, GeographicAttractor, TerritorialStatus}; @@ -46,15 +46,15 @@ pub enum CascadeLayer { /// `match_cities`. The carried `SeedChain` is unused here; later stochastic /// layers (Layer 4+) will consume it. Settlement, - /// Layer — RegionProfile (~1 km carriers, D-239 §1, T-1023). Pure function of + /// Layer — DistrictProfile (~1 km carriers, D-239 §1, T-1023). Pure function of /// `(seed, body_params, terrain_analysis)`. Appended after Settlement so /// declaration order (= Ord) is preserved — never reorder (D-010). - RegionProfile, + DistrictProfile, /// Layer 2 — inter-settlement road/rail graph (D-211, T-1038). Pure function /// of `(Layer-3 placements, Layer-1 terrain)`; RNG-free. Semantically "Layer /// 2", but it depends only on Settlement + Topography, so it is **appended /// last** to honour the append-only `Ord` rule (it neither needs nor blocks - /// the RegionProfile layer; requesting it runs RegionProfile first, harmlessly). + /// the DistrictProfile layer; requesting it runs DistrictProfile first, harmlessly). RoadGraph, } @@ -74,19 +74,19 @@ pub struct CascadeSnapshot { pub layer1: Option, /// Layer 3 — settlement placement. `Some` once [`CascadeLayer::Settlement`] has run. pub layer3: Option, - /// RegionProfile layer — ~1 km carriers. `Some` once - /// [`CascadeLayer::RegionProfile`] has run (T-1023, D-239 §1). - pub layer_region: Option, + /// DistrictProfile layer — ~1 km carriers. `Some` once + /// [`CascadeLayer::DistrictProfile`] has run (T-1023, D-239 §1). + pub layer_district: Option, /// Layer 2 — inter-settlement road/rail graph. `Some` once /// [`CascadeLayer::RoadGraph`] has run (D-211, T-1038). pub road_graph: Option, } -/// RegionProfile layer output (T-1023, D-239 §1): per-region (~1 km) terrain -/// profiles covering the whole body. Stored in `BodyWorldState.regions`. +/// DistrictProfile layer output (T-1023, D-239 §1): per-district (~1 km) terrain +/// profiles covering the whole body. Stored in `BodyWorldState.districts`. #[derive(Debug, Clone, Default)] -pub struct LayerRegionOutput { - pub regions: std::collections::BTreeMap, +pub struct LayerDistrictOutput { + pub districts: std::collections::BTreeMap, } /// Layer 3 output (#955, D-211): attractor-matched settlement placements for the @@ -107,7 +107,10 @@ impl CascadeSnapshot { None => (RiverNetwork::default(), Vec::new(), Vec::new()), }; let placements = self.layer3.map(|l3| l3.placements).unwrap_or_default(); - let regions = self.layer_region.map(|lr| lr.regions).unwrap_or_default(); + let districts = self + .layer_district + .map(|lr| lr.districts) + .unwrap_or_default(); let road_graph = self.road_graph.unwrap_or_default(); BodyWorldState { body_id: self.body_id, @@ -120,7 +123,7 @@ impl CascadeSnapshot { placements, road_graph, quarters: std::collections::BTreeMap::new(), - regions, + districts, last_accessed: 0, } } @@ -168,8 +171,8 @@ fn run_layer3( /// `dominant_faction` is the body's authored system faction (D-237); it drives /// the `TerritorialStatus` on each province and the per-settlement spatial /// character (#956). `None` → `FrontierUnclaimed`. -/// `body_params` supplies the physical parameters needed for the RegionProfile -/// layer (T-1023); `None` → region layer skips (empty `regions` map). +/// `body_params` supplies the physical parameters needed for the DistrictProfile +/// layer (T-1023); `None` → district layer skips (empty `districts` map). pub fn run_cascade_from_heightmap( body_seed: SeedChain, heightmap: BodyHeightmap, @@ -184,7 +187,7 @@ pub fn run_cascade_from_heightmap( heightmap, layer1: None, layer3: None, - layer_region: None, + layer_district: None, road_graph: None, }; @@ -223,11 +226,11 @@ pub fn run_cascade_from_heightmap( snapshot.layer3 = Some(l3); } - // RegionProfile (T-1023, D-239 §1) and RoadGraph (Layer 2, T-1038) both need a + // DistrictProfile (T-1023, D-239 §1) and RoadGraph (Layer 2, T-1038) both need a // TerrainAnalysis, which needs a drainage pass. Layer 1 already ran drainage // inside run_layer1, but neither result is stored on Layer1Output, so we re-run // both here once and share them. Pure → determinism preserved, but the drainage - // re-run is NOT free at the ~6 000-regions/body working scale (D-203). + // re-run is NOT free at the ~6 000-districts/body working scale (D-203). // PERF/TODO(T-1044): cache TerrainAnalysis on Layer1Output to drop this // redundant drainage pass, and validate the combined cost against the D-239 §10 // ~45 ms/body budget in the T-1031 verification harness. This is a LIVE @@ -235,10 +238,10 @@ pub fn run_cascade_from_heightmap( // body runs this path. // // The terrain analysis is computed only when it will actually be used: - // body_params present (RegionProfile) or RoadGraph requested. RegionProfile + // body_params present (DistrictProfile) or RoadGraph requested. DistrictProfile // skips silently without body_params (e.g. unit tests without DB), but the // road graph needs no body params, so RoadGraph runs regardless. - if up_to >= CascadeLayer::RegionProfile + if up_to >= CascadeLayer::DistrictProfile && (body_params.is_some() || up_to >= CascadeLayer::RoadGraph) { use crate::atlas::drainage; @@ -250,14 +253,14 @@ pub fn run_cascade_from_heightmap( ); let ta = TerrainAnalysis::analyze(&snapshot.heightmap, &dr); - // RegionProfile layer — pure derivation from body params + terrain. + // DistrictProfile layer — pure derivation from body params + terrain. if let Some(params) = body_params { - // ~8 cells per region on a 128×64 working grid → ~80×32 = ~2 560 regions; + // ~8 cells per district on a 128×64 working grid → ~80×32 = ~2 560 districts; // at full working resolution the budget is ~6 000/body (D-203). const CELLS_PER_REGION: usize = 8; - let regions = - region_profile::derive_all_regions(body_seed, params, &ta, CELLS_PER_REGION); - snapshot.layer_region = Some(LayerRegionOutput { regions }); + let districts = + district_profile::derive_all_districts(body_seed, params, &ta, CELLS_PER_REGION); + snapshot.layer_district = Some(LayerDistrictOutput { districts }); } // Layer 2 — inter-settlement road/rail graph (D-211, T-1038). Pure @@ -417,8 +420,8 @@ mod tests { // breaking which layers run. assert!(CascadeLayer::Heightmap < CascadeLayer::Topography); assert!(CascadeLayer::Topography < CascadeLayer::Settlement); - assert!(CascadeLayer::Settlement < CascadeLayer::RegionProfile); - assert!(CascadeLayer::RegionProfile < CascadeLayer::RoadGraph); + assert!(CascadeLayer::Settlement < CascadeLayer::DistrictProfile); + assert!(CascadeLayer::DistrictProfile < CascadeLayer::RoadGraph); } #[test] @@ -437,10 +440,10 @@ mod tests { assert!(res.is_err(), "missing heightmap must Err, not panic"); } - /// RegionProfile layer runs, produces regions, and is deterministic (T-1023). + /// DistrictProfile layer runs, produces districts, and is deterministic (T-1023). #[test] - fn region_profile_layer_runs_and_is_deterministic() { - use crate::atlas::region_profile::BodyParams; + fn district_profile_layer_runs_and_is_deterministic() { + use crate::atlas::district_profile::BodyParams; let params = BodyParams { hydrosphere: Some("ocean".into()), @@ -455,22 +458,22 @@ mod tests { &[], None, Some(¶ms), - CascadeLayer::RegionProfile, + CascadeLayer::DistrictProfile, ) }; let snap1 = run(); let snap2 = run(); - let lr1 = snap1.layer_region.expect("layer_region should be Some"); - let lr2 = snap2.layer_region.expect("layer_region should be Some"); - assert!(!lr1.regions.is_empty(), "regions map must not be empty"); + let lr1 = snap1.layer_district.expect("layer_district should be Some"); + let lr2 = snap2.layer_district.expect("layer_district should be Some"); + assert!(!lr1.districts.is_empty(), "districts map must not be empty"); assert_eq!( - lr1.regions.len(), - lr2.regions.len(), - "region count deterministic" + lr1.districts.len(), + lr2.districts.len(), + "district count deterministic" ); // BTreeMap iteration order is deterministic — compare all entries. - for (pos, p1) in &lr1.regions { - let p2 = lr2.regions.get(pos).expect("matching pos in second run"); + for (pos, p1) in &lr1.districts { + let p2 = lr2.districts.get(pos).expect("matching pos in second run"); assert_eq!(p1.river_threshold, p2.river_threshold); assert_eq!(p1.tectonic_class, p2.tectonic_class); assert_eq!(p1.glaciation_grade, p2.glaciation_grade); diff --git a/server/src/atlas/chunk_context.rs b/server/src/atlas/chunk_context.rs index 5a1692899..1efe9cfe7 100644 --- a/server/src/atlas/chunk_context.rs +++ b/server/src/atlas/chunk_context.rs @@ -1,7 +1,7 @@ //! ChunkContext — 64 m carrier of the D-239 three-tier refinement chain (T-1028). //! //! `ChunkContext` is the middle tier: coarser than a voxel (1 m), finer than a -//! region (~1 km). It is derived purely from the covering `RegionProfile`(s) and +//! district (2 km, D-243). It is derived purely from the covering `DistrictProfile`(s) and //! the world seed — no stored state, no side effects. //! //! ## Scale contract (D-239 §10) @@ -10,18 +10,18 @@ //! **coarser than a chunk** — so there is no per-tile `flow_direction[64×64]` //! here. Instead the chunk carries: //! - **`basin_direction`** — a cardinal basin-flow direction (N/E/S/W, 4-way) -//! derived from the dominant D8 thalweg in the covering region. +//! derived from the dominant D8 thalweg in the covering district. //! - **`meander_phase`** and **`meander_wavelength_m`** — global meander-curve //! params for the MeanderReach and AlluvialPlain families. //! -//! Features with wavelength > 64 m seed from **Region-or-higher** (the covering -//! `RegionProfile`), not from the chunk-local seed. This is enforced by structure: +//! Features with wavelength > 64 m seed from **District-or-higher** (the covering +//! `DistrictProfile`), not from the chunk-local seed. This is enforced by structure: //! the chunk seed is only used for sub-chunk (<64 m) scatter. //! //! The same rule places the feature axes themselves (T-1040/T-1041): the channel //! centreline and landform axis (`channel_anchor_m`) and the coast-face line -//! (`coast_anchor_m`) are **world-metre coordinates derived once per region** -//! from the region-scale seed. Voxel generators measure distance to these +//! (`coast_anchor_m`) are **world-metre coordinates derived once per district** +//! from the district-scale seed. Voxel generators measure distance to these //! anchors in continuous world coordinates — never from the world origin and //! never folded into the 64 m chunk frame. //! @@ -39,7 +39,8 @@ use serde::{Deserialize, Serialize}; -use crate::atlas::region_profile::RegionProfile; +use crate::atlas::district_profile::DistrictProfile; +use crate::atlas::scale; use crate::seed::{SeedChain, SeedDomain}; // --------------------------------------------------------------------------- @@ -47,7 +48,7 @@ use crate::seed::{SeedChain, SeedDomain}; // --------------------------------------------------------------------------- /// Cardinal basin-flow direction — the dominant D8 thalweg direction in the -/// covering region. Coarser than a chunk (D-239 §10); derived from region slope +/// covering district. Coarser than a chunk (D-239 §10); derived from district slope /// and morphology, NOT from a per-tile D8 grid. /// /// Integer-discriminant, append-only (D-010). @@ -69,30 +70,21 @@ pub enum BasinDirection { // ChunkPos — position on the 64 m chunk grid // --------------------------------------------------------------------------- -/// Position of a chunk on the 64 m grid, in chunk-units (not metres). -/// -/// A body at ~6 000 regions, each region ~1 km², gets ~1 000 × 1 000 chunks. -/// `BTreeMap` key — implements `Ord` for D-010 determinism. -pub type ChunkPos = (i32, i32); +/// Chunk grid position + the chunk edge metres come from the canonical ladder +/// ([`crate::atlas::scale`], D-243) — chunk_context no longer defines its own +/// scale (the Q-110 failure mode). The covering district is `scale::DISTRICT_M` +/// = 2 048 m = `scale::CHUNKS_PER_DISTRICT` (32) chunks; the district-index +/// mapping is `chunk >> scale::CHUNK_DISTRICT_SHIFT`. +pub use crate::atlas::scale::{ChunkPos, CHUNK_M}; -/// Chunk edge length in metres. -pub const CHUNK_M: i32 = 64; - -/// Region edge length in chunks (region ≈ 1 km = 16 chunks). Must match the -/// `>> REGION_CHUNKS_SHIFT` region-index mapping used for the region-scale seed. -const REGION_CHUNKS_SHIFT: u32 = 4; - -/// Region edge length in metres (1 024 m). -const REGION_M: i32 = CHUNK_M << REGION_CHUNKS_SHIFT; - -/// Margin keeping a region's feature anchor away from the region edge, so the +/// Margin keeping a district's feature anchor away from the district edge, so the /// channel's full swept band (max meander amplitude wavelength/4 ≈ 162 m + -/// channel edge + levee band + warp bound ≈ 187 m) stays inside the region. -/// Cross-region feature continuity is the stage-2 Voronoi model (T-1040). +/// channel edge + levee band + warp bound ≈ 187 m) stays inside the district. +/// Cross-district feature continuity is the stage-2 Voronoi model (T-1040). const ANCHOR_MARGIN_M: i32 = 192; -/// Seed-addressable anchor span within a region (REGION_M − 2 × margin). -const ANCHOR_SPAN_M: i32 = REGION_M - 2 * ANCHOR_MARGIN_M; +/// Seed-addressable anchor span within a district (`scale::DISTRICT_M` − 2 × margin). +const ANCHOR_SPAN_M: i32 = scale::DISTRICT_M - 2 * ANCHOR_MARGIN_M; /// Maximum levee band width in metres (`voxel::in_levee_band`: 4 + 3 jitter). const LEVEE_BAND_MAX_M: i32 = 7; @@ -105,9 +97,9 @@ const WARP_BOUND_M: i32 = 8; // ChunkContext // --------------------------------------------------------------------------- -/// 64 m carrier derived from `RegionProfile`(s) — the second tier of D-239 §1. +/// 64 m carrier derived from `DistrictProfile`(s) — the second tier of D-239 §1. /// -/// Pure deterministic function of `(seed, body_id, region, chunk_pos)`. +/// Pure deterministic function of `(seed, body_id, district, chunk_pos)`. /// Never stored; derived on demand and cached (D-227). /// /// ## Fields @@ -116,15 +108,15 @@ const WARP_BOUND_M: i32 = 8; /// - `meander_phase` — integer phase offset (0–255) for the meander curve. /// Used by MeanderReach and AlluvialPlain voxel generators to place the channel. /// - `meander_wavelength_m` — meander wavelength in metres. Derived from -/// region-level morphology (slope, moisture), seeded at region scale (> 64 m). +/// district-level morphology (slope, moisture), seeded at district scale (> 64 m). /// f64 for positional physics (D-239 §4); structural decisions consume it /// only via deterministic i32 truncation (the `has_active_channel` band). /// - `has_active_channel` — whether a water channel is present in this chunk: -/// the region has water presence AND the channel's swept band around +/// the district has water presence AND the channel's swept band around /// `channel_anchor_m` crosses this chunk (T-1040). /// - `channel_width_m` — channel width in metres (integer; D-010). 0 if no /// active channel. -/// - `channel_anchor_m` / `coast_anchor_m` — region-anchored feature axes in +/// - `channel_anchor_m` / `coast_anchor_m` — district-anchored feature axes in /// world metres (T-1040/T-1041, D-239 §10). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChunkContext { @@ -132,34 +124,34 @@ pub struct ChunkContext { pub basin_direction: BasinDirection, /// Integer phase offset for the meander curve (0–255). - /// Derived at region scale (wavelength > 64 m), NOT from the chunk seed. + /// Derived at district scale (wavelength > 64 m), NOT from the chunk seed. pub meander_phase: u8, /// Meander wavelength in metres. Positional physics value (f64 — D-239 §4). - /// Derived at region scale; typically 80–500 m for AlluvialPlain. + /// Derived at district scale; typically 80–500 m for AlluvialPlain. pub meander_wavelength_m: f64, /// Whether this chunk contains an active water channel. - /// True when the region has water presence (`ocean_fraction_q` ≥ 10) AND + /// True when the district has water presence (`ocean_fraction_q` ≥ 10) AND /// the channel's swept band around `channel_anchor_m` crosses this chunk's - /// cross-axis range (T-1040 — channels exist where the region says, not - /// region-wide and not only at the world origin). + /// cross-axis range (T-1040 — channels exist where the district says, not + /// district-wide and not only at the world origin). pub has_active_channel: bool, /// Active channel width in metres (integer; D-010). 0 if no active channel. - /// Derived from region morphology and slope. + /// Derived from district morphology and slope. pub channel_width_m: i32, - /// Cross-axis world-metre coordinate of the region's feature centreline: + /// Cross-axis world-metre coordinate of the district's feature centreline: /// channel/meander axis, fjord trough, gorge floor, braid-fan axis. - /// Derived once per region from the region-scale seed (T-1040/T-1041, - /// D-239 §10) — constant across all chunks of a region, so the feature is + /// Derived once per district from the district-scale seed (T-1040/T-1041, + /// D-239 §10) — constant across all chunks of a district, so the feature is /// continuous across chunk boundaries. Cross axis = x for N/S basins, /// y for E/W basins. pub channel_anchor_m: i32, /// Along-axis (basin-axis) world-metre coordinate of the CliffCoast face - /// line. Region-scale (T-1041): one continuous coast per region, not a + /// line. District-scale (T-1041): one continuous coast per district, not a /// 64 m sawtooth. Along axis = y for N/S basins, x for E/W basins. pub coast_anchor_m: i32, } @@ -170,59 +162,65 @@ pub struct ChunkContext { /// Derive a `ChunkContext` for the chunk at `chunk_pos` on the 64 m grid. /// -/// Pure function of `(seed, body_id, region, chunk_pos)`. Takes the covering -/// region's `RegionProfile`; in the future a blend of adjacent profiles will -/// handle cross-region chunk seams, but for the walking skeleton one profile +/// Pure function of `(seed, body_id, district, chunk_pos)`. Takes the covering +/// district's `DistrictProfile`; in the future a blend of adjacent profiles will +/// handle cross-district chunk seams, but for the walking skeleton one profile /// is sufficient. /// /// ## Seed usage /// -/// The **meander phase** and **basin direction** are seeded at region scale so +/// The **meander phase** and **basin direction** are seeded at district scale so /// features with wavelength > 64 m are consistent across chunk boundaries. /// The chunk-local seed (keyed on `chunk_pos`) is reserved for sub-chunk scatter /// in the voxel pass — not consumed here. pub fn derive_chunk_context( world_seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, chunk_pos: ChunkPos, ) -> ChunkContext { - // Region-scale seed — features with wavelength > 64 m derive from here. - // Keyed on the chunk position mapped to region-scale units (>> 4 gives - // the ~1 km region index if a region is ~16 chunks wide). - let region_scale_id = pos_to_id((chunk_pos.0 >> 4, chunk_pos.1 >> 4)); - let region_seed = - SeedChain::for_body(world_seed, body_id).derive(SeedDomain::ChunkContext, region_scale_id); + // District-scale seed — features with wavelength > 64 m derive from here. + // Keyed on the chunk position mapped to district-scale units: `>> + // scale::CHUNK_DISTRICT_SHIFT` gives the 2 km district index (32 chunks). + // MUST match the same shift in `derive_district_anchor` so the seed-district + // and the anchor-district are the same cell (D-243; the canonical ladder). + let district_scale_id = pos_to_id(( + chunk_pos.0 >> scale::CHUNK_DISTRICT_SHIFT, + chunk_pos.1 >> scale::CHUNK_DISTRICT_SHIFT, + )); + let district_seed = SeedChain::for_body(world_seed, body_id) + .derive(SeedDomain::ChunkContext, district_scale_id); - // Basin direction — derived from region slope_q (which encodes the - // dominant terrain gradient). We use the region's `elev_q` gradient + // Basin direction — derived from district slope_q (which encodes the + // dominant terrain gradient). We use the district's `elev_q` gradient // direction as a proxy for the D8 thalweg direction. // All integer arithmetic (D-010). - let basin_direction = derive_basin_direction(region, region_seed.seed()); + let basin_direction = derive_basin_direction(district, district_seed.seed()); - // Meander phase — region-scale integer offset so the channel is consistent - // across all chunks in the same region. 0–255. - let meander_phase = (region_seed.seed() >> 8) as u8; + // Meander phase — district-scale integer offset so the channel is consistent + // across all chunks in the same district. 0–255. + let meander_phase = (district_seed.seed() >> 8) as u8; - // Meander wavelength — derived from slope and morphology, region-scale. + // Meander wavelength — derived from slope and morphology, district-scale. // Lower slope → longer wavelength (wider meanders); integer inputs, f64 result // is positional physics (D-239 §4, not a gate comparison). - let meander_wavelength_m = derive_meander_wavelength(region); + let meander_wavelength_m = derive_meander_wavelength(district); - // Region-anchored feature axes (T-1040/T-1041, D-239 §10): channel and + // District-anchored feature axes (T-1040/T-1041, D-239 §10): channel and // landform centrelines have wavelength > 64 m, so their position derives - // from the region-scale seed — never the chunk frame or the world origin. + // from the district-scale seed — never the chunk frame or the world origin. // Cross axis ⊥ basin_direction (x for N/S, y for E/W); along axis ∥ basin. let (cross_chunk, along_chunk) = match basin_direction { BasinDirection::North | BasinDirection::South => (chunk_pos.0, chunk_pos.1), BasinDirection::East | BasinDirection::West => (chunk_pos.1, chunk_pos.0), }; - let channel_anchor_m = derive_region_anchor(cross_chunk, (region_seed.seed() >> 16) & 0xFFFF); - let coast_anchor_m = derive_region_anchor(along_chunk, (region_seed.seed() >> 32) & 0xFFFF); + let channel_anchor_m = + derive_district_anchor(cross_chunk, (district_seed.seed() >> 16) & 0xFFFF); + let coast_anchor_m = derive_district_anchor(along_chunk, (district_seed.seed() >> 32) & 0xFFFF); // Active channel — water presence (ocean_fraction_q >= 10 indicates a - // perennial waterway or water body covers at least 10% of the region) AND - // the channel's swept band around the region anchor crosses this chunk + // perennial waterway or water body covers at least 10% of the district) AND + // the channel's swept band around the district anchor crosses this chunk // (T-1040). The river_threshold governs D8 drainage accumulation at a finer // scale; at the chunk level ocean_fraction_q is the direct proxy for water // presence (D-239 §10: one basin-direction, not a per-tile flow grid). @@ -230,8 +228,8 @@ pub fn derive_chunk_context( // The band is generous (it must cover every chunk that can contain channel, // levee, or warped-channel voxels — a gate-off chunk renders dry), using // the larger MeanderReach amplitude (wavelength/4) for both channel families. - let channel_width = derive_channel_width(region); - let has_active_channel = region.ocean_fraction_q >= 10 && { + let channel_width = derive_channel_width(district); + let has_active_channel = district.ocean_fraction_q >= 10 && { let wavelength_i = (meander_wavelength_m as i32).max(10); let amplitude_max = (wavelength_i / 4).max(3); let edge_max = (channel_width / 2).max(2) + 3; // half-width + max edge jitter @@ -259,18 +257,18 @@ pub fn derive_chunk_context( } } -/// World-metre anchor coordinate for a region-scale feature axis on one axis. +/// World-metre anchor coordinate for a district-scale feature axis on one axis. /// -/// The anchor sits in `[region_origin + ANCHOR_MARGIN_M, region_origin + -/// REGION_M − ANCHOR_MARGIN_M)` so the feature's full swept band stays inside -/// its region (no cross-region band spill; region-seam continuity is the -/// stage-2 Voronoi model, T-1040). Same value for every chunk of the region: -/// the region index is `axis_chunk >> REGION_CHUNKS_SHIFT` (arithmetic shift = -/// floor division, correct for negative chunks) and `seed_bits` comes from the -/// shared region-scale seed. Integer arithmetic (D-010). -fn derive_region_anchor(axis_chunk: i32, seed_bits: u64) -> i32 { - let region_idx = axis_chunk >> REGION_CHUNKS_SHIFT; - let origin_m = region_idx * REGION_M; +/// The anchor sits in `[district_origin + ANCHOR_MARGIN_M, district_origin + +/// scale::DISTRICT_M − ANCHOR_MARGIN_M)` so the feature's full swept band stays +/// inside its district (no cross-district band spill; district-seam continuity is +/// the stage-2 Voronoi model, T-1040). Same value for every chunk of the district: +/// the district index is `axis_chunk >> scale::CHUNK_DISTRICT_SHIFT` (arithmetic +/// shift = floor division, correct for negative chunks) and `seed_bits` comes from +/// the shared district-scale seed. Integer arithmetic (D-010). +fn derive_district_anchor(axis_chunk: i32, seed_bits: u64) -> i32 { + let district_idx = axis_chunk >> scale::CHUNK_DISTRICT_SHIFT; + let origin_m = district_idx * scale::DISTRICT_M; origin_m + ANCHOR_MARGIN_M + (seed_bits % ANCHOR_SPAN_M as u64) as i32 } @@ -292,22 +290,22 @@ pub(crate) fn pos_to_id(pos: (i32, i32)) -> u64 { .wrapping_add(y) } -/// Derive basin direction from the region profile and a region-scale seed. +/// Derive basin direction from the district profile and a district-scale seed. /// /// Uses `elev_q` and `slope_q` as a proxy for the dominant D8 gradient /// direction. In lieu of a full D8 computation at this scale, the basin -/// direction is derived from the region's terrain characteristics: -/// - Coastal regions (high `ocean_fraction_q`) flow toward ocean (West fallback) -/// - High-elevation regions flow away from ridges (seed-derived direction) -/// - Low-slope regions use the seed for unbiased direction +/// direction is derived from the district's terrain characteristics: +/// - Coastal districts (high `ocean_fraction_q`) flow toward ocean (West fallback) +/// - High-elevation districts flow away from ridges (seed-derived direction) +/// - Low-slope districts use the seed for unbiased direction /// /// All integer arithmetic (D-010). -fn derive_basin_direction(region: &RegionProfile, region_seed: u64) -> BasinDirection { +fn derive_basin_direction(district: &DistrictProfile, district_seed: u64) -> BasinDirection { // Coastal: flow toward the ocean (use seed to pick E/W/N/S with coastal bias). - if region.ocean_fraction_q >= 15 { + if district.ocean_fraction_q >= 15 { // The low 2 bits of seed give 4 directions; bias toward the most common // coastal configurations (N or S for equatorial coasts, E/W for shelf). - return match (region_seed >> 2) & 0x3 { + return match (district_seed >> 2) & 0x3 { 0 => BasinDirection::South, 1 => BasinDirection::East, 2 => BasinDirection::North, @@ -316,7 +314,7 @@ fn derive_basin_direction(region: &RegionProfile, region_seed: u64) -> BasinDire } // Interior: pure seed-derived direction (unbiased). - match region_seed & 0x3 { + match district_seed & 0x3 { 0 => BasinDirection::North, 1 => BasinDirection::East, 2 => BasinDirection::South, @@ -324,31 +322,31 @@ fn derive_basin_direction(region: &RegionProfile, region_seed: u64) -> BasinDire } } -/// Derive meander wavelength in metres from region morphology. +/// Derive meander wavelength in metres from district morphology. /// /// Low slope + high moisture → longer wavelength (wide meanders). /// High slope → short wavelength (confined/straight channels). /// Result is f64 positional physics (D-239 §4), not used in gate comparisons. -fn derive_meander_wavelength(region: &RegionProfile) -> f64 { +fn derive_meander_wavelength(district: &DistrictProfile) -> f64 { // Base wavelength range: 80–500 m for AlluvialPlain. // slope_q 0 → 500 m; slope_q 100 → 80 m. Linear interpolation. - let slope_clamped = region.slope_q.clamp(0, 100) as f64; + let slope_clamped = district.slope_q.clamp(0, 100) as f64; let base = 500.0 - (slope_clamped / 100.0) * 420.0; // Moisture boost: high moisture → slightly longer wavelength (more sinuous). - let moisture_factor = 1.0 + (region.moisture_q.clamp(0, 100) as f64 / 100.0) * 0.3; + let moisture_factor = 1.0 + (district.moisture_q.clamp(0, 100) as f64 / 100.0) * 0.3; base * moisture_factor } -/// Derive active channel width in metres from region morphology. +/// Derive active channel width in metres from district morphology. /// /// Returns an integer metre value (D-010). -fn derive_channel_width(region: &RegionProfile) -> i32 { +fn derive_channel_width(district: &DistrictProfile) -> i32 { // Water presence drives width; ocean_fraction_q is our proxy. // Meander channels: 3–15 m (D-239 §9 game-feel constraint). // We derive in that range from ocean_fraction_q. - let base = match region.ocean_fraction_q { + let base = match district.ocean_fraction_q { 0..=9 => 3, 10..=19 => 5, 20..=34 => 8, @@ -356,7 +354,7 @@ fn derive_channel_width(region: &RegionProfile) -> i32 { _ => 15, }; // Slope modifier: high slope → narrower (gorge-like); low slope → wider. - let slope_penalty = (region.slope_q / 20).min(3); + let slope_penalty = (district.slope_q / 20).min(3); (base - slope_penalty).max(3) } @@ -367,13 +365,13 @@ fn derive_channel_width(region: &RegionProfile) -> i32 { #[cfg(test)] mod tests { use super::*; - use crate::atlas::region_profile::{ + use crate::atlas::district_profile::{ GlaciationGrade, PrecipitationClass, TectonicClass, VegetationClass, }; use crate::simulation::generator::MorphologyZone; - fn alluvial_region() -> RegionProfile { - RegionProfile { + fn alluvial_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::AlluvialPlain, tectonic_class: TectonicClass::Stable, glaciation_grade: GlaciationGrade::None, @@ -388,10 +386,10 @@ mod tests { } } - /// The chunk of region (0, 0) whose cross-range contains the region's + /// The chunk of district (0, 0) whose cross-range contains the district's /// channel anchor — guaranteed inside the T-1040 channel band. - fn anchor_chunk_pos(world_seed: u64, body_id: &str, region: &RegionProfile) -> ChunkPos { - let probe = derive_chunk_context(world_seed, body_id, region, (0, 0)); + fn anchor_chunk_pos(world_seed: u64, body_id: &str, district: &DistrictProfile) -> ChunkPos { + let probe = derive_chunk_context(world_seed, body_id, district, (0, 0)); let idx = probe.channel_anchor_m.div_euclid(CHUNK_M); match probe.basin_direction { BasinDirection::North | BasinDirection::South => (idx, 0), @@ -401,9 +399,9 @@ mod tests { #[test] fn derive_chunk_context_is_deterministic() { - let region = alluvial_region(); - let a = derive_chunk_context(42, "GJ1c", ®ion, (10, 20)); - let b = derive_chunk_context(42, "GJ1c", ®ion, (10, 20)); + let district = alluvial_district(); + let a = derive_chunk_context(42, "GJ1c", &district, (10, 20)); + let b = derive_chunk_context(42, "GJ1c", &district, (10, 20)); assert_eq!(a.basin_direction, b.basin_direction); assert_eq!(a.meander_phase, b.meander_phase); assert_eq!(a.meander_wavelength_m, b.meander_wavelength_m); @@ -413,10 +411,10 @@ mod tests { #[test] fn different_positions_yield_different_phases() { - let region = alluvial_region(); - let a = derive_chunk_context(42, "GJ1c", ®ion, (0, 0)); - let b = derive_chunk_context(42, "GJ1c", ®ion, (200, 100)); - // Different region-scale ids → different phases (high probability). + let district = alluvial_district(); + let a = derive_chunk_context(42, "GJ1c", &district, (0, 0)); + let b = derive_chunk_context(42, "GJ1c", &district, (200, 100)); + // Different district-scale ids → different phases (high probability). assert_ne!( a.meander_phase, b.meander_phase, "chunks far apart should differ in meander phase" @@ -424,31 +422,31 @@ mod tests { } #[test] - fn alluvial_region_has_active_channel() { - let region = alluvial_region(); - // T-1040: the channel is region-anchored — the chunk under the anchor + fn alluvial_district_has_active_channel() { + let district = alluvial_district(); + // T-1040: the channel is district-anchored — the chunk under the anchor // must claim it (ocean_fraction_q=15 → water present). - let pos = anchor_chunk_pos(42, "GJ1c", ®ion); - let ctx = derive_chunk_context(42, "GJ1c", ®ion, pos); + let pos = anchor_chunk_pos(42, "GJ1c", &district); + let ctx = derive_chunk_context(42, "GJ1c", &district, pos); assert!( ctx.has_active_channel, - "anchor-covering chunk of a watered region must have active channel" + "anchor-covering chunk of a watered district must have active channel" ); } #[test] fn chunk_outside_channel_band_has_no_active_channel() { - // T-1040: channels are region-anchored, not region-wide. A chunk whose + // T-1040: channels are district-anchored, not district-wide. A chunk whose // cross-range lies outside the channel's swept band (max reach < 192 m // = 3 chunks) must not claim a channel — pre-fix every chunk of a - // watered region did, while voxels rendered dry floodplain. - let region = alluvial_region(); + // watered district did, while voxels rendered dry floodplain. + let district = alluvial_district(); let (anchor_pos, probe) = { - let pos = anchor_chunk_pos(42, "GJ1c", ®ion); - (pos, derive_chunk_context(42, "GJ1c", ®ion, pos)) + let pos = anchor_chunk_pos(42, "GJ1c", &district); + (pos, derive_chunk_context(42, "GJ1c", &district, pos)) }; // 8 cross-chunks away (512 m) is past any band reach but still inside - // region (0, 0) — the anchor margin keeps the anchor chunk in [3, 12]. + // district (0, 0) — the anchor margin keeps the anchor chunk in [3, 12]. let anchor_idx = anchor_pos.0.max(anchor_pos.1); let far_idx = if anchor_idx < 8 { anchor_idx + 8 @@ -459,7 +457,7 @@ mod tests { BasinDirection::North | BasinDirection::South => (far_idx, 0), BasinDirection::East | BasinDirection::West => (0, far_idx), }; - let far_ctx = derive_chunk_context(42, "GJ1c", ®ion, far_pos); + let far_ctx = derive_chunk_context(42, "GJ1c", &district, far_pos); assert!( !far_ctx.has_active_channel, "chunk {far_pos:?} outside the channel band must not claim a channel" @@ -468,41 +466,48 @@ mod tests { far_ctx.channel_width_m, 0, "no active channel → channel_width_m must be 0" ); - // Region-scale params stay constant across the region's chunks. + // District-scale params stay constant across the district's chunks. assert_eq!(far_ctx.channel_anchor_m, probe.channel_anchor_m); assert_eq!(far_ctx.meander_phase, probe.meander_phase); } #[test] fn anchors_constant_within_region_and_inside_it() { - // T-1040/T-1041: feature anchors are a region property — identical for - // every chunk of the region, and positioned inside the region's extent. - let region = alluvial_region(); - let base = derive_chunk_context(42, "GJ1c", ®ion, (0, 0)); + // T-1040/T-1041: feature anchors are a district property — identical for + // every chunk of the district, and positioned inside the district's extent. + let district = alluvial_district(); + let base = derive_chunk_context(42, "GJ1c", &district, (0, 0)); for pos in [(1, 0), (0, 1), (15, 15), (7, 12)] { - let ctx = derive_chunk_context(42, "GJ1c", ®ion, pos); + let ctx = derive_chunk_context(42, "GJ1c", &district, pos); assert_eq!( ctx.channel_anchor_m, base.channel_anchor_m, - "channel anchor must be region-constant (chunk {pos:?})" + "channel anchor must be district-constant (chunk {pos:?})" ); assert_eq!( ctx.coast_anchor_m, base.coast_anchor_m, - "coast anchor must be region-constant (chunk {pos:?})" + "coast anchor must be district-constant (chunk {pos:?})" ); } - // Region (0, 0) spans [0, 1024) m on both axes. - assert!((0..1024).contains(&base.channel_anchor_m)); - assert!((0..1024).contains(&base.coast_anchor_m)); - // A different region derives its anchors inside its own extent. - let far = derive_chunk_context(42, "GJ1c", ®ion, (1000, -750)); - assert!((62 * 1024..63 * 1024).contains(&far.channel_anchor_m) || (-47 * 1024..-46 * 1024).contains(&far.channel_anchor_m), - "far region anchor {} must lie inside its region extent (cross axis depends on basin direction)", - far.channel_anchor_m); + // District (0, 0) spans [0, scale::DISTRICT_M) m on both axes. + assert!((0..scale::DISTRICT_M).contains(&base.channel_anchor_m)); + assert!((0..scale::DISTRICT_M).contains(&base.coast_anchor_m)); + // A different district derives its anchors inside its own extent. Chunk + // (1000, -750) → district (1000 >> 5, -750 >> 5) = (31, -24); the cross + // axis (and thus which district index the channel anchor sits in) depends + // on the basin direction. + let far = derive_chunk_context(42, "GJ1c", &district, (1000, -750)); + let dm = scale::DISTRICT_M; + assert!( + (31 * dm..32 * dm).contains(&far.channel_anchor_m) + || (-24 * dm..-23 * dm).contains(&far.channel_anchor_m), + "far district anchor {} must lie inside its district extent (cross axis depends on basin direction)", + far.channel_anchor_m + ); } #[test] fn dry_region_has_no_active_channel() { - let region = RegionProfile { + let district = DistrictProfile { morphology_zone: MorphologyZone::AlluvialPlain, tectonic_class: TectonicClass::Stable, glaciation_grade: GlaciationGrade::None, @@ -515,10 +520,10 @@ mod tests { moisture_q: 5, vegetation_class: VegetationClass::Barren, }; - let ctx = derive_chunk_context(42, "dry_body", ®ion, (5, 5)); + let ctx = derive_chunk_context(42, "dry_body", &district, (5, 5)); assert!( !ctx.has_active_channel, - "arid region with ocean_fraction_q=0 must not have active channel" + "arid district with ocean_fraction_q=0 must not have active channel" ); } @@ -526,9 +531,9 @@ mod tests { fn channel_width_in_game_feel_range() { // D-239 §9: river crossings 3–15 m. Measured on a chunk that carries // the channel (T-1040 gating zeroes the width elsewhere). - let region = alluvial_region(); - let pos = anchor_chunk_pos(42, "GJ1c", ®ion); - let ctx = derive_chunk_context(42, "GJ1c", ®ion, pos); + let district = alluvial_district(); + let pos = anchor_chunk_pos(42, "GJ1c", &district); + let ctx = derive_chunk_context(42, "GJ1c", &district, pos); assert!( (3..=15).contains(&ctx.channel_width_m), "channel_width_m {} out of game-feel range [3, 15]", @@ -538,8 +543,8 @@ mod tests { #[test] fn meander_wavelength_within_physics_range() { - let region = alluvial_region(); - let ctx = derive_chunk_context(42, "GJ1c", ®ion, (5, 5)); + let district = alluvial_district(); + let ctx = derive_chunk_context(42, "GJ1c", &district, (5, 5)); // AlluvialPlain flat (slope_q=5): should be near max wavelength. assert!( ctx.meander_wavelength_m > 400.0 && ctx.meander_wavelength_m < 700.0, diff --git a/server/src/atlas/region_profile.rs b/server/src/atlas/district_profile.rs similarity index 93% rename from server/src/atlas/region_profile.rs rename to server/src/atlas/district_profile.rs index 388442490..d0dd1e4d2 100644 --- a/server/src/atlas/region_profile.rs +++ b/server/src/atlas/district_profile.rs @@ -1,8 +1,8 @@ -//! RegionProfile carrier — ~1 km scale tier of the D-239 refinement chain (T-1023). +//! DistrictProfile carrier — ~1 km scale tier of the D-239 refinement chain (T-1023). //! -//! A `RegionProfile` is the coarsest derivation stage: ~1 km² cells (~6 000 per body, +//! A `DistrictProfile` is the coarsest derivation stage: ~1 km² cells (~6 000 per body, //! roughly 80 × 75), each a pure deterministic function of -//! `(seed, body_params, terrain_analysis, pos)`. Stored in `BodyWorldState.regions` +//! `(seed, body_params, terrain_analysis, pos)`. Stored in `BodyWorldState.districts` //! so the Atlas can read zone labels without triggering voxel derivation (D-239 §10, //! D-203). //! @@ -89,7 +89,7 @@ pub enum PrecipitationClass { /// Vegetation cover class (D-239 §2, §8 climate→vegetation law). /// /// Ordered by density: `Forest` > `Scrub` > `Barren`. The D-239 §8 binding law -/// states "Forest→Scrub→Barren, no skip" — a region cannot jump from Forest to +/// states "Forest→Scrub→Barren, no skip" — a district cannot jump from Forest to /// Barren. Integer-discriminant, D-010 compliant. /// /// `None` variant: airless body (`temperature_c == None`) — entire @@ -121,7 +121,7 @@ pub enum VegetationClass { // Body parameters (input to derivation) // --------------------------------------------------------------------------- -/// Body-level physical parameters needed to derive `RegionProfile`. +/// Body-level physical parameters needed to derive `DistrictProfile`. /// /// Modelled on the `BodyRow` reader at `bin/atlas/common.rs`. Source columns /// live on the `bodies` table: `hydrosphere`, `atmosphere`, `planet_class`. @@ -147,10 +147,10 @@ pub struct BodyParams { /// "stable" | "active" | "volcanic" | "tidally_forced". If absent, derived /// from `planet_class`. pub tectonic_activity: Option, - /// Latitude of the region's centre in the body's reference frame, in degrees. + /// Latitude of the district's centre in the body's reference frame, in degrees. /// 0.0 = equator, ±90.0 = poles. Used for latitude-band temperature gradient. - pub region_latitude_deg: f64, - /// Mean elevation of this region relative to sea level, in km. Used for lapse rate. + pub district_latitude_deg: f64, + /// Mean elevation of this district relative to sea level, in km. Used for lapse rate. pub elevation_km: f64, /// `bodies.body_radius_km` (D-204) — the body's radius in km. The single /// body-specific input to the D-243 elastic seam (`scale::regions_per_equator`). @@ -159,27 +159,26 @@ pub struct BodyParams { } // --------------------------------------------------------------------------- -// RegionPos — position on the ~1 km region grid +// DistrictPos — position on the ~1 km district grid // --------------------------------------------------------------------------- -/// Position on the per-body ~1 km region grid. -/// -/// Each axis is a cell index; a ~6 000-region body has ~80 columns × ~75 rows. -/// `BTreeMap` key — implements `Ord` for D-010 determinism. -pub type RegionPos = (i32, i32); +/// Position on the 2 km district grid — re-exported from the canonical ladder +/// ([`crate::atlas::scale`], D-243) so the carrier shares one `DistrictPos` with +/// the addressing layer. `BTreeMap` key — `Ord` for D-010 determinism. +pub use crate::atlas::scale::DistrictPos; // --------------------------------------------------------------------------- -// RegionProfile +// DistrictProfile // --------------------------------------------------------------------------- -/// Per-region (~1 km) terrain classification derived from body params + heightmap. +/// Per-district (~1 km) terrain classification derived from body params + heightmap. /// /// Pure derivation — no per-body authoring. All gating parameters come from /// `BodyParams`; lore-anchored bodies honour their params, not override hooks. -/// Stored in `BodyWorldState.regions` (D-203, D-239 §10). +/// Stored in `BodyWorldState.districts` (D-203, D-239 §10). #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RegionProfile { - /// Region morphology zone (D-239 §6). Uses the **existing** `MorphologyZone` +pub struct DistrictProfile { + /// District morphology zone (D-239 §6). Uses the **existing** `MorphologyZone` /// enum — reconciliation to the D-239 frozen 17-zone vocabulary is T-1027. pub morphology_zone: MorphologyZone, @@ -192,20 +191,20 @@ pub struct RegionProfile { /// Precipitation class, derived from hydrosphere + body params. pub precipitation_class: PrecipitationClass, - /// Integer-quantized slope summary for the region (0–100 scale, units: 1 = ~0.45 °). - /// Derived from the `TerrainAnalysis.slope_deg` cells that fall in this region. + /// Integer-quantized slope summary for the district (0–100 scale, units: 1 = ~0.45 °). + /// Derived from the `TerrainAnalysis.slope_deg` cells that fall in this district. pub slope_q: i32, /// Integer-quantized elevation percentile summary (0–100 scale). - /// Average of `TerrainAnalysis.elev_pct` × 100 across region cells. + /// Average of `TerrainAnalysis.elev_pct` × 100 across district cells. pub elev_q: i32, - /// Ocean fraction for this region (0–100 scale, integer). + /// Ocean fraction for this district (0–100 scale, integer). pub ocean_fraction_q: i32, - /// Per-region river threshold (D-239 §1). Replaces the global + /// Per-district river threshold (D-239 §1). Replaces the global /// `RIVER_THRESHOLD = 200` for tile-layer consumers. The drainage constant - /// itself is unchanged — this value is what `RegionProfile` carries downstream. + /// itself is unchanged — this value is what `DistrictProfile` carries downstream. pub river_threshold: i32, /// Nullable mean-annual temperature in °C (T-1024 forward declaration). @@ -353,8 +352,8 @@ pub fn derive_glaciation_grade_from_climate( /// Airless body (`temperature_c == None`) → `VegetationClass::Absent`. /// All comparisons use integer °C + integer elev_q / moisture_q (D-010). /// -/// `near_perennial_water` signals whether this region is within the riparian -/// band (i.e. the region or an adjacent region carries a perennial waterway). +/// `near_perennial_water` signals whether this district is within the riparian +/// band (i.e. the district or an adjacent district carries a perennial waterway). /// When true the riparian sub-variant is returned: `RiparianThicket` over /// Forest, `RiparianScrub` over Scrub/Barren. pub fn derive_vegetation( @@ -446,7 +445,7 @@ pub fn derive_vegetation( } } -/// Derive per-region river threshold from tectonic class + precipitation (D-239 §1). +/// Derive per-district river threshold from tectonic class + precipitation (D-239 §1). /// /// Replaces the global `RIVER_THRESHOLD = 200` for tile-layer consumers. /// Higher precipitation → lower threshold (more rivers). @@ -469,7 +468,7 @@ pub fn derive_river_threshold(tectonic: TectonicClass, precip: PrecipitationClas (200 + precip_factor + tectonic_bonus).clamp(20, 500) } -/// Derive `MorphologyZone` from `RegionProfile` gating params (D-239 §5, §6, §7). +/// Derive `MorphologyZone` from `DistrictProfile` gating params (D-239 §5, §6, §7). /// /// Implements the 8-family decision tree over integer inputs, in the gate order /// specified by D-239 §5. Hard boolean gates are pre-selection (§5); output is @@ -497,7 +496,7 @@ pub fn derive_river_threshold(tectonic: TectonicClass, precip: PrecipitationClas /// - TidalFlat: very low elev + ocean signal; emitted from the BraidedDelta and DuneStrand gates AND a standalone low-coast gate after Family 5. /// - Estuarine: Delta family + strong ocean signal (brackish tidal zone, river mouth). /// - ValleyFloor and RiverBank are their own gates (ValleyFloor between IncisedGorge and MeanderReach; RiverBank within the MeanderReach gate). -/// - BraidedPlain (§6) is NOT emitted at region scale — distinguishing it from Delta needs a lithology signal (§8 Gravel→braided) RegionProfile lacks; deferred to ChunkContext (see the D-239 §6 implementation note). +/// - BraidedPlain (§6) is NOT emitted at district scale — distinguishing it from Delta needs a lithology signal (§8 Gravel→braided) DistrictProfile lacks; deferred to ChunkContext (see the D-239 §6 implementation note). /// /// D-010: all gates are integer comparisons. No float arithmetic in this function. pub fn derive_morphology_zone( @@ -511,7 +510,7 @@ pub fn derive_morphology_zone( // ── Tier 0: fully submerged ────────────────────────────────────────────── if ocean_fraction_q >= 80 { // Very high ocean fraction: open ocean or lake depending on context. - // No body-scale salinity signal at region level yet; treat all as OpenOcean. + // No body-scale salinity signal at district level yet; treat all as OpenOcean. // Lake differentiation lives at ChunkContext (D-239 §10). return MorphologyZone::OpenOcean; } @@ -613,7 +612,7 @@ pub fn derive_morphology_zone( // Climate constants (T-1024, D-239 §2) // --------------------------------------------------------------------------- -/// Maximum per-region elevation (km), used to scale a region's normalized +/// Maximum per-district elevation (km), used to scale a district's normalized /// elevation (`elev_q`, 0–100) into the temperature-lapse input. Earth-like /// span (~Everest); a coarse body-agnostic constant for now — a per-body relief /// span can replace it when body relief data is carried (T-1032). @@ -813,7 +812,7 @@ pub fn derive_temperature_c( let maritime = constants.maritime_factor(hydrosphere); let mid = (cold + warm) * 0.5; let half = band_width * 0.5 * maritime; - let lat_frac = (params.region_latitude_deg.abs() as f32 / 90.0).clamp(0.0, 1.0); + let lat_frac = (params.district_latitude_deg.abs() as f32 / 90.0).clamp(0.0, 1.0); // equator (frac 0) → mid + half; pole (frac 1) → mid − half. let t_lat = (mid + half) - (2.0 * half) * lat_frac; @@ -892,32 +891,32 @@ pub fn derive_moisture_q(params: &BodyParams) -> i32 { // Public derivation function // --------------------------------------------------------------------------- -/// Derive a `RegionProfile` for the region at `pos` on a `~1km` grid. +/// Derive a `DistrictProfile` for the district at `pos` on a `~1km` grid. /// /// Pure (no I/O, no side effects). Inputs are the body's params and the /// pre-computed `TerrainAnalysis` from Layer 1. /// -/// `grid_cells_per_region` controls how many heightmap cells map to one region -/// cell; default is 8 (at 128×64 working grid, that yields ~80×64 regions ≈ -/// ~5 000 regions/body, within the D-203 ~6 000/body budget). +/// `grid_cells_per_district` controls how many heightmap cells map to one district +/// cell; default is 8 (at 128×64 working grid, that yields ~80×64 districts ≈ +/// ~5 000 districts/body, within the D-203 ~6 000/body budget). /// /// Temperature and moisture are derived inline via D-239 §2 / D-240 climate /// functions (T-1024). Pass a `&ClimateConstants` to control the tuning constants. /// The seed chain provides the body-scoped seed for the D-240 temperature nudge. -pub fn derive_region_profile( +pub fn derive_district_profile( seed: SeedChain, body_params: &BodyParams, ta: &TerrainAnalysis, - pos: RegionPos, - grid_cells_per_region: usize, + pos: DistrictPos, + grid_cells_per_district: usize, climate: &ClimateConstants, -) -> RegionProfile { +) -> DistrictProfile { let (rx, ry) = pos; let w = ta.w; let h = ta.h; - let gcpr = grid_cells_per_region.max(1); + let gcpr = grid_cells_per_district.max(1); - // Compute aggregate terrain statistics over the cells in this region. + // Compute aggregate terrain statistics over the cells in this district. // All arithmetic is integer or quantized-integer (D-010). let mut slope_sum: i64 = 0; let mut elev_sum: i64 = 0; @@ -954,12 +953,12 @@ pub fn derive_region_profile( let tectonic_class = derive_tectonic_class(body_params); // Climate derivation (T-1024, D-239 §2). Temperature lapse must vary by THIS - // region's elevation — otherwise every region on a body shares one body-level + // district's elevation — otherwise every district on a body shares one body-level // elevation and gets an identical lapse, defeating the per-district temperature - // primitive. Build a region-local BodyParams whose elevation_km comes from the - // region's own normalized elevation (elev_q, 0–100) scaled to the body's - // elevation span. region_latitude_deg is already per-region (set by the caller - // / derive_all_regions). Per-cell refinement happens later at ChunkContext. + // primitive. Build a district-local BodyParams whose elevation_km comes from the + // district's own normalized elevation (elev_q, 0–100) scaled to the body's + // elevation span. district_latitude_deg is already per-district (set by the caller + // / derive_all_districts). Per-cell refinement happens later at ChunkContext. let region_climate_params = BodyParams { elevation_km: (elev_q as f64 / 100.0) * MAX_REGION_ELEVATION_KM, ..body_params.clone() @@ -976,7 +975,7 @@ pub fn derive_region_profile( let glaciation_grade = derive_glaciation_grade_from_climate(temperature_c, moisture_q); let river_threshold = derive_river_threshold(tectonic_class, precipitation_class); - // Vegetation class (T-1025, D-239 §8). No riparian signal at region scale yet + // Vegetation class (T-1025, D-239 §8). No riparian signal at district scale yet // (requires perennial waterway map from L2+); default to false for now. // L2 ChunkContext will override per-tile once drainage data is threaded through. let vegetation_class = derive_vegetation(temperature_c, moisture_q, elev_q, false); @@ -990,7 +989,7 @@ pub fn derive_region_profile( moisture_q, ); - RegionProfile { + DistrictProfile { morphology_zone, tectonic_class, glaciation_grade, @@ -1005,47 +1004,47 @@ pub fn derive_region_profile( } } -/// Derive region profiles for all regions covering the body. +/// Derive district profiles for all districts covering the body. /// -/// Returns a `BTreeMap` covering the full -/// heightmap at the given region-grid resolution. +/// Returns a `BTreeMap` covering the full +/// heightmap at the given district-grid resolution. /// -/// `grid_cells_per_region = 8` means each region is 8×8 heightmap cells. +/// `grid_cells_per_district = 8` means each district is 8×8 heightmap cells. /// -/// Region latitude is derived from the row index: ry=0 maps to the north pole -/// (+90°), ry=region_rows-1 maps to the south pole (-90°). This is a linear +/// District latitude is derived from the row index: ry=0 maps to the north pole +/// (+90°), ry=district_rows-1 maps to the south pole (-90°). This is a linear /// mapping across the equirectangular heightmap. -pub fn derive_all_regions( +pub fn derive_all_districts( seed: SeedChain, body_params: &BodyParams, ta: &TerrainAnalysis, - grid_cells_per_region: usize, -) -> BTreeMap { + grid_cells_per_district: usize, +) -> BTreeMap { let climate = ClimateConstants::default(); - let gcpr = grid_cells_per_region.max(1); - let region_cols = ta.w.div_ceil(gcpr) as i32; - let region_rows = ta.h.div_ceil(gcpr) as i32; + let gcpr = grid_cells_per_district.max(1); + let district_cols = ta.w.div_ceil(gcpr) as i32; + let district_rows = ta.h.div_ceil(gcpr) as i32; let mut out = BTreeMap::new(); - for ry in 0..region_rows { + for ry in 0..district_rows { // Map ry to latitude: row 0 → +90°, row (rows-1) → -90°. // For a single-row grid, latitude is 0°. - let lat_deg = if region_rows > 1 { - 90.0 - (ry as f64 / (region_rows - 1) as f64) * 180.0 + let lat_deg = if district_rows > 1 { + 90.0 - (ry as f64 / (district_rows - 1) as f64) * 180.0 } else { 0.0 }; - for rx in 0..region_cols { + for rx in 0..district_cols { let pos = (rx, ry); - // Build per-region params: body-level params + this region's latitude. - // Per-region elevation is NOT set here — derive_region_profile owns it, - // deriving elevation_km from the region's own elev_q (not the caller's + // Build per-district params: body-level params + this district's latitude. + // Per-district elevation is NOT set here — derive_district_profile owns it, + // deriving elevation_km from the district's own elev_q (not the caller's // body_params.elevation_km). Per-cell refinement happens at ChunkContext (D-239). - let region_params = BodyParams { - region_latitude_deg: lat_deg, + let district_params = BodyParams { + district_latitude_deg: lat_deg, ..body_params.clone() }; - let profile = derive_region_profile(seed, ®ion_params, ta, pos, gcpr, &climate); + let profile = derive_district_profile(seed, &district_params, ta, pos, gcpr, &climate); out.insert(pos, profile); } } @@ -1092,18 +1091,18 @@ mod tests { } #[test] - fn derive_all_regions_covers_full_heightmap() { + fn derive_all_districts_covers_full_heightmap() { let hm = test_hm(); let ta = test_ta(&hm); let params = BodyParams::default(); - let regions = derive_all_regions(test_seed(), ¶ms, &ta, 8); + let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8); - // Expected: ceil(64/8) × ceil(32/8) = 8 × 4 = 32 regions. - assert_eq!(regions.len(), 32, "region count mismatch"); + // Expected: ceil(64/8) × ceil(32/8) = 8 × 4 = 32 districts. + assert_eq!(districts.len(), 32, "district count mismatch"); } #[test] - fn derive_region_profile_is_deterministic() { + fn derive_district_profile_is_deterministic() { let hm = test_hm(); let ta = test_ta(&hm); // D-240: no orbit/star fields in BodyParams — only class/atmo/hydro. @@ -1115,8 +1114,8 @@ mod tests { }; let pos = (2, 1); let climate = ClimateConstants::default(); - let p1 = derive_region_profile(test_seed(), ¶ms, &ta, pos, 8, &climate); - let p2 = derive_region_profile(test_seed(), ¶ms, &ta, pos, 8, &climate); + let p1 = derive_district_profile(test_seed(), ¶ms, &ta, pos, 8, &climate); + let p2 = derive_district_profile(test_seed(), ¶ms, &ta, pos, 8, &climate); // Equality via serialized fields (no PartialEq on MorphologyZone — compare by name). assert_eq!( format!("{:?}", p1.morphology_zone), @@ -1210,10 +1209,10 @@ mod tests { let hm = test_hm(); let ta = test_ta(&hm); let params = BodyParams::default(); - let regions = derive_all_regions(test_seed(), ¶ms, &ta, 8); + let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8); // BTreeMap iterates in sorted key order — verify the first key is (0,0). - let first = regions.keys().next().expect("at least one region"); - assert_eq!(*first, (0, 0), "first region must be at origin"); + let first = districts.keys().next().expect("at least one district"); + assert_eq!(*first, (0, 0), "first district must be at origin"); } #[test] @@ -1241,7 +1240,7 @@ mod tests { let params = BodyParams { planet_class: Some(planet_class.into()), atmosphere: Some(atmosphere.into()), - region_latitude_deg: lat, + district_latitude_deg: lat, elevation_km: elev_km, ..Default::default() }; @@ -1317,7 +1316,7 @@ mod tests { let params = BodyParams { atmosphere: Some("thin".into()), planet_class: Some("arid".into()), - region_latitude_deg: 45.0, + district_latitude_deg: 45.0, elevation_km: 1.5, ..Default::default() }; @@ -1334,7 +1333,7 @@ mod tests { let params = BodyParams { atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), - region_latitude_deg: 30.0, + district_latitude_deg: 30.0, elevation_km: 0.0, ..Default::default() }; @@ -1389,7 +1388,7 @@ mod tests { planet_class: Some((*class).into()), atmosphere: Some((*atmo).into()), hydrosphere: Some(hydro.into()), - region_latitude_deg: lat, + district_latitude_deg: lat, elevation_km: elev, ..Default::default() }; @@ -1418,7 +1417,7 @@ mod tests { planet_class: Some("temperate".into()), atmosphere: Some("standard".into()), hydrosphere: Some(hydro.into()), - region_latitude_deg: lat, + district_latitude_deg: lat, elevation_km: 0.0, ..Default::default() }; @@ -1444,7 +1443,7 @@ mod tests { let params = BodyParams { planet_class: Some("unknown_alien_class".into()), atmosphere: Some("breathable".into()), - region_latitude_deg: 0.0, + district_latitude_deg: 0.0, elevation_km: 0.0, ..Default::default() }; @@ -1739,14 +1738,14 @@ mod tests { #[test] fn vegetation_riparian_upgrades_scrub_to_riparian_scrub() { - // A scrub-zone region near perennial water → RiparianScrub. + // A scrub-zone district near perennial water → RiparianScrub. let v = derive_vegetation(Some(20.0), 60, 85, true); // high elev = scrub zone assert_eq!(v, VegetationClass::RiparianScrub); } #[test] fn vegetation_riparian_upgrades_forest_to_riparian_thicket() { - // A forest-zone region near perennial water → RiparianThicket. + // A forest-zone district near perennial water → RiparianThicket. let v = derive_vegetation(Some(20.0), 60, 10, true); // low elev = forest zone assert_eq!(v, VegetationClass::RiparianThicket); } @@ -1839,7 +1838,7 @@ mod tests { #[test] fn morphology_zone_region_scale_emits_16_of_17() { - // Enumerate the zones the region-scale classifier can emit across a + // Enumerate the zones the district-scale classifier can emit across a // representative input grid. 16 of the 17 frozen zones are reachable here; // BraidedPlain is the lone exception — see the assertion comment below. use std::collections::BTreeSet; @@ -1866,15 +1865,15 @@ mod tests { } } } - // BraidedPlain (discriminant 11) is the ONE frozen zone the region-scale + // BraidedPlain (discriminant 11) is the ONE frozen zone the district-scale // classifier never emits: distinguishing it from Delta needs a lithology - // signal (§8 Gravel→braided) that RegionProfile doesn't carry yet, so it is + // signal (§8 Gravel→braided) that DistrictProfile doesn't carry yet, so it is // deferred to ChunkContext (D-239 §6 implementation note). All other 16 are // reachable. (Lake IS reachable — ocean_fraction 60–79 → Lake, ≥80 → OpenOcean.) assert_eq!( seen.len(), 16, - "expected exactly 16/17 region-reachable zones (BraidedPlain deferred), got: {seen:?}" + "expected exactly 16/17 district-reachable zones (BraidedPlain deferred), got: {seen:?}" ); } diff --git a/server/src/atlas/gen_queue.rs b/server/src/atlas/gen_queue.rs index 496a6eda5..0af3c458d 100644 --- a/server/src/atlas/gen_queue.rs +++ b/server/src/atlas/gen_queue.rs @@ -30,8 +30,8 @@ use crossbeam_channel::{Receiver, Sender}; use crate::atlas::attractor_matching::CityRecord; use crate::atlas::body_world_state::BodyWorldState; use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer}; +use crate::atlas::district_profile::BodyParams; use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W}; -use crate::atlas::region_profile::BodyParams; use crate::atlas::skeleton_gen::{assign_all_block_tags, generate_quarter_skeleton}; use crate::seed::SeedChain; use crate::simulation::generator::{CityGenerationContext, QuarterWorldState}; @@ -77,8 +77,8 @@ pub enum GenWorkItem { /// time. Drives Layer-3 TerritorialStatus + spatial character (#956, /// D-212/214/215). `None` → `FrontierUnclaimed`. dominant_faction: Option, - /// Body physical parameters for the RegionProfile layer (T-1023, D-239 §1). - /// Pre-resolved at dispatch time. `None` → region layer skipped for this body. + /// Body physical parameters for the DistrictProfile layer (T-1023, D-239 §1). + /// Pre-resolved at dispatch time. `None` → district layer skipped for this body. /// Boxed: `BodyParams` is large relative to other variants (clippy /// large_enum_variant) — boxing keeps `GenWorkItem` compact. body_params: Option>, @@ -375,8 +375,8 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion { hm }; // Run the full cascade through RoadGraph (Layer 2, T-1038), the - // terminal layer. It subsumes Settlement, RegionProfile (T-1023), - // and all prior layers. RegionProfile derivation still gates on + // terminal layer. It subsumes Settlement, DistrictProfile (T-1023), + // and all prior layers. DistrictProfile derivation still gates on // body_params internally (skipped when absent — e.g. a body with no // params row), but the road graph needs no body params, so it runs // for every analyzed body. diff --git a/server/src/atlas/layer_proxy.rs b/server/src/atlas/layer_proxy.rs index 8939554ea..0e3d60306 100644 --- a/server/src/atlas/layer_proxy.rs +++ b/server/src/atlas/layer_proxy.rs @@ -67,11 +67,11 @@ pub struct AtlasLayerResponse { /// cascade still runs Layer 1; the body just gets no settlement placements. /// /// `body_params_reader` supplies the body's physical parameters for the -/// RegionProfile carrier layer (T-1032, D-239 §1), read on a cache miss. +/// DistrictProfile carrier layer (T-1032, D-239 §1), read on a cache miss. /// `None` (or a read failure) passes `body_params: None` to the work item, /// causing the cascade to stop at `CascadeLayer::Settlement` (pre-T-1032 /// behaviour). A successful read passes `Some(Box::new(params))`, enabling -/// the full `CascadeLayer::RegionProfile` path. +/// the full `CascadeLayer::DistrictProfile` path. pub fn handle_atlas_request( req: &AtlasLayerRequest, cache: &mut BodyWorldStateCache, @@ -145,7 +145,7 @@ pub fn handle_atlas_request( tracing::warn!( body_id = %req.body_id, error = %e, - "body_params read failed; region layer skipped" + "body_params read failed; district layer skipped" ); None }), @@ -272,7 +272,7 @@ mod tests { placements: vec![], road_graph: crate::atlas::road_graph::RoadGraph::default(), quarters: std::collections::BTreeMap::new(), - regions: std::collections::BTreeMap::new(), + districts: std::collections::BTreeMap::new(), last_accessed: 0, }); let (_db, resolver) = empty_resolver(); @@ -371,6 +371,7 @@ mod tests { hydrosphere TEXT, atmosphere TEXT, planet_class TEXT, + body_radius_km REAL, orbital_period_days REAL, axial_tilt_deg REAL );", @@ -383,8 +384,8 @@ mod tests { ) .unwrap(); conn.execute( - "INSERT INTO bodies (body_id, system_id, terrain_reference, hydrosphere, atmosphere, planet_class, orbital_period_days, axial_tilt_deg) - VALUES (?1, 'GJ-1', ?2, 'ocean', 'breathable', 'temperate', 365.25, 23.5)", + "INSERT INTO bodies (body_id, system_id, terrain_reference, hydrosphere, atmosphere, planet_class, body_radius_km, orbital_period_days, axial_tilt_deg) + VALUES (?1, 'GJ-1', ?2, 'ocean', 'breathable', 'temperate', 6371.0, 365.25, 23.5)", rusqlite::params![body_id, REL], ) .unwrap(); @@ -400,7 +401,7 @@ mod tests { } /// With body_params_reader wired, a cache miss enqueues an AnalyzeBody that - /// completes with populated `regions` (RegionProfile layer ran). + /// completes with populated `districts` (DistrictProfile layer ran). #[test] fn body_params_reader_wired_produces_populated_regions() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); @@ -435,12 +436,12 @@ mod tests { .expect("AnalyzeBody must complete for GJ1c"); assert!( - !body_state.regions.is_empty(), - "regions must be populated when body_params_reader is wired (T-1032 dispatch path)" + !body_state.districts.is_empty(), + "districts must be populated when body_params_reader is wired (T-1032 dispatch path)" ); } - /// Without body_params_reader (None), regions is empty — pre-T-1032 behaviour. + /// Without body_params_reader (None), districts is empty — pre-T-1032 behaviour. #[test] fn no_body_params_reader_leaves_regions_empty() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); @@ -474,8 +475,8 @@ mod tests { .expect("AnalyzeBody must complete for GJ1c"); assert!( - body_state.regions.is_empty(), - "regions must remain empty when no body_params_reader is wired" + body_state.districts.is_empty(), + "districts must remain empty when no body_params_reader is wired" ); } } diff --git a/server/src/atlas/mod.rs b/server/src/atlas/mod.rs index 7fc16f8c4..b34c7f340 100644 --- a/server/src/atlas/mod.rs +++ b/server/src/atlas/mod.rs @@ -11,6 +11,7 @@ pub mod cascade; pub mod chunk_context; pub mod city_context_reader; pub mod district_mix; +pub mod district_profile; pub mod domain_warp; pub mod drainage; pub mod features; @@ -19,7 +20,6 @@ pub mod heightmap; pub mod layer1; pub mod layer_proxy; pub mod plugin; -pub mod region_profile; pub mod road_graph; pub mod scale; pub mod skeleton_gen; diff --git a/server/src/atlas/voxel.rs b/server/src/atlas/voxel.rs index b9898d09d..94d982b67 100644 --- a/server/src/atlas/voxel.rs +++ b/server/src/atlas/voxel.rs @@ -1,7 +1,7 @@ //! VoxelColumn — 1 m derivation tier of the D-239 refinement chain (T-1028). //! //! A `VoxelColumn` is the finest derivation tier: derived on-demand from a -//! `ChunkContext` (64 m) and the covering `RegionProfile` (~1 km). Never stored, +//! `ChunkContext` (64 m) and the covering `DistrictProfile` (~1 km). Never stored, //! never persisted — cached in `VoxelCache` (D-227). //! //! ## D-228 composite tile axes @@ -16,7 +16,7 @@ //! //! **Snow/Ice are NOT `TerrainMaterial`** — they are a seasonal cover overlay //! per D-228. `derive_cover` (T-1030) computes the static mean-state cover from -//! the region's integer temperature + water/terrain type + spatially-coherent +//! the district's integer temperature + water/terrain type + spatially-coherent //! cluster scatter (D-239 §3). The transient/clock-bound part (cover forms in the //! cold phase, melts in the warm phase) is deferred to Q-105. //! @@ -57,8 +57,8 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::atlas::chunk_context::ChunkContext; +use crate::atlas::district_profile::{DistrictProfile, VegetationClass}; use crate::atlas::domain_warp::domain_warp; -use crate::atlas::region_profile::{RegionProfile, VegetationClass}; use crate::seed::{SeedChain, SeedDomain}; use crate::simulation::generator::MorphologyZone; @@ -107,7 +107,7 @@ pub enum FloorMaterial { /// Ground cover (D-228). Carries cover/concealment, movement sound profile, /// and economic yield (timber/crops). /// -/// Derived from the region's `VegetationClass` (D-239 §8 climate→vegetation law). +/// Derived from the district's `VegetationClass` (D-239 §8 climate→vegetation law). /// Integer-discriminant, append-only (D-010). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[repr(u8)] @@ -130,9 +130,9 @@ pub enum Vegetation { } impl Vegetation { - /// Map a region-level `VegetationClass` to the per-voxel `Vegetation` axis. + /// Map a district-level `VegetationClass` to the per-voxel `Vegetation` axis. /// - /// The region class establishes the dominant cover; per-voxel scatter + /// The district class establishes the dominant cover; per-voxel scatter /// (Grass vs Scrub at the margin) is handled by the voxel generator using /// the sub-chunk seed. This function gives the canonical deterministic /// baseline before scatter is applied. @@ -170,7 +170,7 @@ pub enum Water { /// /// Snow and Ice are **not** `TerrainMaterial` — they are a derived overlay that /// sits on top of the permanent ground material. This axis captures the -/// **static mean-state** cover: whether the region's mean-annual temperature +/// **static mean-state** cover: whether the district's mean-annual temperature /// puts this voxel firmly in the frozen zone. The **transient, clock-bound** part /// (cover forms in the cold diurnal/seasonal phase, melts in the warm phase → /// time-of-day passability shifts) is out of scope here. @@ -181,7 +181,7 @@ pub enum Water { /// day/night), in the marginal band ice/snow forms in the cold phase and melts in /// the warm phase — dawn frost burns off, a stream iced at dawn is crossable by /// noon. Passability is therefore time-of-day dynamic." This dynamic behaviour -/// requires the cheap region seasonal/clock state described in Q-105 — the +/// requires the cheap district seasonal/clock state described in Q-105 — the /// `cover` field here captures only the mean-state freeze. When Q-105 is /// implemented, consumers must additionally consult the regional clock-phase /// before treating `cover` as a passability gate. The static `SeasonalCover` @@ -207,7 +207,7 @@ pub enum SeasonalCover { /// The finest derivation tier — the D-228 composite tile axes for one 1 m column. /// -/// Derived on demand from `(seed, body_id, region, chunk_context, voxel_pos)`. +/// Derived on demand from `(seed, body_id, district, chunk_context, voxel_pos)`. /// Never stored, never persisted. Cached in `VoxelCache` (D-227). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct VoxelColumn { @@ -215,7 +215,7 @@ pub struct VoxelColumn { pub terrain: TerrainMaterial, /// Built surface over the ground (D-228). `None` in Phase 4. pub floor: FloorMaterial, - /// Ground cover (D-228). Derived from region `VegetationClass`. + /// Ground cover (D-228). Derived from district `VegetationClass`. pub vegetation: Vegetation, /// Local depth state (D-228). pub water: Water, @@ -246,7 +246,7 @@ pub type VoxelPos = (i32, i32); /// Internal family identifier for the 8-family morphology dispatch (D-239 §5). /// /// Derived from `MorphologyZone` by `zone_to_family`. The dispatch gates are -/// pre-computed at zone classification time (RegionProfile); the family is the +/// pre-computed at zone classification time (DistrictProfile); the family is the /// structural decision that drives voxel geometry. /// /// All 8 families have dedicated generators: `AlluvialPlain` (T-1028, also the @@ -275,10 +275,10 @@ enum MorphologyFamily { /// /// Sub-classified zones (TidalFlat, Estuarine, Alpine, Wetland) map to their /// parent family for voxel geometry — their distinction is captured in the -/// region zone label, not in the voxel generator dispatch. +/// district zone label, not in the voxel generator dispatch. /// -/// BraidedPlain maps to BraidedDelta (its parent at region scale, per the -/// D-239 §6 implementation note — BraidedPlain is deferred from RegionProfile +/// BraidedPlain maps to BraidedDelta (its parent at district scale, per the +/// D-239 §6 implementation note — BraidedPlain is deferred from DistrictProfile /// to ChunkContext sub-classification). fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily { match zone { @@ -320,7 +320,7 @@ fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily { /// /// - `world_seed` — master world seed. /// - `body_id` — stable string body identifier (used for domain separation). -/// - `region` — the covering `RegionProfile` (~1 km). +/// - `district` — the covering `DistrictProfile` (~1 km). /// - `chunk` — the covering `ChunkContext` (64 m). /// - `tile_x`, `tile_y` — logical tile position in **integer metres** (D-010). /// @@ -330,7 +330,7 @@ fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily { pub fn derive_voxel_column( world_seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, tile_x: i32, tile_y: i32, @@ -346,45 +346,45 @@ pub fn derive_voxel_column( // ── 2. Sub-chunk seed (for features with wavelength < 64 m) ─────────── // Keyed on integer voxel address after warp — deterministic (D-010). Uses the // dedicated SeedDomain::Voxel (not ChunkContext) so the per-voxel stream can - // never collide with the region-scale meander seed (D-224 domain separation). + // never collide with the district-scale meander seed (D-224 domain separation). let sub_chunk_seed = SeedChain::for_body(world_seed, body_id) .derive(SeedDomain::Voxel, voxel_pos_to_id(voxel_pos)) .seed(); // ── 3. Family dispatch (D-239 §5) ───────────────────────────────────── - let family = zone_to_family(®ion.morphology_zone); + let family = zone_to_family(&district.morphology_zone); let mut column = match family { MorphologyFamily::AlluvialPlain => { - generate_alluvial_plain(region, chunk, voxel_pos, sub_chunk_seed) + generate_alluvial_plain(district, chunk, voxel_pos, sub_chunk_seed) } - MorphologyFamily::LavaField => generate_lava_field(region, voxel_pos, sub_chunk_seed), + MorphologyFamily::LavaField => generate_lava_field(district, voxel_pos, sub_chunk_seed), MorphologyFamily::FjordWall => { - generate_fjord_wall(region, chunk, voxel_pos, sub_chunk_seed) + generate_fjord_wall(district, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::CliffCoast => { - generate_cliff_coast(region, chunk, voxel_pos, sub_chunk_seed) + generate_cliff_coast(district, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::BraidedDelta => { - generate_braided_delta(region, chunk, voxel_pos, sub_chunk_seed) + generate_braided_delta(district, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::DuneStrand => { - generate_dune_strand(region, chunk, voxel_pos, sub_chunk_seed) + generate_dune_strand(district, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::IncisedGorge => { - generate_incised_gorge(region, chunk, voxel_pos, sub_chunk_seed) + generate_incised_gorge(district, chunk, voxel_pos, sub_chunk_seed) } MorphologyFamily::MeanderReach => { - generate_meander_reach(region, chunk, voxel_pos, sub_chunk_seed) + generate_meander_reach(district, chunk, voxel_pos, sub_chunk_seed) } }; // ── 4. Seasonal cover overlay (D-239 §3, T-1030) ────────────────────── // Applied AFTER family dispatch: the 8 family generators produce the base // axes (terrain/water/vegetation/elevation); cover is a separate orthogonal - // axis derived from the region's mean temperature + water/terrain + coherent + // axis derived from the district's mean temperature + water/terrain + coherent // cluster scatter. One site, set here — no family generator needs changing. - column.cover = derive_cover(world_seed, body_id, region, &column, voxel_pos); + column.cover = derive_cover(world_seed, body_id, district, &column, voxel_pos); column } @@ -397,8 +397,8 @@ pub fn derive_voxel_column( /// /// Flat floodplain with: /// - `Soil` terrain material (D-239 §8 Soil law: rolling/floodplain) -/// - Vegetation from the region's `vegetation_class` (D-239 §2) -/// - Flat elevation derived from region `elev_q` + small local scatter +/// - Vegetation from the district's `vegetation_class` (D-239 §2) +/// - Flat elevation derived from district `elev_q` + small local scatter /// - Meander channel cut from `ChunkContext` (D-239 §10: one basin direction /// + global meander params, NOT a per-tile flow_direction grid) /// - Water state: Shallow/Deep in channel, Dry elsewhere @@ -406,14 +406,14 @@ pub fn derive_voxel_column( /// ## Meander channel placement /// /// The channel is a sine-wave approximation running along `basin_direction`, -/// centred on the region-anchored `channel_anchor_m` (T-1040). The +/// centred on the district-anchored `channel_anchor_m` (T-1040). The /// perpendicular distance from the voxel to the wave determines whether the /// voxel is in-channel. This avoids per-tile flow grids while producing a /// spatially coherent channel continuous across chunk boundaries (D-239 §10). /// /// Channel width is `chunk.channel_width_m` (game-feel range 3–15 m). fn generate_alluvial_plain( - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, @@ -421,8 +421,8 @@ fn generate_alluvial_plain( // ── Terrain material ─────────────────────────────────────────────────── // AlluvialPlain → Soil (D-239 §8 Soil law). Wetland sub-zone → Wetland // substrate when moisture is very high (§8 Wetland ≤5° flats law). - let terrain = if matches!(region.morphology_zone, MorphologyZone::Wetland) - || (region.slope_q <= 5 && region.moisture_q >= 60) + let terrain = if matches!(district.morphology_zone, MorphologyZone::Wetland) + || (district.slope_q <= 5 && district.moisture_q >= 60) { TerrainMaterial::Wetland } else { @@ -430,17 +430,17 @@ fn generate_alluvial_plain( }; // ── Vegetation ───────────────────────────────────────────────────────── - // Base vegetation from region class; sub-chunk scatter (a few Grass/Scrub + // Base vegetation from district class; sub-chunk scatter (a few Grass/Scrub // variations) uses `sub_chunk_seed` — wavelength < 64 m. - let base_veg = Vegetation::from_vegetation_class(region.vegetation_class); + let base_veg = Vegetation::from_vegetation_class(district.vegetation_class); let vegetation = scatter_vegetation(base_veg, sub_chunk_seed); // ── Base elevation ────────────────────────────────────────────────────── - // Convert region `elev_q` (0–100) to metres. We scale 0–100 → 0–50 m here + // Convert district `elev_q` (0–100) to metres. We scale 0–100 → 0–50 m here // (walking-skeleton fidelity: a rough body-relative elevation). // Sub-chunk micro-relief: small integer scatter in [-4, +3] m from the seed // (3 low bits, minus 4). The final elevation is clamped to >= 0 below. - let base_elev_m = region.elev_q / 2; + let base_elev_m = district.elev_q / 2; let micro_relief = (sub_chunk_seed & 0x7) as i32 - 4; // 3 bits → [-4, +3] let elevation_m = (base_elev_m + micro_relief).max(0); @@ -503,7 +503,7 @@ fn generate_alluvial_plain( /// - No active water channel regardless of `chunk.has_active_channel` — /// immature drainage means no organised river network on fresh lava. fn generate_lava_field( - region: &RegionProfile, + district: &DistrictProfile, _voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { @@ -512,7 +512,7 @@ fn generate_lava_field( // the shield adds a small parabolic falloff from the centre. At voxel scale // we simulate this as a low-amplitude multi-scale noise on a flat base. // Integer arithmetic throughout (D-010). - let base_elev_m = region.elev_q / 2; + let base_elev_m = district.elev_q / 2; // Coarse lava block texture: bits [0:2] of seed → ±2 m vertical scatter. let lava_block = (sub_chunk_seed & 0x7) as i32 - 3; // [−3, +4] @@ -532,7 +532,7 @@ fn generate_lava_field( // A lava tube depression may collect a shallow puddle (moisture-gated). // Very rare — only in humid zones (moisture_q ≥ 70). No Deep water on // shield slopes (immature drainage has no deep organised channels). - let water = if tube_depression < -2 && region.moisture_q >= 70 { + let water = if tube_depression < -2 && district.moisture_q >= 70 { Water::Shallow } else { Water::Dry @@ -563,11 +563,11 @@ fn generate_lava_field( /// - Valley floor is narrow (chokepoint — D-239 §9) with `Deep` water. /// - Walls rise steeply; rocky, very little vegetation. /// -/// The fjord trough runs along `basin_direction` at the region-anchored +/// The fjord trough runs along `basin_direction` at the district-anchored /// centreline `chunk.channel_anchor_m` (T-1041, D-239 §10): ONE valley spans -/// the region's chunks. Cross-section: wall | moraine | Deep water | moraine | wall. +/// the district's chunks. Cross-section: wall | moraine | Deep water | moraine | wall. fn generate_fjord_wall( - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, @@ -582,7 +582,7 @@ fn generate_fjord_wall( | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; - // Cross-channel distance from the region-anchored trough centreline + // Cross-channel distance from the district-anchored trough centreline // (T-1041): continuous world coordinates — folding into the 64 m chunk // frame repeated a complete fjord in every chunk (D-239 §10 violation). let cross_from_centre = (cross - chunk.channel_anchor_m).abs(); @@ -593,7 +593,7 @@ fn generate_fjord_wall( // // GlaciationGrade modifies the floor width, wall steepness, and cirque presence. // Upstream classifier gate guarantees GlaciationGrade ≥ 2 for FjordWall (D-239 §5). - let glacier_grade = region.glaciation_grade as i32; // guaranteed ≥ 2 + let glacier_grade = district.glaciation_grade as i32; // guaranteed ≥ 2 // Fjord floor half-width: 2–4 m (total 4–8 m, D-239 §9 compliant). // Stronger glaciation carves a wider U-trough, but stays within §9 spec. @@ -602,7 +602,7 @@ fn generate_fjord_wall( let moraine_outer = floor_half + 6 + glacier_grade; // 10–14 m from centre // Base elevation for the wall platform — high ground context. - let wall_elev_base = (region.elev_q / 2).max(20); // at least 20 m walls + let wall_elev_base = (district.elev_q / 2).max(20); // at least 20 m walls let (elevation_m, water, vegetation) = if cross_from_centre <= floor_half { // ── Fjord floor — deep water ───────────────────────────────────── @@ -626,7 +626,7 @@ fn generate_fjord_wall( }; let elev = (wall_elev_base / 4 + moraine_height + moraine_noise).max(0); // Moraines hold some Shallow pools (meltwater) in humid zones. - let water = if region.moisture_q >= 60 && moraine_noise >= 2 { + let water = if district.moisture_q >= 60 && moraine_noise >= 2 { Water::Shallow } else { Water::Dry @@ -639,8 +639,8 @@ fn generate_fjord_wall( let wall_dist = cross_from_centre - moraine_outer; // Steep linear rise: 6 m per metre of cross-distance (approximates // vertical), saturating at a 120 m plateau 20 m out — cross distance - // is region-scale post-T-1041, so the rise must not grow unbounded to - // the region edge (clamp before multiplying: i32 overflow otherwise). + // is district-scale post-T-1041, so the rise must not grow unbounded to + // the district edge (clamp before multiplying: i32 overflow otherwise). let wall_rise = wall_dist.min(20) * 6; // Cirques (grade ≥ 2): occasional hollowed pocket (2–4 m depression) at // upper wall. seed-gated, ~1-in-8 frequency. @@ -654,7 +654,7 @@ fn generate_fjord_wall( let wall_noise = ((sub_chunk_seed >> 1) & 0x7) as i32 - 4; // [−4, +3] let elev = (wall_elev_base + wall_rise + wall_noise - cirque_depression).max(0); // Very sparse vegetation on high rocky walls. - let veg = match region.vegetation_class { + let veg = match district.vegetation_class { VegetationClass::Forest | VegetationClass::Scrub => { if (sub_chunk_seed >> 12) & 0xF < 2 { Vegetation::Scrub @@ -690,21 +690,21 @@ fn generate_fjord_wall( /// Vertical rock face at the coastal water edge: /// - `Rock` terrain (D-239 §8 Rock→vertical faces law). /// - Sharp elevation drop to `Shallow`/`Deep` water at the base. -/// - The coast face sits on the region-anchored line `chunk.coast_anchor_m` +/// - The coast face sits on the district-anchored line `chunk.coast_anchor_m` /// along the seaward axis (T-1041, D-239 §10): ONE continuous coast line -/// per region (warp-displaced), not a 64 m sawtooth. Inland of the line is +/// per district (warp-displaced), not a 64 m sawtooth. Inland of the line is /// high ground; seaward is ocean. /// - Narrow ledge/platform at the cliff base (passable ground, Shallow). /// - Virtually no flat ground (D-239 §8 — Rock→vertical, not terraced). fn generate_cliff_coast( - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Coast orientation ───────────────────────────────────────────────── // basin_direction points seaward (water flows to ocean). coast_d is the - // signed seaward distance from the region-anchored coast line in + // signed seaward distance from the district-anchored coast line in // continuous world metres (T-1041): negative = inland, positive = seaward. let coast_d = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North => chunk.coast_anchor_m - voxel_pos.1, @@ -719,8 +719,8 @@ fn generate_cliff_coast( // Ledge (8..13): rocky beach platform, Shallow. // Ocean (≥13): Deep water. // - // Cliff height derived from region elevation. - let cliff_top_elev = (region.elev_q / 2).max(15); // at least 15 m cliff + // Cliff height derived from district elevation. + let cliff_top_elev = (district.elev_q / 2).max(15); // at least 15 m cliff // Rock-face jaggedness noise: bits [0:2] → ±3 m. let face_noise = (sub_chunk_seed & 0x7) as i32 - 4; @@ -749,8 +749,8 @@ fn generate_cliff_coast( // Vegetation: nearly barren on rock faces; tiny scatter of Scrub on the // inland top (≥18 m / ≥28 m inland of the face — same offsets as the old - // chunk-frame layout, now measured from the region-anchored coast line). - let vegetation = match region.vegetation_class { + // chunk-frame layout, now measured from the district-anchored coast line). + let vegetation = match district.vegetation_class { VegetationClass::Forest | VegetationClass::RiparianThicket => { if coast_d < -18 && (sub_chunk_seed >> 10) & 0x7 < 3 { Vegetation::Scrub @@ -790,14 +790,14 @@ fn generate_cliff_coast( /// - Very low elevation — near sea level (D-239 §8 drainage monotonicity: /// mouths at sea level). /// - Channel determination: 3 independent braided threads anastomosing -/// across a fan belt centred on the region-anchored axis +/// across a fan belt centred on the district-anchored axis /// `chunk.channel_anchor_m` (T-1041, D-239 §10), each thread 3–8 m wide. -/// Thread offsets are seeded at region scale (meander_phase encodes them), -/// so the threads run continuously across the region's chunks instead of +/// Thread offsets are seeded at district scale (meander_phase encodes them), +/// so the threads run continuously across the district's chunks instead of /// restarting every 64 m. /// - `Shallow` in-channel; `Dry` on gravel bars between threads. fn generate_braided_delta( - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, @@ -805,8 +805,8 @@ fn generate_braided_delta( // ── Base elevation ──────────────────────────────────────────────────── // Delta is near sea level. elev_q typically very low in delta zones; cap // at 5 m to honour drainage monotonicity (mouths at sea level). - let base_elev_m = (region.elev_q / 4).min(5); // 0–5 m; near sea level - // Gravel bar micro-relief: ±1 m scatter on bars between channels. + let base_elev_m = (district.elev_q / 4).min(5); // 0–5 m; near sea level + // Gravel bar micro-relief: ±1 m scatter on bars between channels. let bar_noise = (sub_chunk_seed & 0x3) as i32; // 0–3 // ── Braided thread geometry ─────────────────────────────────────────── @@ -819,8 +819,8 @@ fn generate_braided_delta( }; // Three braided threads with well-separated centres derived from meander_phase, - // spread across a 64 m fan belt centred on the region-anchored axis - // (T-1041 — world cross coordinates, constant for all chunks of the region). + // spread across a 64 m fan belt centred on the district-anchored axis + // (T-1041 — world cross coordinates, constant for all chunks of the district). // Each centre uses independent bit-mixing (splitmix64-style) so threads are // provably separated regardless of phase value — no correlated bit-slices. let phase_u64 = chunk.meander_phase as u64; @@ -878,8 +878,8 @@ fn generate_braided_delta( }; // Vegetation: riparian scrub on stable bars; barren on active channel sediment. - let vegetation = if !in_any_thread && region.moisture_q >= 50 { - match region.vegetation_class { + let vegetation = if !in_any_thread && district.moisture_q >= 50 { + match district.vegetation_class { VegetationClass::Forest | VegetationClass::RiparianThicket | VegetationClass::RiparianScrub => { @@ -926,7 +926,7 @@ fn generate_braided_delta( /// - Mostly `Dry`; `Shallow` at the seaward toe where surf meets sand. /// - `Barren` or very sparse vegetation (coastal strand). fn generate_dune_strand( - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, @@ -947,12 +947,12 @@ fn generate_dune_strand( // Low slope → long dunes (8–20 m wavelength); high slope → shorter (4–8 m). // Integer arithmetic (D-010). let dune_wavelength = { - let slope_clamped = region.slope_q.clamp(0, 40); + let slope_clamped = district.slope_q.clamp(0, 40); 20 - slope_clamped / 2 // 20 m flat → 4 m at slope 32 } .max(4); - // Phase offset from meander_phase (region-scale, D-239 §10). + // Phase offset from meander_phase (district-scale, D-239 §10). let phase = chunk.meander_phase as i32; let along_mod = (along_wind + phase).rem_euclid(dune_wavelength); @@ -961,11 +961,11 @@ fn generate_dune_strand( // D-239 §8 Sand law: ≤32° angle of repose enforced by the integer physics cap: // tan(32°) ≈ 0.625 → max_height ≤ 0.625 × (wavelength/2). // The integer form `(wavelength * 625) / 2000` directly encodes this. - // Also capped by slope_q/4 (region terrain height proxy) to give local variety. + // Also capped by slope_q/4 (district terrain height proxy) to give local variety. let max_height = { let physics_cap = (dune_wavelength * 625) / 2000; // tan(32°)×wavelength/2, integer - let region_cap = (region.slope_q / 4).max(1); // region terrain height proxy - physics_cap.min(region_cap).max(1) + let district_cap = (district.slope_q / 4).max(1); // district terrain height proxy + physics_cap.min(district_cap).max(1) }; let half_wl = dune_wavelength / 2; let dune_height = if along_mod < half_wl { @@ -981,7 +981,7 @@ fn generate_dune_strand( // Bits [4:6] of seed → ±1 m additional scatter on the dune surface. let surface_scatter = ((sub_chunk_seed >> 4) & 0x3) as i32 - 1; // [−1, +2] - let base_strand_elev = (region.elev_q / 8).max(0); // 0–12 m strand base + let base_strand_elev = (district.elev_q / 8).max(0); // 0–12 m strand base let elevation_m = (base_strand_elev + dune_height + cross_noise + surface_scatter).max(0); // ── Water state ─────────────────────────────────────────────────────── @@ -996,7 +996,7 @@ fn generate_dune_strand( // ── Vegetation ──────────────────────────────────────────────────────── // Dunes are mostly barren; scattered dune grass on stable crests // in humid zones. No trees (wind/salt prevents forest). - let vegetation = if region.moisture_q >= 55 && dune_height >= max_height / 2 { + let vegetation = if district.moisture_q >= 55 && dune_height >= max_height / 2 { if (sub_chunk_seed >> 8) & 0x7 < 2 { Vegetation::Grass // dune grass on humid crests } else { @@ -1029,11 +1029,11 @@ fn generate_dune_strand( /// - Massive elevation differential: walls are 20–60 m above the floor. /// - `Shallow` water in the incised channel at the floor. /// - Cross-section: wall | wall | narrow floor (with channel) | wall | wall. -/// - The gorge runs along `basin_direction` at the region-anchored +/// - The gorge runs along `basin_direction` at the district-anchored /// centreline `chunk.channel_anchor_m` (T-1041, D-239 §10): ONE gorge -/// spans the region's chunks. +/// spans the district's chunks. fn generate_incised_gorge( - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, @@ -1046,7 +1046,7 @@ fn generate_incised_gorge( | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; - // Distance from the region-anchored gorge centreline (T-1041): continuous + // Distance from the district-anchored gorge centreline (T-1041): continuous // world coordinates — folding into the 64 m chunk frame carved a gorge in // every chunk (D-239 §10 violation). let cross_from_centre = (cross - chunk.channel_anchor_m).abs(); @@ -1058,9 +1058,9 @@ fn generate_incised_gorge( // ── Wall geometry ───────────────────────────────────────────────────── // The wall rises steeply: each metre away from the floor adds ~8 m of - // height. Wall top at region elev_q. - let wall_top = (region.elev_q / 2).max(30); // at least 30 m wall height - let floor_elev = (region.elev_q / 8).max(0); // floor is much lower than surroundings + // height. Wall top at district elev_q. + let wall_top = (district.elev_q / 2).max(30); // at least 30 m wall height + let floor_elev = (district.elev_q / 8).max(0); // floor is much lower than surroundings // Rock wall jaggedness: bits [0:3] → ±4 m. let wall_noise = (sub_chunk_seed & 0xF) as i32 - 8; // [−8, +7] @@ -1080,7 +1080,7 @@ fn generate_incised_gorge( let wall_dist = cross_from_centre - floor_half; // Steep wall: 8 m rise per metre of wall distance (near-vertical, §8 // Rock law). The rise saturates at the wall_top clamp below; clamp the - // region-scale distance (T-1041) before multiplying — i32 overflow at + // district-scale distance (T-1041) before multiplying — i32 overflow at // extreme coordinates otherwise. let wall_rise = wall_dist.min(16) * 8; let elev = (floor_elev + wall_rise + wall_noise).clamp(floor_elev, wall_top); @@ -1089,8 +1089,8 @@ fn generate_incised_gorge( // Vegetation: barren in the gorge (shadow / rock). Tiny scatter of Scrub // on the upper wall rim in humid temperate zones. - let vegetation = if cross_from_centre > (floor_half + 20) && region.moisture_q >= 60 { - match region.vegetation_class { + let vegetation = if cross_from_centre > (floor_half + 20) && district.moisture_q >= 60 { + match district.vegetation_class { VegetationClass::Forest | VegetationClass::RiparianThicket => { if (sub_chunk_seed >> 8) & 0x7 < 2 { Vegetation::Scrub @@ -1133,17 +1133,17 @@ fn generate_incised_gorge( /// - `river crossings 3–15 m` width (D-239 §9 game-feel constraint). /// - Levees above / channel below the floodplain surface /// (D-239 §9 ElevationDelta: channels below, levees above high-water threshold). -/// - Floodplain vegetation from region class with riparian band at waterway. +/// - Floodplain vegetation from district class with riparian band at waterway. fn generate_meander_reach( - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, voxel_pos: VoxelPos, sub_chunk_seed: u64, ) -> VoxelColumn { // ── Terrain material ─────────────────────────────────────────────────── // MeanderReach → Soil. Wetland sub-zone possible when very wet and flat. - let terrain = if matches!(region.morphology_zone, MorphologyZone::Wetland) - || (region.slope_q <= 3 && region.moisture_q >= 70) + let terrain = if matches!(district.morphology_zone, MorphologyZone::Wetland) + || (district.slope_q <= 3 && district.moisture_q >= 70) { TerrainMaterial::Wetland } else { @@ -1154,7 +1154,7 @@ fn generate_meander_reach( // MeanderReach is a low-gradient floodplain; similar to AlluvialPlain but // typically lower (mature river valley). Map elev_q → metres with a // slightly lower ceiling than AlluvialPlain. - let base_elev_m = region.elev_q / 3; // 0–33 m range (lower than Alluvial's /2) + let base_elev_m = district.elev_q / 3; // 0–33 m range (lower than Alluvial's /2) let micro_relief = (sub_chunk_seed & 0x7) as i32 - 4; // [−4, +3] m let floodplain_elev = (base_elev_m + micro_relief).max(0); @@ -1184,7 +1184,7 @@ fn generate_meander_reach( // Shallow channel margin — 2 m below floodplain. let elev = (floodplain_elev - 2).max(0); // Riparian thicket along the channel bank (D-239 §8 climate→vegetation). - let veg = match region.vegetation_class { + let veg = match district.vegetation_class { VegetationClass::Absent | VegetationClass::Barren => Vegetation::Barren, _ => Vegetation::Thicket, }; @@ -1203,8 +1203,8 @@ fn generate_meander_reach( floodplain_elev + levee_extra }; - // Vegetation from region class; riparian thicket near the levee. - let base_veg = Vegetation::from_vegetation_class(region.vegetation_class); + // Vegetation from district class; riparian thicket near the levee. + let base_veg = Vegetation::from_vegetation_class(district.vegetation_class); let vegetation = scatter_vegetation(base_veg, sub_chunk_seed); (levee_elev, Water::Dry, vegetation) }; @@ -1408,13 +1408,13 @@ fn cluster_scatter(world_seed: u64, body_id: &str, voxel_x: i32, voxel_y: i32) - pub fn derive_cover( world_seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, column: &VoxelColumn, voxel_pos: VoxelPos, ) -> SeasonalCover { // ── Airless body (D-239 §2) ─────────────────────────────────────────── // No atmosphere → no climate branch → no seasonal cover. - let Some(temp_f) = region.temperature_c else { + let Some(temp_f) = district.temperature_c else { return SeasonalCover::None; }; @@ -1429,7 +1429,7 @@ pub fn derive_cover( // this value, producing ragged clustered patches (D-239 §3). let scatter = cluster_scatter(world_seed, body_id, voxel_x, voxel_y); - let surface = classify_surface(®ion.morphology_zone, column.water); + let surface = classify_surface(&district.morphology_zone, column.water); match surface { // ── Fresh water (lakes / rivers) ────────────────────────────────── @@ -1522,7 +1522,7 @@ pub fn derive_cover( // ── Land (snow) ─────────────────────────────────────────────────── SurfaceClass::Land => { // Moisture gate: cold + dry → bare frozen ground (no snow). - if region.moisture_q < SNOW_MOISTURE_GATE { + if district.moisture_q < SNOW_MOISTURE_GATE { return SeasonalCover::None; } if temp_i >= SNOW_BAND_HIGH_C { @@ -1578,7 +1578,7 @@ fn compute_meander_reach_channel( amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl.max(1) }; - // Region-anchored centreline (T-1040) — same anchor as AlluvialPlain. + // District-anchored centreline (T-1040) — same anchor as AlluvialPlain. let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); // Edge jitter from sub-chunk seed (±1 m), same as AlluvialPlain. @@ -1620,7 +1620,7 @@ fn in_levee_band(voxel_pos: VoxelPos, chunk: &ChunkContext, sub_chunk_seed: u64) amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl.max(1) }; - // Region-anchored centreline (T-1040) — same anchor as the channel itself. + // District-anchored centreline (T-1040) — same anchor as the channel itself. let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); let edge_jitter = (sub_chunk_seed & 0x3) as i32; @@ -1690,9 +1690,9 @@ fn compute_channel_state( amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl }; - // Perpendicular distance from voxel to the region-anchored channel + // Perpendicular distance from voxel to the district-anchored channel // centreline (T-1040): the channel axis is `chunk.channel_anchor_m` on the - // cross axis — a region-scale world coordinate, never the world origin. + // cross axis — a district-scale world coordinate, never the world origin. let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); // Sub-chunk noise: ±1 m jitter on the channel edge (wavelength < 64 m). @@ -1710,7 +1710,7 @@ fn compute_channel_state( /// Apply sub-chunk vegetation scatter using the sub-chunk seed. /// -/// The base class from the region establishes the dominant cover type; the +/// The base class from the district establishes the dominant cover type; the /// scatter adds variety within the class (e.g. some Grass in a Forest zone /// at clearings). Wavelength < 64 m — seeded from sub_chunk_seed (D-239 §10). /// @@ -1834,7 +1834,7 @@ impl VoxelCache { &mut self, world_seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, chunk: &ChunkContext, tile_x: i32, tile_y: i32, @@ -1859,7 +1859,7 @@ impl VoxelCache { } // Cache miss — derive and insert. - let column = derive_voxel_column(world_seed, body_id, region, chunk, tile_x, tile_y); + let column = derive_voxel_column(world_seed, body_id, district, chunk, tile_x, tile_y); // Evict LRU if at capacity. if self.entries.len() >= self.capacity { @@ -1938,7 +1938,7 @@ pub const DEFAULT_VOXEL_CACHE_CAPACITY: usize = 8192; mod tests { use super::*; use crate::atlas::chunk_context::{derive_chunk_context, BasinDirection}; - use crate::atlas::region_profile::{ + use crate::atlas::district_profile::{ GlaciationGrade, PrecipitationClass, TectonicClass, VegetationClass, }; @@ -1946,18 +1946,18 @@ mod tests { // Test fixtures // ----------------------------------------------------------------------- - /// The ChunkContext of the region-(0,0) chunk whose cross-range contains + /// The ChunkContext of the district-(0,0) chunk whose cross-range contains /// the channel anchor — guaranteed inside the T-1040 channel band. - /// Feature placement is region-anchored, so tests sample around + /// Feature placement is district-anchored, so tests sample around /// `chunk.channel_anchor_m` instead of assuming chunk-frame positions. - fn anchor_chunk(seed: u64, body: &str, region: &RegionProfile) -> ChunkContext { - let probe = derive_chunk_context(seed, body, region, (0, 0)); + fn anchor_chunk(seed: u64, body: &str, district: &DistrictProfile) -> ChunkContext { + let probe = derive_chunk_context(seed, body, district, (0, 0)); let idx = probe.channel_anchor_m.div_euclid(64); let pos = match probe.basin_direction { BasinDirection::North | BasinDirection::South => (idx, 0), BasinDirection::East | BasinDirection::West => (0, idx), }; - derive_chunk_context(seed, body, region, pos) + derive_chunk_context(seed, body, district, pos) } /// Map a (cross, along) coordinate pair to (tile_x, tile_y) for the @@ -1969,8 +1969,8 @@ mod tests { } } - fn alluvial_region() -> RegionProfile { - RegionProfile { + fn alluvial_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::AlluvialPlain, tectonic_class: TectonicClass::Stable, glaciation_grade: GlaciationGrade::None, @@ -1985,8 +1985,8 @@ mod tests { } } - fn alluvial_chunk(region: &RegionProfile) -> ChunkContext { - derive_chunk_context(42, "GJ1c", region, (10, 20)) + fn alluvial_chunk(district: &DistrictProfile) -> ChunkContext { + derive_chunk_context(42, "GJ1c", district, (10, 20)) } // ----------------------------------------------------------------------- @@ -2017,10 +2017,10 @@ mod tests { #[test] fn alluvial_plain_produces_soil_terrain() { - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); // Sample a voxel that shouldn't be in a wetland zone. - let col = derive_voxel_column(42, "GJ1c", ®ion, &chunk, 100, 100); + let col = derive_voxel_column(42, "GJ1c", &district, &chunk, 100, 100); assert_eq!( col.terrain, TerrainMaterial::Soil, @@ -2030,26 +2030,26 @@ mod tests { #[test] fn alluvial_plain_floor_is_none() { - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); - let col = derive_voxel_column(42, "GJ1c", ®ion, &chunk, 100, 100); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); + let col = derive_voxel_column(42, "GJ1c", &district, &chunk, 100, 100); assert_eq!(col.floor, FloorMaterial::None, "Phase 4 floor must be None"); } #[test] - fn alluvial_plain_vegetation_from_region_class() { - let region = alluvial_region(); // vegetation_class = Forest - let chunk = alluvial_chunk(®ion); + fn alluvial_plain_vegetation_from_district_class() { + let district = alluvial_district(); // vegetation_class = Forest + let chunk = alluvial_chunk(&district); // Multiple samples to account for sub-chunk scatter; all should be // in the Forest/Scrub/Grass range (D-239 §8: no-skip law — no Barren in Forest zone). for tx in [100, 101, 102, 103, 104] { - let col = derive_voxel_column(42, "GJ1c", ®ion, &chunk, tx, 100); + let col = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, 100); assert!( matches!( col.vegetation, Vegetation::Forest | Vegetation::Scrub | Vegetation::Grass ), - "Forest region must produce Forest/Scrub/Grass vegetation, got {:?}", + "Forest district must produce Forest/Scrub/Grass vegetation, got {:?}", col.vegetation ); } @@ -2057,10 +2057,10 @@ mod tests { #[test] fn elevation_is_non_negative() { - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { - let col = derive_voxel_column(42, "GJ1c", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, ty); assert!( col.elevation_m >= 0, "elevation must be >= 0, got {}", @@ -2073,17 +2073,17 @@ mod tests { // Determinism — the core T-1028 contract // ----------------------------------------------------------------------- - /// End-to-end determinism: same (seed, body, region) → same voxel columns. + /// End-to-end determinism: same (seed, body, district) → same voxel columns. /// Two calls at the same position must produce identical results. #[test] fn voxel_derivation_is_deterministic() { - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); let positions = [(100, 100), (0, 0), (-50, 25), (200, -10), (64, 64)]; for (tx, ty) in positions { - let a = derive_voxel_column(42, "GJ1c", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(42, "GJ1c", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, ty); + let b = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, ty); assert_eq!( a, b, "voxel at ({tx},{ty}) must be identical on repeated derivation" @@ -2094,10 +2094,10 @@ mod tests { /// Different positions must (in general) produce different columns. #[test] fn different_positions_produce_different_outputs() { - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); - let a = derive_voxel_column(42, "GJ1c", ®ion, &chunk, 100, 100); - let b = derive_voxel_column(42, "GJ1c", ®ion, &chunk, 1000, 2000); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); + let a = derive_voxel_column(42, "GJ1c", &district, &chunk, 100, 100); + let b = derive_voxel_column(42, "GJ1c", &district, &chunk, 1000, 2000); // Not strictly guaranteed (hash collision possible) but overwhelmingly likely. assert!( a != b || a.elevation_m != b.elevation_m, @@ -2108,11 +2108,11 @@ mod tests { /// Different world seeds produce different results. #[test] fn different_seeds_produce_different_outputs() { - let region = alluvial_region(); - let chunk1 = derive_chunk_context(1, "GJ1c", ®ion, (10, 20)); - let chunk2 = derive_chunk_context(2, "GJ1c", ®ion, (10, 20)); - let a = derive_voxel_column(1, "GJ1c", ®ion, &chunk1, 100, 100); - let b = derive_voxel_column(2, "GJ1c", ®ion, &chunk2, 100, 100); + let district = alluvial_district(); + let chunk1 = derive_chunk_context(1, "GJ1c", &district, (10, 20)); + let chunk2 = derive_chunk_context(2, "GJ1c", &district, (10, 20)); + let a = derive_voxel_column(1, "GJ1c", &district, &chunk1, 100, 100); + let b = derive_voxel_column(2, "GJ1c", &district, &chunk2, 100, 100); assert!( a != b, "different world seeds must produce different voxel outputs" @@ -2125,13 +2125,13 @@ mod tests { #[test] fn cache_returns_same_column_as_direct_derivation() { - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); let body_id = "GJ1c"; let mut cache = VoxelCache::new(64); - let cached = cache.get_or_derive(42, body_id, ®ion, &chunk, 100, 100); - let direct = derive_voxel_column(42, body_id, ®ion, &chunk, 100, 100); + let cached = cache.get_or_derive(42, body_id, &district, &chunk, 100, 100); + let direct = derive_voxel_column(42, body_id, &district, &chunk, 100, 100); assert_eq!( cached, direct, "cache must return same result as direct derivation" @@ -2140,14 +2140,14 @@ mod tests { #[test] fn cache_second_call_is_a_hit() { - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); let body_id = "GJ1c"; let mut cache = VoxelCache::new(64); - let first = cache.get_or_derive(42, body_id, ®ion, &chunk, 100, 100); + let first = cache.get_or_derive(42, body_id, &district, &chunk, 100, 100); assert_eq!(cache.len(), 1, "first call must insert one entry"); - let second = cache.get_or_derive(42, body_id, ®ion, &chunk, 100, 100); + let second = cache.get_or_derive(42, body_id, &district, &chunk, 100, 100); assert_eq!( cache.len(), 1, @@ -2158,8 +2158,8 @@ mod tests { #[test] fn cache_evicts_lru_on_overflow() { - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); let body_id = "GJ1c"; let capacity = 8; @@ -2167,16 +2167,16 @@ mod tests { // Fill cache to capacity with distinct positions. for i in 0..capacity as i32 { - cache.get_or_derive(42, body_id, ®ion, &chunk, i * 64, 0); + cache.get_or_derive(42, body_id, &district, &chunk, i * 64, 0); } assert_eq!(cache.len(), capacity); // Access position 0 to refresh it (not the LRU). - cache.get_or_derive(42, body_id, ®ion, &chunk, 0, 0); + cache.get_or_derive(42, body_id, &district, &chunk, 0, 0); // Insert a new entry — should evict the LRU (position 64, not the // just-refreshed position 0). - cache.get_or_derive(42, body_id, ®ion, &chunk, 999, 999); + cache.get_or_derive(42, body_id, &district, &chunk, 999, 999); assert_eq!( cache.len(), capacity, @@ -2200,22 +2200,22 @@ mod tests { #[test] fn cache_eviction_does_not_break_derivation() { // After eviction, re-deriving the evicted entry must produce the same result. - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); let body_id = "GJ1c"; let capacity = 2; let mut cache = VoxelCache::new(capacity); // Populate the cache fully. - let col0 = cache.get_or_derive(42, body_id, ®ion, &chunk, 0, 0); - let _col1 = cache.get_or_derive(42, body_id, ®ion, &chunk, 100, 0); + let col0 = cache.get_or_derive(42, body_id, &district, &chunk, 0, 0); + let _col1 = cache.get_or_derive(42, body_id, &district, &chunk, 100, 0); // Insert a third entry — evicts (0,0) which is the LRU. - let _col2 = cache.get_or_derive(42, body_id, ®ion, &chunk, 200, 0); + let _col2 = cache.get_or_derive(42, body_id, &district, &chunk, 200, 0); // Re-derive position (0,0) — cache miss, must re-derive correctly. - let col0_again = cache.get_or_derive(42, body_id, ®ion, &chunk, 0, 0); + let col0_again = cache.get_or_derive(42, body_id, &district, &chunk, 0, 0); assert_eq!( col0, col0_again, "re-derived column after cache eviction must be identical" @@ -2231,12 +2231,12 @@ mod tests { // The warp should produce different columns at nearby integer tile // coordinates — the warp displaces the lookup point, so adjacent tiles // should not always look identical even when terrain is homogeneous. - let region = alluvial_region(); - let chunk = alluvial_chunk(®ion); + let district = alluvial_district(); + let chunk = alluvial_chunk(&district); // Sample a grid of 4 neighbouring tiles; at least some should differ. let cols: Vec = (0..4) - .map(|i| derive_voxel_column(42, "GJ1c", ®ion, &chunk, i, 0)) + .map(|i| derive_voxel_column(42, "GJ1c", &district, &chunk, i, 0)) .collect(); let all_same = cols.windows(2).all(|w| w[0] == w[1]); assert!( @@ -2253,8 +2253,8 @@ mod tests { // T-1029 — LavaField // ----------------------------------------------------------------------- - fn lava_region() -> RegionProfile { - RegionProfile { + fn lava_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::Volcanic, tectonic_class: TectonicClass::Volcanic, vegetation_class: VegetationClass::Barren, @@ -2262,16 +2262,16 @@ mod tests { slope_q: 8, elev_q: 30, moisture_q: 20, - ..alluvial_region() + ..alluvial_district() } } #[test] fn lava_field_terrain_is_lava() { - let region = lava_region(); - let chunk = derive_chunk_context(42, "Io", ®ion, (0, 0)); + let district = lava_district(); + let chunk = derive_chunk_context(42, "Io", &district, (0, 0)); for pos in [(10, 10), (0, 0), (50, 25), (-5, 5)] { - let col = derive_voxel_column(42, "Io", ®ion, &chunk, pos.0, pos.1); + let col = derive_voxel_column(42, "Io", &district, &chunk, pos.0, pos.1); assert_eq!( col.terrain, TerrainMaterial::Lava, @@ -2283,10 +2283,10 @@ mod tests { #[test] fn lava_field_vegetation_is_barren() { - let region = lava_region(); - let chunk = derive_chunk_context(42, "Io", ®ion, (0, 0)); + let district = lava_district(); + let chunk = derive_chunk_context(42, "Io", &district, (0, 0)); for tx in 0..20i32 { - let col = derive_voxel_column(42, "Io", ®ion, &chunk, tx, 10); + let col = derive_voxel_column(42, "Io", &district, &chunk, tx, 10); assert_eq!( col.vegetation, Vegetation::Barren, @@ -2298,10 +2298,10 @@ mod tests { #[test] fn lava_field_no_deep_water() { // Immature drainage law (D-239 §8): no deep organised channels on lava. - let region = lava_region(); - let chunk = derive_chunk_context(42, "Io", ®ion, (0, 0)); + let district = lava_district(); + let chunk = derive_chunk_context(42, "Io", &district, (0, 0)); for (tx, ty) in (0..50).map(|i| (i * 7, i * 3)) { - let col = derive_voxel_column(42, "Io", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "Io", &district, &chunk, tx, ty); assert_ne!( col.water, Water::Deep, @@ -2312,21 +2312,21 @@ mod tests { #[test] fn lava_field_is_deterministic() { - let region = lava_region(); - let chunk = derive_chunk_context(7, "Io", ®ion, (5, 3)); + let district = lava_district(); + let chunk = derive_chunk_context(7, "Io", &district, (5, 3)); for (tx, ty) in [(0, 0), (10, 20), (-5, 7)] { - let a = derive_voxel_column(7, "Io", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(7, "Io", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(7, "Io", &district, &chunk, tx, ty); + let b = derive_voxel_column(7, "Io", &district, &chunk, tx, ty); assert_eq!(a, b, "LavaField must be deterministic at ({tx},{ty})"); } } #[test] fn lava_field_elevation_non_negative() { - let region = lava_region(); - let chunk = derive_chunk_context(42, "Io", ®ion, (0, 0)); + let district = lava_district(); + let chunk = derive_chunk_context(42, "Io", &district, (0, 0)); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { - let col = derive_voxel_column(42, "Io", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "Io", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "LavaField elevation must be >= 0"); } } @@ -2335,8 +2335,8 @@ mod tests { // T-1029 — FjordWall // ----------------------------------------------------------------------- - fn fjord_region() -> RegionProfile { - RegionProfile { + fn fjord_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::Fjord, tectonic_class: TectonicClass::Active, glaciation_grade: GlaciationGrade::Moderate, // grade ≥ 2 @@ -2345,16 +2345,16 @@ mod tests { ocean_fraction_q: 30, moisture_q: 70, vegetation_class: VegetationClass::Barren, - ..alluvial_region() + ..alluvial_district() } } #[test] fn fjord_wall_terrain_is_rock() { - let region = fjord_region(); - let chunk = derive_chunk_context(42, "Fjordheim", ®ion, (0, 0)); + let district = fjord_district(); + let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0)); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { - let col = derive_voxel_column(42, "Fjordheim", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Rock, @@ -2365,15 +2365,15 @@ mod tests { #[test] fn fjord_wall_centre_has_deep_water() { - // The fjord trough (region-anchored centreline, T-1041) must have Deep + // The fjord trough (district-anchored centreline, T-1041) must have Deep // water. Sample a small window around the anchor — the domain warp can // displace any single tile off the 4–8 m floor. - let region = fjord_region(); - let chunk = derive_chunk_context(42, "Fjordheim", ®ion, (0, 0)); + let district = fjord_district(); + let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0)); let anchor = chunk.channel_anchor_m; let any_deep = (anchor - 4..=anchor + 4).any(|c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); - derive_voxel_column(42, "Fjordheim", ®ion, &chunk, tx, ty).water == Water::Deep + derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty).water == Water::Deep }); assert!( any_deep, @@ -2384,20 +2384,20 @@ mod tests { #[test] fn fjord_wall_walls_are_high_relative_to_floor() { // Wall elevation must be substantially higher than fjord floor. - let region = fjord_region(); - let chunk = derive_chunk_context(42, "Fjordheim", ®ion, (0, 0)); + let district = fjord_district(); + let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0)); let anchor = chunk.channel_anchor_m; // Floor: the lowest tile in the trough window around the anchor. let floor_elev = (anchor - 4..=anchor + 4) .map(|c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); - derive_voxel_column(42, "Fjordheim", ®ion, &chunk, tx, ty).elevation_m + derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty).elevation_m }) .min() .unwrap(); // Wall: 30 m off the centreline (past the moraine band at ≤14 m). let (wx, wy) = cross_along_to_tile(&chunk, anchor - 30, 50); - let wall_col = derive_voxel_column(42, "Fjordheim", ®ion, &chunk, wx, wy); + let wall_col = derive_voxel_column(42, "Fjordheim", &district, &chunk, wx, wy); assert!( wall_col.elevation_m > floor_elev, "FjordWall walls ({}) must be higher than the floor ({})", @@ -2408,21 +2408,21 @@ mod tests { #[test] fn fjord_wall_is_deterministic() { - let region = fjord_region(); - let chunk = derive_chunk_context(11, "Fjordheim", ®ion, (2, 3)); + let district = fjord_district(); + let chunk = derive_chunk_context(11, "Fjordheim", &district, (2, 3)); for (tx, ty) in [(32, 50), (0, 50), (63, 20)] { - let a = derive_voxel_column(11, "Fjordheim", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(11, "Fjordheim", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(11, "Fjordheim", &district, &chunk, tx, ty); + let b = derive_voxel_column(11, "Fjordheim", &district, &chunk, tx, ty); assert_eq!(a, b, "FjordWall must be deterministic at ({tx},{ty})"); } } #[test] fn fjord_wall_elevation_non_negative() { - let region = fjord_region(); - let chunk = derive_chunk_context(42, "Fjordheim", ®ion, (0, 0)); + let district = fjord_district(); + let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0)); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { - let col = derive_voxel_column(42, "Fjordheim", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "FjordWall elevation must be >= 0"); } } @@ -2432,15 +2432,15 @@ mod tests { // D-239 §9: fjord floor (Deep water) must be 2–8 m wide across a cross-section. // With floor_half = glacier_grade.clamp(2,4) and grade ≥ 2 (upstream gate), // total Deep-water width = 2×floor_half + noise ∈ [4, 8] m. - // Cross-section spans the region-anchored trough (T-1041). - let region = fjord_region(); // glaciation_grade = Moderate (= 2) - let chunk = derive_chunk_context(42, "Fjordheim", ®ion, (0, 0)); + // Cross-section spans the district-anchored trough (T-1041). + let district = fjord_district(); // glaciation_grade = Moderate (= 2) + let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0)); let anchor = chunk.channel_anchor_m; // Count Deep-water tiles across a 64-tile cross-section around the anchor. let deep_tiles = (anchor - 32..anchor + 32) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); - derive_voxel_column(42, "Fjordheim", ®ion, &chunk, tx, ty).water == Water::Deep + derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty).water == Water::Deep }) .count(); assert!( @@ -2453,8 +2453,8 @@ mod tests { // T-1029 — CliffCoast // ----------------------------------------------------------------------- - fn cliff_region() -> RegionProfile { - RegionProfile { + fn cliff_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::CliffCoast, tectonic_class: TectonicClass::Active, slope_q: 70, @@ -2462,16 +2462,16 @@ mod tests { ocean_fraction_q: 40, moisture_q: 50, vegetation_class: VegetationClass::Barren, - ..alluvial_region() + ..alluvial_district() } } #[test] fn cliff_coast_terrain_is_rock() { - let region = cliff_region(); - let chunk = derive_chunk_context(42, "Velen", ®ion, (0, 0)); + let district = cliff_district(); + let chunk = derive_chunk_context(42, "Velen", &district, (0, 0)); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { - let col = derive_voxel_column(42, "Velen", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "Velen", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Rock, @@ -2484,16 +2484,16 @@ mod tests { fn cliff_coast_steep_elevation_drop_near_water() { // Verify the CliffCoast structural property: there must be tiles with Deep // water AND there must be a significant elevation range (high ground + ocean). - // The coast face sits on the region-anchored line `coast_anchor_m` along + // The coast face sits on the district-anchored line `coast_anchor_m` along // the seaward (basin) axis (T-1041) — sample a transect across it. - let region = cliff_region(); - let chunk = derive_chunk_context(42, "Velen", ®ion, (0, 0)); + let district = cliff_district(); + let chunk = derive_chunk_context(42, "Velen", &district, (0, 0)); let coast = chunk.coast_anchor_m; // Sample 64 positions along the seaward axis, crossing the coast line. let cols: Vec = (coast - 32..coast + 32) .map(|a| { let (tx, ty) = cross_along_to_tile(&chunk, 50, a); - derive_voxel_column(42, "Velen", ®ion, &chunk, tx, ty) + derive_voxel_column(42, "Velen", &district, &chunk, tx, ty) }) .collect(); let has_deep = cols.iter().any(|c| c.water == Water::Deep); @@ -2516,21 +2516,21 @@ mod tests { #[test] fn cliff_coast_is_deterministic() { - let region = cliff_region(); - let chunk = derive_chunk_context(5, "Velen", ®ion, (1, 2)); + let district = cliff_district(); + let chunk = derive_chunk_context(5, "Velen", &district, (1, 2)); for (tx, ty) in [(5, 50), (30, 20), (62, 50)] { - let a = derive_voxel_column(5, "Velen", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(5, "Velen", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(5, "Velen", &district, &chunk, tx, ty); + let b = derive_voxel_column(5, "Velen", &district, &chunk, tx, ty); assert_eq!(a, b, "CliffCoast must be deterministic at ({tx},{ty})"); } } #[test] fn cliff_coast_elevation_non_negative() { - let region = cliff_region(); - let chunk = derive_chunk_context(42, "Velen", ®ion, (0, 0)); + let district = cliff_district(); + let chunk = derive_chunk_context(42, "Velen", &district, (0, 0)); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { - let col = derive_voxel_column(42, "Velen", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "Velen", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "CliffCoast elevation must be >= 0"); } } @@ -2539,24 +2539,24 @@ mod tests { // T-1029 — BraidedDelta // ----------------------------------------------------------------------- - fn delta_region() -> RegionProfile { - RegionProfile { + fn delta_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::Delta, slope_q: 2, elev_q: 4, ocean_fraction_q: 50, moisture_q: 80, vegetation_class: VegetationClass::Scrub, - ..alluvial_region() + ..alluvial_district() } } #[test] fn braided_delta_terrain_is_gravel() { - let region = delta_region(); - let chunk = derive_chunk_context(42, "delta_body", ®ion, (0, 0)); + let district = delta_district(); + let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0)); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { - let col = derive_voxel_column(42, "delta_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Gravel, @@ -2568,10 +2568,10 @@ mod tests { #[test] fn braided_delta_low_elevation() { // Delta is near sea level — D-239 §8 drainage monotonicity: mouths at sea level. - let region = delta_region(); - let chunk = derive_chunk_context(42, "delta_body", ®ion, (0, 0)); + let district = delta_district(); + let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0)); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5), (63, 0)] { - let col = derive_voxel_column(42, "delta_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty); assert!( col.elevation_m <= 8, "BraidedDelta must be near sea level (<= 8 m), got {} at ({tx},{ty})", @@ -2584,16 +2584,16 @@ mod tests { fn braided_delta_has_multiple_shallow_channels() { // Gravel→braided (D-239 §8): NOT single-thread. Multiple channels means // a range of positions across the fan belt should contain some Shallow - // water. The belt is centred on the region-anchored fan axis (T-1041). - let region = delta_region(); - let chunk = anchor_chunk(42, "delta_body", ®ion); + // water. The belt is centred on the district-anchored fan axis (T-1041). + let district = delta_district(); + let chunk = anchor_chunk(42, "delta_body", &district); let anchor = chunk.channel_anchor_m; // Sample 80 positions across the fan belt (anchor ± 40 covers the // 64 m belt plus thread width). let shallow_count = (anchor - 40..anchor + 40) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 100); - derive_voxel_column(42, "delta_body", ®ion, &chunk, tx, ty).water + derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty).water == Water::Shallow }) .count(); @@ -2611,13 +2611,13 @@ mod tests { // should appear in more than one spatial cluster across the cross-section. // We verify by checking that Shallow water appears in at least two // disjoint groups separated by Dry ground. - let region = delta_region(); - let chunk = anchor_chunk(42, "delta_body", ®ion); + let district = delta_district(); + let chunk = anchor_chunk(42, "delta_body", &district); let anchor = chunk.channel_anchor_m; let waters: Vec = (anchor - 40..anchor + 40) .map(|c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 100); - derive_voxel_column(42, "delta_body", ®ion, &chunk, tx, ty).water + derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty).water }) .collect(); // Count Dry→Shallow transitions = number of channel entrances. @@ -2633,21 +2633,21 @@ mod tests { #[test] fn braided_delta_is_deterministic() { - let region = delta_region(); - let chunk = derive_chunk_context(99, "delta_body", ®ion, (3, 1)); + let district = delta_district(); + let chunk = derive_chunk_context(99, "delta_body", &district, (3, 1)); for (tx, ty) in [(10, 10), (0, 100), (-5, 7)] { - let a = derive_voxel_column(99, "delta_body", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(99, "delta_body", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(99, "delta_body", &district, &chunk, tx, ty); + let b = derive_voxel_column(99, "delta_body", &district, &chunk, tx, ty); assert_eq!(a, b, "BraidedDelta must be deterministic at ({tx},{ty})"); } } #[test] fn braided_delta_elevation_non_negative() { - let region = delta_region(); - let chunk = derive_chunk_context(42, "delta_body", ®ion, (0, 0)); + let district = delta_district(); + let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0)); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { - let col = derive_voxel_column(42, "delta_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "BraidedDelta elevation must be >= 0"); } } @@ -2656,24 +2656,24 @@ mod tests { // T-1029 — DuneStrand // ----------------------------------------------------------------------- - fn dune_region() -> RegionProfile { - RegionProfile { + fn dune_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::DuneStrand, slope_q: 8, elev_q: 6, ocean_fraction_q: 30, moisture_q: 40, vegetation_class: VegetationClass::Barren, - ..alluvial_region() + ..alluvial_district() } } #[test] fn dune_strand_terrain_is_sand() { - let region = dune_region(); - let chunk = derive_chunk_context(42, "dune_body", ®ion, (0, 0)); + let district = dune_district(); + let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0)); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { - let col = derive_voxel_column(42, "dune_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "dune_body", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Sand, @@ -2688,11 +2688,11 @@ mod tests { // the maximum allowed height H satisfies H ≤ tan(32°) × (W/2) ≈ 0.625 × W/2. // We verify by checking that elevation differences between adjacent tiles // don't exceed the physics cap. - let region = dune_region(); - let chunk = derive_chunk_context(42, "dune_body", ®ion, (0, 0)); + let district = dune_district(); + let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0)); // Sample a row of tiles along the dune wave direction. let elevations: Vec = (0..40i32) - .map(|y| derive_voxel_column(42, "dune_body", ®ion, &chunk, 10, y).elevation_m) + .map(|y| derive_voxel_column(42, "dune_body", &district, &chunk, 10, y).elevation_m) .collect(); // Max elevation in the sample. let max_elev = *elevations.iter().max().unwrap(); @@ -2710,11 +2710,11 @@ mod tests { #[test] fn dune_strand_mostly_dry() { // Dunes are mostly dry; only the strand toe may be Shallow. - let region = dune_region(); - let chunk = derive_chunk_context(42, "dune_body", ®ion, (0, 0)); + let district = dune_district(); + let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0)); let dry_or_shallow_count = (0..100i32) .filter(|&x| { - let col = derive_voxel_column(42, "dune_body", ®ion, &chunk, x, 50); + let col = derive_voxel_column(42, "dune_body", &district, &chunk, x, 50); col.water != Water::Deep // no Deep water on dunes }) .count(); @@ -2726,21 +2726,21 @@ mod tests { #[test] fn dune_strand_is_deterministic() { - let region = dune_region(); - let chunk = derive_chunk_context(3, "dune_body", ®ion, (1, 0)); + let district = dune_district(); + let chunk = derive_chunk_context(3, "dune_body", &district, (1, 0)); for (tx, ty) in [(10, 10), (0, 30), (-5, 7)] { - let a = derive_voxel_column(3, "dune_body", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(3, "dune_body", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(3, "dune_body", &district, &chunk, tx, ty); + let b = derive_voxel_column(3, "dune_body", &district, &chunk, tx, ty); assert_eq!(a, b, "DuneStrand must be deterministic at ({tx},{ty})"); } } #[test] fn dune_strand_elevation_non_negative() { - let region = dune_region(); - let chunk = derive_chunk_context(42, "dune_body", ®ion, (0, 0)); + let district = dune_district(); + let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0)); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { - let col = derive_voxel_column(42, "dune_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "dune_body", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "DuneStrand elevation must be >= 0"); } } @@ -2749,8 +2749,8 @@ mod tests { // T-1029 — IncisedGorge // ----------------------------------------------------------------------- - fn gorge_region() -> RegionProfile { - RegionProfile { + fn gorge_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::MountainPass, tectonic_class: TectonicClass::Active, slope_q: 80, @@ -2758,16 +2758,16 @@ mod tests { ocean_fraction_q: 5, moisture_q: 50, vegetation_class: VegetationClass::Barren, - ..alluvial_region() + ..alluvial_district() } } #[test] fn incised_gorge_terrain_is_rock() { - let region = gorge_region(); - let chunk = derive_chunk_context(42, "gorge_body", ®ion, (0, 0)); + let district = gorge_district(); + let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0)); for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] { - let col = derive_voxel_column(42, "gorge_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty); assert_eq!( col.terrain, TerrainMaterial::Rock, @@ -2781,17 +2781,27 @@ mod tests { // D-239 §9: gorge floors 2–8 m wide. We verify by checking that the // total width of the Shallow water zone (the incised channel on the floor) // across a cross-section is within [2, 8] m. Cross-section spans the - // region-anchored gorge centreline (T-1041). - let region = gorge_region(); - let chunk = derive_chunk_context(42, "gorge_body", ®ion, (0, 0)); + // district-anchored gorge centreline (T-1041). + let district = gorge_district(); + let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0)); let anchor = chunk.channel_anchor_m; - let floor_tiles = (anchor - 32..anchor + 32) - .filter(|&c| { - let (tx, ty) = cross_along_to_tile(&chunk, c, 100); - derive_voxel_column(42, "gorge_body", ®ion, &chunk, tx, ty).water - == Water::Shallow + // The floor's intrinsic width is the D-239 §9 quantity. A single warped + // cross-section is noisy (the ±8 m domain warp can split the band at some + // along-rows), so measure across several rows and take the widest — the + // floor's true cross-section, independent of where the anchor falls. + let floor_tiles = (60..160) + .step_by(7) + .map(|along| { + (anchor - 32..anchor + 32) + .filter(|&c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, along); + derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty).water + == Water::Shallow + }) + .count() }) - .count(); + .max() + .unwrap(); assert!( (2..=8).contains(&floor_tiles), "IncisedGorge floor width {floor_tiles} m outside [2, 8] m spec (D-239 §9)" @@ -2801,21 +2811,21 @@ mod tests { #[test] fn incised_gorge_walls_much_higher_than_floor() { // Walls must be substantially higher than the gorge floor. - let region = gorge_region(); - let chunk = derive_chunk_context(42, "gorge_body", ®ion, (0, 0)); + let district = gorge_district(); + let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0)); let anchor = chunk.channel_anchor_m; // Floor: the lowest tile in the trough window around the anchor // (the warp can displace any single tile off the 2–8 m floor). let floor_elev = (anchor - 4..=anchor + 4) .map(|c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 100); - derive_voxel_column(42, "gorge_body", ®ion, &chunk, tx, ty).elevation_m + derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty).elevation_m }) .min() .unwrap(); // Wall: 30 m off the centreline. let (wx, wy) = cross_along_to_tile(&chunk, anchor + 30, 100); - let wall_col = derive_voxel_column(42, "gorge_body", ®ion, &chunk, wx, wy); + let wall_col = derive_voxel_column(42, "gorge_body", &district, &chunk, wx, wy); assert!( wall_col.elevation_m > floor_elev + 10, "IncisedGorge walls ({}) must be >> gorge floor ({})", @@ -2826,21 +2836,21 @@ mod tests { #[test] fn incised_gorge_is_deterministic() { - let region = gorge_region(); - let chunk = derive_chunk_context(17, "gorge_body", ®ion, (4, 2)); + let district = gorge_district(); + let chunk = derive_chunk_context(17, "gorge_body", &district, (4, 2)); for (tx, ty) in [(32, 100), (62, 100), (10, 50)] { - let a = derive_voxel_column(17, "gorge_body", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(17, "gorge_body", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(17, "gorge_body", &district, &chunk, tx, ty); + let b = derive_voxel_column(17, "gorge_body", &district, &chunk, tx, ty); assert_eq!(a, b, "IncisedGorge must be deterministic at ({tx},{ty})"); } } #[test] fn incised_gorge_elevation_non_negative() { - let region = gorge_region(); - let chunk = derive_chunk_context(42, "gorge_body", ®ion, (0, 0)); + let district = gorge_district(); + let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0)); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { - let col = derive_voxel_column(42, "gorge_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "IncisedGorge elevation must be >= 0"); } } @@ -2849,15 +2859,15 @@ mod tests { // T-1029 — MeanderReach // ----------------------------------------------------------------------- - fn meander_region() -> RegionProfile { - RegionProfile { + fn meander_district() -> DistrictProfile { + DistrictProfile { morphology_zone: MorphologyZone::MeanderReach, slope_q: 3, elev_q: 15, ocean_fraction_q: 20, moisture_q: 70, vegetation_class: VegetationClass::Forest, - ..alluvial_region() + ..alluvial_district() } } @@ -2865,10 +2875,10 @@ mod tests { fn meander_reach_terrain_is_soil_or_wetland() { // MeanderReach terrain is Soil (or Wetland sub-zone on very flat/wet ground). // D-239 §8 Soil law: rolling/floodplain. Wetland is a valid sub-classification - // for saturated floodplains (slope_q ≤ 3, moisture_q ≥ 70 in the meander_region). + // for saturated floodplains (slope_q ≤ 3, moisture_q ≥ 70 in the meander_district). // This test checks that NO other material appears (no Rock, Sand, Gravel, Lava). - let region = meander_region(); - let chunk = derive_chunk_context(42, "meander_body", ®ion, (0, 0)); + let district = meander_district(); + let chunk = derive_chunk_context(42, "meander_body", &district, (0, 0)); for (tx, ty) in [ (0, 50), (5, 50), @@ -2879,7 +2889,7 @@ mod tests { (300, 50), (400, 50), ] { - let col = derive_voxel_column(42, "meander_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty); assert!( matches!( col.terrain, @@ -2893,20 +2903,20 @@ mod tests { #[test] fn meander_reach_non_wetland_region_produces_soil() { - // A less-wet MeanderReach region (moisture_q < 70) should produce Soil, not Wetland. - let region = RegionProfile { + // A less-wet MeanderReach district (moisture_q < 70) should produce Soil, not Wetland. + let district = DistrictProfile { morphology_zone: MorphologyZone::MeanderReach, slope_q: 5, // above wetland slope threshold elev_q: 15, ocean_fraction_q: 20, moisture_q: 55, // below wetland moisture threshold vegetation_class: VegetationClass::Forest, - ..alluvial_region() + ..alluvial_district() }; - let chunk = derive_chunk_context(42, "meander_body2", ®ion, (0, 0)); + let chunk = derive_chunk_context(42, "meander_body2", &district, (0, 0)); let mut soil_count = 0; for tx in [0, 5, 10, 15, 100, 200] { - let col = derive_voxel_column(42, "meander_body2", ®ion, &chunk, tx, 50); + let col = derive_voxel_column(42, "meander_body2", &district, &chunk, tx, 50); if col.terrain == TerrainMaterial::Soil { soil_count += 1; } @@ -2920,9 +2930,9 @@ mod tests { #[test] fn meander_reach_has_active_channel() { // MeanderReach must have a channel with Shallow/Deep water somewhere in - // a transect across the region-anchored channel band (T-1040). - let region = meander_region(); - let chunk = anchor_chunk(42, "meander_body", ®ion); + // a transect across the district-anchored channel band (T-1040). + let district = meander_district(); + let chunk = anchor_chunk(42, "meander_body", &district); // The anchor-covering chunk has an active channel (ocean_fraction_q=20 >= 10). assert!( chunk.has_active_channel, @@ -2933,7 +2943,7 @@ mod tests { let wet_count = (anchor - 200..anchor + 200) .filter(|&c| { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); - let col = derive_voxel_column(42, "meander_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty); col.water != Water::Dry }) .count(); @@ -2949,14 +2959,14 @@ mod tests { // tiles in the vicinity. Per-voxel micro-relief is ±4 m, so a single // wet/dry tile pair is noise — compare the channel bottom (min wet) // against the floodplain/levee top (max dry) across the transect. - let region = meander_region(); - let chunk = anchor_chunk(42, "meander_body", ®ion); + let district = meander_district(); + let chunk = anchor_chunk(42, "meander_body", &district); let anchor = chunk.channel_anchor_m; let mut min_wet: Option = None; let mut max_dry: Option = None; for c in anchor - 200..anchor + 200 { let (tx, ty) = cross_along_to_tile(&chunk, c, 50); - let col = derive_voxel_column(42, "meander_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty); if col.water != Water::Dry { min_wet = Some(min_wet.map_or(col.elevation_m, |e| e.min(col.elevation_m))); } else { @@ -2978,7 +2988,7 @@ mod tests { #[test] fn meander_reach_stronger_sinuosity_than_alluvial() { // MeanderReach uses channel amplitude = wavelength/4 vs AlluvialPlain's - // wavelength/6. Same region params, same seed, same body — the ONLY + // wavelength/6. Same district params, same seed, same body — the ONLY // difference is the morphology_zone driving the dispatch. Different // amplitudes shift the channel centreline position differently across the // chunk, so the set of wet-tile x-positions must differ. If MeanderReach @@ -2987,21 +2997,21 @@ mod tests { // // We use the SAME elev_q, slope_q, ocean_fraction_q, and seed for both — // so the only driver of the difference is the sinuosity amplitude. - let shared_region_base = RegionProfile { + let shared_district_base = DistrictProfile { slope_q: 5, elev_q: 20, ocean_fraction_q: 20, // ensure has_active_channel = true moisture_q: 55, vegetation_class: VegetationClass::Forest, - ..alluvial_region() + ..alluvial_district() }; - let meander_reg = RegionProfile { + let meander_reg = DistrictProfile { morphology_zone: MorphologyZone::MeanderReach, - ..shared_region_base.clone() + ..shared_district_base.clone() }; - let alluvial_reg = RegionProfile { + let alluvial_reg = DistrictProfile { morphology_zone: MorphologyZone::AlluvialPlain, - ..shared_region_base + ..shared_district_base }; // Same seed + body → same ChunkContext (same wavelength, phase, anchor, // channel_width). Use the anchor-covering chunk (T-1040 band gating). @@ -3063,21 +3073,21 @@ mod tests { #[test] fn meander_reach_is_deterministic() { - let region = meander_region(); - let chunk = derive_chunk_context(23, "meander_body", ®ion, (2, 5)); + let district = meander_district(); + let chunk = derive_chunk_context(23, "meander_body", &district, (2, 5)); for (tx, ty) in [(0, 50), (50, 200), (-20, 100)] { - let a = derive_voxel_column(23, "meander_body", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(23, "meander_body", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(23, "meander_body", &district, &chunk, tx, ty); + let b = derive_voxel_column(23, "meander_body", &district, &chunk, tx, ty); assert_eq!(a, b, "MeanderReach must be deterministic at ({tx},{ty})"); } } #[test] fn meander_reach_elevation_non_negative() { - let region = meander_region(); - let chunk = derive_chunk_context(42, "meander_body", ®ion, (0, 0)); + let district = meander_district(); + let chunk = derive_chunk_context(42, "meander_body", &district, (0, 0)); for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] { - let col = derive_voxel_column(42, "meander_body", ®ion, &chunk, tx, ty); + let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty); assert!(col.elevation_m >= 0, "MeanderReach elevation must be >= 0"); } } @@ -3107,7 +3117,7 @@ mod tests { (-500, 300), ]; for zone in &zones { - let region = RegionProfile { + let district = DistrictProfile { morphology_zone: zone.clone(), tectonic_class: TectonicClass::Volcanic, glaciation_grade: GlaciationGrade::Moderate, @@ -3115,12 +3125,12 @@ mod tests { elev_q: 40, ocean_fraction_q: 20, moisture_q: 50, - ..alluvial_region() + ..alluvial_district() }; - let chunk = derive_chunk_context(42, "stress_body", ®ion, (0, 0)); + let chunk = derive_chunk_context(42, "stress_body", &district, (0, 0)); for (tx, ty) in &positions { // Must not panic. - let col = derive_voxel_column(42, "stress_body", ®ion, &chunk, *tx, *ty); + let col = derive_voxel_column(42, "stress_body", &district, &chunk, *tx, *ty); assert!( col.elevation_m >= 0, "elevation must be non-negative for {:?} at ({tx},{ty})", @@ -3142,7 +3152,7 @@ mod tests { (MorphologyZone::MountainPass, TerrainMaterial::Rock), ]; for (zone, expected_terrain) in cases { - let region = RegionProfile { + let district = DistrictProfile { morphology_zone: zone.clone(), tectonic_class: TectonicClass::Volcanic, glaciation_grade: GlaciationGrade::Moderate, @@ -3151,10 +3161,10 @@ mod tests { ocean_fraction_q: 5, moisture_q: 30, vegetation_class: VegetationClass::Barren, - ..alluvial_region() + ..alluvial_district() }; - let chunk = derive_chunk_context(42, "mat_check", ®ion, (0, 0)); - let col = derive_voxel_column(42, "mat_check", ®ion, &chunk, 10, 10); + let chunk = derive_chunk_context(42, "mat_check", &district, (0, 0)); + let col = derive_voxel_column(42, "mat_check", &district, &chunk, 10, 10); assert_eq!( col.terrain, *expected_terrain, "Zone {:?} must produce {:?} terrain", @@ -3190,13 +3200,13 @@ mod tests { } } - /// Build a RegionProfile for cover tests with explicit temperature and moisture. - fn cover_region(zone: MorphologyZone, temp_c: f32, moisture_q: i32) -> RegionProfile { - RegionProfile { + /// Build a DistrictProfile for cover tests with explicit temperature and moisture. + fn cover_district(zone: MorphologyZone, temp_c: f32, moisture_q: i32) -> DistrictProfile { + DistrictProfile { morphology_zone: zone, temperature_c: Some(temp_c), moisture_q, - ..alluvial_region() + ..alluvial_district() } } @@ -3205,10 +3215,10 @@ mod tests { #[test] fn cover_airless_body_is_none() { // D-239 §2: no atmosphere → temperature_c == None → cover None always. - let mut region = alluvial_region(); - region.temperature_c = None; + let mut district = alluvial_district(); + district.temperature_c = None; let col = make_column(Water::Dry); - let cover = derive_cover(42, "test_body", ®ion, &col, (0, 0)); + let cover = derive_cover(42, "test_body", &district, &col, (0, 0)); assert_eq!( cover, SeasonalCover::None, @@ -3218,10 +3228,10 @@ mod tests { #[test] fn cover_airless_body_even_with_water() { - let mut region = cover_region(MorphologyZone::Lake, 15.0, 80); - region.temperature_c = None; + let mut district = cover_district(MorphologyZone::Lake, 15.0, 80); + district.temperature_c = None; let col = make_column(Water::Deep); - let cover = derive_cover(42, "test_body", ®ion, &col, (50, 50)); + let cover = derive_cover(42, "test_body", &district, &col, (50, 50)); assert_eq!( cover, SeasonalCover::None, @@ -3234,11 +3244,11 @@ mod tests { #[test] fn cover_freshwater_above_band_is_none() { // Above +5 °C: always None (open water), regardless of scatter. - let region = cover_region(MorphologyZone::Lake, 6.0, 60); + let district = cover_district(MorphologyZone::Lake, 6.0, 60); let col = make_column(Water::Deep); // Sample many positions — all must be None above the band. for i in 0..50i32 { - let cover = derive_cover(42, "test_body", ®ion, &col, (i * 7, i * 3)); + let cover = derive_cover(42, "test_body", &district, &col, (i * 7, i * 3)); assert_eq!( cover, SeasonalCover::None, @@ -3252,10 +3262,10 @@ mod tests { #[test] fn cover_freshwater_below_band_is_all_ice() { // Below −10 °C: always Ice (no scatter). - let region = cover_region(MorphologyZone::Lake, -11.0, 60); + let district = cover_district(MorphologyZone::Lake, -11.0, 60); let col = make_column(Water::Deep); for i in 0..50i32 { - let cover = derive_cover(42, "test_body", ®ion, &col, (i * 11, i * 5)); + let cover = derive_cover(42, "test_body", &district, &col, (i * 11, i * 5)); assert_eq!( cover, SeasonalCover::Ice, @@ -3270,14 +3280,14 @@ mod tests { fn cover_freshwater_within_band_has_mix_of_ice_and_none() { // Within the scatter band [−10, +5]: should produce a mix of Ice and None // across different positions. Not all-Ice, not all-None. - let region = cover_region(MorphologyZone::Lake, -3.0, 60); + let district = cover_district(MorphologyZone::Lake, -3.0, 60); let col = make_column(Water::Deep); let mut ice_count = 0; let mut none_count = 0; for i in 0..200i32 { // Spread across a large area to span many cluster cells. let pos = (i * CLUSTER_M * 2, i * CLUSTER_M); - let cover = derive_cover(42, "test_body", ®ion, &col, pos); + let cover = derive_cover(42, "test_body", &district, &col, pos); match cover { SeasonalCover::Ice => ice_count += 1, SeasonalCover::None => none_count += 1, @@ -3299,10 +3309,10 @@ mod tests { #[test] fn cover_salt_above_onset_is_none() { // Above −2 °C: no sea ice. - let region = cover_region(MorphologyZone::OpenOcean, 0.0, 50); + let district = cover_district(MorphologyZone::OpenOcean, 0.0, 50); let col = make_column(Water::Deep); for i in 0..30i32 { - let cover = derive_cover(42, "test_body", ®ion, &col, (i * 13, i * 7)); + let cover = derive_cover(42, "test_body", &district, &col, (i * 13, i * 7)); assert_eq!( cover, SeasonalCover::None, @@ -3314,10 +3324,10 @@ mod tests { #[test] fn cover_salt_below_band_is_all_ice() { // Below −14 °C: permanent pack ice everywhere. - let region = cover_region(MorphologyZone::OpenOcean, -15.0, 50); + let district = cover_district(MorphologyZone::OpenOcean, -15.0, 50); let col = make_column(Water::Deep); for i in 0..30i32 { - let cover = derive_cover(42, "test_body", ®ion, &col, (i * 17, i * 9)); + let cover = derive_cover(42, "test_body", &district, &col, (i * 17, i * 9)); assert_eq!( cover, SeasonalCover::Ice, @@ -3330,10 +3340,10 @@ mod tests { fn cover_freshwater_not_triggered_for_salt_zone() { // OpenOcean should use the salt-water band (onset −2°C), NOT the // freshwater band (onset +5°C). At temp = +3°C, ocean must be None. - let region = cover_region(MorphologyZone::OpenOcean, 3.0, 50); + let district = cover_district(MorphologyZone::OpenOcean, 3.0, 50); let col = make_column(Water::Deep); for i in 0..20i32 { - let cover = derive_cover(42, "test_body", ®ion, &col, (i * 7, i * 5)); + let cover = derive_cover(42, "test_body", &district, &col, (i * 7, i * 5)); assert_eq!( cover, SeasonalCover::None, @@ -3347,10 +3357,10 @@ mod tests { #[test] fn cover_snow_cold_wet_land_produces_snow() { // Cold + wet land must produce Snow in the permanent zone (below −10°C). - let region = cover_region(MorphologyZone::AlluvialPlain, -12.0, 60); + let district = cover_district(MorphologyZone::AlluvialPlain, -12.0, 60); let col = make_column(Water::Dry); for i in 0..30i32 { - let cover = derive_cover(42, "test_body", ®ion, &col, (i * 9, i * 3)); + let cover = derive_cover(42, "test_body", &district, &col, (i * 9, i * 3)); assert_eq!( cover, SeasonalCover::Snow, @@ -3362,10 +3372,10 @@ mod tests { #[test] fn cover_snow_cold_dry_land_produces_none() { // Cold + dry (moisture_q < SNOW_MOISTURE_GATE) → bare frozen ground, not Snow. - let region = cover_region(MorphologyZone::AlluvialPlain, -12.0, 20); + let district = cover_district(MorphologyZone::AlluvialPlain, -12.0, 20); let col = make_column(Water::Dry); for i in 0..30i32 { - let cover = derive_cover(42, "test_body", ®ion, &col, (i * 9, i * 3)); + let cover = derive_cover(42, "test_body", &district, &col, (i * 9, i * 3)); assert_eq!( cover, SeasonalCover::None, @@ -3377,10 +3387,10 @@ mod tests { #[test] fn cover_snow_warm_land_produces_none() { // Warm land (above snow band): no snow. - let region = cover_region(MorphologyZone::AlluvialPlain, 10.0, 80); + let district = cover_district(MorphologyZone::AlluvialPlain, 10.0, 80); let col = make_column(Water::Dry); for i in 0..30i32 { - let cover = derive_cover(42, "test_body", ®ion, &col, (i * 5, i * 2)); + let cover = derive_cover(42, "test_body", &district, &col, (i * 5, i * 2)); assert_eq!( cover, SeasonalCover::None, @@ -3392,13 +3402,13 @@ mod tests { #[test] fn cover_snow_within_band_has_mix() { // Within the snow scatter band (e.g. −5°C) + moist: mix of Snow and None. - let region = cover_region(MorphologyZone::AlluvialPlain, -5.0, 60); + let district = cover_district(MorphologyZone::AlluvialPlain, -5.0, 60); let col = make_column(Water::Dry); let mut snow_count = 0; let mut none_count = 0; for i in 0..200i32 { let pos = (i * CLUSTER_M * 2, i * CLUSTER_M); - let cover = derive_cover(42, "test_body", ®ion, &col, pos); + let cover = derive_cover(42, "test_body", &district, &col, pos); match cover { SeasonalCover::Snow => snow_count += 1, SeasonalCover::None => none_count += 1, @@ -3421,12 +3431,12 @@ mod tests { fn cover_dry_riverbank_is_land_not_freshwater() { // A RiverBank tile that is Water::Dry should be treated as land (snow), // not freshwater (ice). D-239 §3: fresh water only when the voxel is wet. - let region = cover_region(MorphologyZone::RiverBank, -12.0, 60); + let district = cover_district(MorphologyZone::RiverBank, -12.0, 60); let dry_col = make_column(Water::Dry); let wet_col = make_column(Water::Shallow); - let dry_cover = derive_cover(42, "test_body", ®ion, &dry_col, (0, 0)); - let wet_cover = derive_cover(42, "test_body", ®ion, &wet_col, (0, 0)); + let dry_cover = derive_cover(42, "test_body", &district, &dry_col, (0, 0)); + let wet_cover = derive_cover(42, "test_body", &district, &wet_col, (0, 0)); assert_eq!( dry_cover, @@ -3466,9 +3476,9 @@ mod tests { /// within the same cluster cell is the clean contract-level test. #[test] fn cover_cluster_coherence_not_per_tile() { - // Use a cold lake region so cover is in the scatter band (mix of Ice/None) + // Use a cold lake district so cover is in the scatter band (mix of Ice/None) // — solid-frozen would pass trivially regardless of coherence. - let region = cover_region(MorphologyZone::Lake, -3.0, 60); + let district = cover_district(MorphologyZone::Lake, -3.0, 60); let col = make_column(Water::Shallow); let world_seed: u64 = 42; let body_id = "coherence_body"; @@ -3490,7 +3500,7 @@ mod tests { derive_cover( world_seed, body_id, - ®ion, + &district, &col, (base_x + dx, base_y + dy), ) @@ -3517,7 +3527,7 @@ mod tests { // Also verify that cover varies across different cluster cells in the scatter // band — confirming the hash is not degenerate (not all-Ice or all-None). let cell_covers: Vec = (-25..25i32) - .map(|cx| derive_cover(world_seed, body_id, ®ion, &col, (cx * CLUSTER_M, 0))) + .map(|cx| derive_cover(world_seed, body_id, &district, &col, (cx * CLUSTER_M, 0))) .collect(); let has_ice = cell_covers.contains(&SeasonalCover::Ice); let has_none = cell_covers.contains(&SeasonalCover::None); @@ -3533,12 +3543,12 @@ mod tests { #[test] fn cover_derivation_is_deterministic() { // Same inputs → same cover, always. - let region = cover_region(MorphologyZone::Lake, -3.0, 60); + let district = cover_district(MorphologyZone::Lake, -3.0, 60); let col = make_column(Water::Shallow); for i in 0..50i32 { let pos = (i * CLUSTER_M, i * 3); - let a = derive_cover(42, "test_body", ®ion, &col, pos); - let b = derive_cover(42, "test_body", ®ion, &col, pos); + let a = derive_cover(42, "test_body", &district, &col, pos); + let b = derive_cover(42, "test_body", &district, &col, pos); assert_eq!( a, b, @@ -3552,11 +3562,11 @@ mod tests { #[test] fn cover_end_to_end_via_derive_voxel_column_is_deterministic() { // Cover must be deterministic through the full derive_voxel_column path. - let region = cover_region(MorphologyZone::AlluvialPlain, -12.0, 60); - let chunk = derive_chunk_context(42, "cold_body", ®ion, (0, 0)); + let district = cover_district(MorphologyZone::AlluvialPlain, -12.0, 60); + let chunk = derive_chunk_context(42, "cold_body", &district, (0, 0)); for (tx, ty) in [(0, 0), (50, 100), (-20, 30), (200, -10)] { - let a = derive_voxel_column(42, "cold_body", ®ion, &chunk, tx, ty); - let b = derive_voxel_column(42, "cold_body", ®ion, &chunk, tx, ty); + let a = derive_voxel_column(42, "cold_body", &district, &chunk, tx, ty); + let b = derive_voxel_column(42, "cold_body", &district, &chunk, tx, ty); assert_eq!( a.cover, b.cover, "cover must be deterministic at ({tx},{ty})" diff --git a/server/src/main.rs b/server/src/main.rs index 6e9fa5fed..853bd182a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -216,7 +216,7 @@ fn main() { ), } - // Body physical params reader for RegionProfile carrier layer (T-1032, D-239 §1, D-240): + // Body physical params reader for DistrictProfile carrier layer (T-1032, D-239 §1, D-240): // reads hydrosphere / atmosphere / planet_class on a cache miss so the Rayon // cascade work item stays DB-free (D-225 pattern). // D-240: orbit/star fields are non-canonical and are no longer read. @@ -229,7 +229,7 @@ fn main() { ); } Err(e) => tracing::warn!( - "Body params reader unavailable ({}). RegionProfile layer will be skipped.", + "Body params reader unavailable ({}). DistrictProfile layer will be skipped.", e ), } diff --git a/server/tests/derivation_harness.rs b/server/tests/derivation_harness.rs index 667632300..a68419f38 100644 --- a/server/tests/derivation_harness.rs +++ b/server/tests/derivation_harness.rs @@ -3,13 +3,13 @@ //! Capstone test for the T-974 tile-derivation epic. Covers four domains: //! //! **1. Golden-seed determinism regression (D-239 §1 / D-010).** -//! Pins (seed, region, chunk_pos, tile_pos) → VoxelColumn bindings as a JSON +//! Pins (seed, district, chunk_pos, tile_pos) → VoxelColumn bindings as a JSON //! golden. Any future change to the derivation chain is caught immediately. //! Pattern mirrors cascade_golden.rs. Regenerate: //! `UPDATE_GOLDEN=1 cargo test --test derivation_harness` //! //! **2. §8 binding-law assertions (D-239 §8).** -//! Sweeps representative `RegionProfile` inputs and asserts the four +//! Sweeps representative `DistrictProfile` inputs and asserts the four //! believability laws hold across the parameter space: //! - Drainage monotonicity (channel tiles at or below surrounding terrain) //! - Lithology→landform (family emits its mandated TerrainMaterial) @@ -40,14 +40,15 @@ use std::path::PathBuf; use std::time::Instant; use settled_reach_server::atlas::chunk_context::{derive_chunk_context, BasinDirection, ChunkPos}; +use settled_reach_server::atlas::district_profile::{ + derive_district_profile, derive_morphology_zone, derive_precipitation_class_from_climate, + derive_river_threshold, derive_vegetation, BodyParams, ClimateConstants, DistrictProfile, + GlaciationGrade, TectonicClass, VegetationClass, +}; use settled_reach_server::atlas::drainage; use settled_reach_server::atlas::features::TerrainAnalysis; use settled_reach_server::atlas::heightmap::BodyHeightmap; -use settled_reach_server::atlas::region_profile::{ - derive_morphology_zone, derive_precipitation_class_from_climate, derive_region_profile, - derive_river_threshold, derive_vegetation, BodyParams, ClimateConstants, GlaciationGrade, - RegionProfile, TectonicClass, VegetationClass, -}; +use settled_reach_server::atlas::scale; use settled_reach_server::atlas::voxel::{derive_voxel_column, TerrainMaterial, VoxelCache, Water}; use settled_reach_server::seed::{SeedChain, SeedDomain}; use settled_reach_server::simulation::generator::MorphologyZone; @@ -74,18 +75,18 @@ struct GoldenEntry { cover: u8, } -/// Derive a GoldenEntry for a fixed (seed, region, chunk_pos, tile_pos) tuple. +/// Derive a GoldenEntry for a fixed (seed, district, chunk_pos, tile_pos) tuple. fn derive_golden( label: &str, seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, chunk_pos: (i32, i32), tile_x: i32, tile_y: i32, ) -> GoldenEntry { - let chunk = derive_chunk_context(seed, body_id, region, chunk_pos); - let col = derive_voxel_column(seed, body_id, region, &chunk, tile_x, tile_y); + let chunk = derive_chunk_context(seed, body_id, district, chunk_pos); + let col = derive_voxel_column(seed, body_id, district, &chunk, tile_x, tile_y); GoldenEntry { label: label.to_string(), seed, @@ -100,25 +101,25 @@ fn derive_golden( } } -/// Compute the chunk position + tile coordinates sitting ON the region's +/// Compute the chunk position + tile coordinates sitting ON the district's /// channel anchor at a given along-axis chunk index (T-1040/T-1041): feature -/// placement is region-anchored, so the golden pins a voxel on the anchor +/// placement is district-anchored, so the golden pins a voxel on the anchor /// column. Relocates automatically if the anchor derivation changes — which /// flips the golden values anyway. /// -/// `along_chunk` must stay within region (0, 0) — i.e. in `0..16` — so the -/// probed anchor belongs to the same region as the returned chunk. +/// `along_chunk` must stay within district (0, 0) — i.e. in `0..scale::CHUNKS_PER_DISTRICT` — so the +/// probed anchor belongs to the same district as the returned chunk. fn anchor_golden_pos( seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, along_chunk: i32, ) -> ((i32, i32), i32, i32) { assert!( - (0..16).contains(&along_chunk), - "along_chunk must stay within region (0, 0)" + (0..scale::CHUNKS_PER_DISTRICT).contains(&along_chunk), + "along_chunk must stay within district (0, 0)" ); - let probe = derive_chunk_context(seed, body_id, region, (0, 0)); + let probe = derive_chunk_context(seed, body_id, district, (0, 0)); let anchor = probe.channel_anchor_m; let along_tile = along_chunk * 64 + 32; match probe.basin_direction { @@ -136,15 +137,15 @@ fn golden_cases() -> Vec<( &'static str, u64, &'static str, - RegionProfile, + DistrictProfile, (i32, i32), i32, i32, )> { - // Cases A and C pin voxels on the region channel anchor (T-1040/T-1041): - // the channel/trough centreline is region-anchored, not at the world + // Cases A and C pin voxels on the district channel anchor (T-1040/T-1041): + // the channel/trough centreline is district-anchored, not at the world // origin (A) or the chunk centre (C). - let alluvial_region = make_region( + let alluvial_district = make_region( MorphologyZone::AlluvialPlain, TectonicClass::Active, GlaciationGrade::None, @@ -156,8 +157,8 @@ fn golden_cases() -> Vec<( VegetationClass::Forest, ); let (alluvial_chunk_pos, alluvial_tx, alluvial_ty) = - anchor_golden_pos(0xdeadbeef_cafebabe_u64, "GJ144d", &alluvial_region, 12); - let fjord_region = make_region( + anchor_golden_pos(0xdeadbeef_cafebabe_u64, "GJ144d", &alluvial_district, 12); + let fjord_district = make_region( MorphologyZone::Fjord, TectonicClass::Active, GlaciationGrade::Moderate, @@ -169,14 +170,14 @@ fn golden_cases() -> Vec<( VegetationClass::Barren, ); let (fjord_chunk_pos, fjord_tx, fjord_ty) = - anchor_golden_pos(0xfeedface_0badc0de_u64, "GJ447c", &fjord_region, 5); + anchor_golden_pos(0xfeedface_0badc0de_u64, "GJ447c", &fjord_district, 5); vec![ // Case A: AlluvialPlain — temperate forest, on the active-channel anchor. ( "alluvial_forest_active_channel", 0xdeadbeef_cafebabe_u64, "GJ144d", - alluvial_region, + alluvial_district, alluvial_chunk_pos, alluvial_tx, alluvial_ty, @@ -201,13 +202,13 @@ fn golden_cases() -> Vec<( 320, 448, ), - // Case C: FjordWall — glaciated, rocky walls; tile on the region-anchored + // Case C: FjordWall — glaciated, rocky walls; tile on the district-anchored // trough centreline (Deep water unless the warp nudges it onto the floor edge). ( "fjord_wall_glaciated", 0xfeedface_0badc0de_u64, "GJ447c", - fjord_region, + fjord_district, fjord_chunk_pos, fjord_tx, fjord_ty, @@ -224,14 +225,14 @@ fn golden_seed_determinism_regression() { // compare against the golden. This is the core D-010 determinism contract. let run1: Vec = golden_cases() .into_iter() - .map(|(lbl, seed, body, region, cp, tx, ty)| { - derive_golden(lbl, seed, body, ®ion, cp, tx, ty) + .map(|(lbl, seed, body, district, cp, tx, ty)| { + derive_golden(lbl, seed, body, &district, cp, tx, ty) }) .collect(); let run2: Vec = golden_cases() .into_iter() - .map(|(lbl, seed, body, region, cp, tx, ty)| { - derive_golden(lbl, seed, body, ®ion, cp, tx, ty) + .map(|(lbl, seed, body, district, cp, tx, ty)| { + derive_golden(lbl, seed, body, &district, cp, tx, ty) }) .collect(); @@ -296,10 +297,10 @@ fn assert_drainage_monotonicity( seed: u64, body_id: &str, label: &str, - region: &RegionProfile, + district: &DistrictProfile, chunk_pos: (i32, i32), ) -> bool { - let chunk = derive_chunk_context(seed, body_id, region, chunk_pos); + let chunk = derive_chunk_context(seed, body_id, district, chunk_pos); if !chunk.has_active_channel { return false; // No channel → monotonicity trivially satisfied. } @@ -313,7 +314,8 @@ fn assert_drainage_monotonicity( let base_y = chunk_pos.1 * 64; for dy in 0..64i32 { for dx in 0..64i32 { - let col = derive_voxel_column(seed, body_id, region, &chunk, base_x + dx, base_y + dy); + let col = + derive_voxel_column(seed, body_id, district, &chunk, base_x + dx, base_y + dy); match col.water { Water::Dry => { max_dry_elev = max_dry_elev.max(col.elevation_m); @@ -348,7 +350,7 @@ fn assert_drainage_monotonicity( #[test] fn law_drainage_monotonicity_alluvial_sweep() { - let region = make_region( + let district = make_region( MorphologyZone::AlluvialPlain, TectonicClass::Stable, GlaciationGrade::None, @@ -360,12 +362,12 @@ fn law_drainage_monotonicity_alluvial_sweep() { VegetationClass::Forest, ); let mut checked = false; - // Sweep a spread of region chunks plus the channel-anchor band (T-1040: - // the channel is region-anchored, so only band chunks carry wet tiles). + // Sweep a spread of district chunks plus the channel-anchor band (T-1040: + // the channel is district-anchored, so only band chunks carry wet tiles). let mut positions = vec![(0, 0), (1, 0), (0, 1), (4, 4), (8, 3)]; - positions.extend(anchor_band_chunks(42, "GJ144d", ®ion, (0, 0))); + positions.extend(anchor_band_chunks(42, "GJ144d", &district, (0, 0))); for pos in positions { - checked |= assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", ®ion, pos); + checked |= assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", &district, pos); } assert!( checked, @@ -376,7 +378,7 @@ fn law_drainage_monotonicity_alluvial_sweep() { #[test] fn law_drainage_monotonicity_meander_sweep() { - let region = make_region( + let district = make_region( MorphologyZone::MeanderReach, TectonicClass::Stable, GlaciationGrade::None, @@ -389,9 +391,9 @@ fn law_drainage_monotonicity_meander_sweep() { ); let mut checked = false; let mut positions = vec![(0, 0), (2, 1), (5, 5)]; - positions.extend(anchor_band_chunks(99, "GJ447c", ®ion, (0, 0))); + positions.extend(anchor_band_chunks(99, "GJ447c", &district, (0, 0))); for pos in positions { - checked |= assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", ®ion, pos); + checked |= assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", &district, pos); } assert!( checked, @@ -404,7 +406,7 @@ fn law_drainage_monotonicity_meander_sweep() { fn law_drainage_monotonicity_fjord_floor_at_sea_level() { // D-239 §8: fjord/cliff/delta floors at sea level (drainage monotonicity). // FjordWall deep-water channel must be near elevation 0. - let region = make_region( + let district = make_region( MorphologyZone::Fjord, TectonicClass::Active, GlaciationGrade::Moderate, @@ -415,16 +417,16 @@ fn law_drainage_monotonicity_fjord_floor_at_sea_level() { Some(-8.0), VegetationClass::Barren, ); - // The fjord trough is region-anchored (T-1041): scan the chunk whose + // The fjord trough is district-anchored (T-1041): scan the chunk whose // cross-range contains the channel anchor — only that chunk column carries // the deep-water trough. - let probe = derive_chunk_context(42, "fjord_body", ®ion, (0, 0)); + let probe = derive_chunk_context(42, "fjord_body", &district, (0, 0)); let anchor_idx = probe.channel_anchor_m.div_euclid(64); let chunk_pos = match probe.basin_direction { BasinDirection::North | BasinDirection::South => (anchor_idx, 0), BasinDirection::East | BasinDirection::West => (0, anchor_idx), }; - let chunk = derive_chunk_context(42, "fjord_body", ®ion, chunk_pos); + let chunk = derive_chunk_context(42, "fjord_body", &district, chunk_pos); let (base_x, base_y) = (chunk_pos.0 * 64, chunk_pos.1 * 64); let mut deep_elevs: Vec = vec![]; let mut dry_elevs: Vec = vec![]; @@ -434,8 +436,14 @@ fn law_drainage_monotonicity_fjord_floor_at_sea_level() { // it entirely and silently pass without ever checking the sea-level claim. for dy in 0..64i32 { for dx in 0..64i32 { - let col = - derive_voxel_column(42, "fjord_body", ®ion, &chunk, base_x + dx, base_y + dy); + let col = derive_voxel_column( + 42, + "fjord_body", + &district, + &chunk, + base_x + dx, + base_y + dy, + ); match col.water { Water::Deep => deep_elevs.push(col.elevation_m), Water::Dry => dry_elevs.push(col.elevation_m), @@ -473,7 +481,7 @@ fn law_drainage_monotonicity_fjord_floor_at_sea_level() { #[test] fn law_drainage_monotonicity_braided_delta() { - let region = make_region( + let district = make_region( MorphologyZone::Delta, TectonicClass::Active, GlaciationGrade::None, @@ -485,12 +493,12 @@ fn law_drainage_monotonicity_braided_delta() { VegetationClass::Scrub, ); let mut checked = false; - // The braid belt sits on the region's channel anchor (T-1041) — sweep the + // The braid belt sits on the district's channel anchor (T-1041) — sweep the // anchor band so the law is exercised on real thread tiles. let mut positions = vec![(0, 0), (1, 1)]; - positions.extend(anchor_band_chunks(17, "delta_body", ®ion, (0, 0))); + positions.extend(anchor_band_chunks(17, "delta_body", &district, (0, 0))); for pos in positions { - checked |= assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", ®ion, pos); + checked |= assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", &district, pos); } assert!( checked, @@ -509,17 +517,18 @@ fn law_drainage_monotonicity_braided_delta() { fn assert_all_terrain_is( seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, chunk_pos: (i32, i32), expected: TerrainMaterial, label: &str, ) { - let chunk = derive_chunk_context(seed, body_id, region, chunk_pos); + let chunk = derive_chunk_context(seed, body_id, district, chunk_pos); let base_x = chunk_pos.0 * 64; let base_y = chunk_pos.1 * 64; for dy in (0..64i32).step_by(8) { for dx in (0..64i32).step_by(8) { - let col = derive_voxel_column(seed, body_id, region, &chunk, base_x + dx, base_y + dy); + let col = + derive_voxel_column(seed, body_id, district, &chunk, base_x + dx, base_y + dy); assert_eq!( col.terrain, expected, @@ -536,7 +545,7 @@ fn assert_all_terrain_is( #[test] fn law_lithology_lava_emits_lava() { // D-239 §8: Lava family → TerrainMaterial::Lava everywhere. - let region = make_region( + let district = make_region( MorphologyZone::Volcanic, TectonicClass::Volcanic, GlaciationGrade::None, @@ -550,7 +559,7 @@ fn law_lithology_lava_emits_lava() { assert_all_terrain_is( 42, "GJ581c", - ®ion, + &district, (3, 3), TerrainMaterial::Lava, "LavaField", @@ -559,7 +568,7 @@ fn law_lithology_lava_emits_lava() { #[test] fn law_lithology_fjord_emits_rock() { - let region = make_region( + let district = make_region( MorphologyZone::Fjord, TectonicClass::Active, GlaciationGrade::Moderate, @@ -573,7 +582,7 @@ fn law_lithology_fjord_emits_rock() { assert_all_terrain_is( 42, "fjord_body", - ®ion, + &district, (0, 0), TerrainMaterial::Rock, "FjordWall", @@ -582,7 +591,7 @@ fn law_lithology_fjord_emits_rock() { #[test] fn law_lithology_cliff_coast_emits_rock() { - let region = make_region( + let district = make_region( MorphologyZone::CliffCoast, TectonicClass::Active, GlaciationGrade::None, @@ -596,7 +605,7 @@ fn law_lithology_cliff_coast_emits_rock() { assert_all_terrain_is( 42, "cliff_body", - ®ion, + &district, (0, 0), TerrainMaterial::Rock, "CliffCoast", @@ -606,7 +615,7 @@ fn law_lithology_cliff_coast_emits_rock() { #[test] fn law_lithology_incised_gorge_emits_rock() { // D-239 §8: IncisedGorge/MountainPass → TerrainMaterial::Rock. - let region = make_region( + let district = make_region( MorphologyZone::MountainPass, TectonicClass::Active, GlaciationGrade::None, @@ -620,7 +629,7 @@ fn law_lithology_incised_gorge_emits_rock() { assert_all_terrain_is( 42, "gorge_body", - ®ion, + &district, (0, 0), TerrainMaterial::Rock, "IncisedGorge", @@ -630,7 +639,7 @@ fn law_lithology_incised_gorge_emits_rock() { #[test] fn law_lithology_dune_strand_emits_sand() { // D-239 §8: DuneStrand → TerrainMaterial::Sand (≤32° angle of repose). - let region = make_region( + let district = make_region( MorphologyZone::DuneStrand, TectonicClass::Stable, GlaciationGrade::None, @@ -644,7 +653,7 @@ fn law_lithology_dune_strand_emits_sand() { assert_all_terrain_is( 42, "dune_body", - ®ion, + &district, (0, 0), TerrainMaterial::Sand, "DuneStrand", @@ -654,7 +663,7 @@ fn law_lithology_dune_strand_emits_sand() { #[test] fn law_lithology_braided_delta_emits_gravel() { // D-239 §8: BraidedDelta (Gravel→braided channels/fans) → TerrainMaterial::Gravel. - let region = make_region( + let district = make_region( MorphologyZone::Delta, TectonicClass::Active, GlaciationGrade::None, @@ -668,7 +677,7 @@ fn law_lithology_braided_delta_emits_gravel() { assert_all_terrain_is( 42, "delta_body", - ®ion, + &district, (0, 0), TerrainMaterial::Gravel, "BraidedDelta", @@ -678,7 +687,7 @@ fn law_lithology_braided_delta_emits_gravel() { #[test] fn law_lithology_alluvial_plain_emits_soil() { // D-239 §8: AlluvialPlain → Soil (non-wetland params: slope_q>5, moisture_q<60). - let region = make_region( + let district = make_region( MorphologyZone::AlluvialPlain, TectonicClass::Stable, GlaciationGrade::None, @@ -692,7 +701,7 @@ fn law_lithology_alluvial_plain_emits_soil() { assert_all_terrain_is( 42, "alluvial_body", - ®ion, + &district, (0, 0), TerrainMaterial::Soil, "AlluvialPlain", @@ -702,7 +711,7 @@ fn law_lithology_alluvial_plain_emits_soil() { #[test] fn law_lithology_meander_reach_emits_soil() { // D-239 §8: MeanderReach (Soil→rolling/floodplain) → TerrainMaterial::Soil. - let region = make_region( + let district = make_region( MorphologyZone::MeanderReach, TectonicClass::Stable, GlaciationGrade::None, @@ -716,7 +725,7 @@ fn law_lithology_meander_reach_emits_soil() { assert_all_terrain_is( 42, "meander_body", - ®ion, + &district, (0, 0), TerrainMaterial::Soil, "MeanderReach", @@ -959,14 +968,14 @@ fn law_climate_vegetation_airless_always_absent() { } // --------------------------------------------------------------------------- -// §2b — Region-anchored feature placement (T-1040 / T-1041, D-239 §10) +// §2b — District-anchored feature placement (T-1040 / T-1041, D-239 §10) // --------------------------------------------------------------------------- // // T-1040: channel centrelines were anchored to the world x=0/y=0 axis — every -// chunk of a watered region claimed has_active_channel while channel voxels +// chunk of a watered district claimed has_active_channel while channel voxels // existed only near the world origin. T-1041: fjord/cliff/gorge/delta folded -// world coordinates into the 64 m chunk frame — region-scale landforms -// repeated every chunk. Both are fixed by region-anchored feature axes +// world coordinates into the 64 m chunk frame — district-scale landforms +// repeated every chunk. Both are fixed by district-anchored feature axes // (`channel_anchor_m` / `coast_anchor_m`); these tests pin the placement // contract far from the origin, at the ticket's example chunk (1000, −750). @@ -975,8 +984,8 @@ fn channel_present_in_active_chunks_far_from_origin() { // T-1040 (a): a has_active_channel chunk at an arbitrary large world // offset contains in-channel voxels — and the gate is honest in both // directions (wet ⇒ gated, ungated ⇒ dry). Sweeps the 16 cross-columns of - // the region containing chunk (1000, −750) at that chunk's along index. - let region = make_region( + // the district containing chunk (1000, −750) at that chunk's along index. + let district = make_region( MorphologyZone::AlluvialPlain, TectonicClass::Stable, GlaciationGrade::None, @@ -988,27 +997,27 @@ fn channel_present_in_active_chunks_far_from_origin() { VegetationClass::Forest, ); let (seed, body) = (42u64, "GJ144d"); - let ns = basin_is_ns(seed, body, ®ion, (1000, -750)); + let ns = basin_is_ns(seed, body, &district, (1000, -750)); let cross_base = if ns { - (1000 >> 4) << 4 + (1000 >> scale::CHUNK_DISTRICT_SHIFT) << scale::CHUNK_DISTRICT_SHIFT } else { - (-750i32 >> 4) << 4 + (-750i32 >> scale::CHUNK_DISTRICT_SHIFT) << scale::CHUNK_DISTRICT_SHIFT }; let mut any_gated_wet = false; let mut any_gate_off = false; let mut total_wet = 0usize; - for i in 0..16 { + for i in 0..scale::CHUNKS_PER_DISTRICT { let pos: ChunkPos = if ns { (cross_base + i, -750) } else { (1000, cross_base + i) }; - let chunk = derive_chunk_context(seed, body, ®ion, pos); + let chunk = derive_chunk_context(seed, body, &district, pos); let (bx, by) = (pos.0 * 64, pos.1 * 64); let mut wet = 0usize; for dy in 0..64i32 { for dx in 0..64i32 { - let col = derive_voxel_column(seed, body, ®ion, &chunk, bx + dx, by + dy); + let col = derive_voxel_column(seed, body, &district, &chunk, bx + dx, by + dy); if col.water != Water::Dry { wet += 1; } @@ -1028,7 +1037,7 @@ fn channel_present_in_active_chunks_far_from_origin() { } assert!( total_wet > 0, - "T-1040: the region at chunk (1000, −750) must contain channel voxels \ + "T-1040: the district at chunk (1000, −750) must contain channel voxels \ (pre-fix: zero — channels existed only near the world-origin axis)" ); assert!( @@ -1037,7 +1046,7 @@ fn channel_present_in_active_chunks_far_from_origin() { ); assert!( any_gate_off, - "T-1040: the channel band must not blanket the region — some chunks must gate off" + "T-1040: the channel band must not blanket the district — some chunks must gate off" ); } @@ -1047,7 +1056,7 @@ fn channel_continuous_across_chunk_boundary_far_from_origin() { // Every tile derives under its PRODUCTION covering chunk; the wet band's // midpoint may not jump at a 64 m along-boundary (chunk-frame dependence // would jump by up to a chunk width or drop out entirely). - let region = make_region( + let district = make_region( MorphologyZone::AlluvialPlain, TectonicClass::Stable, GlaciationGrade::None, @@ -1059,7 +1068,7 @@ fn channel_continuous_across_chunk_boundary_far_from_origin() { VegetationClass::Forest, ); let (seed, body) = (42u64, "GJ144d"); - let probe = derive_chunk_context(seed, body, ®ion, (1000, -750)); + let probe = derive_chunk_context(seed, body, &district, (1000, -750)); let ns = matches!( probe.basin_direction, BasinDirection::North | BasinDirection::South @@ -1069,12 +1078,12 @@ fn channel_continuous_across_chunk_boundary_far_from_origin() { let row_mid = |along: i32| -> Option { let wet: Vec = (anchor - 220..anchor + 220) .filter(|&c| { - derive_at_cross_along(seed, body, ®ion, ns, c, along).water != Water::Dry + derive_at_cross_along(seed, body, &district, ns, c, along).water != Water::Dry }) .collect(); wet.first().map(|f| (f + wet.last().unwrap()) / 2) }; - // An interior along-boundary of the region containing chunk (1000, −750): + // An interior along-boundary of the district containing chunk (1000, −750): // between along-chunks −744 and −743 (N/S) or 1004 and 1005 (E/W). let boundary = if ns { -743 * 64 } else { 1005 * 64 }; let mut prev: Option = None; @@ -1100,11 +1109,11 @@ fn channel_continuous_across_chunk_boundary_far_from_origin() { } #[test] -fn fjord_region_has_one_valley_spanning_chunks() { - // T-1041 (b): a FjordWall region contains ONE deep-water trough spanning +fn fjord_district_has_one_valley_spanning_chunks() { + // T-1041 (b): a FjordWall district contains ONE deep-water trough spanning // its chunks — pre-fix a complete fjord cross-section repeated in every - // 64 m chunk. Representative sweep: two far regions × three along rows. - let region = make_region( + // 64 m chunk. Representative sweep: two far districts × three along rows. + let district = make_region( MorphologyZone::Fjord, TectonicClass::Active, GlaciationGrade::Moderate, @@ -1116,30 +1125,34 @@ fn fjord_region_has_one_valley_spanning_chunks() { VegetationClass::Barren, ); let (seed, body) = (42u64, "fjord_body"); - for region_chunk in [(640, -480), (-336, 992)] { - let probe = derive_chunk_context(seed, body, ®ion, region_chunk); + for district_chunk in [(640, -480), (-336, 992)] { + let probe = derive_chunk_context(seed, body, &district, district_chunk); let ns = matches!( probe.basin_direction, BasinDirection::North | BasinDirection::South ); let (cross_chunk, along_chunk) = if ns { - (region_chunk.0, region_chunk.1) + (district_chunk.0, district_chunk.1) } else { - (region_chunk.1, region_chunk.0) + (district_chunk.1, district_chunk.0) }; - let cross_base = (cross_chunk >> 4) * 1024; - let along_base = (along_chunk >> 4) * 1024; - let positions: Vec = (cross_base..cross_base + 1024).collect(); - for along in [along_base + 32, along_base + 512, along_base + 992] { + let cross_base = (cross_chunk >> scale::CHUNK_DISTRICT_SHIFT) * scale::DISTRICT_M; + let along_base = (along_chunk >> scale::CHUNK_DISTRICT_SHIFT) * scale::DISTRICT_M; + let positions: Vec = (cross_base..cross_base + scale::DISTRICT_M).collect(); + for along in [ + along_base + 32, + along_base + scale::DISTRICT_M / 2, + along_base + scale::DISTRICT_M - 32, + ] { let clusters = count_feature_clusters( &positions, - |c| derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Deep, + |c| derive_at_cross_along(seed, body, &district, ns, c, along).water == Water::Deep, 16, ); assert_eq!( clusters, 1, - "T-1041: FjordWall region {region_chunk:?} must contain exactly ONE \ - deep-water trough across its 1024 m cross extent at along={along} \ + "T-1041: FjordWall district {district_chunk:?} must contain exactly ONE \ + deep-water trough across its 2048 m cross extent at along={along} \ (got {clusters}; pre-fix: one per 64 m chunk)" ); } @@ -1147,10 +1160,10 @@ fn fjord_region_has_one_valley_spanning_chunks() { } #[test] -fn gorge_region_has_one_valley_spanning_chunks() { - // T-1041 (b): an IncisedGorge region contains ONE shallow-floor gorge +fn gorge_district_has_one_valley_spanning_chunks() { + // T-1041 (b): an IncisedGorge district contains ONE shallow-floor gorge // spanning its chunks — not one per chunk. - let region = make_region( + let district = make_region( MorphologyZone::MountainPass, TectonicClass::Active, GlaciationGrade::None, @@ -1162,32 +1175,37 @@ fn gorge_region_has_one_valley_spanning_chunks() { VegetationClass::Scrub, ); let (seed, body) = (42u64, "gorge_body"); - for region_chunk in [(640, -480), (-336, 992)] { - let probe = derive_chunk_context(seed, body, ®ion, region_chunk); + for district_chunk in [(640, -480), (-336, 992)] { + let probe = derive_chunk_context(seed, body, &district, district_chunk); let ns = matches!( probe.basin_direction, BasinDirection::North | BasinDirection::South ); let (cross_chunk, along_chunk) = if ns { - (region_chunk.0, region_chunk.1) + (district_chunk.0, district_chunk.1) } else { - (region_chunk.1, region_chunk.0) + (district_chunk.1, district_chunk.0) }; - let cross_base = (cross_chunk >> 4) * 1024; - let along_base = (along_chunk >> 4) * 1024; - let positions: Vec = (cross_base..cross_base + 1024).collect(); - for along in [along_base + 32, along_base + 512, along_base + 992] { + let cross_base = (cross_chunk >> scale::CHUNK_DISTRICT_SHIFT) * scale::DISTRICT_M; + let along_base = (along_chunk >> scale::CHUNK_DISTRICT_SHIFT) * scale::DISTRICT_M; + let positions: Vec = (cross_base..cross_base + scale::DISTRICT_M).collect(); + for along in [ + along_base + 32, + along_base + scale::DISTRICT_M / 2, + along_base + scale::DISTRICT_M - 32, + ] { let clusters = count_feature_clusters( &positions, |c| { - derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Shallow + derive_at_cross_along(seed, body, &district, ns, c, along).water + == Water::Shallow }, 16, ); assert_eq!( clusters, 1, - "T-1041: IncisedGorge region {region_chunk:?} must contain exactly ONE \ - shallow gorge floor across its 1024 m cross extent at along={along} \ + "T-1041: IncisedGorge district {district_chunk:?} must contain exactly ONE \ + shallow gorge floor across its 2048 m cross extent at along={along} \ (got {clusters}; pre-fix: one per 64 m chunk)" ); } @@ -1196,11 +1214,11 @@ fn gorge_region_has_one_valley_spanning_chunks() { #[test] fn cliff_coast_one_continuous_coastline_per_region() { - // T-1041 (b): a CliffCoast region has ONE continuous (warp-displaced) - // coast line on the region-anchored face — pre-fix the cliff face sat at + // T-1041 (b): a CliffCoast district has ONE continuous (warp-displaced) + // coast line on the district-anchored face — pre-fix the cliff face sat at // intra-chunk offset 48–55 in every chunk, sawtoothing the coast at 64 m - // pitch. Transect runs along the seaward (basin) axis across the region. - let region = make_region( + // pitch. Transect runs along the seaward (basin) axis across the district. + let district = make_region( MorphologyZone::CliffCoast, TectonicClass::Active, GlaciationGrade::None, @@ -1212,30 +1230,33 @@ fn cliff_coast_one_continuous_coastline_per_region() { VegetationClass::Scrub, ); let (seed, body) = (42u64, "cliff_body"); - for region_chunk in [(656, -464), (-256, 768)] { - let probe = derive_chunk_context(seed, body, ®ion, region_chunk); + for district_chunk in [(656, -464), (-256, 768)] { + let probe = derive_chunk_context(seed, body, &district, district_chunk); let coast = probe.coast_anchor_m; let ns = matches!( probe.basin_direction, BasinDirection::North | BasinDirection::South ); let (cross_chunk, along_chunk) = if ns { - (region_chunk.0, region_chunk.1) + (district_chunk.0, district_chunk.1) } else { - (region_chunk.1, region_chunk.0) + (district_chunk.1, district_chunk.0) }; - let along_base = (along_chunk >> 4) * 1024; + let along_base = (along_chunk >> scale::CHUNK_DISTRICT_SHIFT) * scale::DISTRICT_M; let cross_fixed = cross_chunk * 64 + 32; - let positions: Vec = (along_base..along_base + 1024).collect(); + let positions: Vec = (along_base..along_base + scale::DISTRICT_M).collect(); // Exactly one ocean cluster (the seaward side of the one coast line). let clusters = count_feature_clusters( &positions, - |a| derive_at_cross_along(seed, body, ®ion, ns, cross_fixed, a).water == Water::Deep, + |a| { + derive_at_cross_along(seed, body, &district, ns, cross_fixed, a).water + == Water::Deep + }, 16, ); assert_eq!( clusters, 1, - "T-1041: CliffCoast region {region_chunk:?} must have exactly ONE ocean \ + "T-1041: CliffCoast district {district_chunk:?} must have exactly ONE ocean \ side (got {clusters} Deep clusters; pre-fix: one 64 m sawtooth per chunk)" ); // Monotone coast: well inland of the face line → Dry; well seaward → @@ -1246,7 +1267,7 @@ fn cliff_coast_one_continuous_coastline_per_region() { BasinDirection::North | BasinDirection::West => coast - a, BasinDirection::South | BasinDirection::East => a - coast, }; - let col = derive_at_cross_along(seed, body, ®ion, ns, cross_fixed, a); + let col = derive_at_cross_along(seed, body, &district, ns, cross_fixed, a); if d <= -9 { assert_eq!( col.water, @@ -1267,11 +1288,11 @@ fn cliff_coast_one_continuous_coastline_per_region() { #[test] fn braided_threads_confined_to_region_belt() { - // T-1041: braided threads anastomose across the region-anchored fan belt + // T-1041: braided threads anastomose across the district-anchored fan belt // (anchor ± 32 m + thread width + warp) — pre-fix the same three threads // restarted in every 64 m chunk, spreading thread water across the whole - // region's cross extent. - let region = make_region( + // district's cross extent. + let district = make_region( MorphologyZone::Delta, TectonicClass::Active, GlaciationGrade::None, @@ -1283,28 +1304,28 @@ fn braided_threads_confined_to_region_belt() { VegetationClass::Scrub, ); let (seed, body) = (17u64, "delta_body"); - for region_chunk in [(800, -592)] { - let probe = derive_chunk_context(seed, body, ®ion, region_chunk); + for district_chunk in [(800, -592)] { + let probe = derive_chunk_context(seed, body, &district, district_chunk); let ns = matches!( probe.basin_direction, BasinDirection::North | BasinDirection::South ); let anchor = probe.channel_anchor_m; let (cross_chunk, along_chunk) = if ns { - (region_chunk.0, region_chunk.1) + (district_chunk.0, district_chunk.1) } else { - (region_chunk.1, region_chunk.0) + (district_chunk.1, district_chunk.0) }; - let cross_base = (cross_chunk >> 4) * 1024; + let cross_base = (cross_chunk >> scale::CHUNK_DISTRICT_SHIFT) * scale::DISTRICT_M; let along = along_chunk * 64 + 32; - let wet: Vec = (cross_base..cross_base + 1024) + let wet: Vec = (cross_base..cross_base + scale::DISTRICT_M) .filter(|&c| { - derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Shallow + derive_at_cross_along(seed, body, &district, ns, c, along).water == Water::Shallow }) .collect(); assert!( !wet.is_empty(), - "T-1041: BraidedDelta region {region_chunk:?} must contain thread water" + "T-1041: BraidedDelta district {district_chunk:?} must contain thread water" ); // Belt confinement: thread centres ∈ anchor ± 32, half-width ≤ 4, // warp ≤ 8 → all thread water within anchor ± 44. @@ -1312,7 +1333,7 @@ fn braided_threads_confined_to_region_belt() { assert!( (c - anchor).abs() <= 44, "T-1041: thread water at cross={c} is {} m from the fan axis {anchor} \ - — outside the region belt (pre-fix: threads repeated every chunk)", + — outside the district belt (pre-fix: threads repeated every chunk)", (c - anchor).abs() ); } @@ -1335,17 +1356,17 @@ fn braided_threads_confined_to_region_belt() { fn derive_chunk_timed( seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, chunk_pos: (i32, i32), ) -> (usize, u128) { - let chunk = derive_chunk_context(seed, body_id, region, chunk_pos); + let chunk = derive_chunk_context(seed, body_id, district, chunk_pos); let base_x = chunk_pos.0 * 64; let base_y = chunk_pos.1 * 64; let t0 = Instant::now(); let mut count = 0usize; for dy in 0..64i32 { for dx in 0..64i32 { - let _ = derive_voxel_column(seed, body_id, region, &chunk, base_x + dx, base_y + dy); + let _ = derive_voxel_column(seed, body_id, district, &chunk, base_x + dx, base_y + dy); count += 1; } } @@ -1353,7 +1374,7 @@ fn derive_chunk_timed( } /// All 8 family inputs for the budget sweep. -fn budget_families() -> Vec<(&'static str, RegionProfile)> { +fn budget_families() -> Vec<(&'static str, DistrictProfile)> { vec![ ( "AlluvialPlain", @@ -1486,8 +1507,8 @@ fn budget_per_family_chunk_derivation() { let mut results: Vec<(&str, u128)> = vec![]; - for (label, region) in budget_families() { - let (cnt, us) = derive_chunk_timed(seed, body_id, ®ion, (1, 1)); + for (label, district) in budget_families() { + let (cnt, us) = derive_chunk_timed(seed, body_id, &district, (1, 1)); let ms = us as f64 / 1000.0; eprintln!("[budget] {label}: {ms:.2} ms / {cnt} voxels"); @@ -1540,7 +1561,7 @@ fn budget_per_family_chunk_derivation() { fn budget_voxel_cache_lru_overhead() { let seed = 0xfeedface_12345678_u64; let body_id = "cache_budget_body"; - let region = make_region( + let district = make_region( MorphologyZone::AlluvialPlain, TectonicClass::Stable, GlaciationGrade::None, @@ -1552,7 +1573,7 @@ fn budget_voxel_cache_lru_overhead() { VegetationClass::Forest, ); let chunk_pos = (2, 2); - let chunk = derive_chunk_context(seed, body_id, ®ion, chunk_pos); + let chunk = derive_chunk_context(seed, body_id, &district, chunk_pos); let base_x = chunk_pos.0 * 64; let base_y = chunk_pos.1 * 64; @@ -1564,13 +1585,13 @@ fn budget_voxel_cache_lru_overhead() { let t0 = Instant::now(); for dy in 0..64i32 { for dx in 0..64i32 { - let _ = cache.get_or_derive(seed, body_id, ®ion, &chunk, base_x + dx, base_y + dy); + let _ = cache.get_or_derive(seed, body_id, &district, &chunk, base_x + dx, base_y + dy); } } let cache_us = t0.elapsed().as_micros(); // Direct derivation for comparison. - let (_, direct_us) = derive_chunk_timed(seed, body_id, ®ion, chunk_pos); + let (_, direct_us) = derive_chunk_timed(seed, body_id, &district, chunk_pos); let overhead_ratio = cache_us as f64 / direct_us.max(1) as f64; eprintln!( @@ -1612,8 +1633,9 @@ fn kallast_body_params() -> BodyParams { atmosphere: Some("standard".into()), planet_class: Some("temperate".into()), tectonic_activity: Some("active".into()), - region_latitude_deg: 0.0, + district_latitude_deg: 0.0, elevation_km: 0.0, + body_radius_km: None, } } @@ -1626,8 +1648,9 @@ fn gloedberg_body_params() -> BodyParams { atmosphere: Some("standard".into()), planet_class: Some("volcanic".into()), tectonic_activity: Some("volcanic".into()), - region_latitude_deg: 0.0, + district_latitude_deg: 0.0, elevation_km: 0.5, + body_radius_km: None, } } @@ -1641,23 +1664,24 @@ fn marevna_body_params() -> BodyParams { atmosphere: Some("standard".into()), planet_class: Some("oceanic".into()), tectonic_activity: Some("active".into()), - region_latitude_deg: 0.0, + district_latitude_deg: 0.0, elevation_km: 0.0, + body_radius_km: None, } } -/// Derive a RegionProfile for a body at given latitude/elevation, -/// using the full derive_region_profile pipeline over a minimal dry-land heightmap. +/// Derive a DistrictProfile for a body at given latitude/elevation, +/// using the full derive_district_profile pipeline over a minimal dry-land heightmap. /// /// Uses a heightmap where all cells are above sea level (data = 0.5, sea_level = 0.3), /// so ocean_fraction_q = 0 at all positions. This ensures the morphology classifier /// reaches the tectonic/glaciation gates without being short-circuited by the Tier-0 /// ocean check (which fires at ocean_fraction_q ≥ 60). -fn derive_profile_for_body(params: &BodyParams) -> RegionProfile { +fn derive_profile_for_body(params: &BodyParams) -> DistrictProfile { let climate = ClimateConstants::default(); let seed = SeedChain::root(42).derive(SeedDomain::Body, 1); let ta = make_dry_terrain_analysis(); - derive_region_profile(seed, params, &ta, (0, 0), 8, &climate) + derive_district_profile(seed, params, &ta, (0, 0), 8, &climate) } #[test] @@ -1875,10 +1899,15 @@ fn validation_gruenfeld_skipped_not_in_wiki() { // Shared helpers // --------------------------------------------------------------------------- -/// Whether the region's basin runs north/south (cross axis = x). Region-scale -/// property — identical for every chunk of the region containing `region_chunk`. -fn basin_is_ns(seed: u64, body_id: &str, region: &RegionProfile, region_chunk: ChunkPos) -> bool { - let probe = derive_chunk_context(seed, body_id, region, region_chunk); +/// Whether the district's basin runs north/south (cross axis = x). District-scale +/// property — identical for every chunk of the district containing `district_chunk`. +fn basin_is_ns( + seed: u64, + body_id: &str, + district: &DistrictProfile, + district_chunk: ChunkPos, +) -> bool { + let probe = derive_chunk_context(seed, body_id, district, district_chunk); matches!( probe.basin_direction, BasinDirection::North | BasinDirection::South @@ -1886,31 +1915,31 @@ fn basin_is_ns(seed: u64, body_id: &str, region: &RegionProfile, region_chunk: C } /// The chunk positions of the channel-anchor band (anchor column ± 1) across -/// the full along-extent of the region containing `region_chunk` (T-1040). +/// the full along-extent of the district containing `district_chunk` (T-1040). /// -/// The channel/landform centreline is region-anchored: it lives in the anchor +/// The channel/landform centreline is district-anchored: it lives in the anchor /// chunk column, swinging up to one meander amplitude sideways. The meander -/// wavelength (≤ ~650 m) fits inside the region's 1024 m along-extent, so the +/// wavelength (≤ ~650 m) fits inside the district's 2048 m along-extent, so the /// centreline crosses the anchor column at least once — sweeping this band /// guarantees wet tiles are exercised somewhere in it. fn anchor_band_chunks( seed: u64, body_id: &str, - region: &RegionProfile, - region_chunk: ChunkPos, + district: &DistrictProfile, + district_chunk: ChunkPos, ) -> Vec { - let probe = derive_chunk_context(seed, body_id, region, region_chunk); + let probe = derive_chunk_context(seed, body_id, district, district_chunk); let anchor_idx = probe.channel_anchor_m.div_euclid(64); let ns = matches!( probe.basin_direction, BasinDirection::North | BasinDirection::South ); - // Region base index on the along axis (16-chunk regions; arithmetic shift + // District base index on the along axis (16-chunk districts; arithmetic shift // = floor division, correct for negative chunks). let along_base = if ns { - (region_chunk.1 >> 4) << 4 + (district_chunk.1 >> scale::CHUNK_DISTRICT_SHIFT) << scale::CHUNK_DISTRICT_SHIFT } else { - (region_chunk.0 >> 4) << 4 + (district_chunk.0 >> scale::CHUNK_DISTRICT_SHIFT) << scale::CHUNK_DISTRICT_SHIFT }; let mut out = Vec::new(); for cross in anchor_idx - 1..=anchor_idx + 1 { @@ -1927,15 +1956,15 @@ fn anchor_band_chunks( fn derive_at_cross_along( seed: u64, body_id: &str, - region: &RegionProfile, + district: &DistrictProfile, ns: bool, cross: i32, along: i32, ) -> settled_reach_server::atlas::voxel::VoxelColumn { let (tx, ty) = if ns { (cross, along) } else { (along, cross) }; let chunk_pos = (tx.div_euclid(64), ty.div_euclid(64)); - let chunk = derive_chunk_context(seed, body_id, region, chunk_pos); - derive_voxel_column(seed, body_id, region, &chunk, tx, ty) + let chunk = derive_chunk_context(seed, body_id, district, chunk_pos); + derive_voxel_column(seed, body_id, district, &chunk, tx, ty) } /// Count clusters of positions where `pred` holds across a cross/along @@ -1961,7 +1990,7 @@ fn count_feature_clusters( clusters } -/// Construct a `RegionProfile` directly from parameters, deriving the +/// Construct a `DistrictProfile` directly from parameters, deriving the /// dependent fields (precipitation_class, river_threshold) consistently. fn make_region( zone: MorphologyZone, @@ -1973,9 +2002,9 @@ fn make_region( moisture_q: i32, temperature_c: Option, vegetation_class: VegetationClass, -) -> RegionProfile { +) -> DistrictProfile { let precip = derive_precipitation_class_from_climate(temperature_c, moisture_q); - RegionProfile { + DistrictProfile { morphology_zone: zone, tectonic_class: tectonic, glaciation_grade: glaciation, @@ -1993,7 +2022,7 @@ fn make_region( /// Build a flat all-dry `TerrainAnalysis` for validation body derivation. /// /// All cells have elevation = 0.5 with sea_level = 0.3 → ocean_fraction_q = 0 at -/// every region position. This prevents the Tier-0 ocean short-circuit in +/// every district position. This prevents the Tier-0 ocean short-circuit in /// `derive_morphology_zone` (ocean_fraction_q ≥ 60) from overriding the tectonic /// and glaciation gates that we're testing. /// diff --git a/server/tests/golden/derivation_harness.json b/server/tests/golden/derivation_harness.json index caa02cdd4..d777174ae 100644 --- a/server/tests/golden/derivation_harness.json +++ b/server/tests/golden/derivation_harness.json @@ -3,12 +3,12 @@ "label": "alluvial_forest_active_channel", "seed": 16045690984503098046, "body_id": "GJ144d", - "tile_x": 223, + "tile_x": 1375, "tile_y": 800, "terrain": 0, "vegetation": 4, - "water": 1, - "elevation_m": 8, + "water": 0, + "elevation_m": 14, "cover": 0 }, { @@ -27,12 +27,12 @@ "label": "fjord_wall_glaciated", "seed": 18369614217980264670, "body_id": "GJ447c", - "tile_x": 228, + "tile_x": 1508, "tile_y": 352, "terrain": 3, "vegetation": 0, - "water": 2, - "elevation_m": 0, + "water": 0, + "elevation_m": 13, "cover": 1 } ]