feat(simulation): WaterBody voxel family — oceans/lakes/tidal flats render wet (T-1082)
The water zones (OpenOcean/Lake/TidalFlat) fell through to the dry-land AlluvialPlain fallback, so the sea rendered as dry forested land (the D-245 believability bug). - voxel.rs: new MorphologyFamily::WaterBody (9th family) + generate_water_body, dispatched from zone_to_family. Deep open water with a Shallow shoal band on the district-anchored coast line (wider for shallower bodies); TidalFlat all-Shallow. Seabed Rock (steep) / Sand (gentle) / Wetland (tidal mud); Vegetation::Barren; water surface elevation_m = 0; seasonal Ice via derive_cover (frozen seas). Wetland stays AlluvialPlain (it is land — a marsh, not open water). Integer/D-010. + 4 tests. - believability.rs: fix a sampling bias in the T-1083 enforcer found while verifying this — the metrics sampled the first 64 districts in BTreeMap order (a spatial corner, e.g. all-ocean), which reported a forested world as "0 vegetated". Now a deterministic seeded spread across the body (matches D-245's "randomly-sampled locations"). - D-239 amended (§5 8→9 families; §6 refined — the 3 water zones now dispatch to WaterBody). Believability golden regenerated. Probe (Arbour @ yolo): water-renders-wet 4/64 FAIL → 42/44 PASS; terrain-material variety 1 → 4 PASS; 3/7 → 5/7 criteria. (Remaining fails — moisture gradient, vegetation-present — are T-1080.) clippy --all-targets -D warnings clean; 1579 lib tests + the harness pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2702,3 +2702,18 @@ BASELINE @ believability-v1: Arbour 3/7, Edict 3/7. FAIL: moisture gradient (dis
|
||||
DEFERRED (ticket item 3 — explicitly optional): per-layer PNG maps (the T-960 Atlas-viewer stopgap). The contrast+coherence metrics are the core enforcer; maps are a follow-up if the human-gate review wants them before T-960.
|
||||
|
||||
This is the D-245 enforcer; advisory now, ratchets to strict (BELIEVABILITY_STRICT) once Q-123 calibrates thresholds and T-1080/1081/1082 flip the criteria.', NULL, '2026-06-28 12:56:03', '2026-06-28 12:56:03.498', '2026-06-28 12:56:03.498', NULL, '73ed53bc02a79f3112f30dea16a82dd3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 13:00:26', '2026-06-28 13:00:26.933', '2026-06-28 13:00:26.933', NULL, 'e840ecc301d33695e4866a7acdace1ce', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'status', 'in_progress', 'in_progress', NULL, '2026-06-28 13:01:05', '2026-06-28 13:01:05.826', '2026-06-28 13:01:05.826', NULL, '2db838e8ab9ce3cf6385ea00f484e382', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'description', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it.', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it.
|
||||
|
||||
---
|
||||
Refinement (Si pass + user ruling, 2026-06-28) — resolves the ''direction to discuss'':
|
||||
|
||||
- FAMILY: dedicated WaterBody family (9th D-239 family) for OpenOcean/Lake/TidalFlat, branching internally on morphology_zone (like IncisedGorge covers MountainPass/ValleyFloor/Alpine). Amend D-239 §5/§6 (8→9 families) + zone_to_family.
|
||||
- FIDELITY: RICHER — a Shallow fringe along the district-anchored shoreline (ChunkContext.coast_anchor_m, as CliffCoast uses) + Deep toward the interior; TidalFlat = all Shallow (intertidal).
|
||||
- SUBSTRATE: reuse existing TerrainMaterial (no new variant). Rock seabed/shore where slope_q is steep (Rock IS in the enum — rocky available both above and below water, per user), Sand where gentle; TidalFlat = Wetland (mud). Vegetation = Barren in all water voxels (fixes ''Forest on the sea'').
|
||||
- SURFACE: water surface elevation_m = 0 (the sea-level convention the existing coastal families already use — raw heightmap sea_level does NOT reach the voxel tier; only ocean_fraction_q/elev_q do). Depth is conveyed by the Water axis (Deep/Shallow), not by elevation_m.
|
||||
- COVER: reuse derive_cover so cold-body water gets Ice (frozen seas on Edict) — important for the frozen-world believability case.
|
||||
- SCOPE: Wetland zone STAYS AlluvialPlain (it is land — a marsh, not open water; only the 3 named zones get WaterBody). Transient tidal wet/dry alternation deferred to Q-105.
|
||||
- DETERMINISM: integer arithmetic only (D-010); warp before the integer voxel address, as the other families.
|
||||
- VERIFICATION: the T-1083 believability harness must flip ''water renders wet'' toward green for Arbour + Edict; update the golden (UPDATE_GOLDEN) to capture the improvement.', NULL, '2026-06-28 13:10:56', '2026-06-28 13:10:56.408', '2026-06-28 13:10:56.408', NULL, 'f9737eab39f048ff84b8478ca11f5215', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -3635,3 +3635,18 @@ BASELINE @ believability-v1: Arbour 3/7, Edict 3/7. FAIL: moisture gradient (dis
|
||||
DEFERRED (ticket item 3 — explicitly optional): per-layer PNG maps (the T-960 Atlas-viewer stopgap). The contrast+coherence metrics are the core enforcer; maps are a follow-up if the human-gate review wants them before T-960.
|
||||
|
||||
This is the D-245 enforcer; advisory now, ratchets to strict (BELIEVABILITY_STRICT) once Q-123 calibrates thresholds and T-1080/1081/1082 flip the criteria.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:55:19.712', '2026-06-28 12:56:03.498', NULL, 'e39a34b2d5f662cef1b8fee88a70c8f9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'No water-body voxel generator — OpenOcean/Lake/TidalFlat render as dry land', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:58.429', '2026-06-28 13:00:26.933', NULL, '12c4956809610e71cdf625d28fc48bdb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'No water-body voxel generator — OpenOcean/Lake/TidalFlat render as dry land', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:58.429', '2026-06-28 13:01:05.826', NULL, '330a98a5774e1be834fd8fef02e2f1ff', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSJWWZNWYT3C4T828ZF1QCG', 'story', '06FGSJNP8YM537DJCC82ZD6544', 'No water-body voxel generator — OpenOcean/Lake/TidalFlat render as dry land', 'Probe evidence (Arbour @ yolo, an OCEAN world): OpenOcean districts derive water=Dry 100% with vegetation Forest 62% on Wetland substrate — i.e. the sea is painted as dry forested land. Root cause (voxel.rs zone_to_family): OpenOcean/Lake/TidalFlat/Wetland fall back to the AlluvialPlain (dry-land) generator; there is no water-body generator that lays Water::Deep/Shallow for water zones, sets a sea/lake surface elevation, or forms coastlines. On an ocean world this means the majority of the surface renders wrong. Known incompleteness (the fallback is documented), filed so it has a home. Direction to discuss: a water-body morphology family (ocean / lake / tidal-flat) that sets Deep/Shallow water + substrate + coast, and how sea level (heightmap sea_level threshold) feeds it.
|
||||
|
||||
---
|
||||
Refinement (Si pass + user ruling, 2026-06-28) — resolves the ''direction to discuss'':
|
||||
|
||||
- FAMILY: dedicated WaterBody family (9th D-239 family) for OpenOcean/Lake/TidalFlat, branching internally on morphology_zone (like IncisedGorge covers MountainPass/ValleyFloor/Alpine). Amend D-239 §5/§6 (8→9 families) + zone_to_family.
|
||||
- FIDELITY: RICHER — a Shallow fringe along the district-anchored shoreline (ChunkContext.coast_anchor_m, as CliffCoast uses) + Deep toward the interior; TidalFlat = all Shallow (intertidal).
|
||||
- SUBSTRATE: reuse existing TerrainMaterial (no new variant). Rock seabed/shore where slope_q is steep (Rock IS in the enum — rocky available both above and below water, per user), Sand where gentle; TidalFlat = Wetland (mud). Vegetation = Barren in all water voxels (fixes ''Forest on the sea'').
|
||||
- SURFACE: water surface elevation_m = 0 (the sea-level convention the existing coastal families already use — raw heightmap sea_level does NOT reach the voxel tier; only ocean_fraction_q/elev_q do). Depth is conveyed by the Water axis (Deep/Shallow), not by elevation_m.
|
||||
- COVER: reuse derive_cover so cold-body water gets Ice (frozen seas on Edict) — important for the frozen-world believability case.
|
||||
- SCOPE: Wetland zone STAYS AlluvialPlain (it is land — a marsh, not open water; only the 3 named zones get WaterBody). Transient tidal wet/dry alternation deferred to Q-105.
|
||||
- DETERMINISM: integer arithmetic only (D-010); warp before the integer voxel address, as the other families.
|
||||
- VERIFICATION: the T-1083 believability harness must flip ''water renders wet'' toward green for Arbour + Edict; update the golden (UPDATE_GOLDEN) to capture the improvement.', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-28 05:54:58.429', '2026-06-28 13:10:56.408', NULL, '8f55fc31d54598d0756308b8e6ef645c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -1776,6 +1776,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Dissent:** Tyre's initial cross-family elevation-blend was resolved against (prevent-at-source). Early-integer-truncation of the warp (raised against Gestalt's `ElevationDelta` ranges and by Tyre) was resolved against in favour of f64-to-voxel.
|
||||
- **Implementation note (T-1024, 2026-06-07):** §1 per-body `RIVER_THRESHOLD` is now a derived field (`derive_river_threshold`) on `RegionProfile`. §2 district temperature is a nullable `f32` on `RegionProfile` sampled from a per-region climate derivation (`derive_temperature_c`); moisture is a separate integer primitive (`derive_moisture_q`, 0–100). Both populate the `RegionProfile` carrier during the L4 cascade run. Climate inputs use a **hybrid strategy**: stellar luminosity and spectral class are sourced from the import pipeline (`bodies.axial_tilt_deg`, `star_systems.spectral_class`); greenhouse offsets and diurnal amplitudes are tunable at runtime via `server/data/climate_constants.toml` (source-canonical TOML, not hardcoded).
|
||||
- **Implementation note (T-1025/T-1027, 2026-06-08):** §6's frozen 17-zone `MorphologyZone` enum is realised in `server/src/simulation/generator.rs` (`repr(u8)`, discriminant-pinned). The §5 8-family gated classifier + §7 compatibility-matrix invariants live in `derive_morphology_zone` (`region_profile.rs`). **16 of the 17 zones are reachable at RegionProfile scale; `BraidedPlain` is the exception** — distinguishing it from `Delta` needs a lithology signal (§8 Gravel→braided) that `RegionProfile` does not carry, so `BraidedPlain` is **deferred to ChunkContext** sub-classification. The §7 compatibility invariant is enforced as a **classifier-gate-ordering** property (a build-time test that a single region cannot yield a forbidden pair), per §7's "build-time test" language; genuine cross-region sharp seams (cliff↔fjord, lithology faults) remain permitted. §2 climate-derived fields (`precipitation_class`, `glaciation_grade`, `vegetation_class`) all derive from the temperature(+moisture) primitive (T-1025).
|
||||
- **Amendment (T-1082, 2026-06-28):** §5's family set grows from **8 to 9** — a `WaterBody` generator family is added for the water zones **OpenOcean / Lake / TidalFlat**, which previously fell through to the dry-land `AlluvialPlain` fallback (the D-245 believability bug: oceans rendered as dry forested land — `water=Dry` + `Forest` on the sea). `WaterBody` lays `Water::Deep`/`Shallow` (a Shallow shoal band on the district-anchored coast line `coast_anchor_m`, Deep beyond; TidalFlat is all-Shallow), a seabed `TerrainMaterial` (`Rock` on steep districts / `Sand` on gentle / `Wetland` for tidal mud), `Vegetation::Barren`, the water surface at `elevation_m = 0` (the §8 mouths-at-sea-level convention), and seasonal `Ice` via `derive_cover` (frozen seas). This **refines §6**: open ocean / lake / tidal flat are still derived zone *labels*, but their *generator* is now `WaterBody`, not `AlluvialPlain` — so §6's "tidal flat … not [a] distinct generator famil[y]" is superseded for the dispatch of those three zones. `Wetland` stays a land zone on `AlluvialPlain` (a marsh — saturated ground, not open water). Lives in `server/src/atlas/voxel.rs` (`MorphologyFamily::WaterBody` + `generate_water_body`, dispatched by `zone_to_family`); verified by the T-1083 believability harness (water-renders-wet flips PASS for Arbour + Edict). The §5 8-family decision-tree classifier (`derive_morphology_zone`) is unchanged — this is a voxel-tier *generator* family, selected by zone, not a new RegionProfile classifier branch.
|
||||
- **Cross-reference:** [D-227](#d-227) (derive-don't-store voxel model), [D-228](#d-228) (composite tile axes / cohesion / seasonal state), [D-210](#d-210) (temperature proxy — formalised), [D-203](#d-203) (BodyWorldState cache), [D-206](#d-206) (background analysis pass), [D-208](#d-208) (drainage / D8), [D-010](#d-010) (determinism), [D-234](#d-234) (street/footprint geometry — consumes morphology), [D-142](content.md#d-142) (zone types), [D-217](#d-217) (tile condition), [Q-102](../questions/architecture.md#q-102) (cohesion = the warp), [Q-103](../questions/architecture.md#q-103) (mutator schema — open), [Q-105](../questions/architecture.md#q-105) (seasonal/clock state — temperature/ElevationDelta forward contract)
|
||||
|
||||
---
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
//!
|
||||
//! [`analyze`] is a pure, deterministic function of `(world_seed, body_id, districts)`:
|
||||
//! scalar/categorical contrast is computed over **all** districts; the voxel-derived
|
||||
//! metrics sample the first [`VOXEL_SAMPLE_DISTRICTS`] in `BTreeMap` order (sorted →
|
||||
//! stable). Suitable for golden-snapshot regression.
|
||||
//! metrics sample a **seeded spread** of [`VOXEL_SAMPLE_DISTRICTS`] across the body
|
||||
//! (deterministic from the world seed — D-245's "randomly-sampled locations", and
|
||||
//! unbiased unlike a contiguous corner). Suitable for golden-snapshot regression.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
@@ -60,8 +61,9 @@ const DEFAULT_SEA_LEVEL: f32 = 0.3;
|
||||
/// `ocean_fraction_q` at or above this marks a district water-present (D-239 §10 uses
|
||||
/// `ocean_fraction_q` as the chunk-scale water proxy).
|
||||
const WATER_PRESENCE_Q: i32 = 10;
|
||||
/// Districts voxel-sampled for the derived metrics — the first N in `BTreeMap` order
|
||||
/// (deterministic). Scalar/categorical contrast uses *all* districts (cheap).
|
||||
/// Districts voxel-sampled for the derived metrics — a deterministic seeded spread
|
||||
/// across the body (D-245 randomly-sampled-anywhere). Scalar/categorical contrast
|
||||
/// uses *all* districts (cheap).
|
||||
const VOXEL_SAMPLE_DISTRICTS: usize = 64;
|
||||
/// Stride over a district's representative 64 m chunk (8 → an 8×8 = 64-voxel sample).
|
||||
const VOXEL_SAMPLE_STRIDE: usize = 8;
|
||||
@@ -171,14 +173,21 @@ pub fn analyze(
|
||||
terrain_materials: 0, // filled from the voxel sample below
|
||||
};
|
||||
|
||||
// ── Voxel-derived metrics over a deterministic subset of districts ────────
|
||||
// ── Voxel-derived metrics over a deterministic seeded spread of districts ──
|
||||
// Sampling must span the whole body, not a contiguous prefix: BTreeMap order is
|
||||
// a spatial corner (e.g. an all-ocean edge), which would report a forested world
|
||||
// as "0 vegetated". A seeded spread is both unbiased and matches D-245's
|
||||
// "randomly-sampled locations across the body" wording.
|
||||
let mut terrain_set: BTreeSet<String> = BTreeSet::new();
|
||||
let mut coh = CoherenceMetrics::default();
|
||||
let mut sampled = 0usize;
|
||||
|
||||
for (dp, prof) in districts.iter().take(VOXEL_SAMPLE_DISTRICTS) {
|
||||
let keys: Vec<DistrictPos> = districts.keys().copied().collect();
|
||||
for idx in sample_indices(world_seed, keys.len(), VOXEL_SAMPLE_DISTRICTS) {
|
||||
let dp = keys[idx];
|
||||
let prof = &districts[&dp];
|
||||
sampled += 1;
|
||||
let chunk = district_centre_chunk(*dp);
|
||||
let chunk = district_centre_chunk(dp);
|
||||
let ctx = derive_chunk_context(world_seed, body_id, prof, chunk, None);
|
||||
|
||||
let mut any_wet = false;
|
||||
@@ -339,6 +348,29 @@ fn district_centre_chunk(dp: DistrictPos) -> ChunkPos {
|
||||
)
|
||||
}
|
||||
|
||||
/// Deterministic seeded spread of up to `take` distinct indices in `0..n` — a
|
||||
/// reproducible "random" sample across the whole district list (D-245's
|
||||
/// randomly-sampled-anywhere requirement), unbiased unlike a contiguous prefix.
|
||||
/// Returns all of `0..n` when `n <= take`. splitmix64 mixing keyed on the world seed.
|
||||
fn sample_indices(world_seed: u64, n: usize, take: usize) -> Vec<usize> {
|
||||
if n <= take {
|
||||
return (0..n).collect();
|
||||
}
|
||||
let mut picked: BTreeSet<usize> = BTreeSet::new();
|
||||
let mut i: u64 = 0;
|
||||
while picked.len() < take {
|
||||
let mut z = world_seed
|
||||
.wrapping_add(i)
|
||||
.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^= z >> 31;
|
||||
picked.insert((z % n as u64) as usize);
|
||||
i += 1;
|
||||
}
|
||||
picked.into_iter().collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Loader — resolve committed data + run the real cascade (shared by the probe
|
||||
// binary and the regression harness, so both measure the same thing)
|
||||
|
||||
+186
-13
@@ -40,12 +40,13 @@
|
||||
//!
|
||||
//! ## Family dispatch (D-239 §5, T-1028/T-1029)
|
||||
//!
|
||||
//! The 8-family tree is dispatched through `MorphologyFamily`. All 8 family
|
||||
//! generators are implemented: `AlluvialPlain` (the D-239 §5 fallback) landed
|
||||
//! in T-1028; the other 7 (LavaField, FjordWall, CliffCoast, BraidedDelta,
|
||||
//! DuneStrand, IncisedGorge, MeanderReach) landed in T-1029. Flat/water zones
|
||||
//! without a dedicated family (OpenOcean, Lake, TidalFlat, Wetland) map to
|
||||
//! `AlluvialPlain` per D-239 §5.
|
||||
//! 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
|
||||
//!
|
||||
@@ -269,6 +270,10 @@ enum MorphologyFamily {
|
||||
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.
|
||||
@@ -293,13 +298,14 @@ fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily {
|
||||
MorphologyFamily::IncisedGorge
|
||||
}
|
||||
MorphologyZone::MeanderReach | MorphologyZone::RiverBank => MorphologyFamily::MeanderReach,
|
||||
// AlluvialPlain fallback covers the flat/water zones without a dedicated
|
||||
// family: AlluvialPlain, OpenOcean, Lake, TidalFlat, Wetland (D-239 §5).
|
||||
MorphologyZone::AlluvialPlain
|
||||
| MorphologyZone::OpenOcean
|
||||
| MorphologyZone::Lake
|
||||
| MorphologyZone::TidalFlat
|
||||
| MorphologyZone::Wetland => MorphologyFamily::AlluvialPlain,
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,6 +422,9 @@ pub fn derive_voxel_column(
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
// ── 4. Seasonal cover overlay (D-239 §3, T-1030) ──────────────────────
|
||||
@@ -528,6 +537,78 @@ fn generate_alluvial_plain(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2062,6 +2143,98 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// TerrainMaterial discriminant pins
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -27,15 +27,15 @@
|
||||
},
|
||||
"morphology_zones": 9,
|
||||
"vegetation_classes": 3,
|
||||
"terrain_materials": 1
|
||||
"terrain_materials": 4
|
||||
},
|
||||
"coherence": {
|
||||
"water_districts": 64,
|
||||
"water_districts_wet": 4,
|
||||
"drainage_samples": 16,
|
||||
"drainage_monotonic": 4,
|
||||
"water_districts": 46,
|
||||
"water_districts_wet": 43,
|
||||
"drainage_samples": 10,
|
||||
"drainage_monotonic": 8,
|
||||
"vegetation_samples": 64,
|
||||
"vegetated_districts": 64
|
||||
"vegetated_districts": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66,13 +66,13 @@
|
||||
},
|
||||
"morphology_zones": 9,
|
||||
"vegetation_classes": 1,
|
||||
"terrain_materials": 2
|
||||
"terrain_materials": 4
|
||||
},
|
||||
"coherence": {
|
||||
"water_districts": 20,
|
||||
"water_districts_wet": 1,
|
||||
"drainage_samples": 4,
|
||||
"drainage_monotonic": 1,
|
||||
"water_districts_wet": 17,
|
||||
"drainage_samples": 6,
|
||||
"drainage_monotonic": 6,
|
||||
"vegetation_samples": 64,
|
||||
"vegetated_districts": 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user