Files
settled-reach/server/tests/derivation_harness.rs
T
jpmschweitzerandClaude Opus 4.8 5cc4ff6c0c test(simulation): address PR #165 review (T-1031)
Hoshe review findings — close silent-pass gaps in the §8 drainage tests:
- assert_drainage_monotonicity now returns whether it actually checked (chunk
  had both wet and dry tiles); the three sweep tests (Alluvial/Meander/Braided
  Delta) assert at least one chunk exercised the law, so a regression that
  zeroes all channels fails loudly instead of passing vacuously.
- The fjord-floor test scans the full 64x64 chunk instead of a single Y=32 row
  and asserts the deep-water trough exists, so the sea-level floor law can no
  longer be skipped by a probe that missed the trough.
- Remove the unreferenced make_test_terrain_analysis helper (was behind
  #[allow(dead_code)] with a 'future use' comment — the maintenance trap the
  review flagged); make_dry_terrain_analysis covers the validation bodies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:27:02 +02:00

1504 lines
51 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.
//! D-239 Tile-Derivation Verification Harness (T-1031).
//!
//! Capstone test for the T-974 tile-derivation epic. Covers four domains:
//!
//! **1. Golden-seed determinism regression (D-239 §1 / D-010).**
//! Pins (seed, region, chunk_pos, tile_pos) → VoxelColumn bindings as a JSON
//! golden. Any future change to the derivation chain is caught immediately.
//! Pattern mirrors cascade_golden.rs. Regenerate:
//! `UPDATE_GOLDEN=1 cargo test --test derivation_harness`
//!
//! **2. §8 binding-law assertions (D-239 §8).**
//! Sweeps representative `RegionProfile` inputs and asserts the four
//! believability laws hold across the parameter space:
//! - Drainage monotonicity (channel tiles at or below surrounding terrain)
//! - Lithology→landform (family emits its mandated TerrainMaterial)
//! - Glaciation→form (FjordWall only at GlaciationGrade ≥ 2; grade 0 ≠ fjord)
//! - Climate→vegetation (Forest→Scrub→Barren no-skip; riparian band present)
//!
//! **3. Per-family <5 ms/chunk budget (D-239 §10).**
//! Derives a full 64×64 chunk (4096 voxels) for each of the 8 families.
//! Hard gate (< 5 ms) is activated by `BUDGET_ASSERT=1` env var (release build).
//! Debug builds print timings without failing. VoxelCache is exercised to
//! measure LRU eviction overhead (O(capacity) scan, noted on T-1031).
//!
//! **4. Validation body cases (D-239 §1).**
//! Lore-anchored bodies verified from wiki body params (DB-free). Bodies absent
//! from the wiki are skipped with an explicit logged note.
//!
//! - Kallast (GJ144d): alluvial plain / Soil — VALIDATED
//! - Glødberg (GJ581c): volcanic immature drainage — VALIDATED (Cygni-B proxy)
//! - Marevna (GJ447c): ocean island / OpenOcean — VALIDATED (Ross 128)
//! - Velen (tidal-flat/dune coast): NOT IN WIKI — SKIPPED
//! - Gruenfeld (marginal Gravel/Scrub): NOT IN WIKI — SKIPPED
//!
//! Run all: `cargo test --test derivation_harness`
//! Update golden: `UPDATE_GOLDEN=1 cargo test --test derivation_harness`
//! Budget gate: `BUDGET_ASSERT=1 cargo test --test derivation_harness -- budget`
use std::path::PathBuf;
use std::time::Instant;
use settled_reach_server::atlas::chunk_context::derive_chunk_context;
use settled_reach_server::atlas::drainage;
use settled_reach_server::atlas::features::TerrainAnalysis;
use settled_reach_server::atlas::heightmap::BodyHeightmap;
use settled_reach_server::atlas::region_profile::{
derive_morphology_zone, derive_precipitation_class_from_climate, derive_region_profile,
derive_river_threshold, derive_vegetation, BodyParams, ClimateConstants, GlaciationGrade,
RegionProfile, TectonicClass, VegetationClass,
};
use settled_reach_server::atlas::voxel::{derive_voxel_column, TerrainMaterial, VoxelCache, Water};
use settled_reach_server::seed::{SeedChain, SeedDomain};
use settled_reach_server::simulation::generator::MorphologyZone;
// ---------------------------------------------------------------------------
// §1 — Golden-seed determinism regression
// ---------------------------------------------------------------------------
const GOLDEN_FILE: &str = "tests/golden/derivation_harness.json";
/// Compact representation of a VoxelColumn for golden pinning.
/// All fields are their integer-discriminant u8 values (D-010) or i32 elevation.
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)]
struct GoldenEntry {
label: String,
seed: u64,
body_id: String,
tile_x: i32,
tile_y: i32,
terrain: u8,
vegetation: u8,
water: u8,
elevation_m: i32,
cover: u8,
}
/// Derive a GoldenEntry for a fixed (seed, region, chunk_pos, tile_pos) tuple.
fn derive_golden(
label: &str,
seed: u64,
body_id: &str,
region: &RegionProfile,
chunk_pos: (i32, i32),
tile_x: i32,
tile_y: i32,
) -> GoldenEntry {
let chunk = derive_chunk_context(seed, body_id, region, chunk_pos);
let col = derive_voxel_column(seed, body_id, region, &chunk, tile_x, tile_y);
GoldenEntry {
label: label.to_string(),
seed,
body_id: body_id.to_string(),
tile_x,
tile_y,
terrain: col.terrain as u8,
vegetation: col.vegetation as u8,
water: col.water as u8,
elevation_m: col.elevation_m,
cover: col.cover as u8,
}
}
/// The three fixed golden inputs. Varied families and climate states.
fn golden_cases() -> Vec<(
&'static str,
u64,
&'static str,
RegionProfile,
(i32, i32),
i32,
i32,
)> {
vec![
// Case A: AlluvialPlain — temperate forest, active channel.
(
"alluvial_forest_active_channel",
0xdeadbeef_cafebabe_u64,
"GJ144d",
make_region(
MorphologyZone::AlluvialPlain,
TectonicClass::Active,
GlaciationGrade::None,
6,
22,
18,
68,
Some(12.0),
VegetationClass::Forest,
),
(10, 20),
640,
1280,
),
// Case B: LavaField — barren volcanic, no channel.
(
"lava_field_barren",
0x12345678_90abcdef_u64,
"GJ581c",
make_region(
MorphologyZone::Volcanic,
TectonicClass::Volcanic,
GlaciationGrade::None,
12,
38,
0,
22,
Some(45.0),
VegetationClass::Barren,
),
(5, 7),
320,
448,
),
// Case C: FjordWall — glaciated, rocky walls, deep water in trough.
(
"fjord_wall_glaciated",
0xfeedface_0badc0de_u64,
"GJ447c",
make_region(
MorphologyZone::Fjord,
TectonicClass::Active,
GlaciationGrade::Moderate,
55,
60,
28,
55,
Some(-8.0),
VegetationClass::Barren,
),
(14, 8),
// Centre of the chunk (fjord trough) — should be Deep water, Rock.
14 * 64 + 32,
8 * 64 + 32,
),
]
}
#[test]
fn golden_seed_determinism_regression() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let golden_path = manifest.join(GOLDEN_FILE);
// Derive once, then derive again — the two must be identical before we
// compare against the golden. This is the core D-010 determinism contract.
let run1: Vec<GoldenEntry> = golden_cases()
.into_iter()
.map(|(lbl, seed, body, region, cp, tx, ty)| {
derive_golden(lbl, seed, body, &region, cp, tx, ty)
})
.collect();
let run2: Vec<GoldenEntry> = golden_cases()
.into_iter()
.map(|(lbl, seed, body, region, cp, tx, ty)| {
derive_golden(lbl, seed, body, &region, cp, tx, ty)
})
.collect();
assert_eq!(
run1, run2,
"double-derivation mismatch — determinism is broken (D-010)"
);
let actual_json = serde_json::to_string_pretty(&run1).expect("serialize") + "\n";
if std::env::var("UPDATE_GOLDEN").is_ok() {
std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden");
std::fs::write(&golden_path, &actual_json).expect("write golden");
eprintln!(
"Golden written: {} ({} bytes)",
golden_path.display(),
actual_json.len()
);
return;
}
let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
panic!(
"Golden file not found: {}.\n\
First run: UPDATE_GOLDEN=1 cargo test --test derivation_harness\n{e}",
golden_path.display()
)
});
let actual_v: serde_json::Value = serde_json::from_str(&actual_json).expect("reparse actual");
let golden_v: serde_json::Value = serde_json::from_str(&golden_json).expect("parse golden");
if actual_v != golden_v {
panic!(
"Derivation golden mismatch — derivation chain changed.\n\
Update: UPDATE_GOLDEN=1 cargo test --test derivation_harness\n\
Golden: {}\nActual: {}",
golden_json.trim(),
actual_json.trim()
);
}
}
// ---------------------------------------------------------------------------
// §2 — §8 Binding-law assertions
// ---------------------------------------------------------------------------
// ── §8 Law 1: Drainage monotonicity ─────────────────────────────────────────
//
// Channel / water tiles must sit at or below surrounding dry terrain.
// D-239 §8: "drainage monotonicity: tributaries join upstream; mouths at sea
// level; BraidedDelta/CliffCoast/FjordWall floors at sea level."
// Tested by deriving a full 64×64 chunk and checking min(wet_elev) ≤ max(dry_elev).
/// Returns `true` if the monotonicity assertion was actually exercised (the chunk
/// produced both wet and dry tiles). A `false` return means the chunk had no wet
/// tiles, so the law was trivially satisfied without checking anything — callers
/// sweep several chunks and assert at least one returned `true`, so a derivation
/// regression that silently zeroes all channels fails loudly instead of passing.
#[must_use]
fn assert_drainage_monotonicity(
seed: u64,
body_id: &str,
label: &str,
region: &RegionProfile,
chunk_pos: (i32, i32),
) -> bool {
let chunk = derive_chunk_context(seed, body_id, region, chunk_pos);
if !chunk.has_active_channel {
return false; // No channel → monotonicity trivially satisfied.
}
let mut max_dry_elev = i32::MIN;
let mut min_wet_elev = i32::MAX;
let mut wet_count = 0usize;
let mut dry_count = 0usize;
let base_x = chunk_pos.0 * 64;
let base_y = chunk_pos.1 * 64;
for dy in 0..64i32 {
for dx in 0..64i32 {
let col = derive_voxel_column(seed, body_id, region, &chunk, base_x + dx, base_y + dy);
match col.water {
Water::Dry => {
max_dry_elev = max_dry_elev.max(col.elevation_m);
dry_count += 1;
}
Water::Shallow | Water::Deep => {
min_wet_elev = min_wet_elev.min(col.elevation_m);
wet_count += 1;
}
}
}
}
if wet_count > 0 && dry_count > 0 {
// Tolerance +3 m: meander levees are elevated above the floodplain
// (D-239 §9 ElevationDelta), so a channel tile adjacent to a levee tile
// will appear "slightly below" but the levee reads higher. We allow a
// small tolerance to avoid false positives at the levee-channel boundary.
let tolerance = 3;
assert!(
min_wet_elev <= max_dry_elev + tolerance,
"§8 drainage monotonicity VIOLATED in '{label}' chunk ({},{}): \
min wet-tile elevation {min_wet_elev} m > max dry-tile elevation \
{max_dry_elev} m (tolerance +{tolerance} m).",
chunk_pos.0,
chunk_pos.1
);
return true;
}
false
}
#[test]
fn law_drainage_monotonicity_alluvial_sweep() {
let region = make_region(
MorphologyZone::AlluvialPlain,
TectonicClass::Stable,
GlaciationGrade::None,
5,
20,
18,
60,
Some(15.0),
VegetationClass::Forest,
);
let mut checked = false;
for (cx, cy) in [(0, 0), (1, 0), (0, 1), (4, 4), (8, 3)] {
checked |= assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", &region, (cx, cy));
}
assert!(
checked,
"AlluvialPlain sweep exercised no wet tiles — the monotonicity law was never \
actually checked; a derivation regression could silently pass this test."
);
}
#[test]
fn law_drainage_monotonicity_meander_sweep() {
let region = make_region(
MorphologyZone::MeanderReach,
TectonicClass::Stable,
GlaciationGrade::None,
8,
18,
20,
65,
Some(14.0),
VegetationClass::Forest,
);
let mut checked = false;
for (cx, cy) in [(0, 0), (2, 1), (5, 5)] {
checked |= assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", &region, (cx, cy));
}
assert!(
checked,
"MeanderReach sweep exercised no wet tiles — the monotonicity law was never \
actually checked; a derivation regression could silently pass this test."
);
}
#[test]
fn law_drainage_monotonicity_fjord_floor_at_sea_level() {
// D-239 §8: fjord/cliff/delta floors at sea level (drainage monotonicity).
// FjordWall deep-water channel must be near elevation 0.
let region = make_region(
MorphologyZone::Fjord,
TectonicClass::Active,
GlaciationGrade::Moderate,
55,
60,
28,
55,
Some(-8.0),
VegetationClass::Barren,
);
let chunk = derive_chunk_context(42, "fjord_body", &region, (0, 0));
let mut deep_elevs: Vec<i32> = vec![];
let mut dry_elevs: Vec<i32> = vec![];
// Scan the full 64×64 chunk, not a single row — the deep-water trough axis is
// not guaranteed to intersect any fixed Y, so a single-row probe could miss it
// entirely and silently pass without ever checking the sea-level claim.
for dy in 0..64i32 {
for dx in 0..64i32 {
let col = derive_voxel_column(42, "fjord_body", &region, &chunk, dx, dy);
match col.water {
Water::Deep => deep_elevs.push(col.elevation_m),
Water::Dry => dry_elevs.push(col.elevation_m),
_ => {}
}
}
}
// A glaciated fjord chunk MUST carve a deep-water trough — if it doesn't, the
// derivation regressed and the sea-level law below would never run. Fail loudly.
assert!(
!deep_elevs.is_empty(),
"§8 FjordWall: no deep-water tiles found in the fjord chunk — the trough \
derivation regressed; the sea-level floor law was never exercised."
);
assert!(
!dry_elevs.is_empty(),
"§8 FjordWall: no dry wall tiles found in the fjord chunk."
);
let max_deep = *deep_elevs.iter().max().unwrap();
let min_dry = *dry_elevs.iter().min().unwrap();
// Fjord floor (deep water) must be below wall elevation.
assert!(
max_deep <= min_dry,
"§8 FjordWall drainage: deep-water max elev {max_deep} m must be \
≤ dry wall min elev {min_dry} m"
);
// Fjord floor must be near sea level (D-239 §8).
assert!(
max_deep <= 5,
"§8 FjordWall: deep-water floor elev {max_deep} m must be near sea level (≤5 m)"
);
}
#[test]
fn law_drainage_monotonicity_braided_delta() {
let region = make_region(
MorphologyZone::Delta,
TectonicClass::Active,
GlaciationGrade::None,
3,
8,
25,
50,
Some(18.0),
VegetationClass::Scrub,
);
let mut checked = false;
for (cx, cy) in [(0, 0), (1, 1)] {
checked |=
assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", &region, (cx, cy));
}
assert!(
checked,
"BraidedDelta sweep exercised no wet tiles — the monotonicity law was never \
actually checked; a derivation regression could silently pass this test."
);
}
// ── §8 Law 2: Lithology → Landform ──────────────────────────────────────────
//
// D-239 §8: Lava→Lava; Cliff/Fjord/Gorge→Rock; Sand→Dune; Gravel→Braided;
// Soil→Meander/Alluvial; Wetland→Wetland.
/// Derive 64 tile samples (every 8th tile in a 64×64 chunk) and assert all
/// have the expected TerrainMaterial.
fn assert_all_terrain_is(
seed: u64,
body_id: &str,
region: &RegionProfile,
chunk_pos: (i32, i32),
expected: TerrainMaterial,
label: &str,
) {
let chunk = derive_chunk_context(seed, body_id, region, chunk_pos);
let base_x = chunk_pos.0 * 64;
let base_y = chunk_pos.1 * 64;
for dy in (0..64i32).step_by(8) {
for dx in (0..64i32).step_by(8) {
let col = derive_voxel_column(seed, body_id, region, &chunk, base_x + dx, base_y + dy);
assert_eq!(
col.terrain,
expected,
"§8 lithology VIOLATED for '{label}': voxel ({},{}) returned {:?}, expected {:?}",
base_x + dx,
base_y + dy,
col.terrain,
expected
);
}
}
}
#[test]
fn law_lithology_lava_emits_lava() {
// D-239 §8: Lava family → TerrainMaterial::Lava everywhere.
let region = make_region(
MorphologyZone::Volcanic,
TectonicClass::Volcanic,
GlaciationGrade::None,
15,
35,
0,
20,
Some(40.0),
VegetationClass::Barren,
);
assert_all_terrain_is(
42,
"GJ581c",
&region,
(3, 3),
TerrainMaterial::Lava,
"LavaField",
);
}
#[test]
fn law_lithology_fjord_emits_rock() {
let region = make_region(
MorphologyZone::Fjord,
TectonicClass::Active,
GlaciationGrade::Moderate,
55,
60,
28,
55,
Some(-8.0),
VegetationClass::Barren,
);
assert_all_terrain_is(
42,
"fjord_body",
&region,
(0, 0),
TerrainMaterial::Rock,
"FjordWall",
);
}
#[test]
fn law_lithology_cliff_coast_emits_rock() {
let region = make_region(
MorphologyZone::CliffCoast,
TectonicClass::Active,
GlaciationGrade::None,
60,
40,
20,
30,
Some(10.0),
VegetationClass::Scrub,
);
assert_all_terrain_is(
42,
"cliff_body",
&region,
(0, 0),
TerrainMaterial::Rock,
"CliffCoast",
);
}
#[test]
fn law_lithology_incised_gorge_emits_rock() {
// D-239 §8: IncisedGorge/MountainPass → TerrainMaterial::Rock.
let region = make_region(
MorphologyZone::MountainPass,
TectonicClass::Active,
GlaciationGrade::None,
50,
65,
5,
40,
Some(5.0),
VegetationClass::Scrub,
);
assert_all_terrain_is(
42,
"gorge_body",
&region,
(0, 0),
TerrainMaterial::Rock,
"IncisedGorge",
);
}
#[test]
fn law_lithology_dune_strand_emits_sand() {
// D-239 §8: DuneStrand → TerrainMaterial::Sand (≤32° angle of repose).
let region = make_region(
MorphologyZone::DuneStrand,
TectonicClass::Stable,
GlaciationGrade::None,
12,
15,
20,
25,
Some(22.0),
VegetationClass::Barren,
);
assert_all_terrain_is(
42,
"dune_body",
&region,
(0, 0),
TerrainMaterial::Sand,
"DuneStrand",
);
}
#[test]
fn law_lithology_braided_delta_emits_gravel() {
// D-239 §8: BraidedDelta (Gravel→braided channels/fans) → TerrainMaterial::Gravel.
let region = make_region(
MorphologyZone::Delta,
TectonicClass::Active,
GlaciationGrade::None,
3,
8,
25,
50,
Some(18.0),
VegetationClass::Scrub,
);
assert_all_terrain_is(
42,
"delta_body",
&region,
(0, 0),
TerrainMaterial::Gravel,
"BraidedDelta",
);
}
#[test]
fn law_lithology_alluvial_plain_emits_soil() {
// D-239 §8: AlluvialPlain → Soil (non-wetland params: slope_q>5, moisture_q<60).
let region = make_region(
MorphologyZone::AlluvialPlain,
TectonicClass::Stable,
GlaciationGrade::None,
10,
25,
12,
45,
Some(16.0),
VegetationClass::Forest,
);
assert_all_terrain_is(
42,
"alluvial_body",
&region,
(0, 0),
TerrainMaterial::Soil,
"AlluvialPlain",
);
}
#[test]
fn law_lithology_meander_reach_emits_soil() {
// D-239 §8: MeanderReach (Soil→rolling/floodplain) → TerrainMaterial::Soil.
let region = make_region(
MorphologyZone::MeanderReach,
TectonicClass::Stable,
GlaciationGrade::None,
8,
18,
20,
55,
Some(14.0),
VegetationClass::Forest,
);
assert_all_terrain_is(
42,
"meander_body",
&region,
(0, 0),
TerrainMaterial::Soil,
"MeanderReach",
);
}
// ── §8 Law 3: Glaciation → Form ──────────────────────────────────────────────
//
// D-239 §5/§8: FjordWall requires GlaciationGrade ≥ 2 (Moderate).
// Grade 0 must NEVER produce Fjord regardless of slope/coastal params.
// Grade 1 (Light) also must NOT produce Fjord (gate is ≥ 2, not ≥ 1).
#[test]
fn law_glaciation_grade_0_never_produces_fjord() {
// Sweep slope_q and ocean_fraction_q at grade 0. No combination should yield Fjord.
for slope_q in [30, 40, 50, 55, 60, 70] {
for ocean_q in [15, 25, 35, 45, 55] {
let zone = derive_morphology_zone(
TectonicClass::Active,
GlaciationGrade::None, // grade 0
slope_q,
60,
ocean_q,
55,
);
assert_ne!(
zone,
MorphologyZone::Fjord,
"§8 glaciation law VIOLATED: GlaciationGrade::None + slope_q={slope_q} \
+ ocean_q={ocean_q} produced Fjord zone. Gate requires ≥ 2."
);
}
}
}
#[test]
fn law_glaciation_grade_1_never_produces_fjord() {
// Grade 1 (Light) is below the fjord gate. Must also not produce Fjord.
for slope_q in [40, 55, 70] {
for ocean_q in [20, 35] {
let zone = derive_morphology_zone(
TectonicClass::Active,
GlaciationGrade::Light, // grade 1 — gate does NOT open
slope_q,
60,
ocean_q,
55,
);
assert_ne!(
zone,
MorphologyZone::Fjord,
"§8 glaciation law VIOLATED: GlaciationGrade::Light (grade 1) + \
slope_q={slope_q} + ocean_q={ocean_q} produced Fjord. Gate requires ≥ 2."
);
}
}
}
#[test]
fn law_glaciation_grade_2_enables_fjord_with_correct_params() {
// Grade 2 (Moderate): with the correct slope + coastal params, the fjord gate opens.
// This is the positive test — the gate MUST open at exactly grade 2.
let zone = derive_morphology_zone(
TectonicClass::Active,
GlaciationGrade::Moderate, // grade 2 — gate opens
55, // steep enough
60,
25, // coastal
55,
);
assert_eq!(
zone,
MorphologyZone::Fjord,
"§8 glaciation law: GlaciationGrade::Moderate (grade 2) with slope_q=55 + \
ocean_q=25 should produce Fjord zone — gate should be open at grade 2."
);
}
#[test]
fn law_glaciation_grade_4_also_enables_fjord() {
// IceCap (grade 4): also above the gate. Should produce Fjord with correct params.
let zone = derive_morphology_zone(
TectonicClass::Active,
GlaciationGrade::IceCap,
55,
60,
25,
55,
);
assert_eq!(
zone,
MorphologyZone::Fjord,
"§8 glaciation law: GlaciationGrade::IceCap (grade 4) should also enable Fjord gate"
);
}
// ── §8 Law 4: Climate → Vegetation ──────────────────────────────────────────
//
// D-239 §8: "treeline Forest→Scrub→Barren, no skip."
// Rule: as temperature decreases (or elevation increases), the sequence must
// pass through Scrub before reaching Barren. A direct Forest→Barren jump is
// forbidden. Riparian variants are permitted anywhere they're produced.
#[test]
fn law_climate_vegetation_no_skip_temperature_sweep() {
// Sweep temperature from +30°C to -60°C at fixed mid-elevation.
// The sequence must have no Forest→Barren jump.
let moisture_q = 50;
let elev_q = 40;
let mut last: Option<VegetationClass> = None;
for t_i in (-60i32..=30).rev().step_by(5) {
let temp = Some(t_i as f32);
let vc = derive_vegetation(temp, moisture_q, elev_q, false);
if let Some(prev) = last {
if prev == VegetationClass::Forest && vc == VegetationClass::Barren {
panic!(
"§8 vegetation no-skip VIOLATED: Forest→Barren at temp={t_i}°C \
(elev_q={elev_q}, moisture_q={moisture_q})"
);
}
}
last = Some(vc);
}
}
#[test]
fn law_climate_vegetation_no_skip_elevation_sweep() {
// Sweep elevation from 0 to 100 at fixed warm temperature.
let temp = Some(15.0f32);
let moisture_q = 55;
let mut last: Option<VegetationClass> = None;
for elev_q in (0i32..=100).step_by(5) {
let vc = derive_vegetation(temp, moisture_q, elev_q, false);
if let Some(prev) = last {
if prev == VegetationClass::Forest && vc == VegetationClass::Barren {
panic!(
"§8 vegetation no-skip VIOLATED: Forest→Barren at elev_q={elev_q} \
(temp=+15°C, moisture_q={moisture_q})"
);
}
}
last = Some(vc);
}
}
#[test]
fn law_climate_vegetation_full_grid_no_skip() {
// Full grid sweep: all (moisture_q, elev_q) combinations, temperature from warm to cold.
// At each slice, verify no Forest→Barren jump as temperature drops.
let temps_desc: Vec<Option<f32>> = [
Some(30.0),
Some(20.0),
Some(10.0),
Some(5.0),
Some(0.0),
Some(-5.0),
Some(-10.0),
Some(-20.0),
Some(-30.0),
Some(-40.0),
Some(-55.0),
None,
]
.to_vec();
for moisture_q in (0i32..=100).step_by(10) {
for elev_q in (0i32..=100).step_by(10) {
let mut last: Option<VegetationClass> = None;
for &temp in &temps_desc {
let vc = derive_vegetation(temp, moisture_q, elev_q, false);
if let Some(prev) = last {
if prev == VegetationClass::Forest && vc == VegetationClass::Barren {
panic!(
"§8 full-grid no-skip VIOLATED: Forest→Barren at \
temp={temp:?}, moisture_q={moisture_q}, elev_q={elev_q}"
);
}
}
last = Some(vc);
}
}
}
}
#[test]
fn law_climate_vegetation_riparian_near_perennial_water() {
// D-239 §8: "riparian Thicket/Scrub 13 tiles along perennial waterways."
// near_perennial_water=true must produce a Riparian variant in viable zones.
// Forest zone → RiparianThicket.
let vc_forest = derive_vegetation(Some(18.0), 60, 20, true);
assert!(
matches!(
vc_forest,
VegetationClass::RiparianThicket | VegetationClass::RiparianScrub
),
"§8 riparian: Forest zone near water should → Riparian variant, got {:?}",
vc_forest
);
// Scrub zone → RiparianScrub.
let vc_scrub = derive_vegetation(Some(5.0), 30, 50, true);
assert!(
matches!(
vc_scrub,
VegetationClass::RiparianScrub | VegetationClass::RiparianThicket
),
"§8 riparian: Scrub zone near water should → Riparian variant, got {:?}",
vc_scrub
);
// Hyper-arid Barren zone + perennial water → RiparianScrub oasis (D-239 §8).
let vc_arid = derive_vegetation(Some(20.0), 3, 10, true);
assert_eq!(
vc_arid,
VegetationClass::RiparianScrub,
"§8 riparian: hyper-arid zone (moisture_q=3) near perennial water → \
RiparianScrub oasis, got {:?}",
vc_arid
);
}
#[test]
fn law_climate_vegetation_airless_always_absent() {
// D-239 §2: airless body (temperature_c = None) → VegetationClass::Absent always.
for moisture_q in [0, 30, 70, 100] {
for elev_q in [0, 50, 100] {
for near_water in [false, true] {
let vc = derive_vegetation(None, moisture_q, elev_q, near_water);
assert_eq!(
vc,
VegetationClass::Absent,
"§8 vegetation: airless body must produce Absent, got {:?} \
(moisture_q={moisture_q}, elev_q={elev_q})",
vc
);
}
}
}
}
// ---------------------------------------------------------------------------
// §3 — Per-family <5 ms/chunk budget (D-239 §10)
// ---------------------------------------------------------------------------
/// Derive all 4096 voxels in a 64×64 chunk and return (count, elapsed_µs).
fn derive_chunk_timed(
seed: u64,
body_id: &str,
region: &RegionProfile,
chunk_pos: (i32, i32),
) -> (usize, u128) {
let chunk = derive_chunk_context(seed, body_id, region, chunk_pos);
let base_x = chunk_pos.0 * 64;
let base_y = chunk_pos.1 * 64;
let t0 = Instant::now();
let mut count = 0usize;
for dy in 0..64i32 {
for dx in 0..64i32 {
let _ = derive_voxel_column(seed, body_id, region, &chunk, base_x + dx, base_y + dy);
count += 1;
}
}
(count, t0.elapsed().as_micros())
}
/// All 8 family inputs for the budget sweep.
fn budget_families() -> Vec<(&'static str, RegionProfile)> {
vec![
(
"AlluvialPlain",
make_region(
MorphologyZone::AlluvialPlain,
TectonicClass::Stable,
GlaciationGrade::None,
5,
20,
18,
60,
Some(15.0),
VegetationClass::Forest,
),
),
(
"LavaField",
make_region(
MorphologyZone::Volcanic,
TectonicClass::Volcanic,
GlaciationGrade::None,
12,
35,
0,
20,
Some(40.0),
VegetationClass::Barren,
),
),
(
"FjordWall",
make_region(
MorphologyZone::Fjord,
TectonicClass::Active,
GlaciationGrade::Moderate,
55,
60,
28,
55,
Some(-8.0),
VegetationClass::Barren,
),
),
(
"CliffCoast",
make_region(
MorphologyZone::CliffCoast,
TectonicClass::Active,
GlaciationGrade::None,
60,
40,
20,
30,
Some(10.0),
VegetationClass::Scrub,
),
),
(
"BraidedDelta",
make_region(
MorphologyZone::Delta,
TectonicClass::Active,
GlaciationGrade::None,
3,
8,
25,
50,
Some(18.0),
VegetationClass::Scrub,
),
),
(
"DuneStrand",
make_region(
MorphologyZone::DuneStrand,
TectonicClass::Stable,
GlaciationGrade::None,
12,
15,
20,
25,
Some(22.0),
VegetationClass::Barren,
),
),
(
"IncisedGorge",
make_region(
MorphologyZone::MountainPass,
TectonicClass::Active,
GlaciationGrade::None,
50,
65,
5,
40,
Some(5.0),
VegetationClass::Scrub,
),
),
(
"MeanderReach",
make_region(
MorphologyZone::MeanderReach,
TectonicClass::Stable,
GlaciationGrade::None,
8,
18,
20,
55,
Some(14.0),
VegetationClass::Forest,
),
),
]
}
#[test]
fn budget_per_family_chunk_derivation() {
let seed = 0xdeadbeef_12345678_u64;
let body_id = "budget_test_body";
let hard_gate = std::env::var("BUDGET_ASSERT").is_ok();
// Warm up with the fallback family first (also establishes baseline).
let (_, _) = derive_chunk_timed(seed, body_id, &budget_families()[0].1, (0, 0));
let (_, baseline_us) = derive_chunk_timed(seed, body_id, &budget_families()[0].1, (0, 0));
eprintln!(
"[budget] AlluvialPlain baseline: {} µs / 4096 voxels",
baseline_us
);
let mut results: Vec<(&str, u128)> = vec![];
for (label, region) in budget_families() {
let (cnt, us) = derive_chunk_timed(seed, body_id, &region, (1, 1));
let ms = us as f64 / 1000.0;
eprintln!("[budget] {label}: {ms:.2} ms / {cnt} voxels");
assert_eq!(cnt, 4096, "chunk must produce exactly 4096 voxels");
// Hard gate: < 5 ms. Only enforced when BUDGET_ASSERT=1 (release build).
// D-239 §10 target: ~2.24.2 ms/chunk.
if hard_gate {
assert!(
ms < 5.0,
"§10 budget EXCEEDED for '{label}': {ms:.2} ms > 5 ms. \
(Ensure release mode: cargo test --test derivation_harness --release)"
);
}
results.push((label, us));
}
// Relative assertion: FjordWall/IncisedGorge are noted as costlier than
// AlluvialPlain (D-239 §10), but must not be pathologically worse (>40×).
// In debug builds this catches O(n²) algorithmic defects regardless of
// absolute timing.
for (label, us) in &results {
if *label == "FjordWall" || *label == "IncisedGorge" {
let ratio = *us as f64 / baseline_us.max(1) as f64;
assert!(
ratio < 40.0,
"§10 relative budget: '{label}' is {ratio:.1}× slower than AlluvialPlain \
({:.2} ms vs {:.2} ms baseline). Check for algorithmic regression.",
*us as f64 / 1000.0,
baseline_us as f64 / 1000.0
);
}
}
}
// ── VoxelCache LRU eviction scan ─────────────────────────────────────────────
//
// D-239 §10 / T-1031 note: VoxelCache uses an O(capacity) LRU eviction scan
// (linear BTreeMap iteration for min access_gen). This is accepted behaviour
// for Phase 4 (no production render loop yet). We measure the overhead here
// to document it empirically.
//
// T-1031 baseline: ~4055× overhead in debug builds at capacity=1024, 4096 voxels.
// The gate is 200× (catastrophic regression detection only).
// Phase 5 upgrade: replace the BTreeMap linear scan with a min-heap or
// generation-indexed secondary structure to achieve <5× overhead.
#[test]
fn budget_voxel_cache_lru_overhead() {
let seed = 0xfeedface_12345678_u64;
let body_id = "cache_budget_body";
let region = make_region(
MorphologyZone::AlluvialPlain,
TectonicClass::Stable,
GlaciationGrade::None,
5,
20,
18,
60,
Some(15.0),
VegetationClass::Forest,
);
let chunk_pos = (2, 2);
let chunk = derive_chunk_context(seed, body_id, &region, chunk_pos);
let base_x = chunk_pos.0 * 64;
let base_y = chunk_pos.1 * 64;
// Adversarial capacity: one-quarter of a chunk (1024) forces frequent
// LRU evictions on every sequential pass through the full 4096 voxels.
let capacity = 1024;
let mut cache = VoxelCache::new(capacity);
let t0 = Instant::now();
for dy in 0..64i32 {
for dx in 0..64i32 {
let _ = cache.get_or_derive(seed, body_id, &region, &chunk, base_x + dx, base_y + dy);
}
}
let cache_us = t0.elapsed().as_micros();
// Direct derivation for comparison.
let (_, direct_us) = derive_chunk_timed(seed, body_id, &region, chunk_pos);
let overhead_ratio = cache_us as f64 / direct_us.max(1) as f64;
eprintln!(
"[cache_lru] direct: {direct_us} µs | cache (cap={capacity}): {cache_us} µs | \
overhead: {overhead_ratio:.1}× \
[T-1031 note: O(capacity) BTreeMap scan; accepted for Phase 4, ~4055× measured]"
);
// Measured overhead in debug builds: ~4055× (capacity=1024, 4096 voxels).
// Root cause: BTreeMap linear scan for min access_gen on every eviction — O(n) per
// insert when cache is full. Accepted for Phase 4 (no production render loop).
//
// Gate at 200× to catch catastrophic regressions only (e.g. nested scans,
// quadratic growth). The 4055× figure is intentionally documented here as the
// T-1031 baseline so future maintainers know what "acceptable" looks like.
// When a render loop is wired (Phase 5), upgrade to a min-heap or generation
// bitmap and lower this gate to <5×.
assert!(
overhead_ratio < 200.0,
"VoxelCache LRU overhead {overhead_ratio:.1}× exceeds 200×. \
The O({capacity}) BTreeMap scan has regressed beyond the T-1031 Phase 4 baseline. \
Consider upgrading the eviction strategy."
);
}
// ---------------------------------------------------------------------------
// §4 — Validation body cases (D-239 §1)
// ---------------------------------------------------------------------------
//
// Body params are sourced directly from wiki frontmatter (DB-free).
// If a body is absent from the wiki, the test SKIPS with a logged note.
/// Kallast (GJ144d) params — wiki: star-systems/GJ-144/bodies/GJ144d/index.md
fn kallast_body_params() -> BodyParams {
// planet_class: temperate | atmosphere: standard | hydrosphere: ocean | tectonics: active
// (orbit/star data is non-canonical per D-240 and no longer feeds temperature)
BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("standard".into()),
planet_class: Some("temperate".into()),
tectonic_activity: Some("active".into()),
region_latitude_deg: 0.0,
elevation_km: 0.0,
}
}
/// Glødberg (GJ581c) params — wiki: star-systems/GJ-581/bodies/GJ581c/index.md
fn gloedberg_body_params() -> BodyParams {
// planet_class: volcanic | atmosphere: standard | hydrosphere: subsurface_liquid | tectonics: extreme
// (orbit/star data is non-canonical per D-240 and no longer feeds temperature)
BodyParams {
hydrosphere: Some("subsurface".into()),
atmosphere: Some("standard".into()),
planet_class: Some("volcanic".into()),
tectonic_activity: Some("volcanic".into()),
region_latitude_deg: 0.0,
elevation_km: 0.5,
}
}
/// Marevna (GJ447c) params — wiki: star-systems/GJ-447/bodies/GJ447c/index.md
/// GJ-447 is the real-world identifier for Ross 128.
fn marevna_body_params() -> BodyParams {
// planet_class: oceanic | atmosphere: standard | hydrosphere: ocean | tectonics: active
// (orbit/star data is non-canonical per D-240 and no longer feeds temperature)
BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("standard".into()),
planet_class: Some("oceanic".into()),
tectonic_activity: Some("active".into()),
region_latitude_deg: 0.0,
elevation_km: 0.0,
}
}
/// Derive a RegionProfile for a body at given latitude/elevation,
/// using the full derive_region_profile pipeline over a minimal dry-land heightmap.
///
/// Uses a heightmap where all cells are above sea level (data = 0.5, sea_level = 0.3),
/// so ocean_fraction_q = 0 at all positions. This ensures the morphology classifier
/// reaches the tectonic/glaciation gates without being short-circuited by the Tier-0
/// ocean check (which fires at ocean_fraction_q ≥ 60).
fn derive_profile_for_body(params: &BodyParams) -> RegionProfile {
let climate = ClimateConstants::default();
let seed = SeedChain::root(42).derive(SeedDomain::Body, 1);
let ta = make_dry_terrain_analysis();
derive_region_profile(seed, params, &ta, (0, 0), 8, &climate)
}
#[test]
fn validation_kallast_alluvial_plain() {
// D-239 §1: Kallast = alluvial plain / Soil. GJ144d, K-star, temperate.
eprintln!("[validation] Kallast (GJ144d): checking alluvial plain / non-volcanic / non-fjord");
let profile = derive_profile_for_body(&kallast_body_params());
// Not volcanic — Kallast is a temperate world.
assert_ne!(
profile.tectonic_class,
TectonicClass::Volcanic,
"Kallast: TectonicClass must not be Volcanic (param: active)"
);
// Not fjord — no glaciation at temperate K-star orbit.
assert_ne!(
profile.morphology_zone,
MorphologyZone::Fjord,
"Kallast: temperate body must not produce Fjord"
);
// Not volcanic morphology.
assert_ne!(
profile.morphology_zone,
MorphologyZone::Volcanic,
"Kallast: temperate body must not produce Volcanic"
);
// Plausible terrestrial/coastal zone for a temperate body with ocean hydro.
let plausible = matches!(
profile.morphology_zone,
MorphologyZone::AlluvialPlain
| MorphologyZone::MeanderReach
| MorphologyZone::RiverBank
| MorphologyZone::Delta
| MorphologyZone::Estuarine
| MorphologyZone::TidalFlat
| MorphologyZone::Lake
| MorphologyZone::OpenOcean
| MorphologyZone::Wetland
);
assert!(
plausible,
"Kallast: expected alluvial/coastal zone, got {:?}. \
If wrong, fix body params — not the derivation (D-239 §1).",
profile.morphology_zone
);
// Temperature plausible for K-star temperate world.
if let Some(t) = profile.temperature_c {
assert!(
(-20.0..=50.0).contains(&t),
"Kallast: temperature {t}°C outside plausible range [-20, 50]"
);
}
eprintln!(
"[validation] Kallast: zone={:?} temp={:?}°C glaciation={:?} tectonic={:?} — PASS",
profile.morphology_zone,
profile.temperature_c,
profile.glaciation_grade,
profile.tectonic_class
);
}
#[test]
fn validation_gloedberg_volcanic_immature_drainage() {
// D-239 §1: "Cygni B = volcanic immature drainage."
// No body named "Cygni B" exists in the current wiki. GJ581c (Glødberg) is
// the closest volcanic match. NOTE: update this test if a "Cygni B" body is
// added to the wiki in the future.
eprintln!(
"[validation] Glødberg (GJ581c): volcanic proxy — checking LavaField + immature drainage"
);
let profile = derive_profile_for_body(&gloedberg_body_params());
// Assert volcanic tectonic class.
assert_eq!(
profile.tectonic_class,
TectonicClass::Volcanic,
"Glødberg: volcanic body must have TectonicClass::Volcanic"
);
// Assert Volcanic morphology (LavaField classifier fires first — §5 gate order).
assert_eq!(
profile.morphology_zone,
MorphologyZone::Volcanic,
"Glødberg: volcanic body must produce Volcanic zone (LavaField family)"
);
// No glaciation on a hot close-orbit volcanic world.
assert_eq!(
profile.glaciation_grade,
GlaciationGrade::None,
"Glødberg: hot volcanic body at 12-day orbit must have no glaciation"
);
// Derive a voxel and check TerrainMaterial::Lava (§8 lithology law).
let chunk = derive_chunk_context(42, "GJ581c", &profile, (0, 0));
let col = derive_voxel_column(42, "GJ581c", &profile, &chunk, 100, 100);
assert_eq!(
col.terrain,
TerrainMaterial::Lava,
"Glødberg: voxel must be Lava (§8 lava law)"
);
// Immature drainage: no active channels on lava fields (D-239 §8).
// The LavaField generator explicitly ignores has_active_channel.
assert_eq!(
col.water,
Water::Dry,
"Glødberg: volcanic immature drainage — voxel at (100,100) must be Dry \
(LavaField ignores active channel per §8 immature drainage law)"
);
eprintln!(
"[validation] Glødberg: zone={:?} temp={:?}°C terrain={:?} — PASS",
profile.morphology_zone, profile.temperature_c, col.terrain
);
}
#[test]
fn validation_marevna_ocean_island() {
// D-239 §1: "Ross 128 = ocean island." GJ-447 is Ross 128; GJ447c = Marevna.
eprintln!(
"[validation] Marevna (GJ447c / Ross 128): ocean island — checking coastal/ocean zone"
);
let profile = derive_profile_for_body(&marevna_body_params());
// Not volcanic.
assert_ne!(
profile.morphology_zone,
MorphologyZone::Volcanic,
"Marevna: oceanic world must not produce Volcanic"
);
// Not fjord — warm M-star body, no glaciation expected.
assert_ne!(
profile.morphology_zone,
MorphologyZone::Fjord,
"Marevna: warm oceanic world must not produce Fjord (no glaciation)"
);
// Plausible zone for an oceanic high-moisture world on a flat dry-land test terrain.
// moisture_q = 80 (ocean hydro + standard atmo) + slope_q ≈ 0 → Wetland is the
// dominant output from the derivation (§8 Wetland ≤5° flats). AlluvialPlain and
// coastal zones are also plausible depending on ocean_fraction_q signal.
let plausible = matches!(
profile.morphology_zone,
MorphologyZone::OpenOcean
| MorphologyZone::Lake
| MorphologyZone::Delta
| MorphologyZone::Estuarine
| MorphologyZone::TidalFlat
| MorphologyZone::MeanderReach
| MorphologyZone::AlluvialPlain
| MorphologyZone::RiverBank
| MorphologyZone::Wetland // Expected on flat high-moisture terrain (moisture_q=80, slope≈0)
);
assert!(
plausible,
"Marevna: oceanic world expected coastal/wetland zone, got {:?}. \
If wrong, fix body params — not the derivation (D-239 §1).",
profile.morphology_zone
);
// Temperature is deterministically derived from the planet_class envelope (D-240),
// not orbit/star data: oceanic band [-12, 28]°C, maritime factor 0.6 for an "ocean"
// hydrosphere. At the equator (latitude 0) the latitude lerp sits at the warm sub-band
// edge (~+20°C) and the standard-atmosphere greenhouse fraction lifts it to ~+26°C —
// a habitable result, as expected for an oceanic world. The assertion stays loose: we
// verify the pipeline ran without panic and produced a physically plausible value.
if let Some(t) = profile.temperature_c {
assert!(
t > -90.0 && t < 90.0,
"Marevna: temperature {t}°C is outside the oceanic-class plausible range [-90, 90]"
);
}
eprintln!(
"[validation] Marevna: zone={:?} temp={:?}°C moisture_q={} — PASS \
(oceanic class-envelope temp at equator per D-240)",
profile.morphology_zone, profile.temperature_c, profile.moisture_q
);
}
#[test]
fn validation_velen_skipped_not_in_wiki() {
// D-239 §1: "Velen = tidal-flat/dune coast."
// D-239 §1 caveat: "tidal flats require a moon param for the D-228 tidal
// term — verify Velen's params before treating its coast as a contract."
//
// STATUS: Body named "Velen" does NOT exist in the current wiki.
// Searched wiki/star-systems/**/*.md for `name: Velen` → NOT FOUND.
// The tidal/moon param cannot be verified (no body to inspect).
// This test is a deliberate skip with documented reason.
// Re-enable when Velen is created in the wiki and systems.db.
eprintln!(
"[validation] SKIPPED Velen (tidal-flat/dune coast):\n \
Body 'Velen' not found in wiki/star-systems/**/*.md. Lore anchor not\n \
yet materialised as a named body in systems.db. D-239 §1 caveat:\n \
moon/tidal param not verifiable. Re-enable when Velen is created."
);
}
#[test]
fn validation_gruenfeld_skipped_not_in_wiki() {
// D-239 §1: "Gruenfeld = marginal Gravel/Scrub."
//
// STATUS: Body named "Gruenfeld" does NOT exist in the current wiki.
// Searched wiki/star-systems/**/*.md for `name: Gruenfeld` → NOT FOUND.
// Re-enable when Gruenfeld is created in the wiki and systems.db.
eprintln!(
"[validation] SKIPPED Gruenfeld (marginal Gravel/Scrub):\n \
Body 'Gruenfeld' not found in wiki/star-systems/**/*.md. Lore anchor\n \
not yet materialised. Re-enable when Gruenfeld is created."
);
}
// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------
/// Construct a `RegionProfile` directly from parameters, deriving the
/// dependent fields (precipitation_class, river_threshold) consistently.
fn make_region(
zone: MorphologyZone,
tectonic: TectonicClass,
glaciation: GlaciationGrade,
slope_q: i32,
elev_q: i32,
ocean_fraction_q: i32,
moisture_q: i32,
temperature_c: Option<f32>,
vegetation_class: VegetationClass,
) -> RegionProfile {
let precip = derive_precipitation_class_from_climate(temperature_c, moisture_q);
RegionProfile {
morphology_zone: zone,
tectonic_class: tectonic,
glaciation_grade: glaciation,
precipitation_class: precip,
slope_q,
elev_q,
ocean_fraction_q,
river_threshold: derive_river_threshold(tectonic, precip),
temperature_c,
moisture_q,
vegetation_class,
}
}
/// Build a flat all-dry `TerrainAnalysis` for validation body derivation.
///
/// All cells have elevation = 0.5 with sea_level = 0.3 → ocean_fraction_q = 0 at
/// every region position. This prevents the Tier-0 ocean short-circuit in
/// `derive_morphology_zone` (ocean_fraction_q ≥ 60) from overriding the tectonic
/// and glaciation gates that we're testing.
///
/// Uses a gentle constant slope (no variation) so slope_q ~ 0 and elev_q ~ 75,
/// which places the morphology classifier in the AlluvialPlain fallback unless the
/// tectonic/glaciation gates fire first. That is the correct pre-condition for
/// lore body tests.
fn make_dry_terrain_analysis() -> TerrainAnalysis {
let (w, h) = (64u32, 32u32);
let n = (w * h) as usize;
// Constant 0.5: all above sea_level=0.3 → zero ocean cells.
let data: Vec<f32> = vec![0.5_f32; n];
let hm = BodyHeightmap {
body_id: "dry_test".into(),
width: w,
height: h,
data,
sea_level: 0.3,
};
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
TerrainAnalysis::analyze(&hm, &dr)
}