Files
settled-reach/server/src/atlas/chunk_context.rs
T
jpmschweitzerandClaude Opus 4.8 0263b68df9 feat(simulation): cache TerrainAnalysis + derive basin_direction from the D8 thalweg (T-1044, T-1047)
T-1044: run_layer1 now returns TerrainAnalysis (carried transiently on
CascadeSnapshot, dropped after the district + road-graph passes), eliminating the
redundant per-body drainage::analyze + TerrainAnalysis::analyze re-run flagged by
PERF/TODO(T-1044). Not persisted on the LRU-cached state (D-203/T-1048 size concern).

T-1047: basin_direction is now derived from the real D8 thalweg. run_layer1
aggregates a per-district dominant D8 direction from the live fdir grid (carried
transiently on DrainageResult), threaded via Layer1Output.district_basin_dirs ->
derive_all_districts -> DistrictProfile.basin_direction; derive_chunk_context reads
it directly. Removed the false derive_basin_direction (it branched on ocean_fraction_q
then read seed bits despite a doc comment claiming an elev_q/slope_q D8 proxy) +
corrected the module contract. D-239 §8 (D8 thalweg) now actually honoured.

1559 tests pass; golden byte-identical (district_basin_dirs is #[serde(skip)], transient).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:46:42 +02:00

699 lines
34 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.
//! ChunkContext — 64 m carrier of the D-239 three-tier refinement chain (T-1028).
//!
//! `ChunkContext` is the middle tier: coarser than a voxel (1 m), finer than a
//! district (2 km, D-243). It is derived purely from the covering `DistrictProfile`(s) and
//! the world seed — no stored state, no side effects.
//!
//! ## Scale contract (D-239 §10)
//!
//! A chunk is 64 m × 64 m. The D8 drainage direction grid is ~152 m/cell —
//! **coarser than a chunk** — so there is no per-tile `flow_direction[64×64]`
//! here. Instead the chunk carries:
//! - **`basin_direction`** — a cardinal basin-flow direction (N/E/S/W, 4-way)
//! sourced directly from `DistrictProfile.basin_direction`, which holds the
//! **true D8-computed dominant thalweg direction** aggregated in `run_layer1`
//! (T-1047, D-239 §8). Prior to T-1047 this was incorrectly derived from
//! seed bits; the false derivation (`derive_basin_direction`) has been removed.
//! - **`meander_phase`** and **`meander_wavelength_m`** — global meander-curve
//! params for the MeanderReach and AlluvialPlain families.
//!
//! Features with wavelength > 64 m seed from **District-or-higher** (the covering
//! `DistrictProfile`), not from the chunk-local seed. This is enforced by structure:
//! the chunk seed is only used for sub-chunk (<64 m) scatter.
//!
//! The same rule places the feature axes themselves (T-1040/T-1041): the channel
//! centreline and landform axis (`channel_anchor_m`) and the coast-face line
//! (`coast_anchor_m`) are **world-metre coordinates derived once per district**
//! from the district-scale seed. Voxel generators measure distance to these
//! anchors in continuous world coordinates — never from the world origin and
//! never folded into the 64 m chunk frame.
//!
//! ## Cross-district blending (T-1042, D-239 §4/§7)
//!
//! Chunks within one chunk (64 m) of a district boundary carry a `secondary`
//! `DistrictProfile` and a `blend_weight` (255 = 100% primary; 128 = 50-50 blend).
//! The blend applies to **continuous positional params only**: `meander_wavelength_m`,
//! `channel_width_m`, `meander_phase` (all pre-blended here on the context) and
//! `elev_q` / `moisture_q` (blended in `derive_voxel_column` before family dispatch).
//!
//! **Morphology family seams stay sharp** (D-239 §7): `secondary` is carried for
//! elevation/moisture blending only; the primary district's `morphology_zone` is
//! never overridden by the secondary.
//!
//! ## D-010 compliance
//!
//! All derivation is integer arithmetic. The only f64 in this module is
//! `meander_wavelength_m` (positional physics, D-239 §4). Structural decisions
//! consume it only via deterministic i32 truncation (the `has_active_channel`
//! band reach), mirroring the established voxel.rs truncate-before-decision
//! convention — only correctly-rounded f64 +,-,*,/ feed the truncation.
//!
//! ## SeedDomain extension
//!
//! Uses `SeedDomain::ChunkContext = 8` (appended, never renumber).
use serde::{Deserialize, Serialize};
use crate::atlas::district_profile::DistrictProfile;
use crate::atlas::scale;
use crate::seed::{SeedChain, SeedDomain};
// Re-export so existing consumers (voxel.rs etc.) keep their `chunk_context::BasinDirection` path.
// The canonical definition lives in `scale.rs` (T-1047) — `BasinDirection` is shared
// between `DistrictProfile` (district_profile.rs) and `ChunkContext` (this module),
// and placing it in the scale ladder breaks the potential circular import.
pub use crate::atlas::scale::BasinDirection;
// ---------------------------------------------------------------------------
// ChunkPos — position on the 64 m chunk grid
// ---------------------------------------------------------------------------
/// Chunk grid position + the chunk edge metres come from the canonical ladder
/// ([`crate::atlas::scale`], D-243) — chunk_context no longer defines its own
/// scale (the Q-110 failure mode). The covering district is `scale::DISTRICT_M`
/// = 2 048 m = `scale::CHUNKS_PER_DISTRICT` (32) chunks; the district-index
/// mapping is `chunk >> scale::CHUNK_DISTRICT_SHIFT`.
pub use crate::atlas::scale::{ChunkPos, CHUNK_M};
/// Margin keeping a district's feature anchor away from the district edge, so the
/// channel's full swept band (max meander amplitude wavelength/4 ≈ 162 m +
/// channel edge + levee band + warp bound ≈ 187 m) stays inside the district.
/// Cross-district feature continuity is the stage-2 Voronoi model (T-1040).
const ANCHOR_MARGIN_M: i32 = 192;
/// Seed-addressable anchor span within a district (`scale::DISTRICT_M` 2 × margin).
const ANCHOR_SPAN_M: i32 = scale::DISTRICT_M - 2 * ANCHOR_MARGIN_M;
/// Maximum levee band width in metres (`voxel::in_levee_band`: 4 + 3 jitter).
const LEVEE_BAND_MAX_M: i32 = 7;
/// Domain-warp displacement bound in metres (mirrors `domain_warp::WARP_BOUND`,
/// D-239 §4 ±8 m). Integer here — used only to widen the channel gate band.
const WARP_BOUND_M: i32 = 8;
// ---------------------------------------------------------------------------
// ChunkContext
// ---------------------------------------------------------------------------
/// 64 m carrier derived from `DistrictProfile`(s) — the second tier of D-239 §1.
///
/// Pure deterministic function of `(seed, body_id, district, chunk_pos)`.
/// Never stored; derived on demand and cached (D-227).
///
/// ## Fields
///
/// - `basin_direction` — dominant drainage direction (cardinal) for this chunk.
/// - `meander_phase` — integer phase offset (0255) for the meander curve.
/// Used by MeanderReach and AlluvialPlain voxel generators to place the channel.
/// When `blend_weight < 255`, this is already blended between the primary and
/// secondary district values (T-1042).
/// - `meander_wavelength_m` — meander wavelength in metres. Derived from
/// district-level morphology (slope, moisture), seeded at district scale (> 64 m).
/// f64 for positional physics (D-239 §4); structural decisions consume it
/// only via deterministic i32 truncation (the `has_active_channel` band).
/// When `blend_weight < 255`, this is already blended (T-1042).
/// - `has_active_channel` — whether a water channel is present in this chunk:
/// the district has water presence AND the channel's swept band around
/// `channel_anchor_m` crosses this chunk (T-1040).
/// - `channel_width_m` — channel width in metres (integer; D-010). 0 if no
/// active channel. When `blend_weight < 255`, this is already blended (T-1042).
/// - `channel_anchor_m` / `coast_anchor_m` — district-anchored feature axes in
/// world metres (T-1040/T-1041, D-239 §10).
/// - `secondary` — adjacent district profile for cross-district blending (T-1042,
/// D-239 §4). `None` when the chunk is interior (≥ 1 chunk from any district edge).
/// Only continuous terrain params (`elev_q`, `moisture_q`) are blended from this
/// in `derive_voxel_column`; morphology family stays primary (D-239 §7).
/// - `blend_weight` — blend weight toward the primary district. 255 = fully primary
/// (no blend), 128 = 50-50 blend. Meaningful only when `secondary` is `Some`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkContext {
/// Dominant basin-flow direction for this chunk's drainage catchment.
pub basin_direction: BasinDirection,
/// Integer phase offset for the meander curve (0255).
/// Derived at district scale (wavelength > 64 m), NOT from the chunk seed.
/// Pre-blended between primary and secondary when `blend_weight < 255` (T-1042).
pub meander_phase: u8,
/// Meander wavelength in metres. Positional physics value (f64 — D-239 §4).
/// Derived at district scale; typically 80500 m for AlluvialPlain.
/// Pre-blended between primary and secondary when `blend_weight < 255` (T-1042).
pub meander_wavelength_m: f64,
/// Whether this chunk contains an active water channel.
/// True when the district has water presence (`ocean_fraction_q` ≥ 10) AND
/// the channel's swept band around `channel_anchor_m` crosses this chunk's
/// cross-axis range (T-1040 — channels exist where the district says, not
/// district-wide and not only at the world origin).
pub has_active_channel: bool,
/// Active channel width in metres (integer; D-010). 0 if no active channel.
/// Derived from district morphology and slope.
/// Pre-blended between primary and secondary when `blend_weight < 255` (T-1042).
pub channel_width_m: i32,
/// Cross-axis world-metre coordinate of the district's feature centreline:
/// channel/meander axis, fjord trough, gorge floor, braid-fan axis.
/// Derived once per district from the district-scale seed (T-1040/T-1041,
/// D-239 §10) — constant across all chunks of a district, so the feature is
/// continuous across chunk boundaries. Cross axis = x for N/S basins,
/// y for E/W basins.
pub channel_anchor_m: i32,
/// Along-axis (basin-axis) world-metre coordinate of the CliffCoast face
/// line. District-scale (T-1041): one continuous coast per district, not a
/// 64 m sawtooth. Along axis = y for N/S basins, x for E/W basins.
pub coast_anchor_m: i32,
/// Adjacent-district profile for cross-district terrain blending (T-1042,
/// D-239 §4/§7). `None` for interior chunks (≥ 1 chunk from any district edge).
///
/// Only continuous terrain params (`elev_q`, `moisture_q`) are blended from
/// this in `derive_voxel_column`. Morphology family selection always uses the
/// **primary** district's zone — seams stay sharp (D-239 §7).
/// `meander_wavelength_m`, `channel_width_m`, `meander_phase` are blended
/// here at context derivation time.
pub secondary: Option<DistrictProfile>,
/// Blend weight toward the primary district (D-010 integer arithmetic).
///
/// - `255` — fully primary; `secondary` is ignored (no blend).
/// - `128` — 50-50 blend (chunk at the district boundary).
///
/// Meaningful only when `secondary` is `Some`. Formula used throughout:
/// `blended = (primary * blend_weight as i32 + secondary * (255 - blend_weight) as i32 + 127) / 255`
/// (rounded integer lerp, symmetric for 128).
pub blend_weight: u8,
}
// ---------------------------------------------------------------------------
// Derivation
// ---------------------------------------------------------------------------
/// Derive a `ChunkContext` for the chunk at `chunk_pos` on the 64 m grid.
///
/// Pure function of `(seed, body_id, district, chunk_pos, secondary)`.
///
/// `secondary` supplies the adjacent `DistrictProfile` and a `blend_weight`
/// (255 = fully primary, 128 = 50-50) for cross-district terrain blending
/// (T-1042, D-239 §4/§7). Pass `None` for interior chunks. The caller is
/// responsible for detecting whether the chunk lies within one chunk (64 m)
/// of a district boundary and supplying the adjacent profile.
///
/// When `secondary` is `Some`, the context-level continuous params
/// (`meander_wavelength_m`, `channel_width_m`, `meander_phase`) are blended
/// here using integer arithmetic (D-010). The voxel-level params (`elev_q`,
/// `moisture_q`) are blended in `derive_voxel_column` before family dispatch.
/// Morphology family selection always uses the **primary** district's zone —
/// seams stay sharp (D-239 §7).
///
/// ## Seed usage
///
/// The **meander phase** is seeded at district scale so features with
/// wavelength > 64 m are consistent across chunk boundaries. The chunk-local
/// seed (keyed on `chunk_pos`) is reserved for sub-chunk scatter in the voxel
/// pass — not consumed here.
///
/// ## Basin direction
///
/// `basin_direction` is read directly from `district.basin_direction` — the
/// **true D8-computed dominant thalweg direction** threaded from `run_layer1`
/// (T-1047, D-239 §8). No seed-bit proxy is used here. The former
/// `derive_basin_direction` helper (which branched on `ocean_fraction_q` and
/// seed bits, not on the actual D8 grid) has been removed.
pub fn derive_chunk_context(
world_seed: u64,
body_id: &str,
district: &DistrictProfile,
chunk_pos: ChunkPos,
secondary: Option<(&DistrictProfile, u8)>,
) -> ChunkContext {
// District-scale seed — features with wavelength > 64 m derive from here.
// Keyed on the chunk position mapped to district-scale units: `>>
// scale::CHUNK_DISTRICT_SHIFT` gives the 2 km district index (32 chunks).
// MUST match the same shift in `derive_district_anchor` so the seed-district
// and the anchor-district are the same cell (D-243; the canonical ladder).
let district_scale_id = pos_to_id((
chunk_pos.0 >> scale::CHUNK_DISTRICT_SHIFT,
chunk_pos.1 >> scale::CHUNK_DISTRICT_SHIFT,
));
let district_seed = SeedChain::for_body(world_seed, body_id)
.derive(SeedDomain::ChunkContext, district_scale_id);
// Basin direction — read directly from DistrictProfile (T-1047, D-239 §8).
// This is the true D8-computed dominant thalweg direction aggregated in
// run_layer1 from the fdir grid; it replaces the former seed-bit proxy.
let basin_direction = district.basin_direction;
// Meander phase — district-scale integer offset so the channel is consistent
// across all chunks in the same district. 0255.
let primary_meander_phase = (district_seed.seed() >> 8) as u8;
// Meander wavelength — derived from slope and morphology, district-scale.
// Lower slope → longer wavelength (wider meanders); integer inputs, f64 result
// is positional physics (D-239 §4, not a gate comparison).
let primary_wavelength_m = derive_meander_wavelength(district);
// Channel width from primary district — integer metres (D-010).
let primary_channel_width = derive_channel_width(district);
// District-anchored feature axes (T-1040/T-1041, D-239 §10): channel and
// landform centrelines have wavelength > 64 m, so their position derives
// from the district-scale seed — never the chunk frame or the world origin.
// Cross axis ⊥ basin_direction (x for N/S, y for E/W); along axis ∥ basin.
let (cross_chunk, along_chunk) = match basin_direction {
BasinDirection::North | BasinDirection::South => (chunk_pos.0, chunk_pos.1),
BasinDirection::East | BasinDirection::West => (chunk_pos.1, chunk_pos.0),
};
let channel_anchor_m =
derive_district_anchor(cross_chunk, (district_seed.seed() >> 16) & 0xFFFF);
let coast_anchor_m = derive_district_anchor(along_chunk, (district_seed.seed() >> 32) & 0xFFFF);
// ── Cross-district blending of context-level continuous params (T-1042) ──
//
// When a secondary district is supplied, blend `meander_wavelength_m`,
// `channel_width_m`, and `meander_phase` between the primary and secondary
// district values. These are the context-level continuous positional params
// (D-239 §4; spec: "blend them on the context, do NOT re-read from the
// profile in the blend path"). All integer arithmetic (D-010).
//
// Morphology family, basin direction, feature anchors, and has_active_channel
// are NOT blended — they are structural decisions driven by the primary
// district only (D-239 §7: morphology seams stay sharp).
let (blend_weight, secondary_stored) = match secondary {
Some((sec, weight)) => {
let sec_wavelength = derive_meander_wavelength(sec);
let sec_channel_width = derive_channel_width(sec);
let sec_phase = {
// The secondary district's true meander phase would require its
// district-scale seed, which in turn requires knowing the adjacent
// district's chunk coordinates — information not available at this
// call site. We use a bounded structural approximation: a hash of
// the secondary's morphology scalars (slope_q, moisture_q), which
// are the same inputs that drive `derive_meander_wavelength` and
// therefore capture the secondary district's channel character.
//
// This approximation is intentionally asymmetric in an acknowledged
// way: the blended phase is a cosmetic continuity aid at the 2 km
// seam (meander phase), not a structural gate decision. The seam
// is already invisible at the elevation level from elev_q blending;
// the phase contribution is second-order. If the real secondary seed
// is ever threaded through here, replace this with the proper
// district-scale derivation. Integer arithmetic (D-010).
sec.slope_q.wrapping_add(sec.moisture_q) as u8
};
let w = weight as i32;
let w_sec = 255 - w;
// Integer lerp: `(a * w + b * w_sec + 127) / 255`.
// The +127 biases the division round to nearest (symmetric at w=128).
let blended_phase =
((primary_meander_phase as i32 * w + sec_phase as i32 * w_sec + 127) / 255) as u8;
let blended_wavelength =
(primary_wavelength_m * w as f64 + sec_wavelength * w_sec as f64) / 255.0;
let blended_channel_width =
(primary_channel_width * w + sec_channel_width * w_sec + 127) / 255;
(
weight,
Some((
blended_phase,
blended_wavelength,
blended_channel_width,
sec.clone(),
)),
)
}
None => (255u8, None),
};
let (meander_phase, meander_wavelength_m, primary_channel_width_final, secondary_profile) =
match secondary_stored {
Some((ph, wl, cw, prof)) => (ph, wl, cw, Some(prof)),
None => (
primary_meander_phase,
primary_wavelength_m,
primary_channel_width,
None,
),
};
// Active channel — water presence (ocean_fraction_q >= 10 indicates a
// perennial waterway or water body covers at least 10% of the district) AND
// the channel's swept band around the district anchor crosses this chunk
// (T-1040). The river_threshold governs D8 drainage accumulation at a finer
// scale; at the chunk level ocean_fraction_q is the direct proxy for water
// presence (D-239 §10: one basin-direction, not a per-tile flow grid).
//
// The band is generous (it must cover every chunk that can contain channel,
// levee, or warped-channel voxels — a gate-off chunk renders dry), using
// the larger MeanderReach amplitude (wavelength/4) for both channel families.
let has_active_channel = district.ocean_fraction_q >= 10 && {
let wavelength_i = (meander_wavelength_m as i32).max(10);
let amplitude_max = (wavelength_i / 4).max(3);
let edge_max = (primary_channel_width_final / 2).max(2) + 3; // half-width + max edge jitter
// Floor: the BraidedDelta belt reaches anchor ±(32 thread-centre + 4
// thread-half) before warp regardless of wavelength — the band must
// cover it even at the short-wavelength extreme (costs ≤4 m of extra
// gate generosity for the other families).
let reach = (amplitude_max + edge_max + LEVEE_BAND_MAX_M).max(32 + 4) + WARP_BOUND_M;
let cross_lo = cross_chunk * CHUNK_M;
let cross_hi = cross_lo + CHUNK_M - 1;
cross_lo <= channel_anchor_m + reach && cross_hi >= channel_anchor_m - reach
};
// Channel width — integer metres; 0 when no active channel in this chunk.
let channel_width_m = if has_active_channel {
primary_channel_width_final
} else {
0
};
ChunkContext {
basin_direction,
meander_phase,
meander_wavelength_m,
has_active_channel,
channel_width_m,
channel_anchor_m,
coast_anchor_m,
secondary: secondary_profile,
blend_weight,
}
}
/// Compute the cross-district blend weight for a chunk position (T-1042).
///
/// Returns `(is_near_boundary, blend_weight)` for the given chunk's district
/// proximity on either axis. `blend_weight` is 255 when interior (no blend),
/// or 128 when the chunk is the outermost within its district on either axis
/// (one chunk from the district boundary). The `is_near_boundary` flag is `true`
/// only when `blend_weight < 255`.
///
/// The caller uses `is_near_boundary` to decide whether to look up the adjacent
/// `DistrictProfile` and supply it to `derive_chunk_context`. Only the last chunk
/// of a district (chunk index `CHUNKS_PER_DISTRICT - 1` = 31 within the district)
/// triggers a blend; the first chunk of the next district does not — this way the
/// blend seam is always on the outgoing side, and the incoming district's first
/// chunk reads clean from its own primary profile.
///
/// Integer arithmetic (D-010).
pub fn district_boundary_blend_weight(chunk_pos: ChunkPos) -> (bool, u8) {
// Chunk index within its district on each axis (0..32).
let cx = chunk_pos.0.rem_euclid(scale::CHUNKS_PER_DISTRICT);
let cy = chunk_pos.1.rem_euclid(scale::CHUNKS_PER_DISTRICT);
// The last chunk (index 31) is within 64 m of the district boundary.
let near_x = cx == scale::CHUNKS_PER_DISTRICT - 1;
let near_y = cy == scale::CHUNKS_PER_DISTRICT - 1;
if near_x || near_y {
(true, 128u8)
} else {
(false, 255u8)
}
}
/// World-metre anchor coordinate for a district-scale feature axis on one axis.
///
/// The anchor sits in `[district_origin + ANCHOR_MARGIN_M, district_origin +
/// scale::DISTRICT_M ANCHOR_MARGIN_M)` so the feature's full swept band stays
/// inside its district (no cross-district band spill; district-seam continuity is
/// the stage-2 Voronoi model, T-1040). Same value for every chunk of the district:
/// the district index is `axis_chunk >> scale::CHUNK_DISTRICT_SHIFT` (arithmetic
/// shift = floor division, correct for negative chunks) and `seed_bits` comes from
/// the shared district-scale seed. Integer arithmetic (D-010).
fn derive_district_anchor(axis_chunk: i32, seed_bits: u64) -> i32 {
let district_idx = axis_chunk >> scale::CHUNK_DISTRICT_SHIFT;
let origin_m = district_idx * scale::DISTRICT_M;
origin_m + ANCHOR_MARGIN_M + (seed_bits % ANCHOR_SPAN_M as u64) as i32
}
/// Fold `(x, y)` chunk coordinates into a single u64 id for seed derivation.
///
/// Mirrors `domain_warp::pos_to_id` — zigzag-encode + Cantor pairing.
/// Integer-only (D-010).
#[inline]
pub(crate) fn pos_to_id(pos: (i32, i32)) -> 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)
}
/// Derive meander wavelength in metres from district morphology.
///
/// Low slope + high moisture → longer wavelength (wide meanders).
/// High slope → short wavelength (confined/straight channels).
/// Result is f64 positional physics (D-239 §4), not used in gate comparisons.
fn derive_meander_wavelength(district: &DistrictProfile) -> f64 {
// Base wavelength range: 80500 m for AlluvialPlain.
// slope_q 0 → 500 m; slope_q 100 → 80 m. Linear interpolation.
let slope_clamped = district.slope_q.clamp(0, 100) as f64;
let base = 500.0 - (slope_clamped / 100.0) * 420.0;
// Moisture boost: high moisture → slightly longer wavelength (more sinuous).
let moisture_factor = 1.0 + (district.moisture_q.clamp(0, 100) as f64 / 100.0) * 0.3;
base * moisture_factor
}
/// Derive active channel width in metres from district morphology.
///
/// Returns an integer metre value (D-010).
fn derive_channel_width(district: &DistrictProfile) -> i32 {
// Water presence drives width; ocean_fraction_q is our proxy.
// Meander channels: 315 m (D-239 §9 game-feel constraint).
// We derive in that range from ocean_fraction_q.
let base = match district.ocean_fraction_q {
0..=9 => 3,
10..=19 => 5,
20..=34 => 8,
35..=49 => 10,
_ => 15,
};
// Slope modifier: high slope → narrower (gorge-like); low slope → wider.
let slope_penalty = (district.slope_q / 20).min(3);
(base - slope_penalty).max(3)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::atlas::district_profile::{
GlaciationGrade, PrecipitationClass, TectonicClass, VegetationClass,
};
use crate::simulation::generator::MorphologyZone;
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,
}
}
/// The chunk of district (0, 0) whose cross-range contains the district's
/// channel anchor — guaranteed inside the T-1040 channel band.
fn anchor_chunk_pos(world_seed: u64, body_id: &str, district: &DistrictProfile) -> ChunkPos {
let probe = derive_chunk_context(world_seed, body_id, district, (0, 0), None);
let idx = probe.channel_anchor_m.div_euclid(CHUNK_M);
match probe.basin_direction {
BasinDirection::North | BasinDirection::South => (idx, 0),
BasinDirection::East | BasinDirection::West => (0, idx),
}
}
#[test]
fn derive_chunk_context_is_deterministic() {
let district = alluvial_district();
let a = derive_chunk_context(42, "GJ1c", &district, (10, 20), None);
let b = derive_chunk_context(42, "GJ1c", &district, (10, 20), None);
assert_eq!(a.basin_direction, b.basin_direction);
assert_eq!(a.meander_phase, b.meander_phase);
assert_eq!(a.meander_wavelength_m, b.meander_wavelength_m);
assert_eq!(a.has_active_channel, b.has_active_channel);
assert_eq!(a.channel_width_m, b.channel_width_m);
}
#[test]
fn different_positions_yield_different_phases() {
let district = alluvial_district();
let a = derive_chunk_context(42, "GJ1c", &district, (0, 0), None);
let b = derive_chunk_context(42, "GJ1c", &district, (200, 100), None);
// Different district-scale ids → different phases (high probability).
assert_ne!(
a.meander_phase, b.meander_phase,
"chunks far apart should differ in meander phase"
);
}
#[test]
fn alluvial_district_has_active_channel() {
let district = alluvial_district();
// T-1040: the channel is district-anchored — the chunk under the anchor
// must claim it (ocean_fraction_q=15 → water present).
let pos = anchor_chunk_pos(42, "GJ1c", &district);
let ctx = derive_chunk_context(42, "GJ1c", &district, pos, None);
assert!(
ctx.has_active_channel,
"anchor-covering chunk of a watered district must have active channel"
);
}
#[test]
fn chunk_outside_channel_band_has_no_active_channel() {
// T-1040: channels are district-anchored, not district-wide. A chunk whose
// cross-range lies outside the channel's swept band (max reach < 192 m
// = 3 chunks) must not claim a channel — pre-fix every chunk of a
// watered district did, while voxels rendered dry floodplain.
let district = alluvial_district();
let (anchor_pos, probe) = {
let pos = anchor_chunk_pos(42, "GJ1c", &district);
(pos, derive_chunk_context(42, "GJ1c", &district, pos, None))
};
// 8 cross-chunks away (512 m) is past any band reach but still inside
// district (0, 0) — the anchor margin keeps the anchor chunk in [3, 12].
let anchor_idx = anchor_pos.0.max(anchor_pos.1);
let far_idx = if anchor_idx < 8 {
anchor_idx + 8
} else {
anchor_idx - 8
};
let far_pos = match probe.basin_direction {
BasinDirection::North | BasinDirection::South => (far_idx, 0),
BasinDirection::East | BasinDirection::West => (0, far_idx),
};
let far_ctx = derive_chunk_context(42, "GJ1c", &district, far_pos, None);
assert!(
!far_ctx.has_active_channel,
"chunk {far_pos:?} outside the channel band must not claim a channel"
);
assert_eq!(
far_ctx.channel_width_m, 0,
"no active channel → channel_width_m must be 0"
);
// District-scale params stay constant across the district's chunks.
assert_eq!(far_ctx.channel_anchor_m, probe.channel_anchor_m);
assert_eq!(far_ctx.meander_phase, probe.meander_phase);
}
#[test]
fn anchors_constant_within_region_and_inside_it() {
// T-1040/T-1041: feature anchors are a district property — identical for
// every chunk of the district, and positioned inside the district's extent.
let district = alluvial_district();
let base = derive_chunk_context(42, "GJ1c", &district, (0, 0), None);
for pos in [(1, 0), (0, 1), (15, 15), (7, 12)] {
let ctx = derive_chunk_context(42, "GJ1c", &district, pos, None);
assert_eq!(
ctx.channel_anchor_m, base.channel_anchor_m,
"channel anchor must be district-constant (chunk {pos:?})"
);
assert_eq!(
ctx.coast_anchor_m, base.coast_anchor_m,
"coast anchor must be district-constant (chunk {pos:?})"
);
}
// District (0, 0) spans [0, scale::DISTRICT_M) m on both axes.
assert!((0..scale::DISTRICT_M).contains(&base.channel_anchor_m));
assert!((0..scale::DISTRICT_M).contains(&base.coast_anchor_m));
// A different district derives its anchors inside its own extent. Chunk
// (1000, -750) → district (1000 >> 5, -750 >> 5) = (31, -24); the cross
// axis (and thus which district index the channel anchor sits in) depends
// on the basin direction.
let far = derive_chunk_context(42, "GJ1c", &district, (1000, -750), None);
let dm = scale::DISTRICT_M;
assert!(
(31 * dm..32 * dm).contains(&far.channel_anchor_m)
|| (-24 * dm..-23 * dm).contains(&far.channel_anchor_m),
"far district anchor {} must lie inside its district extent (cross axis depends on basin direction)",
far.channel_anchor_m
);
}
#[test]
fn dry_region_has_no_active_channel() {
let district = DistrictProfile {
morphology_zone: MorphologyZone::AlluvialPlain,
tectonic_class: TectonicClass::Stable,
glaciation_grade: GlaciationGrade::None,
precipitation_class: PrecipitationClass::Arid,
slope_q: 3,
elev_q: 10,
ocean_fraction_q: 0, // no water at all
river_threshold: 300,
temperature_c: Some(35.0),
moisture_q: 5,
vegetation_class: VegetationClass::Barren,
basin_direction: BasinDirection::North,
};
let ctx = derive_chunk_context(42, "dry_body", &district, (5, 5), None);
assert!(
!ctx.has_active_channel,
"arid district with ocean_fraction_q=0 must not have active channel"
);
}
#[test]
fn channel_width_in_game_feel_range() {
// D-239 §9: river crossings 315 m. Measured on a chunk that carries
// the channel (T-1040 gating zeroes the width elsewhere).
let district = alluvial_district();
let pos = anchor_chunk_pos(42, "GJ1c", &district);
let ctx = derive_chunk_context(42, "GJ1c", &district, pos, None);
assert!(
(3..=15).contains(&ctx.channel_width_m),
"channel_width_m {} out of game-feel range [3, 15]",
ctx.channel_width_m
);
}
#[test]
fn meander_wavelength_within_physics_range() {
let district = alluvial_district();
let ctx = derive_chunk_context(42, "GJ1c", &district, (5, 5), None);
// AlluvialPlain flat (slope_q=5): should be near max wavelength.
assert!(
ctx.meander_wavelength_m > 400.0 && ctx.meander_wavelength_m < 700.0,
"meander wavelength {} out of expected range for flat alluvial",
ctx.meander_wavelength_m
);
}
#[test]
fn basin_direction_discriminants_pinned() {
// Append-only invariant (D-010).
assert_eq!(BasinDirection::North as u8, 0);
assert_eq!(BasinDirection::East as u8, 1);
assert_eq!(BasinDirection::South as u8, 2);
assert_eq!(BasinDirection::West as u8, 3);
}
#[test]
fn pos_to_id_is_injective_for_small_coords() {
// Spot-check that nearby positions produce distinct ids.
let ids: Vec<u64> = [(0, 0), (1, 0), (0, 1), (1, 1), (-1, 0), (0, -1), (-1, -1)]
.iter()
.map(|&p| pos_to_id(p))
.collect();
let unique: std::collections::BTreeSet<u64> = ids.iter().copied().collect();
assert_eq!(ids.len(), unique.len(), "pos_to_id must be injective");
}
}