Files
settled-reach/server/src/atlas/voxel.rs
T
jpmschweitzerandClaude Opus 4.8 677de42bd2 fix(simulation): make voxel relief sub-district + fix integration-test fallout (T-1081)
The push gate's integration tests (not in cargo test --lib) caught two regressions
from the relief change:

- derivation_harness::cross_district_elevation_blend_reduces_seam_step — the relief's
  large per-position term (span 300 → ±200 m, far above the compressed elev_q/2 base)
  swamped the T-1042 seam measurement and clamped heavily at sea level. Fixes:
  (1) shift the voxel-relief octave band to 0.13–1 km (all sub-district, dropping the
  2 km octave that competed with elev_q's district role); (2) reduce VOXEL_RELIEF_SPAN_M
  300 → 100 so relief stays mostly below the base (fewer sea-level clamp artifacts,
  proportional hills); (3) rewrite the seam test's avg_elev to average over an 8 km
  multi-wavelength y-transect so the zero-mean relief cancels, isolating the base seam.
- derivation_harness::golden_seed_determinism_regression — legitimate golden refresh
  (determinism still holds; elevation values changed intentionally).

Believability relief now ≈22 m mean (was 5 m flat; 59 m at the over-aggressive span 300)
— navigable hills without clamp artifacts, 7/8 criteria. Both goldens regenerated; full
cargo test (38 binaries) + clippy --all-targets -D warnings green. D-239 amendment +
Q-123 item 4 updated to the final span/octave/numbers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 21:43:39 +02:00

4072 lines
179 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 `DistrictProfile` (~1 km). Never stored,
//! never persisted — cached in `VoxelCache` (D-227).
//!
//! ## D-228 composite tile axes
//!
//! Each `VoxelColumn` is a bundle of **orthogonal axes** (D-228):
//! - `terrain`: `TerrainMaterial` — permanent natural ground (Soil/Sand/Gravel/Rock/Wetland/Lava)
//! - `floor`: `FloorMaterial` — built surface (None for now; Phase 5)
//! - `vegetation`: `Vegetation` — ground cover (from `VegetationClass`)
//! - `water`: `Water` — local depth state (Dry/Shallow/Deep)
//! - `elevation_m`: scalar metres
//! - `cover`: `SeasonalCover` — seasonal cover overlay (Snow/Ice, D-228/D-239 §3)
//!
//! **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 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.
//!
//! ## Domain warp (D-239 §4)
//!
//! The warp is applied **before** the integer voxel address is computed. The
//! caller passes the logical tile position `(tile_x, tile_y)` in metres; the
//! derivation function calls `domain_warp(seed, body_id, (tile_x, tile_y))` to
//! get `(dx, dy)` in f64, then truncates to the integer voxel address with
//! `as i32` (cast, not comparison — IEEE-754 deterministic, D-239 §4).
//!
//! The warp is **the only f64** in the structural derivation path; all
//! material/morphology decisions downstream operate on the integer voxel address.
//!
//! ## Cache (D-227)
//!
//! `VoxelCache` is a bounded LRU-style cache backed by a `BTreeMap` and a
//! monotonic generation counter. Same (seed, body, pos) → same column;
//! eviction is by generation-counter LRU (oldest entry evicted when capacity
//! is reached). Never written to disk.
//!
//! ## Family dispatch (D-239 §5, T-1028/T-1029)
//!
//! The family tree is dispatched through `MorphologyFamily`. All generators are
//! implemented: `AlluvialPlain` (the D-239 §5 fallback) landed in T-1028; the seven
//! landform families (LavaField, FjordWall, CliffCoast, BraidedDelta, DuneStrand,
//! IncisedGorge, MeanderReach) in T-1029; and `WaterBody` (OpenOcean / Lake / TidalFlat
//! — Deep/Shallow water + seabed) in T-1082, the 9th family that amends D-239 §5/§6's
//! original "8 families". Remaining flat *land* zones (AlluvialPlain, Wetland — a marsh,
//! not open water) still map to `AlluvialPlain` per D-239 §5.
//!
//! ## D-010 compliance
//!
//! All material/morphology decisions are integer. The only f64 path is the
//! domain warp displacement (positional math, truncated before any decision).
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::seed::{SeedChain, SeedDomain};
use crate::simulation::generator::MorphologyZone;
// ---------------------------------------------------------------------------
// D-228 tile axes
// ---------------------------------------------------------------------------
/// Permanent natural ground material (D-228).
///
/// **Snow and Ice are NOT here** — they are a seasonal cover overlay (Q-105).
/// Integer-discriminant, append-only (D-010).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum TerrainMaterial {
/// Loamy soil — rolling/floodplain (D-239 §8 Soil law).
#[default]
Soil = 0,
/// Sand — ≤~32° angle of repose, dunes not cliffs (D-239 §8 Sand law).
Sand = 1,
/// Gravel — braided channels/fans, not single-thread meander (D-239 §8).
Gravel = 2,
/// Rock — vertical faces on steep steps (D-239 §8 Rock law).
Rock = 3,
/// Wetland substrate — saturated organic soil, ≤5° flats (D-239 §8).
Wetland = 4,
/// Lava — sheets/shield slopes + tubes, immature drainage (D-239 §8).
Lava = 5,
}
/// Built surface over natural ground (D-228).
///
/// `None` is the only implemented value for Phase 4 — built surfaces are
/// Phase 5 (player control). The vocabulary is declared here so the type system
/// enforces the distinction from `TerrainMaterial`.
///
/// Integer-discriminant, append-only (D-010).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum FloorMaterial {
/// No built surface — natural ground exposed.
#[default]
None = 0,
// Phase 5 variants: Concrete = 1, Pavement = 2, Carpet = 3, Metal = 4, …
}
/// Ground cover (D-228). Carries cover/concealment, movement sound profile,
/// and economic yield (timber/crops).
///
/// 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)]
pub enum Vegetation {
/// No vegetation — above treeline or hyper-arid.
#[default]
Barren = 0,
/// Grass — open ground cover, low concealment.
Grass = 1,
/// Scrub — transitional shrubs, moderate concealment.
Scrub = 2,
/// Thicket — dense low cover, high concealment.
Thicket = 3,
/// Forest — closed canopy, full concealment.
Forest = 4,
/// Crop — managed agriculture (Phase 6 economic layer).
Crop = 5,
/// Cleared — formerly vegetated, stripped (player/sim action).
Cleared = 6,
}
impl Vegetation {
/// Map a district-level `VegetationClass` to the per-voxel `Vegetation` axis.
///
/// 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.
pub fn from_vegetation_class(vc: VegetationClass) -> Self {
match vc {
VegetationClass::Absent => Vegetation::Barren,
VegetationClass::Barren => Vegetation::Barren,
VegetationClass::Scrub => Vegetation::Scrub,
VegetationClass::Forest => Vegetation::Forest,
VegetationClass::RiparianScrub => Vegetation::Thicket,
VegetationClass::RiparianThicket => Vegetation::Thicket,
}
}
}
/// Local depth state (D-228).
///
/// `Water` is dynamic — changes with season/tide (Q-105) — but for Phase 4
/// derivation we use the static base state (the seasonal overlay is deferred).
///
/// Integer-discriminant, append-only (D-010).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum Water {
/// No standing water.
#[default]
Dry = 0,
/// Shallow water — passable with effort; grants concealment penalty.
Shallow = 1,
/// Deep water — impassable without vessel; full concealment penalty.
Deep = 2,
}
/// Seasonal cover overlay (D-228 / D-239 §3, T-1030).
///
/// 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 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.
///
/// ## Q-105 forward contract
///
/// D-239 §3 states: "Transient: because temperature is clock-bound (season +
/// 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 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`
/// output of `derive_cover` remains valid as the base state; Q-105 applies a
/// time-dependent modifier on top of it.
///
/// Integer-discriminant, append-only (D-010).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum SeasonalCover {
/// No seasonal cover — ground is bare.
#[default]
None = 0,
/// Snow — cold + wet land surface; reduces mobility, provides concealment.
Snow = 1,
/// Ice — frozen water surface or permanent ice; alters passability.
Ice = 2,
}
// ---------------------------------------------------------------------------
// VoxelColumn
// ---------------------------------------------------------------------------
/// The finest derivation tier — the D-228 composite tile axes for one 1 m column.
///
/// 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 {
/// Permanent natural ground material (D-228).
pub terrain: TerrainMaterial,
/// Built surface over the ground (D-228). `None` in Phase 4.
pub floor: FloorMaterial,
/// Ground cover (D-228). Derived from district `VegetationClass`.
pub vegetation: Vegetation,
/// Local depth state (D-228).
pub water: Water,
/// Surface elevation in integer metres (D-010; no f64 in the stored value).
pub elevation_m: i32,
/// Seasonal cover overlay (D-228 / D-239 §3, T-1030).
///
/// Static mean-state: whether mean-annual temperature puts this voxel in
/// the frozen zone, gated by surface type + spatially-coherent cluster
/// scatter. The transient clock-bound layer (Q-105) is not represented here.
pub cover: SeasonalCover,
}
// ---------------------------------------------------------------------------
// Voxel position (integer metres, D-010)
// ---------------------------------------------------------------------------
/// Absolute voxel position — integer metres. The unit coordinate of D-222.
///
/// After domain warp is applied (`tile_x + dx as i32`), the result is the
/// integer voxel address used for all material/morphology decisions (D-239 §4).
pub type VoxelPos = (i32, i32);
// ---------------------------------------------------------------------------
// Family dispatch (D-239 §5)
// ---------------------------------------------------------------------------
/// 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 (DistrictProfile); the family is the
/// structural decision that drives voxel geometry.
///
/// All 8 families have dedicated generators: `AlluvialPlain` (T-1028, also the
/// D-239 §5 fallback for flat/water zones) and the other 7 (T-1029).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MorphologyFamily {
/// Lava field / shield slope. Requires TectonicClass::Volcanic (D-239 §5).
LavaField,
/// Fjord wall. Requires GlaciationGrade ≥ 2 (D-239 §5).
FjordWall,
/// Cliff coast. High slope + coastal (D-239 §5).
CliffCoast,
/// Braided delta. Very flat + low elevation + coastal (D-239 §5).
BraidedDelta,
/// Dune strand. Low slope + coastal + arid (D-239 §5).
DuneStrand,
/// Incised gorge / mountain pass. High slope + inland + high elev (D-239 §5).
IncisedGorge,
/// Meander reach. Gentle slope + water presence (D-239 §5).
MeanderReach,
/// Alluvial plain — the D-239 §5 fallback family (T-1028).
AlluvialPlain,
/// Water body — ocean / lake / tidal flat (T-1082). Renders Deep/Shallow water +
/// seabed substrate where the zones used to fall through to dry AlluvialPlain land.
/// The 9th family (amends D-239 §5/§6's "8 families").
WaterBody,
}
/// Map a `MorphologyZone` to the 8-family `MorphologyFamily` dispatch key.
///
/// Sub-classified zones (TidalFlat, Estuarine, Alpine, Wetland) map to their
/// parent family for voxel geometry — their distinction is captured in the
/// district zone label, not in the voxel generator dispatch.
///
/// 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 {
MorphologyZone::Volcanic => MorphologyFamily::LavaField,
MorphologyZone::Fjord => MorphologyFamily::FjordWall,
MorphologyZone::CliffCoast => MorphologyFamily::CliffCoast,
MorphologyZone::Delta | MorphologyZone::BraidedPlain | MorphologyZone::Estuarine => {
MorphologyFamily::BraidedDelta
}
MorphologyZone::DuneStrand => MorphologyFamily::DuneStrand,
MorphologyZone::MountainPass | MorphologyZone::ValleyFloor | MorphologyZone::Alpine => {
MorphologyFamily::IncisedGorge
}
MorphologyZone::MeanderReach | MorphologyZone::RiverBank => MorphologyFamily::MeanderReach,
// Water bodies get the dedicated WaterBody family (T-1082): they render
// Deep/Shallow water + seabed, not the dry AlluvialPlain land they used to.
MorphologyZone::OpenOcean | MorphologyZone::Lake | MorphologyZone::TidalFlat => {
MorphologyFamily::WaterBody
}
// AlluvialPlain fallback covers the remaining flat *land* zones: AlluvialPlain
// and Wetland (a marsh — saturated land, not open water; D-239 §8 Wetland law).
MorphologyZone::AlluvialPlain | MorphologyZone::Wetland => MorphologyFamily::AlluvialPlain,
}
}
// ---------------------------------------------------------------------------
// Voxel derivation entry point
// ---------------------------------------------------------------------------
/// Derive a `VoxelColumn` for the tile at integer metre position `(tile_x, tile_y)`.
///
/// ## Warp wiring (D-239 §4)
///
/// 1. Call `domain_warp(seed, body_id, (tile_x, tile_y))` → `(dx, dy)` in f64.
/// 2. Apply `voxel_x = (tile_x as f64 + dx) as i32` (and likewise for `voxel_y`).
/// 3. The integer `(voxel_x, voxel_y)` is used for all downstream decisions.
/// This is the **only** f64 in the structural path.
///
/// ## Parameters
///
/// - `world_seed` — master world seed.
/// - `body_id` — stable string body identifier (used for domain separation).
/// - `district` — the covering `DistrictProfile` (~1 km).
/// - `chunk` — the covering `ChunkContext` (64 m).
/// - `tile_x`, `tile_y` — logical tile position in **integer metres** (D-010).
///
/// ## Returns
///
/// A fully derived `VoxelColumn` with all D-228 axes populated.
/// T-1081: the metre span of the voxel-tier mid-scale relief field — the amplitude
/// ceiling for the ≈0.13–1 km rolling/ridged hills added to the family base
/// elevation. Actual relief at a voxel is `voxel_relief(...) * SPAN`, which the
/// envelope (district ruggedness) scales down on gentle terrain, so this is the
/// steep-terrain ceiling, not a uniform amplitude. Provisional — tuned against the
/// believability probe (T-1079) like the moisture gradient; per-body *absolute*
/// elevation span is a later refinement (the T-1080 climate-fields lineage).
///
/// Kept modest so relief stays mostly *below* the family base elevation: the base is
/// the compressed `elev_q/N` scale (max ~50 m) and `elevation_m` clamps at 0 (sea
/// level), so an oversized span clamps away on low ground — drowning the relief and
/// biasing it positive. A larger span belongs with the deferred per-body absolute
/// elevation model that would give the base real headroom.
const VOXEL_RELIEF_SPAN_M: i32 = 100;
pub fn derive_voxel_column(
world_seed: u64,
body_id: &str,
district: &DistrictProfile,
chunk: &ChunkContext,
tile_x: i32,
tile_y: i32,
) -> VoxelColumn {
// ── 1. Domain warp (D-239 §4) ─────────────────────────────────────────
// Apply warp and truncate to integer voxel address. This is the ONLY f64
// in the structural path; all decisions below use integer voxel coords.
// The warp is applied BEFORE the blend boundary, so the seam cannot align
// with a chunk edge (T-1042: "reusing the same warp offset so the blend
// seam cannot align with the chunk edge").
let (dx, dy) = domain_warp(world_seed, body_id, (tile_x, tile_y));
let voxel_x = (tile_x as f64 + dx) as i32;
let voxel_y = (tile_y as f64 + dy) as i32;
let voxel_pos: VoxelPos = (voxel_x, voxel_y);
// ── 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 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();
// ── 2b. Cross-district terrain blending (T-1042, D-239 §4/§7) ─────────
// When a secondary district is present AND blend_weight < 255, blend the
// continuous terrain scalars (`elev_q`, `moisture_q`) between the primary
// and secondary district. This is done BEFORE family dispatch so the
// blended values propagate through ALL family generators.
//
// Morphology family is NOT changed — family dispatch always uses the
// primary district's `morphology_zone` (D-239 §7: morphology seams stay
// sharp). Only `elev_q` and `moisture_q` are blended; all structural
// fields (glaciation_grade, tectonic_class, precipitation_class, etc.)
// stay primary.
//
// Integer blend formula: `(a * w + b * (255 - w) + 127) / 255` (D-010).
// This is a zero-cost branch when blend_weight == 255 (no secondary active).
let effective_district: std::borrow::Cow<DistrictProfile> = if chunk.blend_weight < 255 {
if let Some(ref sec) = chunk.secondary {
let w = chunk.blend_weight as i32;
let w_sec = 255 - w;
let blended_elev_q = (district.elev_q * w + sec.elev_q * w_sec + 127) / 255;
let blended_moisture_q = (district.moisture_q * w + sec.moisture_q * w_sec + 127) / 255;
let mut blended = district.clone();
blended.elev_q = blended_elev_q;
blended.moisture_q = blended_moisture_q;
std::borrow::Cow::Owned(blended)
} else {
std::borrow::Cow::Borrowed(district)
}
} else {
std::borrow::Cow::Borrowed(district)
};
let district_eff: &DistrictProfile = &effective_district;
// ── 3. Family dispatch (D-239 §5) ─────────────────────────────────────
// Family selection uses the PRIMARY district's morphology_zone (D-239 §7:
// seams stay sharp). Blended `district_eff` is passed to the generators
// so their `elev_q`/`moisture_q` reads get the blended values.
let family = zone_to_family(&district.morphology_zone);
let mut column = match family {
MorphologyFamily::AlluvialPlain => {
generate_alluvial_plain(district_eff, chunk, voxel_pos, sub_chunk_seed)
}
MorphologyFamily::LavaField => generate_lava_field(district_eff, voxel_pos, sub_chunk_seed),
MorphologyFamily::FjordWall => {
generate_fjord_wall(district_eff, chunk, voxel_pos, sub_chunk_seed)
}
MorphologyFamily::CliffCoast => {
generate_cliff_coast(district_eff, chunk, voxel_pos, sub_chunk_seed)
}
MorphologyFamily::BraidedDelta => {
generate_braided_delta(district_eff, chunk, voxel_pos, sub_chunk_seed)
}
MorphologyFamily::DuneStrand => {
generate_dune_strand(district_eff, chunk, voxel_pos, sub_chunk_seed)
}
MorphologyFamily::IncisedGorge => {
generate_incised_gorge(district_eff, chunk, voxel_pos, sub_chunk_seed)
}
MorphologyFamily::MeanderReach => {
generate_meander_reach(district_eff, chunk, voxel_pos, sub_chunk_seed)
}
MorphologyFamily::WaterBody => {
generate_water_body(district_eff, chunk, voxel_pos, sub_chunk_seed)
}
};
// ── 3b. Voxel-tier mid-scale relief (T-1081, D-227 / D-243 §2) ────────
// The family generators set a coarse district-derived base elevation plus only
// ±4 m of per-voxel micro-scatter, leaving the walkable surface near-flat — no
// hills or landmarks to navigate by (the T-1081 symptom). Add the mid-scale
// (≈0.25–2 km) rolling/ridged relief the district→voxel seam does not otherwise
// reach (D-243 §2): detail-scatter enveloped by district ruggedness (slope_q) so
// an authored plain stays flat and rugged terrain gains real relief. The relief
// field is body-global (SeedDomain::VoxelRelief, position-keyed); the f64
// perturbation is truncated to integer metres before assignment (D-010,
// truncate-before-decision — the same sanctioned f64 path as the domain warp).
//
// Skipped for the WaterBody family: open ocean / lake / tidal-flat surfaces stay
// at sea level (elevation 0). River channels inside the land families ride up
// with their banks — value noise is smooth at 1 m steps, so a channel voxel and
// its bank get near-identical relief and the family's relative channel cut holds.
// Only the FLAT families take the mid-scale relief. The dramatic families
// (CliffCoast / FjordWall / IncisedGorge) already generate strong internal relief
// from their own geometry — layering a position-varying field over them would warp
// those features (e.g. drown a gorge's wall-to-floor drop under a relief swell).
// WaterBody stays at sea level. The flat families are the ones that read flat and
// need invented relief; their only feature is a 3–15 m channel, narrow enough that
// the smooth relief shifts it bodily with its banks.
let takes_mid_scale_relief = matches!(
family,
MorphologyFamily::AlluvialPlain
| MorphologyFamily::LavaField
| MorphologyFamily::BraidedDelta
| MorphologyFamily::DuneStrand
| MorphologyFamily::MeanderReach
);
if takes_mid_scale_relief {
let relief_seed = SeedChain::for_body(world_seed, body_id)
.derive(SeedDomain::VoxelRelief, 0)
.seed();
// Relief envelope (T-1081): the coarse heightmap (~40–78 km/px) carries almost
// no slope at this tier (slope_q tops out near 0.1 on a real body), so gating on
// slope_q alone would kill the invented relief everywhere. Combine slope with
// elevation — high ground reads rugged, coastal lowlands stay gentle — so
// detail-scatter raises hills where the body has high terrain and keeps flats
// soft. (Tectonic-class ridge sharpening is a later refinement.)
let signal = (district_eff.slope_q * 3 + district_eff.elev_q).clamp(0, 100) as f64 / 100.0;
let relief = crate::atlas::detail_scatter::voxel_relief(
relief_seed,
voxel_x as f64,
voxel_y as f64,
signal,
signal,
);
let relief_m = (relief * VOXEL_RELIEF_SPAN_M as f64) as i32; // truncate (D-010)
column.elevation_m = (column.elevation_m + relief_m).max(0);
}
// ── 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 district's mean temperature + water/terrain + coherent
// cluster scatter. One site, set here — no family generator needs changing.
// Cover derives from the primary district's temperature (not blended) since
// temperature blending is the sibling ticket's domain (T-1078, D-243 §4).
column.cover = derive_cover(world_seed, body_id, district, &column, voxel_pos);
column
}
// ---------------------------------------------------------------------------
// AlluvialPlain generator (T-1028, D-239 §5 fallback)
// ---------------------------------------------------------------------------
/// AlluvialPlain voxel generator — the D-239 §5 fallback family (T-1028).
///
/// Flat floodplain with:
/// - `Soil` terrain material (D-239 §8 Soil law: rolling/floodplain)
/// - 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
///
/// ## Meander channel placement
///
/// The channel is a sine-wave approximation running along `basin_direction`,
/// 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(
district: &DistrictProfile,
chunk: &ChunkContext,
voxel_pos: VoxelPos,
sub_chunk_seed: u64,
) -> VoxelColumn {
// ── 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!(district.morphology_zone, MorphologyZone::Wetland)
|| (district.slope_q <= 5 && district.moisture_q >= 60)
{
TerrainMaterial::Wetland
} else {
TerrainMaterial::Soil
};
// ── Vegetation ─────────────────────────────────────────────────────────
// 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(district.vegetation_class);
let vegetation = scatter_vegetation(base_veg, sub_chunk_seed);
// ── Base elevation ──────────────────────────────────────────────────────
// 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 = 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);
// ── Channel (meander, D-239 §10) ───────────────────────────────────────
// Check if this voxel falls within the meander channel. The channel is a
// sine-wave path through the chunk, parameterised by meander_phase and
// meander_wavelength_m. We compute the perpendicular distance from the voxel
// to the channel centreline; if it's within half the channel width, the voxel
// is in-channel.
//
// All comparison is integer (D-010): we compute a scaled integer distance.
let (in_channel, is_deep) = if chunk.has_active_channel {
compute_channel_state(voxel_pos, chunk, sub_chunk_seed)
} else {
(false, false)
};
let water = if is_deep {
Water::Deep
} else if in_channel {
Water::Shallow
} else {
Water::Dry
};
// ── Channel elevation adjustment ───────────────────────────────────────
// In-channel voxels are slightly lower than the surrounding floodplain
// (the channel is cut below the levee/floodplain surface). This satisfies
// D-239 §9: "channels fall below and levees above the high-water threshold".
// We use a fixed 2 m cut (integer, D-010).
let elevation_m = if in_channel {
(elevation_m - 2).max(0)
} else {
elevation_m
};
VoxelColumn {
terrain,
floor: FloorMaterial::None,
vegetation,
water,
elevation_m,
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// WaterBody generator (T-1082, D-239 §5 amended — the 9th family)
// ---------------------------------------------------------------------------
/// WaterBody voxel generator — ocean / lake / tidal flat (T-1082).
///
/// Renders the predominantly-submerged zones the AlluvialPlain fallback used to paint
/// as dry forested land (the D-245 / T-1082 bug: `water=Dry` + `Forest` on the sea):
/// - `Water::Deep` open water with a `Water::Shallow` shoal band straddling the
/// district-anchored coast line (`chunk.coast_anchor_m`, T-1041) — one continuous
/// shallows per district, wider for shallower (higher-`elev_q`) bodies. TidalFlat
/// is all `Shallow` (an intertidal flat).
/// - Seabed `TerrainMaterial`: `Wetland` (tidal mud), else `Rock` on steep districts
/// / `Sand` on gentle ones (D-239 §8 — rock faces where steep, sandy floor where not).
/// - `Vegetation::Barren` — nothing grows in open water.
/// - Water surface at `elevation_m = 0` (the sea-level convention the coastal families
/// already use, D-239 §8 "mouths at sea level"; the raw heightmap `sea_level` float
/// does not reach this tier). Depth is carried by the `Water` axis, not `elevation_m`.
///
/// Seasonal `cover` (Ice on cold bodies — frozen seas/lakes) is applied centrally by
/// `derive_cover` in [`derive_voxel_column`]. Integer arithmetic only (D-010); the only
/// f64 is the upstream domain warp.
fn generate_water_body(
district: &DistrictProfile,
chunk: &ChunkContext,
voxel_pos: VoxelPos,
sub_chunk_seed: u64,
) -> VoxelColumn {
let is_tidal = matches!(district.morphology_zone, MorphologyZone::TidalFlat);
// Seabed substrate: tidal mud, else rocky on steep districts / sandy on gentle.
let terrain = if is_tidal {
TerrainMaterial::Wetland
} else if district.slope_q >= 18 {
TerrainMaterial::Rock
} else {
TerrainMaterial::Sand
};
// Depth: TidalFlat is all Shallow; ocean/lake get a Shallow shoal band on the
// district-anchored coast line (along-axis ∥ basin, T-1041), Deep beyond.
let water = if is_tidal {
Water::Shallow
} else {
let along = match chunk.basin_direction {
crate::atlas::chunk_context::BasinDirection::North
| crate::atlas::chunk_context::BasinDirection::South => voxel_pos.1,
crate::atlas::chunk_context::BasinDirection::East
| crate::atlas::chunk_context::BasinDirection::West => voxel_pos.0,
};
let coast_d = (along - chunk.coast_anchor_m).abs();
// Shoal half-width: wider for shallower (higher elev_q) bodies, with a little
// sub-chunk jaggedness so the shallows edge is not a straight line.
let band_noise = (sub_chunk_seed & 0x7) as i32 - 3; // [−3, +4]
let shallow_band = (8 + district.elev_q / 4 + band_noise).max(2);
if coast_d <= shallow_band {
Water::Shallow
} else {
Water::Deep
}
};
VoxelColumn {
terrain,
floor: FloorMaterial::None,
vegetation: Vegetation::Barren,
water,
elevation_m: 0, // water surface = sea level
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// LavaField generator (T-1029, D-239 §5 / §8 Lava law)
// ---------------------------------------------------------------------------
/// LavaField voxel generator.
///
/// Shield-slope topology with lava sheets and tube voids:
/// - `Lava` terrain everywhere (D-239 §8 Lava law).
/// - `Barren` vegetation — fresh lava is lifeless.
/// - Low-relief base elevation (shield — gentle radiating slopes, not peaks).
/// - Micro-relief: coarse lava block texture from seed bits.
/// - Tube voids: seed-gated depressions 1–3 m below the surface (immature
/// drainage; no active river network — D-239 §8 immature drainage law).
/// - No active water channel regardless of `chunk.has_active_channel` —
/// immature drainage means no organised river network on fresh lava.
fn generate_lava_field(
district: &DistrictProfile,
_voxel_pos: VoxelPos,
sub_chunk_seed: u64,
) -> VoxelColumn {
// ── Base elevation ─────────────────────────────────────────────────────
// Shield slope: gentle radiating dome. Use elev_q for the regional height;
// 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 = 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]
// Lava tube void: 1-in-8 chance (bits [3:5] == 0b000) of a depression.
// Tubes are below-surface drainage (D-239 §8 immature drainage).
let tube_depression = if (sub_chunk_seed >> 3) & 0x7 == 0 {
let depth = ((sub_chunk_seed >> 6) & 0x3) as i32 + 1; // 1–4 m depression
-depth
} else {
0
};
let elevation_m = (base_elev_m + lava_block + tube_depression).max(0);
// ── Tube void water state ──────────────────────────────────────────────
// 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 && district.moisture_q >= 70 {
Water::Shallow
} else {
Water::Dry
};
VoxelColumn {
terrain: TerrainMaterial::Lava,
floor: FloorMaterial::None,
vegetation: Vegetation::Barren,
water,
elevation_m,
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// FjordWall generator (T-1029, D-239 §5 / §8 glaciation→form law)
// ---------------------------------------------------------------------------
/// FjordWall voxel generator.
///
/// Glacial U-valley topology:
/// - `Rock` terrain (D-239 §8 Rock→vertical faces law).
/// - Steep valley walls meeting deep water in the channel (the fjord).
/// - U-valley cross-section: flat (deep) bottom + near-vertical walls.
/// - GlaciationGrade ≥ 2 produces cirques (D-239 §5/§8 glaciation→form law).
/// - Moraines (debris mounds) at the valley sides for Grade ≥ 1.
/// - 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 district-anchored
/// centreline `chunk.channel_anchor_m` (T-1041, D-239 §10): ONE valley spans
/// the district's chunks. Cross-section: wall | moraine | Deep water | moraine | wall.
fn generate_fjord_wall(
district: &DistrictProfile,
chunk: &ChunkContext,
voxel_pos: VoxelPos,
sub_chunk_seed: u64,
) -> VoxelColumn {
// ── Cross-valley coordinate ───────────────────────────────────────────
// Fjord runs along basin_direction; cross = perpendicular distance from
// the centre-line.
let (along, cross) = match chunk.basin_direction {
crate::atlas::chunk_context::BasinDirection::North
| crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0),
crate::atlas::chunk_context::BasinDirection::East
| crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1),
};
// 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();
// ── U-valley geometry ─────────────────────────────────────────────────
// The fjord bottom is 4–8 m wide (chokepoint, D-239 §9 "gorge floors 2–8 m").
// Deep water within floor_half m of centre; moraine band beyond; wall beyond that.
//
// GlaciationGrade modifies the floor width, wall steepness, and cirque presence.
// Upstream classifier gate guarantees GlaciationGrade ≥ 2 for FjordWall (D-239 §5).
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.
let floor_half = glacier_grade.clamp(2, 4); // total 4–8 m
// Moraine band extends from floor edge to moraine_outer.
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 = (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 ─────────────────────────────────────
// Below sea level (drainage monotonicity: mouth at sea level).
// Elevation 0 m (sea level).
let floor_noise = (sub_chunk_seed & 0x1) as i32; // ±0–1 m
let elev = floor_noise; // 0 or 1 m at most
(elev, Water::Deep, Vegetation::Barren)
} else if cross_from_centre <= moraine_outer {
// ── Moraine band — rocky debris mound ────────────────────────────
// Moraines always present — upstream classifier gate guarantees
// GlaciationGrade ≥ 2 (D-239 §5). Slight elevation bump; Rock substrate.
let moraine_noise = ((sub_chunk_seed >> 2) & 0x3) as i32; // 0–3 m
let moraine_dist = cross_from_centre - floor_half;
// Bell-shape: rises then falls across the band.
let half_band = (moraine_outer - floor_half) / 2;
let moraine_height = if moraine_dist <= half_band {
(moraine_dist * 3) / half_band.max(1) // 0 → 3 m rise
} else {
3 - ((moraine_dist - half_band) * 3) / half_band.max(1) // 3 → 0 m fall
};
let elev = (wall_elev_base / 4 + moraine_height + moraine_noise).max(0);
// Moraines hold some Shallow pools (meltwater) in humid zones.
let water = if district.moisture_q >= 60 && moraine_noise >= 2 {
Water::Shallow
} else {
Water::Dry
};
(elev, water, Vegetation::Barren)
} else {
// ── Valley wall — near-vertical face ─────────────────────────────
// Rock→vertical faces (D-239 §8). Elevation rises steeply from
// moraine_outer to the wall top.
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 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.
let cirque_depression = if glacier_grade >= 2 && (sub_chunk_seed >> 6) & 0x7 == 0 {
// 2–5 m
((sub_chunk_seed >> 9) & 0x3) as i32 + 2
} else {
0
};
// Wall noise: jagged rock face (bits [1:3]).
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 district.vegetation_class {
VegetationClass::Forest | VegetationClass::Scrub => {
if (sub_chunk_seed >> 12) & 0xF < 2 {
Vegetation::Scrub
} else {
Vegetation::Barren
}
}
_ => Vegetation::Barren,
};
(elev, Water::Dry, veg)
};
// ── Along-axis variation (avalanche debris / longitudinal texture) ────
// Use along coordinate modulo the meander wavelength for slow variation.
let _along_used = along; // structural reference; prevents unused-variable warning
VoxelColumn {
terrain: TerrainMaterial::Rock,
floor: FloorMaterial::None,
vegetation,
water,
elevation_m,
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// CliffCoast generator (T-1029, D-239 §5 / §8 Rock law)
// ---------------------------------------------------------------------------
/// CliffCoast voxel generator.
///
/// 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 district-anchored line `chunk.coast_anchor_m`
/// along the seaward axis (T-1041, D-239 §10): ONE continuous coast line
/// 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(
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 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,
crate::atlas::chunk_context::BasinDirection::South => voxel_pos.1 - chunk.coast_anchor_m,
crate::atlas::chunk_context::BasinDirection::East => voxel_pos.0 - chunk.coast_anchor_m,
crate::atlas::chunk_context::BasinDirection::West => chunk.coast_anchor_m - voxel_pos.0,
};
// ── Cliff face ────────────────────────────────────────────────────────
// Inland (coast_d < 0): high ground, rocky top.
// Face (0..8): steep drop (>6 m per metre — near-vertical, D-239 §8).
// Ledge (8..13): rocky beach platform, Shallow.
// Ocean (≥13): Deep water.
//
// 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;
// Ledge variation noise: bits [3:5] → 0–2 m.
let ledge_noise = ((sub_chunk_seed >> 3) & 0x3) as i32;
let (elevation_m, water) = if coast_d < 0 {
// Inland high ground — rocky plateau.
let inland_noise = ((sub_chunk_seed >> 5) & 0xF) as i32 - 7; // [−7, +8]
let elev = (cliff_top_elev + inland_noise).max(cliff_top_elev / 4);
(elev, Water::Dry)
} else if coast_d < 8 {
// Cliff face — steep drop. Each metre seaward drops ~cliff_top/8 metres.
let face_dist = coast_d; // 0–7
let drop = (face_dist * cliff_top_elev) / 8;
let elev = (cliff_top_elev - drop + face_noise).max(1);
(elev, Water::Dry)
} else if coast_d < 13 {
// Rocky ledge / splash zone.
let elev = (1 + ledge_noise).max(0);
(elev, Water::Shallow)
} else {
// Ocean — drainage monotonicity: mouths at sea level (elev 0).
(0, Water::Deep)
};
// 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 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
} else {
Vegetation::Barren
}
}
VegetationClass::Scrub | VegetationClass::RiparianScrub => {
if coast_d < -28 && (sub_chunk_seed >> 10) & 0x3 == 0 {
Vegetation::Scrub
} else {
Vegetation::Barren
}
}
_ => Vegetation::Barren,
};
VoxelColumn {
terrain: TerrainMaterial::Rock,
floor: FloorMaterial::None,
vegetation,
water,
elevation_m,
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// BraidedDelta generator (T-1029, D-239 §5 / §8 Gravel law)
// ---------------------------------------------------------------------------
/// BraidedDelta voxel generator.
///
/// Braided channel / fan topology:
/// - `Gravel` terrain (D-239 §8 Gravel→braided channels/fans law).
/// - Multiple shallow distributary channels (NOT single-thread meander).
/// - 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 district-anchored axis
/// `chunk.channel_anchor_m` (T-1041, D-239 §10), each thread 3–8 m wide.
/// 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(
district: &DistrictProfile,
chunk: &ChunkContext,
voxel_pos: VoxelPos,
sub_chunk_seed: u64,
) -> VoxelColumn {
// ── 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 = (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 ───────────────────────────────────────────
// Cross-channel coordinate (perpendicular to basin_direction).
let (_along, cross) = match chunk.basin_direction {
crate::atlas::chunk_context::BasinDirection::North
| crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0),
crate::atlas::chunk_context::BasinDirection::East
| crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1),
};
// Three braided threads with well-separated centres derived from meander_phase,
// 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;
// Mix A: phase itself through splitmix64 finaliser.
let mix_a = {
let mut h = phase_u64.wrapping_add(0x9e3779b97f4a7c15);
h = (h ^ (h >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
h = (h ^ (h >> 27)).wrapping_mul(0x94d049bb133111eb);
h ^ (h >> 31)
};
// Mix B: phase XOR'd with a distinct salt before mixing.
let mix_b = {
let mut h = phase_u64.wrapping_add(0x9e3779b97f4a7c15) ^ 0xdeadbeefcafe1234;
h = (h ^ (h >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
h = (h ^ (h >> 27)).wrapping_mul(0x94d049bb133111eb);
h ^ (h >> 31)
};
// Mix C: phase XOR'd with a second distinct salt.
let mix_c = {
let mut h = phase_u64.wrapping_add(0x9e3779b97f4a7c15) ^ 0xfeedface0badcafe;
h = (h ^ (h >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
h = (h ^ (h >> 27)).wrapping_mul(0x94d049bb133111eb);
h ^ (h >> 31)
};
let thread_centres: [i32; 3] = [
chunk.channel_anchor_m - 32 + (mix_a % 64) as i32,
chunk.channel_anchor_m - 32 + (mix_b % 64) as i32,
chunk.channel_anchor_m - 32 + (mix_c % 64) as i32,
];
// Thread width: 3–6 m half-width derived from channel_width_m.
let thread_half = (chunk.channel_width_m / 2).clamp(2, 4);
// Check if voxel is in any braided thread — continuous world-coordinate
// distance (the old [0, 64) ring fold restarted the threads every chunk).
let in_any_thread = thread_centres
.iter()
.any(|&centre| (cross - centre).abs() <= thread_half);
let (elevation_m, water) = if in_any_thread {
// In a distributary channel — at or just below base elevation.
// Channels are cut 1 m below gravel bars (D-239 §9 ElevationDelta).
let channel_elev = (base_elev_m - 1).max(0);
(channel_elev, Water::Shallow)
} else {
// Gravel bar between channels — slightly above channel floor.
(base_elev_m + bar_noise.min(1), Water::Dry)
};
// Near the delta mouth (elev = 0), some bars are submerged (Shallow).
let water = if elevation_m == 0 && !in_any_thread && bar_noise == 0 {
Water::Shallow
} else {
water
};
// Vegetation: riparian scrub on stable bars; barren on active channel sediment.
let vegetation = if !in_any_thread && district.moisture_q >= 50 {
match district.vegetation_class {
VegetationClass::Forest
| VegetationClass::RiparianThicket
| VegetationClass::RiparianScrub => {
if (sub_chunk_seed >> 4) & 0x7 < 3 {
Vegetation::Scrub
} else {
Vegetation::Barren
}
}
VegetationClass::Scrub => {
if (sub_chunk_seed >> 4) & 0x3 == 0 {
Vegetation::Scrub
} else {
Vegetation::Barren
}
}
_ => Vegetation::Barren,
}
} else {
Vegetation::Barren
};
VoxelColumn {
terrain: TerrainMaterial::Gravel,
floor: FloorMaterial::None,
vegetation,
water,
elevation_m,
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// DuneStrand generator (T-1029, D-239 §5 / §8 Sand law)
// ---------------------------------------------------------------------------
/// DuneStrand voxel generator.
///
/// Wind-formed dune topology:
/// - `Sand` terrain (D-239 §8 Sand→≤~32° angle of repose, dunes not cliffs).
/// - Wind-aligned ridges (orientation derived from `basin_direction`).
/// - Dune crests and troughs — amplitude ≤ what a 32° slope permits at this
/// scale (slope constraint: at most 1 m rise per ~1.6 m horizontal run).
/// - Mostly `Dry`; `Shallow` at the seaward toe where surf meets sand.
/// - `Barren` or very sparse vegetation (coastal strand).
fn generate_dune_strand(
district: &DistrictProfile,
chunk: &ChunkContext,
voxel_pos: VoxelPos,
sub_chunk_seed: u64,
) -> VoxelColumn {
// ── Dune orientation ──────────────────────────────────────────────────
// Dunes are wind-aligned; dune ridges run perpendicular to basin_direction
// (wind blows onshore, ridges parallel the shoreline).
// Along-wind = basin_direction; dune crest height varies along-wind.
let (along_wind, cross_wind) = match chunk.basin_direction {
crate::atlas::chunk_context::BasinDirection::North
| crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0),
crate::atlas::chunk_context::BasinDirection::East
| crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1),
};
// ── Dune wavelength ───────────────────────────────────────────────────
// Dune spacing is shorter than meander wavelength; derive from slope_q.
// Low slope → long dunes (8–20 m wavelength); high slope → shorter (4–8 m).
// Integer arithmetic (D-010).
let dune_wavelength = {
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 (district-scale, D-239 §10).
let phase = chunk.meander_phase as i32;
let along_mod = (along_wind + phase).rem_euclid(dune_wavelength);
// ── Dune cross-section height ─────────────────────────────────────────
// Triangle-wave: ramp from trough to crest and back over one wavelength.
// 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 (district terrain height proxy) to give local variety.
let max_height = {
let physics_cap = (dune_wavelength * 625) / 2000; // tan(32°)×wavelength/2, integer
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 {
(2 * max_height * along_mod) / dune_wavelength.max(1) // 0 → max
} else {
max_height - ((along_mod - half_wl) * max_height) / half_wl.max(1) // max → 0
};
// ── Base elevation ────────────────────────────────────────────────────
// Beach/strand is near sea level; add dune height above that base.
// Cross-wind variation: sub-dune bar variation (±1 m).
let cross_noise = cross_wind.abs() & 0x1; // 0 or 1
// 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 = (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 ───────────────────────────────────────────────────────
// Shallow at the strand toe (base elev = 0) and in dune troughs near
// the coast. Dry elsewhere.
let water = if elevation_m == 0 || (dune_height == 0 && base_strand_elev <= 1) {
Water::Shallow // surf line / wave-washed troughs
} else {
Water::Dry
};
// ── Vegetation ────────────────────────────────────────────────────────
// Dunes are mostly barren; scattered dune grass on stable crests
// in humid zones. No trees (wind/salt prevents forest).
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 {
Vegetation::Barren
}
} else {
Vegetation::Barren
};
VoxelColumn {
terrain: TerrainMaterial::Sand,
floor: FloorMaterial::None,
vegetation,
water,
elevation_m,
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// IncisedGorge generator (T-1029, D-239 §5 / §8 / §9)
// ---------------------------------------------------------------------------
/// IncisedGorge voxel generator — shared by MountainPass zones.
///
/// Deep narrow gorge:
/// - `Rock` terrain (D-239 §8 Rock→vertical faces).
/// - Steep walls; single incised channel at the gorge floor.
/// - **Gorge floor 2–8 m wide** (D-239 §9 chokepoint constraint).
/// - 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 district-anchored
/// centreline `chunk.channel_anchor_m` (T-1041, D-239 §10): ONE gorge
/// spans the district's chunks.
fn generate_incised_gorge(
district: &DistrictProfile,
chunk: &ChunkContext,
voxel_pos: VoxelPos,
sub_chunk_seed: u64,
) -> VoxelColumn {
// ── Cross-gorge coordinate ─────────────────────────────────────────────
let (_along, cross) = match chunk.basin_direction {
crate::atlas::chunk_context::BasinDirection::North
| crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0),
crate::atlas::chunk_context::BasinDirection::East
| crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1),
};
// 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();
// ── Gorge floor width (D-239 §9: 2–8 m) ──────────────────────────────
// Floor half-width: 1–4 m (total 2–8 m). Derived from channel_width_m
// (capped to 4 m half-width to stay within §9).
let floor_half = chunk.channel_width_m.clamp(1, 4); // 1–4 m half-width
// ── Wall geometry ─────────────────────────────────────────────────────
// The wall rises steeply: each metre away from the floor adds ~8 m of
// 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]
// Floor noise: bits [4:5] → ±1 m floor scatter.
let floor_noise = ((sub_chunk_seed >> 4) & 0x3) as i32 - 1; // [−1, +2]
let (elevation_m, water) = if cross_from_centre <= floor_half {
// ── Gorge floor — narrow and deep ─────────────────────────────────
// Active incised channel (D-239 §9: "single incised channel").
// The channel runs along the floor; water is Shallow.
let elev = (floor_elev + floor_noise).max(0);
(elev, Water::Shallow)
} else {
// ── Gorge wall — near-vertical face ───────────────────────────────
// Elevation rises steeply: each metre of cross-distance from floor edge
// adds wall_step metres. We model this as a steep ramp.
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
// 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);
(elev, Water::Dry)
};
// 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) && district.moisture_q >= 60 {
match district.vegetation_class {
VegetationClass::Forest | VegetationClass::RiparianThicket => {
if (sub_chunk_seed >> 8) & 0x7 < 2 {
Vegetation::Scrub
} else {
Vegetation::Barren
}
}
VegetationClass::Scrub => {
if (sub_chunk_seed >> 8) & 0x3 == 0 {
Vegetation::Scrub
} else {
Vegetation::Barren
}
}
_ => Vegetation::Barren,
}
} else {
Vegetation::Barren
};
VoxelColumn {
terrain: TerrainMaterial::Rock,
floor: FloorMaterial::None,
vegetation,
water,
elevation_m,
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// MeanderReach generator (T-1029, D-239 §5 / §8 / §9)
// ---------------------------------------------------------------------------
/// MeanderReach voxel generator.
///
/// Single-thread meandering river floodplain:
/// - `Soil` terrain (D-239 §8 Soil→rolling/floodplain law).
/// - Stronger single meander than AlluvialPlain (higher sinuosity).
/// - `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 district class with riparian band at waterway.
fn generate_meander_reach(
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!(district.morphology_zone, MorphologyZone::Wetland)
|| (district.slope_q <= 3 && district.moisture_q >= 70)
{
TerrainMaterial::Wetland
} else {
TerrainMaterial::Soil
};
// ── Base elevation ──────────────────────────────────────────────────────
// 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 = 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);
// ── Stronger meander channel (D-239 §9: river crossings 3–15 m) ───────
// MeanderReach uses higher sinuosity than AlluvialPlain. We achieve this
// by increasing the effective amplitude multiplier from 1/6 → 1/4 of
// wavelength, producing ~25% sinuosity (vs ~17% for AlluvialPlain).
//
// Channel computation mirrors AlluvialPlain's compute_channel_state but
// with the stronger amplitude.
let (in_channel, is_deep) = if chunk.has_active_channel {
compute_meander_reach_channel(voxel_pos, chunk, sub_chunk_seed)
} else {
(false, false)
};
// ── Levee / channel elevation (D-239 §9 ElevationDelta) ───────────────
// Levees are natural berms built up by flood deposition, elevated above
// the floodplain. Channels cut below. Elevation relationship:
// levee_top > floodplain > channel_floor (D-239 §9: "levees above,
// channels below the high-water threshold").
let (elevation_m, water, vegetation) = if is_deep {
// Deep channel thalweg — 3 m below floodplain.
let elev = (floodplain_elev - 3).max(0);
(elev, Water::Deep, Vegetation::Thicket)
} else if in_channel {
// 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 district.vegetation_class {
VegetationClass::Absent | VegetationClass::Barren => Vegetation::Barren,
_ => Vegetation::Thicket,
};
(elev, Water::Shallow, veg)
} else {
// ── Off-channel floodplain ────────────────────────────────────────
// Check for levee proximity: within 3 m of channel edge → levee.
// Levee is elevated 1–2 m above floodplain.
let levee_elev = {
let levee_extra = if in_levee_band(voxel_pos, chunk, sub_chunk_seed) {
// 1–4 m
((sub_chunk_seed >> 10) & 0x3) as i32 + 1
} else {
0
};
floodplain_elev + levee_extra
};
// 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)
};
VoxelColumn {
terrain,
floor: FloorMaterial::None,
vegetation,
water,
elevation_m,
cover: SeasonalCover::None, // set by derive_cover in derive_voxel_column
}
}
// ---------------------------------------------------------------------------
// SeasonalCover derivation (D-239 §3, T-1030)
// ---------------------------------------------------------------------------
/// Cluster size for spatially-coherent freeze scatter (D-239 §3).
///
/// Neighbouring voxels within the same CLUSTER_M × CLUSTER_M cell share a
/// per-cluster scatter offset, producing ragged clustered patches rather than
/// per-tile salt-and-pepper. The spec calls for ~4–8 m; we use 6 m.
const CLUSTER_M: i32 = 6;
/// Freshwater freeze band upper edge (district mean °C, D-239 §3).
///
/// Night-frost can ice-over the coldest/most-exposed freshwater tiles even when
/// the district mean is still positive (diurnal swing). +5 °C is the onset of
/// the scatter band.
const FRESH_BAND_HIGH_C: i32 = 5;
/// Freshwater freeze band lower edge (D-239 §3).
///
/// Below −10 °C the entire freshwater surface is frozen across the full
/// day/night cycle. No scatter below this threshold.
const FRESH_BAND_LOW_C: i32 = -10;
/// Salt-water (sea ice) freeze onset temperature (D-239 §3).
///
/// Seawater freezes at ≈ −2 °C due to dissolved salt.
const SALT_ONSET_C: i32 = -2;
/// Salt-water freeze band width in integer °C (D-239 §3).
///
/// The sea-ice band is lower and wider than the freshwater band. We model it
/// as [SALT_ONSET_C, SALT_ONSET_C − SALT_BAND_WIDTH]. Below that, permanent
/// pack ice everywhere (no scatter).
const SALT_BAND_WIDTH: i32 = 12;
/// Snow (land) onset temperature — same scattered band as freshwater (D-239 §3).
///
/// On land, cover transitions from bare frozen ground to Snow as temp descends
/// through this band, but only when moisture_q is above `SNOW_MOISTURE_GATE`.
const SNOW_BAND_HIGH_C: i32 = 2;
/// Snow moisture gate — moisture_q threshold below which cold land stays bare.
///
/// Cold + dry → no snow accumulation (D-239 §3 "gated on moisture").
const SNOW_MOISTURE_GATE: i32 = 30;
/// Classify a `MorphologyZone` as salt (ocean/sea) vs fresh water vs land.
///
/// D-239 §3: "salt vs fresh: derive from `morphology_zone`".
/// - OpenOcean / Sea zones → salt water.
/// - Lake / RiverBank / MeanderReach / Delta / BraidedPlain / Estuarine /
/// TidalFlat → fresh water.
/// - All other zones → land (no water-freeze branch).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SurfaceClass {
SaltWater,
FreshWater,
Land,
}
fn classify_surface(zone: &MorphologyZone, water: Water) -> SurfaceClass {
match zone {
// Salt water: open ocean.
MorphologyZone::OpenOcean => SurfaceClass::SaltWater,
// Fresh surface water: lakes and all river/wetland zones with standing water.
// We additionally check Water state — a RiverBank tile that happens to be
// Dry at this voxel is functionally land from a freeze perspective.
MorphologyZone::Lake => SurfaceClass::FreshWater,
MorphologyZone::RiverBank
| MorphologyZone::MeanderReach
| MorphologyZone::Delta
| MorphologyZone::BraidedPlain
| MorphologyZone::Estuarine
| MorphologyZone::TidalFlat => {
if water != Water::Dry {
SurfaceClass::FreshWater
} else {
SurfaceClass::Land
}
}
// Everything else is land (including Fjord walls, DuneStrand, etc.).
_ => SurfaceClass::Land,
}
}
/// Per-cluster scatter offset (D-239 §3).
///
/// Hash a coarse cluster cell `(cx, cy)` — derived from `(voxel_x, voxel_y)`
/// by `div_euclid(CLUSTER_M)` — into a body-scoped u64 scatter value via
/// `SeedChain`. All voxels that share the same cluster cell get the **same**
/// scatter value (coherent patches). Different world seeds and body ids yield
/// different freeze patterns.
///
/// ## Key correctness properties
///
/// - The cluster-cell coordinates may be negative (voxels at negative positions).
/// We zigzag-encode each signed `cx`/`cy` to a non-negative `u64` before
/// Cantor-pairing, giving a collision-free bijection for the full signed range.
/// (The old cast-to-u64 was wrong for negative inputs — huge two's-complement
/// values broke the "no collision" claim.)
/// - The seed is derived through `SeedChain::for_body(world_seed, body_id)
/// .derive(SeedDomain::Cover, cluster_pair_id)` — a body-scoped, domain-
/// separated derivation, not a raw `splitmix64` on the per-voxel sub_chunk_seed.
/// This is the root fix for the coherence defect: `sub_chunk_seed` is per-voxel,
/// so passing it here gave each voxel its own scatter value → salt-and-pepper.
/// `SeedChain::for_body` is body-scoped and the cluster id is a cluster-cell
/// property — all voxels in the same cell derive the same seed.
#[inline]
fn cluster_scatter(world_seed: u64, body_id: &str, voxel_x: i32, voxel_y: i32) -> u64 {
use crate::seed::{SeedChain, SeedDomain};
// Coarse cluster cell index (Euclidean div so negatives map cleanly).
let cx_i = voxel_x.div_euclid(CLUSTER_M);
let cy_i = voxel_y.div_euclid(CLUSTER_M);
// Zigzag encode signed → unsigned (same pattern as voxel_pos_to_id and
// domain_warp::pos_to_id — bijective over the full i32 range, no collisions
// between positive and negative cluster coords).
let zz = |v: i32| -> u64 {
let v = v as i64;
((v << 1) ^ (v >> 63)) as u64
};
let cx = zz(cx_i);
let cy = zz(cy_i);
// Cantor-pairing (now collision-free because both inputs are non-negative u64).
let s = cx.wrapping_add(cy);
let cluster_pair_id = s
.wrapping_mul(s.wrapping_add(1))
.wrapping_div(2)
.wrapping_add(cy);
// Body-scoped, domain-separated derivation via SeedChain (D-224 / D-010).
// All voxels sharing this cluster cell → same cluster_pair_id → same seed.
SeedChain::for_body(world_seed, body_id)
.derive(SeedDomain::Cover, cluster_pair_id)
.seed()
}
/// Derive the `SeasonalCover` overlay for a single voxel (D-239 §3, T-1030).
///
/// ## Rules (D-239 §3 verbatim mapping)
///
/// ### Airless body
/// `temperature_c == None` → `SeasonalCover::None`. No climate branch.
///
/// ### Fresh water (lakes / rivers)
/// Scatter band **+5 °C → −10 °C** (district mean temperature_c as i32).
/// - Above +5 °C: `None` (open water).
/// - Within the band [−10, +5]: cluster-scatter decides `Ice` vs `None`.
/// The scatter is driven by a per-cluster hash; more of the band → more Ice.
/// - Below −10 °C: `Ice` everywhere (permanent).
///
/// ### Salt water (sea ice)
/// Band onset ≈ −2 °C, width 12 °C (so solid below −14 °C).
/// - Above −2 °C: `None`.
/// - Within the band [−14, −2]: cluster-scatter → `Ice` vs `None`.
/// Pack-ice pattern: larger coherent sheets + occasional open "lead" gaps.
/// Leads use a separate coarser cluster size (3× CLUSTER_M) to produce
/// sheet-scale coherence rather than fine-grained scatter.
/// - Below −14 °C: `Ice` everywhere (permanent pack ice).
///
/// ### Land (snow)
/// Moisture-gated: `moisture_q >= SNOW_MOISTURE_GATE` (30) required.
/// Band **+2 °C → −10 °C** (same low edge as freshwater).
/// - Above +2 °C, or dry (moisture_q < 30): `None` (bare frozen ground if cold).
/// - Within the band [−10, +2] and moist: cluster-scatter → `Snow` vs `None`.
/// - Below −10 °C and moist: `Snow` everywhere (permanent).
///
/// ## Spatial coherence (D-239 §3)
///
/// Coherence is implemented by hashing the **coarse cluster cell**
/// `(voxel_x.div_euclid(CLUSTER_M), voxel_y.div_euclid(CLUSTER_M))` into a
/// per-cluster scatter offset. All voxels in the same 6 m × 6 m cluster share
/// the same scatter decision, producing ragged but spatially coherent freeze
/// patches — "clustered patches, NEVER per-tile dice" (D-239 §3).
///
/// ## Q-105 forward contract
///
/// This function models the **static mean-state** only. The transient
/// (clock-bound) cover — dawn frost that burns off by noon, a stream iced at
/// dawn that is crossable by midday — requires the cheap regional clock state
/// described in Q-105. When Q-105 is implemented, callers should treat the
/// `cover` field as the base state and apply the Q-105 modifier on top.
///
/// ## D-010 compliance
///
/// All decisions are integer. `temperature_c` is cast to `i32` once (positional
/// quantisation, not a structural float gate). All scatter is hash/integer only.
pub fn derive_cover(
world_seed: u64,
body_id: &str,
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) = district.temperature_c else {
return SeasonalCover::None;
};
// Integer cast: all gating is on i32 (D-010; f32→i32 cast is positional
// quantisation, not a structural float comparison).
let temp_i: i32 = temp_f as i32;
let (voxel_x, voxel_y) = voxel_pos;
// Per-cluster scatter: keyed ONLY on (world, body, cluster-cell) — never
// on the per-voxel sub_chunk_seed. All voxels in the same 6×6 m cell share
// 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(&district.morphology_zone, column.water);
match surface {
// ── Fresh water (lakes / rivers) ──────────────────────────────────
SurfaceClass::FreshWater => {
if temp_i >= FRESH_BAND_HIGH_C {
// Above band — open water.
SeasonalCover::None
} else if temp_i < FRESH_BAND_LOW_C {
// Below band — permanently frozen.
SeasonalCover::Ice
} else {
// Within the scatter band [FRESH_BAND_LOW_C, FRESH_BAND_HIGH_C).
// Threshold: scatter % band_width < depth_into_band → Ice.
// depth 1 (just below +5°C) ≈ 1/15 ≈ 7% Ice.
// depth 14 (just above −10°C) ≈ 14/15 ≈ 93% Ice.
// Ice-fraction is monotonically increasing as temp drops.
let band_width = (FRESH_BAND_HIGH_C - FRESH_BAND_LOW_C) as u64; // 15
let depth_into_band = (FRESH_BAND_HIGH_C - temp_i) as u64; // 1..=14
if scatter % band_width < depth_into_band {
SeasonalCover::Ice
} else {
SeasonalCover::None
}
}
}
// ── Salt water (sea ice) ──────────────────────────────────────────
SurfaceClass::SaltWater => {
let salt_band_low = SALT_ONSET_C - SALT_BAND_WIDTH; // −14 °C
if temp_i >= SALT_ONSET_C {
// Above onset — open ocean.
SeasonalCover::None
} else if temp_i < salt_band_low {
// Below band — permanent pack ice.
SeasonalCover::Ice
} else {
// Within the sea-ice scatter band [−14, −2).
//
// Ice-fraction is monotonically increasing as temp drops
// (same linear model as freshwater: scatter % band_width <
// depth_into_band). At onset (depth 1) ≈ 8% ice; at depth 11
// (just above −14°C) ≈ 92% ice.
//
// Pack-ice sheets + leads structure: on the cold, mostly-frozen
// end we apply a 3× coarser cluster (super-cell = 3×CLUSTER_M)
// to carve occasional open "lead" gaps within what would otherwise
// be solid ice. A lead forms when the super-cell scatter indicates
// a gap AND the voxel is in the cold zone where ice would normally
// be solid. This gives sheet-scale coherence (large contiguous ice
// panels) with a small fraction of narrow persistent leads.
let depth_into_band = (SALT_ONSET_C - temp_i) as u64; // 1..=11
let band_width = SALT_BAND_WIDTH as u64; // 12
// Primary ice decision (monotonic, same logic as freshwater).
let is_ice = scatter % band_width < depth_into_band;
if is_ice {
// In the cold half of the band (depth > band_width/2), apply
// the pack-ice lead pattern using a coarser super-cell cluster.
// The lead cluster is keyed on the super-cell index directly,
// not on pre-quantized coordinates, so the intent is clear.
let cold_threshold = band_width / 2;
if depth_into_band > cold_threshold {
// Super-cell index: one step per 3×CLUSTER_M metres.
let super_cell_size = CLUSTER_M * 3;
let scx = voxel_x.div_euclid(super_cell_size);
let scy = voxel_y.div_euclid(super_cell_size);
let sheet_scatter = cluster_scatter(
world_seed,
body_id,
scx * super_cell_size,
scy * super_cell_size,
);
// ~8% open leads in the cold zone (persistent gaps in pack ice).
let is_lead = sheet_scatter % 100 < 8;
if is_lead {
SeasonalCover::None
} else {
SeasonalCover::Ice
}
} else {
SeasonalCover::Ice
}
} else {
SeasonalCover::None
}
}
}
// ── Land (snow) ───────────────────────────────────────────────────
SurfaceClass::Land => {
// Moisture gate: cold + dry → bare frozen ground (no snow).
if district.moisture_q < SNOW_MOISTURE_GATE {
return SeasonalCover::None;
}
if temp_i >= SNOW_BAND_HIGH_C {
// Above snow band — too warm for snow accumulation.
SeasonalCover::None
} else if temp_i < FRESH_BAND_LOW_C {
// Below snow band — permanent snow (same low edge as freshwater).
SeasonalCover::Snow
} else {
// Within the scatter band [FRESH_BAND_LOW_C, SNOW_BAND_HIGH_C).
// Same monotonic model: ice-fraction increases as temp drops.
let band_width = (SNOW_BAND_HIGH_C - FRESH_BAND_LOW_C) as u64; // 12
let depth_into_band = (SNOW_BAND_HIGH_C - temp_i) as u64; // 1..=11
if scatter % band_width < depth_into_band {
SeasonalCover::Snow
} else {
SeasonalCover::None
}
}
}
}
}
/// Compute channel state for MeanderReach — higher sinuosity than AlluvialPlain.
///
/// Uses amplitude = wavelength/4 (~25% sinuosity) vs AlluvialPlain's /6 (~17%).
/// This produces a more pronounced meander — a wider swing across the floodplain,
/// creating more tactical variety (D-239 §9 game-feel).
fn compute_meander_reach_channel(
voxel_pos: VoxelPos,
chunk: &ChunkContext,
sub_chunk_seed: u64,
) -> (bool, bool) {
let (vx, vy) = voxel_pos;
let (along, cross) = match chunk.basin_direction {
crate::atlas::chunk_context::BasinDirection::North
| crate::atlas::chunk_context::BasinDirection::South => (vy, vx),
crate::atlas::chunk_context::BasinDirection::East
| crate::atlas::chunk_context::BasinDirection::West => (vx, vy),
};
let wavelength_m = (chunk.meander_wavelength_m as i32).max(10);
let phase_offset = chunk.meander_phase as i32;
let angle_mod = (along + phase_offset).rem_euclid(wavelength_m);
// MeanderReach amplitude: 1/4 of wavelength (stronger meander than AlluvialPlain).
let amplitude = (wavelength_m / 4).max(3); // at least ±3 m amplitude
let half_wl = wavelength_m / 2;
let meander_displacement = if angle_mod < half_wl {
(2 * amplitude * angle_mod) / half_wl.max(1) - amplitude
} else {
amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl.max(1)
};
// 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.
let edge_jitter = (sub_chunk_seed & 0x3) as i32;
let half_width = (chunk.channel_width_m / 2).max(2); // MeanderReach channels at least 4 m total
let channel_edge = half_width + edge_jitter;
let deep_edge = (half_width / 2).max(1);
let in_channel = perp_distance <= channel_edge;
let is_deep = perp_distance <= deep_edge;
(in_channel, is_deep)
}
/// Determine if a voxel is in the levee band adjacent to the MeanderReach channel.
///
/// Levees are natural berms 3–8 m wide on each bank, just outside the channel
/// edge. They are elevated 1–4 m above the floodplain. The levee band is the
/// zone from channel_edge to channel_edge + levee_width.
fn in_levee_band(voxel_pos: VoxelPos, chunk: &ChunkContext, sub_chunk_seed: u64) -> bool {
let (vx, vy) = voxel_pos;
let (along, cross) = match chunk.basin_direction {
crate::atlas::chunk_context::BasinDirection::North
| crate::atlas::chunk_context::BasinDirection::South => (vy, vx),
crate::atlas::chunk_context::BasinDirection::East
| crate::atlas::chunk_context::BasinDirection::West => (vx, vy),
};
let wavelength_m = (chunk.meander_wavelength_m as i32).max(10);
let phase_offset = chunk.meander_phase as i32;
let angle_mod = (along + phase_offset).rem_euclid(wavelength_m);
let amplitude = (wavelength_m / 4).max(3);
let half_wl = wavelength_m / 2;
let meander_displacement = if angle_mod < half_wl {
(2 * amplitude * angle_mod) / half_wl.max(1) - amplitude
} else {
amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl.max(1)
};
// 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;
let half_width = (chunk.channel_width_m / 2).max(2);
let channel_edge = half_width + edge_jitter;
let levee_width = 4 + ((sub_chunk_seed >> 6) & 0x3) as i32; // 4–7 m levee band
// In levee band: just outside channel, within levee_width metres of channel edge.
perp_distance > channel_edge && perp_distance <= channel_edge + levee_width
}
/// Determine if a voxel is in the meander channel, and if it is deep.
///
/// The channel is a sine-wave path oriented along `chunk.basin_direction`.
/// We compute the perpendicular distance (integer, D-010) from the voxel to
/// the wave centreline. Within half the channel width → in-channel; within
/// one-third of the channel width → deep water.
///
/// ## Integer arithmetic (D-010)
///
/// We use a 10× fixed-point approximation for the sine: `sin(θ) ≈ phase_term / 10`
/// where `phase_term` is derived from the meander phase and the voxel's
/// along-channel coordinate. This avoids any float comparison while producing
/// a spatially coherent (though imprecise) meander curve.
fn compute_channel_state(
voxel_pos: VoxelPos,
chunk: &ChunkContext,
sub_chunk_seed: u64,
) -> (bool, bool) {
let (vx, vy) = voxel_pos;
// Along-channel (parallel) and cross-channel (perpendicular) coordinate.
// basin_direction determines the primary axis.
let (along, cross) = match chunk.basin_direction {
crate::atlas::chunk_context::BasinDirection::North
| crate::atlas::chunk_context::BasinDirection::South => (vy, vx),
crate::atlas::chunk_context::BasinDirection::East
| crate::atlas::chunk_context::BasinDirection::West => (vx, vy),
};
// Meander wavelength in metres (integer approximation: truncate f64 → i32).
// The f64 wavelength is position math (D-239 §4), not a gate comparison —
// we truncate to integer here before any structural decision.
let wavelength_m = (chunk.meander_wavelength_m as i32).max(10);
// Phase angle in [0, wavelength_m). Deterministic integer modulo.
let phase_offset = chunk.meander_phase as i32;
let angle_mod = (along + phase_offset).rem_euclid(wavelength_m);
// Integer sine approximation: map angle_mod to a cross-channel displacement
// in [−amplitude, +amplitude]. We use a triangle wave (cheaper than a
// true sine table but still spatially coherent for a walking skeleton).
//
// Triangle wave on [0, wavelength_m]:
// - [0, wavelength_m/2]: displacement = 2*amplitude * angle / wavelength_m − amplitude
// - [wavelength_m/2, wavelength_m]: displacement = amplitude − 2*amplitude * (angle − wl/2) / wl
//
// Amplitude: ~15% of wavelength for a moderate sinuosity.
let amplitude = wavelength_m / 6; // ~17% sinuosity
let amplitude = amplitude.max(2); // at least ±2 m amplitude
let half_wl = wavelength_m / 2;
let meander_displacement = if angle_mod < half_wl {
// Rising half: ramps from -amplitude to +amplitude.
(2 * amplitude * angle_mod) / half_wl - amplitude
} else {
// Falling half: ramps from +amplitude to -amplitude.
amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl
};
// 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 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).
// Use low bits of seed for integer jitter — D-010.
let edge_jitter = (sub_chunk_seed & 0x3) as i32; // [0, 3]
let half_width = (chunk.channel_width_m / 2).max(1);
let channel_edge = half_width + edge_jitter;
let deep_edge = (half_width / 2).max(1);
let in_channel = perp_distance <= channel_edge;
let is_deep = perp_distance <= deep_edge;
(in_channel, is_deep)
}
/// Apply sub-chunk vegetation scatter using the sub-chunk seed.
///
/// 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).
///
/// All integer arithmetic (D-010).
fn scatter_vegetation(base: Vegetation, sub_chunk_seed: u64) -> Vegetation {
// Use bits [4:7] of the seed (4-bit mask, range [0, 15]) for scatter —
// independent from the channel
// computation which uses bits [0:3].
let scatter_bits = ((sub_chunk_seed >> 4) & 0xF) as u8;
match base {
Vegetation::Forest => {
// 15% Grass clearings, 15% Scrub margin, 70% Forest.
if scatter_bits < 3 {
Vegetation::Grass
} else if scatter_bits < 6 {
Vegetation::Scrub
} else {
Vegetation::Forest
}
}
Vegetation::Scrub => {
// 20% Grass, 80% Scrub.
if scatter_bits < 4 {
Vegetation::Grass
} else {
Vegetation::Scrub
}
}
Vegetation::Thicket => {
// Riparian thicket: mostly Thicket with some Forest.
if scatter_bits < 5 {
Vegetation::Thicket
} else {
Vegetation::Forest
}
}
// Barren, Grass, Crop, Cleared: no scatter.
other => other,
}
}
/// Fold a `VoxelPos` into a u64 id for seed derivation.
///
/// Same bijective approach as `chunk_context::pos_to_id` and
/// `domain_warp::pos_to_id` — zigzag encode + Cantor pairing. Integer-only (D-010).
#[inline]
fn voxel_pos_to_id(pos: VoxelPos) -> u64 {
let zz = |v: i32| -> u64 {
let v = v as i64;
((v << 1) ^ (v >> 63)) as u64
};
let x = zz(pos.0);
let y = zz(pos.1);
let s = x.wrapping_add(y);
s.wrapping_mul(s.wrapping_add(1))
.wrapping_div(2)
.wrapping_add(y)
}
// ---------------------------------------------------------------------------
// VoxelCache — LRU-style on-demand cache (D-227)
// ---------------------------------------------------------------------------
/// Cache key: `(body_id, voxel_pos_after_warp)`.
///
/// The body_id is stored as a u64 hash (FNV-1a) to keep the key `Copy`
/// and `Ord`-able (D-010: `BTreeMap` key).
/// `(world_seed, body_id_hash, post-warp voxel address)`. world_seed is part of
/// the voxel's identity — a cache must never return another world's column.
type CacheKey = (u64, u64, VoxelPos);
/// One cache entry: the derived column plus an access generation counter.
struct CacheEntry {
column: VoxelColumn,
/// Monotonically increasing access counter — the LRU eviction key.
access_gen: u64,
}
/// Bounded LRU-style cache for derived `VoxelColumn`s (D-227).
///
/// ## Invariants
///
/// - At most `capacity` entries are stored at any time.
/// - Entries are evicted in LRU order (lowest `access_gen`).
/// - Nothing is persisted to disk; eviction forces re-derivation.
///
/// ## Caller precondition — chunk context must cover the tile (T-1042)
///
/// After T-1042, `derive_voxel_column` reads `chunk.blend_weight` and
/// `chunk.secondary` to apply cross-district terrain blending. The cache
/// key is `(seed, body_id_hash, post-warp voxel position)` — it does NOT
/// capture the blend parameters. The cache invariant therefore holds only
/// under the following precondition:
///
/// **The `chunk` argument passed to `get_or_derive` must be the
/// `ChunkContext` derived for the chunk that actually contains the tile.**
/// Concretely: `chunk = derive_chunk_context(seed, body_id, district,
/// tile_pos.div_euclid(CHUNK_M), secondary)`. A boundary tile derived with
/// a blended context (blend_weight < 255) and later re-requested with a
/// pure-primary context (blend_weight = 255) would produce a different
/// column — the cache would return the first (blended) value, silently
/// giving the wrong result. The caller must ensure the same context
/// construction is used on every call for the same tile.
///
/// **In practice this is satisfied by construction**: callers derive one
/// `ChunkContext` per chunk and call `get_or_derive` only for tiles within
/// that chunk. The precondition is made explicit here so Phase-5 render-loop
/// callers do not inadvertently mix contexts across calls.
///
/// ## D-010 compliance
///
/// Uses `BTreeMap` (ordered) for the cache store. Iteration order is
/// deterministic; eviction scans for the minimum `access_gen`.
pub struct VoxelCache {
capacity: usize,
entries: BTreeMap<CacheKey, CacheEntry>,
gen: u64,
}
impl VoxelCache {
/// Create a new cache with the given capacity.
///
/// The default capacity in production is [`DEFAULT_VOXEL_CACHE_CAPACITY`].
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
entries: BTreeMap::new(),
gen: 0,
}
}
/// Look up or derive the `VoxelColumn` for the given position.
///
/// Cache hit: returns a clone of the stored column, bumps its access
/// generation (LRU recency).
/// Cache miss: derives the column, inserts it (evicting LRU if full),
/// returns a clone.
///
/// The body-id hash is computed internally via the canonical
/// [`crate::seed::fnv1a_64`] — callers pass the raw `body_id`.
///
/// ## Precondition — context must cover the tile (T-1042)
///
/// `chunk` must be the `ChunkContext` derived for the chunk that
/// contains `(tile_x, tile_y)`. The cache key does not capture
/// `chunk.blend_weight` or `chunk.secondary`; if the same tile is
/// requested with a different context (e.g. once blended, once
/// unblended) the cache will return the first-derived column for both
/// calls. See the struct-level doc for the full constraint.
pub fn get_or_derive(
&mut self,
world_seed: u64,
body_id: &str,
district: &DistrictProfile,
chunk: &ChunkContext,
tile_x: i32,
tile_y: i32,
) -> VoxelColumn {
// Apply domain warp to get the canonical voxel address (same as in
// derive_voxel_column — the cache key is the POST-warp voxel address).
let (dx, dy) = domain_warp(world_seed, body_id, (tile_x, tile_y));
let voxel_x = (tile_x as f64 + dx) as i32;
let voxel_y = (tile_y as f64 + dy) as i32;
let key: CacheKey = (
world_seed,
crate::seed::fnv1a_64(body_id),
(voxel_x, voxel_y),
);
self.gen += 1;
let current_gen = self.gen;
if let Some(entry) = self.entries.get_mut(&key) {
entry.access_gen = current_gen;
return entry.column.clone();
}
// Cache miss — derive and insert.
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 {
self.evict_lru();
}
self.entries.insert(
key,
CacheEntry {
column: column.clone(),
access_gen: current_gen,
},
);
column
}
/// Number of entries currently in the cache.
#[cfg(test)]
pub fn len(&self) -> usize {
self.entries.len()
}
/// Whether the cache holds no entries.
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Test helper: is the voxel for this `(world_seed, body_id, tile)` currently
/// cached? Recomputes the canonical post-warp key without mutating LRU state,
/// so tests can assert exactly which entry survived or was evicted.
#[cfg(test)]
pub fn contains_voxel(&self, world_seed: u64, body_id: &str, tile_x: i32, tile_y: i32) -> bool {
let (dx, dy) = domain_warp(world_seed, body_id, (tile_x, tile_y));
let voxel_x = (tile_x as f64 + dx) as i32;
let voxel_y = (tile_y as f64 + dy) as i32;
let key: CacheKey = (
world_seed,
crate::seed::fnv1a_64(body_id),
(voxel_x, voxel_y),
);
self.entries.contains_key(&key)
}
/// Evict the entry with the lowest `access_gen` (the LRU entry).
fn evict_lru(&mut self) {
if self.entries.is_empty() {
return;
}
// Scan for the minimum access_gen. BTreeMap iteration is ordered by
// key (deterministic, D-010), so a tie is broken by key order.
let lru_key = self
.entries
.iter()
.min_by_key(|(_, v)| v.access_gen)
.map(|(k, _)| *k)
.expect("entries is non-empty");
self.entries.remove(&lru_key);
}
}
/// Default voxel cache capacity for production use.
///
/// 64×64 = 4096 voxels per chunk; with a 4-chunk radius lookahead (common
/// streaming pattern) that is ~65k voxels in flight. 8k entries keeps the
/// most-recently-used chunk and a partial second chunk warm without blowing
/// the memory budget (~200 bytes/entry × 8192 = ~1.6 MB).
pub const DEFAULT_VOXEL_CACHE_CAPACITY: usize = 8192;
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::atlas::chunk_context::{derive_chunk_context, BasinDirection};
use crate::atlas::district_profile::{
GlaciationGrade, PrecipitationClass, TectonicClass, VegetationClass,
};
// -----------------------------------------------------------------------
// Test fixtures
// -----------------------------------------------------------------------
/// 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 district-anchored, so tests sample around
/// `chunk.channel_anchor_m` instead of assuming chunk-frame positions.
fn anchor_chunk(seed: u64, body: &str, district: &DistrictProfile) -> ChunkContext {
let probe = derive_chunk_context(seed, body, district, (0, 0), None);
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, district, pos, None)
}
/// Map a (cross, along) coordinate pair to (tile_x, tile_y) for the
/// chunk's basin axes — cross ⊥ basin_direction, along ∥ basin_direction.
fn cross_along_to_tile(chunk: &ChunkContext, cross: i32, along: i32) -> (i32, i32) {
match chunk.basin_direction {
BasinDirection::North | BasinDirection::South => (cross, along),
BasinDirection::East | BasinDirection::West => (along, cross),
}
}
fn alluvial_district() -> DistrictProfile {
DistrictProfile {
morphology_zone: MorphologyZone::AlluvialPlain,
tectonic_class: TectonicClass::Stable,
glaciation_grade: GlaciationGrade::None,
precipitation_class: PrecipitationClass::Temperate,
slope_q: 5,
elev_q: 20,
ocean_fraction_q: 15,
river_threshold: 200,
temperature_c: Some(18.0),
moisture_q: 55,
vegetation_class: VegetationClass::Forest,
basin_direction: BasinDirection::South,
}
}
fn alluvial_chunk(district: &DistrictProfile) -> ChunkContext {
derive_chunk_context(42, "GJ1c", district, (10, 20), None)
}
fn water_district(zone: MorphologyZone, slope_q: i32) -> DistrictProfile {
DistrictProfile {
morphology_zone: zone,
tectonic_class: TectonicClass::Stable,
glaciation_grade: GlaciationGrade::None,
precipitation_class: PrecipitationClass::Temperate,
slope_q,
elev_q: 5,
ocean_fraction_q: 90,
river_threshold: 200,
temperature_c: Some(10.0),
moisture_q: 80,
vegetation_class: VegetationClass::Forest,
basin_direction: BasinDirection::South,
}
}
// ── WaterBody family (T-1082) ────────────────────────────────────────────
#[test]
fn water_body_open_ocean_is_wet_and_barren() {
// T-1082: OpenOcean used to render as dry forested AlluvialPlain land. It must
// now render wet (Deep/Shallow), Barren, on a sandy/rocky seabed at sea level.
let district = water_district(MorphologyZone::OpenOcean, 3);
let chunk = derive_chunk_context(42, "ocean", &district, (5, 5), None);
let mut saw_deep = false;
for sx in (0..64).step_by(4) {
for sy in (0..64).step_by(4) {
let col =
derive_voxel_column(42, "ocean", &district, &chunk, 5 * 64 + sx, 5 * 64 + sy);
assert_ne!(col.water, Water::Dry, "open ocean must not render dry");
assert_eq!(
col.vegetation,
Vegetation::Barren,
"no vegetation in open water"
);
assert!(matches!(
col.terrain,
TerrainMaterial::Sand | TerrainMaterial::Rock
));
assert_eq!(col.elevation_m, 0, "water surface at sea level");
if col.water == Water::Deep {
saw_deep = true;
}
}
}
assert!(
saw_deep,
"open ocean should have deep water away from the shoal"
);
}
#[test]
fn water_body_tidal_flat_is_shallow_wetland() {
let district = water_district(MorphologyZone::TidalFlat, 2);
let chunk = derive_chunk_context(7, "tf", &district, (3, 3), None);
for sx in (0..64).step_by(8) {
for sy in (0..64).step_by(8) {
let col = derive_voxel_column(7, "tf", &district, &chunk, 3 * 64 + sx, 3 * 64 + sy);
assert_eq!(
col.water,
Water::Shallow,
"tidal flat is intertidal (Shallow)"
);
assert_eq!(col.terrain, TerrainMaterial::Wetland, "tidal flat = mud");
assert_eq!(col.vegetation, Vegetation::Barren);
}
}
}
#[test]
fn water_body_steep_district_has_rock_seabed() {
let district = water_district(MorphologyZone::OpenOcean, 40);
let chunk = derive_chunk_context(1, "rock", &district, (2, 2), None);
let col = derive_voxel_column(1, "rock", &district, &chunk, 2 * 64 + 10, 2 * 64 + 10);
assert_eq!(
col.terrain,
TerrainMaterial::Rock,
"steep water district → rocky seabed"
);
assert_ne!(col.water, Water::Dry);
}
#[test]
fn water_body_is_deterministic() {
let district = water_district(MorphologyZone::Lake, 10);
let chunk = derive_chunk_context(99, "lake", &district, (4, 4), None);
let a = derive_voxel_column(99, "lake", &district, &chunk, 4 * 64 + 20, 4 * 64 + 30);
let b = derive_voxel_column(99, "lake", &district, &chunk, 4 * 64 + 20, 4 * 64 + 30);
assert_eq!(a, b);
}
// ── Voxel-tier mid-scale relief (T-1081) ─────────────────────────────────
#[test]
fn voxel_relief_gives_rugged_land_navigable_hills() {
// T-1081: a rugged land district must produce mid-scale relief across a ~2 km
// span far exceeding the ±4 m micro-scatter (the "flat 0–3 m, no hills" symptom).
let mut district = alluvial_district();
district.slope_q = 60; // rugged → relief headroom
let chunk = alluvial_chunk(&district);
let mut elevs = Vec::new();
for i in 0..12 {
let p = i * 256; // step across the voxel-relief band (0.25–2 km)
let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p);
if col.water == Water::Dry {
elevs.push(col.elevation_m);
}
}
assert!(elevs.len() >= 4, "expected mostly dry-land samples");
let min = *elevs.iter().min().unwrap();
let max = *elevs.iter().max().unwrap();
assert!(
max - min > 20,
"rugged district should have navigable mid-scale relief, got {min}..{max} m"
);
}
#[test]
fn voxel_relief_respects_low_flat_envelope() {
// No slope AND no elevation → no terrain signal → no invented relief (the
// envelope rule: don't sprout hills on a flat sea-level plain). Elevation stays
// the family base (elev_q/2 = 0 m) plus only the ±4 m micro-scatter / channel cut.
let mut district = alluvial_district();
district.slope_q = 0;
district.elev_q = 0;
let chunk = alluvial_chunk(&district);
for i in 0..12 {
let p = i * 256;
let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p);
assert!(
col.elevation_m <= 4,
"low-flat district gained mid-scale relief: {} m",
col.elevation_m
);
}
}
#[test]
fn voxel_relief_from_elevation_even_with_zero_slope() {
// The T-1081 root cause: the coarse heightmap gives slope_q ≈ 0 even on high
// terrain, so ELEVATION must drive the relief envelope. A high district with
// slope_q == 0 must still gain navigable hills (else high ground reads flat).
let mut district = alluvial_district();
district.slope_q = 0;
district.elev_q = 90; // high ground, coarse-heightmap-flat
let chunk = alluvial_chunk(&district);
let mut elevs = Vec::new();
for i in 0..12 {
let p = i * 256;
let col = derive_voxel_column(42, "GJ1c", &district, &chunk, p, p);
if col.water == Water::Dry {
elevs.push(col.elevation_m);
}
}
let range = elevs.iter().max().unwrap() - elevs.iter().min().unwrap();
assert!(
range > 20,
"high terrain must gain relief despite slope_q=0, got range {range} m"
);
}
#[test]
fn voxel_relief_is_deterministic() {
let mut district = alluvial_district();
district.slope_q = 50;
let chunk = alluvial_chunk(&district);
let a = derive_voxel_column(42, "GJ1c", &district, &chunk, 777, 1234);
let b = derive_voxel_column(42, "GJ1c", &district, &chunk, 777, 1234);
assert_eq!(a.elevation_m, b.elevation_m);
}
// -----------------------------------------------------------------------
// TerrainMaterial discriminant pins
// -----------------------------------------------------------------------
#[test]
fn terrain_material_discriminants_pinned() {
// Append-only invariant (D-010).
assert_eq!(TerrainMaterial::Soil as u8, 0);
assert_eq!(TerrainMaterial::Sand as u8, 1);
assert_eq!(TerrainMaterial::Gravel as u8, 2);
assert_eq!(TerrainMaterial::Rock as u8, 3);
assert_eq!(TerrainMaterial::Wetland as u8, 4);
assert_eq!(TerrainMaterial::Lava as u8, 5);
}
#[test]
fn water_discriminants_pinned() {
assert_eq!(Water::Dry as u8, 0);
assert_eq!(Water::Shallow as u8, 1);
assert_eq!(Water::Deep as u8, 2);
}
// -----------------------------------------------------------------------
// AlluvialPlain properties
// -----------------------------------------------------------------------
#[test]
fn alluvial_plain_produces_soil_terrain() {
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", &district, &chunk, 100, 100);
assert_eq!(
col.terrain,
TerrainMaterial::Soil,
"AlluvialPlain must produce Soil terrain"
);
}
#[test]
fn alluvial_plain_floor_is_none() {
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_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", &district, &chunk, tx, 100);
assert!(
matches!(
col.vegetation,
Vegetation::Forest | Vegetation::Scrub | Vegetation::Grass
),
"Forest district must produce Forest/Scrub/Grass vegetation, got {:?}",
col.vegetation
);
}
}
#[test]
fn elevation_is_non_negative() {
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", &district, &chunk, tx, ty);
assert!(
col.elevation_m >= 0,
"elevation must be >= 0, got {}",
col.elevation_m
);
}
}
// -----------------------------------------------------------------------
// Determinism — the core T-1028 contract
// -----------------------------------------------------------------------
/// 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 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", &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"
);
}
}
/// Different positions must (in general) produce different columns.
#[test]
fn different_positions_produce_different_outputs() {
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,
"positions 100m apart should produce different columns"
);
}
/// Different world seeds produce different results.
#[test]
fn different_seeds_produce_different_outputs() {
let district = alluvial_district();
let chunk1 = derive_chunk_context(1, "GJ1c", &district, (10, 20), None);
let chunk2 = derive_chunk_context(2, "GJ1c", &district, (10, 20), None);
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"
);
}
// -----------------------------------------------------------------------
// Cache behaviour (D-227)
// -----------------------------------------------------------------------
#[test]
fn cache_returns_same_column_as_direct_derivation() {
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, &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"
);
}
#[test]
fn cache_second_call_is_a_hit() {
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, &district, &chunk, 100, 100);
assert_eq!(cache.len(), 1, "first call must insert one entry");
let second = cache.get_or_derive(42, body_id, &district, &chunk, 100, 100);
assert_eq!(
cache.len(),
1,
"second call must be a cache hit, not a new insertion"
);
assert_eq!(first, second, "cache hit must return same column");
}
#[test]
fn cache_evicts_lru_on_overflow() {
let district = alluvial_district();
let chunk = alluvial_chunk(&district);
let body_id = "GJ1c";
let capacity = 8;
let mut cache = VoxelCache::new(capacity);
// Fill cache to capacity with distinct positions.
for i in 0..capacity as i32 {
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, &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, &district, &chunk, 999, 999);
assert_eq!(
cache.len(),
capacity,
"cache size must stay at capacity after eviction"
);
// Verify the RIGHT entry was evicted, not just that some eviction happened.
assert!(
cache.contains_voxel(42, body_id, 0, 0),
"refreshed entry (pos 0) must survive eviction"
);
assert!(
!cache.contains_voxel(42, body_id, 64, 0),
"LRU entry (pos 64) must have been the one evicted"
);
assert!(
cache.contains_voxel(42, body_id, 999, 999),
"newly inserted entry must be present"
);
}
#[test]
fn cache_eviction_does_not_break_derivation() {
// After eviction, re-deriving the evicted entry must produce the same result.
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, &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, &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, &district, &chunk, 0, 0);
assert_eq!(
col0, col0_again,
"re-derived column after cache eviction must be identical"
);
}
#[test]
fn cache_boundary_tile_blend_is_stable() {
// T-1042 / H3: VoxelCache.get_or_derive must return identical blended
// output on two successive calls for the same boundary tile. This test
// exercises the `Cow::Owned` path (blend_weight=128, secondary=Some(...))
// through the cache and guards against future cache-key drift where a
// second call with a different context returns a stale unblended entry.
//
// Precondition (VoxelCache struct doc, T-1042): both calls use the SAME
// ChunkContext — the one derived for the chunk containing the tile. This
// is the correct call pattern; the test intentionally validates it.
use crate::atlas::chunk_context::district_boundary_blend_weight;
let district_primary = DistrictProfile {
morphology_zone: MorphologyZone::AlluvialPlain,
tectonic_class: crate::atlas::district_profile::TectonicClass::Stable,
glaciation_grade: crate::atlas::district_profile::GlaciationGrade::None,
precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate,
slope_q: 5,
elev_q: 20, // low elevation
ocean_fraction_q: 0, // no channel — simpler tile layout for elevation check
river_threshold: 200,
temperature_c: Some(18.0),
moisture_q: 55,
vegetation_class: VegetationClass::Forest,
basin_direction: BasinDirection::South,
};
let district_secondary = DistrictProfile {
morphology_zone: MorphologyZone::AlluvialPlain,
tectonic_class: crate::atlas::district_profile::TectonicClass::Stable,
glaciation_grade: crate::atlas::district_profile::GlaciationGrade::None,
precipitation_class: crate::atlas::district_profile::PrecipitationClass::Temperate,
slope_q: 5,
elev_q: 70, // high elevation — makes the blend measurable
ocean_fraction_q: 0,
river_threshold: 200,
temperature_c: Some(18.0),
moisture_q: 55,
vegetation_class: VegetationClass::Forest,
basin_direction: BasinDirection::South,
};
let (seed, body_id) = (42u64, "blend_cache_test");
// Chunk (31, 0): the last chunk in district (0, 0) — at the district boundary.
let boundary_chunk_pos = (31i32, 0i32);
let (near, blend_w) = district_boundary_blend_weight(boundary_chunk_pos);
assert!(near, "chunk (31, 0) must be detected as a boundary chunk");
assert_eq!(blend_w, 128);
let chunk = derive_chunk_context(
seed,
body_id,
&district_primary,
boundary_chunk_pos,
Some((&district_secondary, blend_w)),
);
// A tile inside the boundary chunk — world position = chunk_x * 64 + offset.
let tile_x = boundary_chunk_pos.0 * 64 + 16;
let tile_y = boundary_chunk_pos.1 * 64 + 16;
let mut cache = VoxelCache::new(64);
// First call: cache miss → derives the blended column and inserts it.
let col1 = cache.get_or_derive(seed, body_id, &district_primary, &chunk, tile_x, tile_y);
assert_eq!(cache.len(), 1, "first call must produce one cache entry");
// Second call: cache hit → must return the same blended column.
let col2 = cache.get_or_derive(seed, body_id, &district_primary, &chunk, tile_x, tile_y);
assert_eq!(
cache.len(),
1,
"second call must be a cache hit (len unchanged)"
);
assert_eq!(
col1.elevation_m, col2.elevation_m,
"cache hit must return identical blended elevation for boundary tile"
);
assert_eq!(
col1.terrain, col2.terrain,
"cache hit must return identical terrain for boundary tile"
);
// Sanity: the blended elevation must sit between the two district values.
// primary elev_q=20 → base_elev_m=10; secondary elev_q=70 → base_elev_m=35.
// At 50-50 blend the blended elev_q = (20*128 + 70*127 + 127)/255 = 44 (rounded).
// base_elev_m = 44/2 = 22. With micro-relief ±4 m, range is [18, 25] m.
// This asserts the blend actually ran (not the pure-primary 10 m result).
assert!(
col1.elevation_m > 15,
"blended boundary tile elevation ({}) must be above pure-primary level (~10 m) \
— blend did not apply",
col1.elevation_m
);
}
// -----------------------------------------------------------------------
// Domain warp sanity
// -----------------------------------------------------------------------
#[test]
fn domain_warp_is_applied_before_derivation() {
// 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 district = alluvial_district();
let chunk = alluvial_chunk(&district);
// Sample a grid of 4 neighbouring tiles; at least some should differ.
let cols: Vec<VoxelColumn> = (0..4)
.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!(
!all_same,
"adjacent tiles should differ after domain warp (elevation scatter)"
);
}
// -----------------------------------------------------------------------
// Family dispatch stubs — must not panic
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// T-1029 — LavaField
// -----------------------------------------------------------------------
fn lava_district() -> DistrictProfile {
DistrictProfile {
morphology_zone: MorphologyZone::Volcanic,
tectonic_class: TectonicClass::Volcanic,
vegetation_class: VegetationClass::Barren,
ocean_fraction_q: 0,
slope_q: 8,
elev_q: 30,
moisture_q: 20,
..alluvial_district()
}
}
#[test]
fn lava_field_terrain_is_lava() {
let district = lava_district();
let chunk = derive_chunk_context(42, "Io", &district, (0, 0), None);
for pos in [(10, 10), (0, 0), (50, 25), (-5, 5)] {
let col = derive_voxel_column(42, "Io", &district, &chunk, pos.0, pos.1);
assert_eq!(
col.terrain,
TerrainMaterial::Lava,
"LavaField must produce Lava terrain at {:?}",
pos
);
}
}
#[test]
fn lava_field_vegetation_is_barren() {
let district = lava_district();
let chunk = derive_chunk_context(42, "Io", &district, (0, 0), None);
for tx in 0..20i32 {
let col = derive_voxel_column(42, "Io", &district, &chunk, tx, 10);
assert_eq!(
col.vegetation,
Vegetation::Barren,
"LavaField must be Barren at x={tx}"
);
}
}
#[test]
fn lava_field_no_deep_water() {
// Immature drainage law (D-239 §8): no deep organised channels on lava.
let district = lava_district();
let chunk = derive_chunk_context(42, "Io", &district, (0, 0), None);
for (tx, ty) in (0..50).map(|i| (i * 7, i * 3)) {
let col = derive_voxel_column(42, "Io", &district, &chunk, tx, ty);
assert_ne!(
col.water,
Water::Deep,
"LavaField must have no Deep water (immature drainage)"
);
}
}
#[test]
fn lava_field_is_deterministic() {
let district = lava_district();
let chunk = derive_chunk_context(7, "Io", &district, (5, 3), None);
for (tx, ty) in [(0, 0), (10, 20), (-5, 7)] {
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 district = lava_district();
let chunk = derive_chunk_context(42, "Io", &district, (0, 0), None);
for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] {
let col = derive_voxel_column(42, "Io", &district, &chunk, tx, ty);
assert!(col.elevation_m >= 0, "LavaField elevation must be >= 0");
}
}
// -----------------------------------------------------------------------
// T-1029 — FjordWall
// -----------------------------------------------------------------------
fn fjord_district() -> DistrictProfile {
DistrictProfile {
morphology_zone: MorphologyZone::Fjord,
tectonic_class: TectonicClass::Active,
glaciation_grade: GlaciationGrade::Moderate, // grade ≥ 2
slope_q: 60,
elev_q: 80,
ocean_fraction_q: 30,
moisture_q: 70,
vegetation_class: VegetationClass::Barren,
..alluvial_district()
}
}
#[test]
fn fjord_wall_terrain_is_rock() {
let district = fjord_district();
let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None);
for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] {
let col = derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty);
assert_eq!(
col.terrain,
TerrainMaterial::Rock,
"FjordWall must produce Rock terrain at ({tx},{ty})"
);
}
}
#[test]
fn fjord_wall_centre_has_deep_water() {
// 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 district = fjord_district();
let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None);
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", &district, &chunk, tx, ty).water == Water::Deep
});
assert!(
any_deep,
"FjordWall centreline (anchor {anchor}) must have Deep water (the fjord inlet)"
);
}
#[test]
fn fjord_wall_walls_are_high_relative_to_floor() {
// Wall elevation must be substantially higher than fjord floor.
let district = fjord_district();
let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None);
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", &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", &district, &chunk, wx, wy);
assert!(
wall_col.elevation_m > floor_elev,
"FjordWall walls ({}) must be higher than the floor ({})",
wall_col.elevation_m,
floor_elev
);
}
#[test]
fn fjord_wall_is_deterministic() {
let district = fjord_district();
let chunk = derive_chunk_context(11, "Fjordheim", &district, (2, 3), None);
for (tx, ty) in [(32, 50), (0, 50), (63, 20)] {
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 district = fjord_district();
let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None);
for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] {
let col = derive_voxel_column(42, "Fjordheim", &district, &chunk, tx, ty);
assert!(col.elevation_m >= 0, "FjordWall elevation must be >= 0");
}
}
#[test]
fn fjord_wall_floor_width_in_spec() {
// 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 district-anchored trough (T-1041).
let district = fjord_district(); // glaciation_grade = Moderate (= 2)
let chunk = derive_chunk_context(42, "Fjordheim", &district, (0, 0), None);
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", &district, &chunk, tx, ty).water == Water::Deep
})
.count();
assert!(
(2..=8).contains(&deep_tiles),
"FjordWall floor width {deep_tiles} m outside [2, 8] m spec (D-239 §9)"
);
}
// -----------------------------------------------------------------------
// T-1029 — CliffCoast
// -----------------------------------------------------------------------
fn cliff_district() -> DistrictProfile {
DistrictProfile {
morphology_zone: MorphologyZone::CliffCoast,
tectonic_class: TectonicClass::Active,
slope_q: 70,
elev_q: 60,
ocean_fraction_q: 40,
moisture_q: 50,
vegetation_class: VegetationClass::Barren,
..alluvial_district()
}
}
#[test]
fn cliff_coast_terrain_is_rock() {
let district = cliff_district();
let chunk = derive_chunk_context(42, "Velen", &district, (0, 0), None);
for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] {
let col = derive_voxel_column(42, "Velen", &district, &chunk, tx, ty);
assert_eq!(
col.terrain,
TerrainMaterial::Rock,
"CliffCoast must produce Rock terrain at ({tx},{ty})"
);
}
}
#[test]
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 district-anchored line `coast_anchor_m` along
// the seaward (basin) axis (T-1041) — sample a transect across it.
let district = cliff_district();
let chunk = derive_chunk_context(42, "Velen", &district, (0, 0), None);
let coast = chunk.coast_anchor_m;
// Sample 64 positions along the seaward axis, crossing the coast line.
let cols: Vec<VoxelColumn> = (coast - 32..coast + 32)
.map(|a| {
let (tx, ty) = cross_along_to_tile(&chunk, 50, a);
derive_voxel_column(42, "Velen", &district, &chunk, tx, ty)
})
.collect();
let has_deep = cols.iter().any(|c| c.water == Water::Deep);
let max_elev = cols.iter().map(|c| c.elevation_m).max().unwrap();
let ocean_elev = cols
.iter()
.filter(|c| c.water == Water::Deep)
.map(|c| c.elevation_m)
.min()
.unwrap_or(0);
assert!(
has_deep,
"CliffCoast must have at least one tile with Deep water"
);
assert!(
max_elev > ocean_elev + 5,
"CliffCoast: max elevation ({max_elev}) must be >> ocean elevation ({ocean_elev}) — vertical face law"
);
}
#[test]
fn cliff_coast_is_deterministic() {
let district = cliff_district();
let chunk = derive_chunk_context(5, "Velen", &district, (1, 2), None);
for (tx, ty) in [(5, 50), (30, 20), (62, 50)] {
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 district = cliff_district();
let chunk = derive_chunk_context(42, "Velen", &district, (0, 0), None);
for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] {
let col = derive_voxel_column(42, "Velen", &district, &chunk, tx, ty);
assert!(col.elevation_m >= 0, "CliffCoast elevation must be >= 0");
}
}
// -----------------------------------------------------------------------
// T-1029 — BraidedDelta
// -----------------------------------------------------------------------
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_district()
}
}
#[test]
fn braided_delta_terrain_is_gravel() {
let district = delta_district();
let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0), None);
for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] {
let col = derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty);
assert_eq!(
col.terrain,
TerrainMaterial::Gravel,
"BraidedDelta must produce Gravel terrain at ({tx},{ty})"
);
}
}
#[test]
fn braided_delta_low_elevation() {
// Delta is near sea level — D-239 §8 drainage monotonicity: mouths at sea level.
let district = delta_district();
let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0), None);
for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5), (63, 0)] {
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})",
col.elevation_m
);
}
}
#[test]
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 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", &district, &chunk, tx, ty).water
== Water::Shallow
})
.count();
// Three braided threads, each 3–8 m wide → expect at least 6 shallow tiles
// in the belt transect.
assert!(
shallow_count >= 6,
"BraidedDelta must have multiple braided channels; only {shallow_count} shallow in transect"
);
}
#[test]
fn braided_delta_no_single_thread() {
// BraidedDelta must NOT look like a single-thread meander — the channel
// 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 district = delta_district();
let chunk = anchor_chunk(42, "delta_body", &district);
let anchor = chunk.channel_anchor_m;
let waters: Vec<Water> = (anchor - 40..anchor + 40)
.map(|c| {
let (tx, ty) = cross_along_to_tile(&chunk, c, 100);
derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty).water
})
.collect();
// Count Dry→Shallow transitions = number of channel entrances.
let transitions = waters
.windows(2)
.filter(|w| w[0] == Water::Dry && w[1] == Water::Shallow)
.count();
assert!(
transitions >= 2,
"BraidedDelta must have ≥2 braided channel threads; only {transitions} transitions"
);
}
#[test]
fn braided_delta_is_deterministic() {
let district = delta_district();
let chunk = derive_chunk_context(99, "delta_body", &district, (3, 1), None);
for (tx, ty) in [(10, 10), (0, 100), (-5, 7)] {
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 district = delta_district();
let chunk = derive_chunk_context(42, "delta_body", &district, (0, 0), None);
for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] {
let col = derive_voxel_column(42, "delta_body", &district, &chunk, tx, ty);
assert!(col.elevation_m >= 0, "BraidedDelta elevation must be >= 0");
}
}
// -----------------------------------------------------------------------
// T-1029 — DuneStrand
// -----------------------------------------------------------------------
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_district()
}
}
#[test]
fn dune_strand_terrain_is_sand() {
let district = dune_district();
let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0), None);
for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] {
let col = derive_voxel_column(42, "dune_body", &district, &chunk, tx, ty);
assert_eq!(
col.terrain,
TerrainMaterial::Sand,
"DuneStrand must produce Sand terrain at ({tx},{ty})"
);
}
}
#[test]
fn dune_strand_respects_angle_of_repose() {
// D-239 §8 Sand law: ≤~32° angle of repose. For a dune with period W,
// 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 district = dune_district();
let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0), None);
// Sample a row of tiles along the dune wave direction.
let elevations: Vec<i32> = (0..40i32)
.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();
// Dune wavelength for slope_q=8: 20 - 8/2 = 16 m. Physics cap per wave = 0.625 × 8 = 5 m.
// Allow a margin of 2 for scatter noise.
let physics_cap = 8; // generous upper bound including noise
assert!(
max_elev <= physics_cap,
"DuneStrand max elevation {} exceeds angle-of-repose physics cap {}",
max_elev,
physics_cap
);
}
#[test]
fn dune_strand_mostly_dry() {
// Dunes are mostly dry; only the strand toe may be Shallow.
let district = dune_district();
let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0), None);
let dry_or_shallow_count = (0..100i32)
.filter(|&x| {
let col = derive_voxel_column(42, "dune_body", &district, &chunk, x, 50);
col.water != Water::Deep // no Deep water on dunes
})
.count();
assert_eq!(
dry_or_shallow_count, 100,
"DuneStrand must have no Deep water"
);
}
#[test]
fn dune_strand_is_deterministic() {
let district = dune_district();
let chunk = derive_chunk_context(3, "dune_body", &district, (1, 0), None);
for (tx, ty) in [(10, 10), (0, 30), (-5, 7)] {
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 district = dune_district();
let chunk = derive_chunk_context(42, "dune_body", &district, (0, 0), None);
for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] {
let col = derive_voxel_column(42, "dune_body", &district, &chunk, tx, ty);
assert!(col.elevation_m >= 0, "DuneStrand elevation must be >= 0");
}
}
// -----------------------------------------------------------------------
// T-1029 — IncisedGorge
// -----------------------------------------------------------------------
fn gorge_district() -> DistrictProfile {
DistrictProfile {
morphology_zone: MorphologyZone::MountainPass,
tectonic_class: TectonicClass::Active,
slope_q: 80,
elev_q: 90,
ocean_fraction_q: 5,
moisture_q: 50,
vegetation_class: VegetationClass::Barren,
..alluvial_district()
}
}
#[test]
fn incised_gorge_terrain_is_rock() {
let district = gorge_district();
let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0), None);
for (tx, ty) in [(10, 10), (0, 0), (50, 25), (30, -5)] {
let col = derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty);
assert_eq!(
col.terrain,
TerrainMaterial::Rock,
"IncisedGorge must produce Rock terrain at ({tx},{ty})"
);
}
}
#[test]
fn incised_gorge_floor_width_in_spec() {
// 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
// district-anchored gorge centreline (T-1041).
let district = gorge_district();
let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0), None);
let anchor = chunk.channel_anchor_m;
// 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()
})
.max()
.unwrap();
assert!(
(2..=8).contains(&floor_tiles),
"IncisedGorge floor width {floor_tiles} m outside [2, 8] m spec (D-239 §9)"
);
}
#[test]
fn incised_gorge_walls_much_higher_than_floor() {
// Walls must be substantially higher than the gorge floor.
let district = gorge_district();
let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0), None);
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", &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", &district, &chunk, wx, wy);
assert!(
wall_col.elevation_m > floor_elev + 10,
"IncisedGorge walls ({}) must be >> gorge floor ({})",
wall_col.elevation_m,
floor_elev
);
}
#[test]
fn incised_gorge_is_deterministic() {
let district = gorge_district();
let chunk = derive_chunk_context(17, "gorge_body", &district, (4, 2), None);
for (tx, ty) in [(32, 100), (62, 100), (10, 50)] {
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 district = gorge_district();
let chunk = derive_chunk_context(42, "gorge_body", &district, (0, 0), None);
for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] {
let col = derive_voxel_column(42, "gorge_body", &district, &chunk, tx, ty);
assert!(col.elevation_m >= 0, "IncisedGorge elevation must be >= 0");
}
}
// -----------------------------------------------------------------------
// T-1029 — MeanderReach
// -----------------------------------------------------------------------
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_district()
}
}
#[test]
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_district).
// This test checks that NO other material appears (no Rock, Sand, Gravel, Lava).
let district = meander_district();
let chunk = derive_chunk_context(42, "meander_body", &district, (0, 0), None);
for (tx, ty) in [
(0, 50),
(5, 50),
(10, 50),
(15, 50),
(100, 50),
(200, 50),
(300, 50),
(400, 50),
] {
let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty);
assert!(
matches!(
col.terrain,
TerrainMaterial::Soil | TerrainMaterial::Wetland
),
"MeanderReach terrain must be Soil or Wetland, got {:?} at ({tx},{ty})",
col.terrain
);
}
}
#[test]
fn meander_reach_non_wetland_region_produces_soil() {
// 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_district()
};
let chunk = derive_chunk_context(42, "meander_body2", &district, (0, 0), None);
let mut soil_count = 0;
for tx in [0, 5, 10, 15, 100, 200] {
let col = derive_voxel_column(42, "meander_body2", &district, &chunk, tx, 50);
if col.terrain == TerrainMaterial::Soil {
soil_count += 1;
}
}
assert!(
soil_count >= 5,
"Non-wetland MeanderReach must be mostly Soil, got {soil_count}/6"
);
}
#[test]
fn meander_reach_has_active_channel() {
// MeanderReach must have a channel with Shallow/Deep water somewhere in
// 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,
"MeanderReach anchor chunk must have active channel"
);
let anchor = chunk.channel_anchor_m;
// Sample a wide cross-section and find at least one wet tile.
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", &district, &chunk, tx, ty);
col.water != Water::Dry
})
.count();
assert!(
wet_count >= 3,
"MeanderReach must have a visible channel with wet tiles in cross-section; got {wet_count}"
);
}
#[test]
fn meander_reach_channel_below_levee() {
// D-239 §9 ElevationDelta: channel tiles must sit below off-channel
// 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 district = meander_district();
let chunk = anchor_chunk(42, "meander_body", &district);
let anchor = chunk.channel_anchor_m;
let mut min_wet: Option<i32> = None;
let mut max_dry: Option<i32> = 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", &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 {
max_dry = Some(max_dry.map_or(col.elevation_m, |e| e.max(col.elevation_m)));
}
}
// Both tile kinds must exist — a transect across the anchor that finds
// no channel means the placement regressed (the law must be exercised).
let (min_wet, max_dry) = (
min_wet.expect("transect across the anchor must contain channel tiles"),
max_dry.expect("transect must contain floodplain tiles"),
);
assert!(
min_wet <= max_dry,
"MeanderReach: channel bottom ({min_wet}) must be <= floodplain/levee top ({max_dry})"
);
}
#[test]
fn meander_reach_stronger_sinuosity_than_alluvial() {
// MeanderReach uses channel amplitude = wavelength/4 vs AlluvialPlain's
// 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
// ever regresses to AlluvialPlain's amplitude (or falls back to the same
// code path), the wet-tile sets become identical and this test fails.
//
// 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_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_district()
};
let meander_reg = DistrictProfile {
morphology_zone: MorphologyZone::MeanderReach,
..shared_district_base.clone()
};
let alluvial_reg = DistrictProfile {
morphology_zone: MorphologyZone::AlluvialPlain,
..shared_district_base
};
// Same seed + body → same ChunkContext (same wavelength, phase, anchor,
// channel_width). Use the anchor-covering chunk (T-1040 band gating).
let meander_chunk = anchor_chunk(42, "sinuosity_body", &meander_reg);
let alluvial_chunk = anchor_chunk(42, "sinuosity_body", &alluvial_reg);
// Verify the chunks share the same meander params (confirming the test setup).
assert_eq!(
meander_chunk.meander_phase, alluvial_chunk.meander_phase,
"test setup requires identical chunk params"
);
assert_eq!(
meander_chunk.channel_anchor_m, alluvial_chunk.channel_anchor_m,
"test setup requires identical channel anchors"
);
assert!(
meander_chunk.has_active_channel && alluvial_chunk.has_active_channel,
"both chunks must have active channels for the test to be meaningful"
);
// Collect wet-tile cross-positions over a transect at along=50, centred
// on the shared anchor. We scan far enough to capture the full meander
// swing — amplitude for MeanderReach is wl/4, for AlluvialPlain wl/6,
// so over a ±400 sweep both complete multiple full cycles and their
// centreline positions diverge.
let anchor = meander_chunk.channel_anchor_m;
let meander_wet: Vec<i32> = (anchor - 400..anchor + 400)
.filter(|&c| {
let (tx, ty) = cross_along_to_tile(&meander_chunk, c, 50);
derive_voxel_column(42, "sinuosity_body", &meander_reg, &meander_chunk, tx, ty)
.water
!= Water::Dry
})
.collect();
let alluvial_wet: Vec<i32> = (anchor - 400..anchor + 400)
.filter(|&c| {
let (tx, ty) = cross_along_to_tile(&alluvial_chunk, c, 50);
derive_voxel_column(42, "sinuosity_body", &alluvial_reg, &alluvial_chunk, tx, ty)
.water
!= Water::Dry
})
.collect();
assert!(
!meander_wet.is_empty(),
"MeanderReach must have a wet channel"
);
assert!(
!alluvial_wet.is_empty(),
"AlluvialPlain must have a wet channel"
);
// The wet-tile position sets must differ — proving MeanderReach uses a
// distinct sinuosity from AlluvialPlain. If this fails, the two generators
// produce identical channel positions, meaning MeanderReach has regressed.
assert_ne!(
meander_wet, alluvial_wet,
"MeanderReach and AlluvialPlain must produce different wet-tile positions \
(different channel amplitude = different sinuosity); they were identical, \
which means MeanderReach has regressed to AlluvialPlain amplitude"
);
}
#[test]
fn meander_reach_is_deterministic() {
let district = meander_district();
let chunk = derive_chunk_context(23, "meander_body", &district, (2, 5), None);
for (tx, ty) in [(0, 50), (50, 200), (-20, 100)] {
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 district = meander_district();
let chunk = derive_chunk_context(42, "meander_body", &district, (0, 0), None);
for (tx, ty) in [(100, 100), (0, 0), (-50, 25), (200, -10)] {
let col = derive_voxel_column(42, "meander_body", &district, &chunk, tx, ty);
assert!(col.elevation_m >= 0, "MeanderReach elevation must be >= 0");
}
}
// -----------------------------------------------------------------------
// T-1029 — Cross-family: no panics, correct materials, all deterministic
// -----------------------------------------------------------------------
/// All families (7 T-1029 landform + the T-1082 WaterBody zones) must not panic on
/// any sample position, including extreme coordinates.
#[test]
fn all_families_no_panic_at_extreme_positions() {
let zones = [
MorphologyZone::Volcanic,
MorphologyZone::Fjord,
MorphologyZone::CliffCoast,
MorphologyZone::Delta,
MorphologyZone::DuneStrand,
MorphologyZone::MountainPass,
MorphologyZone::MeanderReach,
// T-1082 WaterBody zones.
MorphologyZone::OpenOcean,
MorphologyZone::Lake,
MorphologyZone::TidalFlat,
];
let positions = [
(0, 0),
(i32::MAX / 4, 0),
(0, i32::MAX / 4),
(-100, -100),
(1000, 2000),
(-500, 300),
];
for zone in &zones {
let district = DistrictProfile {
morphology_zone: *zone,
tectonic_class: TectonicClass::Volcanic,
glaciation_grade: GlaciationGrade::Moderate,
slope_q: 50,
elev_q: 40,
ocean_fraction_q: 20,
moisture_q: 50,
..alluvial_district()
};
let chunk = derive_chunk_context(42, "stress_body", &district, (0, 0), None);
for (tx, ty) in &positions {
// Must not panic.
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})",
zone
);
}
}
}
/// Each T-1029 family produces the expected TerrainMaterial (D-239 §8 lithology law).
#[test]
fn family_terrain_materials_match_lithology_law() {
let cases: &[(MorphologyZone, TerrainMaterial)] = &[
(MorphologyZone::Volcanic, TerrainMaterial::Lava),
(MorphologyZone::Fjord, TerrainMaterial::Rock),
(MorphologyZone::CliffCoast, TerrainMaterial::Rock),
(MorphologyZone::Delta, TerrainMaterial::Gravel),
(MorphologyZone::DuneStrand, TerrainMaterial::Sand),
(MorphologyZone::MountainPass, TerrainMaterial::Rock),
];
for (zone, expected_terrain) in cases {
let district = DistrictProfile {
morphology_zone: *zone,
tectonic_class: TectonicClass::Volcanic,
glaciation_grade: GlaciationGrade::Moderate,
slope_q: 50,
elev_q: 40,
ocean_fraction_q: 5,
moisture_q: 30,
vegetation_class: VegetationClass::Barren,
..alluvial_district()
};
let chunk = derive_chunk_context(42, "mat_check", &district, (0, 0), None);
let col = derive_voxel_column(42, "mat_check", &district, &chunk, 10, 10);
assert_eq!(
col.terrain, *expected_terrain,
"Zone {:?} must produce {:?} terrain",
zone, expected_terrain
);
}
}
// -----------------------------------------------------------------------
// T-1030 — SeasonalCover: discriminant pins and derive_cover tests
// -----------------------------------------------------------------------
#[test]
fn seasonal_cover_discriminants_pinned() {
// Append-only invariant (D-010).
assert_eq!(SeasonalCover::None as u8, 0);
assert_eq!(SeasonalCover::Snow as u8, 1);
assert_eq!(SeasonalCover::Ice as u8, 2);
}
// ── Helpers for derive_cover tests ──────────────────────────────────────
/// Build a minimal VoxelColumn with a given Water state for cover testing.
/// The cover field is irrelevant (derive_cover is called externally in tests).
fn make_column(water: Water) -> VoxelColumn {
VoxelColumn {
terrain: TerrainMaterial::Soil,
floor: FloorMaterial::None,
vegetation: Vegetation::Grass,
water,
elevation_m: 5,
cover: SeasonalCover::None,
}
}
/// 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_district()
}
}
// ── Airless body ─────────────────────────────────────────────────────────
#[test]
fn cover_airless_body_is_none() {
// D-239 §2: no atmosphere → temperature_c == None → cover None always.
let mut district = alluvial_district();
district.temperature_c = None;
let col = make_column(Water::Dry);
let cover = derive_cover(42, "test_body", &district, &col, (0, 0));
assert_eq!(
cover,
SeasonalCover::None,
"airless body must always produce None cover"
);
}
#[test]
fn cover_airless_body_even_with_water() {
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", &district, &col, (50, 50));
assert_eq!(
cover,
SeasonalCover::None,
"airless + lake must still be None"
);
}
// ── Freshwater band edges ─────────────────────────────────────────────────
#[test]
fn cover_freshwater_above_band_is_none() {
// Above +5 °C: always None (open water), regardless of scatter.
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", &district, &col, (i * 7, i * 3));
assert_eq!(
cover,
SeasonalCover::None,
"freshwater at temp +6°C must be None (above band) at pos ({},{})",
i * 7,
i * 3
);
}
}
#[test]
fn cover_freshwater_below_band_is_all_ice() {
// Below −10 °C: always Ice (no scatter).
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", &district, &col, (i * 11, i * 5));
assert_eq!(
cover,
SeasonalCover::Ice,
"freshwater at temp −11°C must be Ice (below band) at pos ({},{})",
i * 11,
i * 5
);
}
}
#[test]
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 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", &district, &col, pos);
match cover {
SeasonalCover::Ice => ice_count += 1,
SeasonalCover::None => none_count += 1,
SeasonalCover::Snow => panic!("freshwater should not produce Snow"),
}
}
assert!(
ice_count > 0,
"scatter band at −3°C must produce some Ice tiles; got 0 Ice in 200 samples"
);
assert!(
none_count > 0,
"scatter band at −3°C must produce some None tiles; got 0 None in 200 samples"
);
}
// ── Sea ice (salt water) ─────────────────────────────────────────────────
#[test]
fn cover_salt_above_onset_is_none() {
// Above −2 °C: no sea ice.
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", &district, &col, (i * 13, i * 7));
assert_eq!(
cover,
SeasonalCover::None,
"salt water at 0°C must be None (above onset −2°C)"
);
}
}
#[test]
fn cover_salt_below_band_is_all_ice() {
// Below −14 °C: permanent pack ice everywhere.
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", &district, &col, (i * 17, i * 9));
assert_eq!(
cover,
SeasonalCover::Ice,
"salt water at −15°C must be Ice (below band)"
);
}
}
#[test]
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 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", &district, &col, (i * 7, i * 5));
assert_eq!(
cover,
SeasonalCover::None,
"ocean at +3°C must be None (salt onset is −2°C, not +5°C)"
);
}
}
// ── Snow (land) — moisture gating ────────────────────────────────────────
#[test]
fn cover_snow_cold_wet_land_produces_snow() {
// Cold + wet land must produce Snow in the permanent zone (below −10°C).
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", &district, &col, (i * 9, i * 3));
assert_eq!(
cover,
SeasonalCover::Snow,
"cold (−12°C) + moist land must produce Snow (permanent zone)"
);
}
}
#[test]
fn cover_snow_cold_dry_land_produces_none() {
// Cold + dry (moisture_q < SNOW_MOISTURE_GATE) → bare frozen ground, not Snow.
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", &district, &col, (i * 9, i * 3));
assert_eq!(
cover,
SeasonalCover::None,
"cold (−12°C) + dry land must produce None (bare frozen ground, not Snow)"
);
}
}
#[test]
fn cover_snow_warm_land_produces_none() {
// Warm land (above snow band): no snow.
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", &district, &col, (i * 5, i * 2));
assert_eq!(
cover,
SeasonalCover::None,
"warm (+10°C) land must produce None even with high moisture"
);
}
}
#[test]
fn cover_snow_within_band_has_mix() {
// Within the snow scatter band (e.g. −5°C) + moist: mix of Snow and None.
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", &district, &col, pos);
match cover {
SeasonalCover::Snow => snow_count += 1,
SeasonalCover::None => none_count += 1,
SeasonalCover::Ice => panic!("land should not produce Ice"),
}
}
assert!(
snow_count > 0,
"scatter band at −5°C moist land must produce some Snow; got 0 Snow"
);
assert!(
none_count > 0,
"scatter band at −5°C moist land must produce some None; got 0 None"
);
}
// ── Land water-state: Dry riverbank should be land, not freshwater ────────
#[test]
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 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", &district, &dry_col, (0, 0));
let wet_cover = derive_cover(42, "test_body", &district, &wet_col, (0, 0));
assert_eq!(
dry_cover,
SeasonalCover::Snow,
"dry RiverBank at −12°C + moist must be Snow (land branch)"
);
assert_eq!(
wet_cover,
SeasonalCover::Ice,
"wet RiverBank at −12°C must be Ice (freshwater branch)"
);
}
// ── Spatial coherence ────────────────────────────────────────────────────
/// Coherence test: exercises the REAL `derive_cover` production signature
/// (with `world_seed` + `body_id` — the fixed API) to catch the defect from
/// PR #163.
///
/// The defect: in the broken code, `cluster_scatter` received `sub_chunk_seed`
/// (a per-voxel value), so every voxel had a distinct scatter hash → salt-and-
/// pepper noise, NOT clustered patches. This test would FAIL against that code
/// because intra-cluster agreement would be ~50% (random coin flip) instead of
/// 100%. It PASSES only with the fix: `cluster_scatter` uses
/// `SeedChain::for_body(world_seed, body_id).derive(Cover, cluster_cell_id)`,
/// keyed on (world, body, cluster-cell) so every voxel in the same cell gets
/// the same hash.
///
/// ## Why we test `derive_cover` directly rather than via `derive_voxel_column`
///
/// `derive_voxel_column` applies domain warp (±8 m) before computing the voxel
/// address. Two logically-adjacent tiles (1 m apart) may warp to positions that
/// fall in *different* 6 m cluster cells — which is correct behavior, not a
/// coherence failure. Testing coherence at the `derive_voxel_column` level would
/// require knowing the post-warp cluster-cell boundaries, making the test
/// fragile. Testing `derive_cover` directly with positions that are explicitly
/// within the same cluster cell is the clean contract-level test.
#[test]
fn cover_cluster_coherence_not_per_tile() {
// 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 district = cover_district(MorphologyZone::Lake, -3.0, 60);
let col = make_column(Water::Shallow);
let world_seed: u64 = 42;
let body_id = "coherence_body";
let mut cluster_agreements = 0;
let mut cluster_total = 0;
// Walk a grid of cluster cells. For each cell, sample 4 voxels that are
// explicitly inside that cell: offsets (0,0), (1,0), (0,1), (CLUSTER_M-1, CLUSTER_M-1).
// All 4 must produce identical cover — same cluster-cell → same SeedChain hash.
for cx in -10..10i32 {
for cy in -10..10i32 {
let base_x = cx * CLUSTER_M;
let base_y = cy * CLUSTER_M;
let offsets = [(0, 0), (1, 0), (0, 1), (CLUSTER_M - 1, CLUSTER_M - 1)];
let covers: Vec<SeasonalCover> = offsets
.iter()
.map(|&(dx, dy)| {
derive_cover(
world_seed,
body_id,
&district,
&col,
(base_x + dx, base_y + dy),
)
})
.collect();
// All 4 must agree.
if covers.windows(2).all(|w| w[0] == w[1]) {
cluster_agreements += 1;
}
cluster_total += 1;
}
}
assert_eq!(
cluster_agreements,
cluster_total,
"ALL voxels within the same cluster cell must agree (same cluster-cell hash); \
{}/{} cells disagreed — this means cluster_scatter is NOT keyed solely on the \
cluster-cell coordinates (salt-and-pepper bug)",
cluster_total - cluster_agreements,
cluster_total
);
// 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<SeasonalCover> = (-25..25i32)
.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);
assert!(
has_ice && has_none,
"cover must vary across cluster cells (both Ice and None expected in scatter \
band −3°C); got has_ice={has_ice} has_none={has_none} — hash may be degenerate"
);
}
// ── Determinism ─────────────────────────────────────────────────────────
#[test]
fn cover_derivation_is_deterministic() {
// Same inputs → same cover, always.
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", &district, &col, pos);
let b = derive_cover(42, "test_body", &district, &col, pos);
assert_eq!(
a,
b,
"derive_cover must be deterministic at pos ({},{})",
i * CLUSTER_M,
i * 3
);
}
}
#[test]
fn cover_end_to_end_via_derive_voxel_column_is_deterministic() {
// Cover must be deterministic through the full derive_voxel_column path.
let district = cover_district(MorphologyZone::AlluvialPlain, -12.0, 60);
let chunk = derive_chunk_context(42, "cold_body", &district, (0, 0), None);
for (tx, ty) in [(0, 0), (50, 100), (-20, 30), (200, -10)] {
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})"
);
}
}
}