From dd296b1ae5f1af921398be81d2117c3b00ebfe44 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 23 May 2026 08:09:26 +0200 Subject: [PATCH] test: review-driven determinism + coverage (#953 #963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the Hoshe/Tyre review: - drainage: assert flow_accumulation/max_accumulation determinism + clamp ≥ 1 (the D-209 strength denominator); isolated-basin merge path (no panic). - subbiome: each derivable variant reachable + Volcanic never emitted. - planet_simulation: new test_sim_determinism.py — same body simulates to a bit-identical elevation array at 1024×512 (the 271-body bake can't be cheaply re-run, so a silent drift = full re-bake). Verified PASS. Co-Authored-By: Claude Opus 4.7 (1M context) --- server/src/atlas/drainage.rs | 33 ++++++++++ server/src/atlas/subbiome.rs | 25 ++++++++ tooling/planet-gen/test_sim_determinism.py | 71 ++++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 tooling/planet-gen/test_sim_determinism.py diff --git a/server/src/atlas/drainage.rs b/server/src/atlas/drainage.rs index 8a73ee06b..20cecd2c0 100644 --- a/server/src/atlas/drainage.rs +++ b/server/src/atlas/drainage.rs @@ -683,4 +683,37 @@ mod tests { "Basin count must be deterministic" ); } + + #[test] + fn flow_accumulation_deterministic_and_clamped() { + // flow_accumulation/max_accumulation are the D-209 strength denominator — + // a silent drift corrupts every attractor strength. Lock them down. + let elev = slope_grid(64, 32); + let r1 = analyze(&elev, 64, 32, 0.3); + let r2 = analyze(&elev, 64, 32, 0.3); + assert_eq!(r1.flow_accumulation, r2.flow_accumulation); + assert_eq!(r1.max_accumulation, r2.max_accumulation); + assert!(r1.max_accumulation >= 1, "max_accumulation must be clamped ≥ 1"); + // Flat / all-ocean world: still well-defined (no division by zero). + let flat = flat_grid(16, 8, 0.5); + let rf = analyze(&flat, 16, 8, 0.9); // sea_level above all terrain + assert!(rf.max_accumulation >= 1); + } + + #[test] + fn isolated_basins_no_panic() { + // Two land patches split by an ocean band (rows 3-4 below sea level): + // exercises basin labeling/merge on a disconnected world. + let (w, h) = (32usize, 8usize); + let mut elev = vec![0.1f32; w * h]; // ocean everywhere + for r in [0, 1, 2, 5, 6, 7] { + for c in 0..w { + // two raised land bands, sloped so they drain internally + elev[r * w + c] = 0.5 + (c as f32 / w as f32) * 0.3; + } + } + let res = analyze(&elev, w as u32, h as u32, 0.3); + let n = res.drainage_basins.len(); + assert!(n >= 1 && n <= 12, "basin count {n} out of range"); + } } diff --git a/server/src/atlas/subbiome.rs b/server/src/atlas/subbiome.rs index 48b001832..6dd2bd180 100644 --- a/server/src/atlas/subbiome.rs +++ b/server/src/atlas/subbiome.rs @@ -156,4 +156,29 @@ mod tests { assert_eq!(c1.to_bits(), c2.to_bits()); assert!(c1 >= 1.0, "cost is at least the grassland baseline"); } + + #[test] + fn each_variant_reachable_and_volcanic_never_emitted() { + use SubBiomeVariant::*; + // (elev_pct, slope, water_dist, temp) → expected variant, per the + // classify_variant branch order. Covers all 10 derivable variants. + let cases: &[(f32, f32, u16, f32, SubBiomeVariant)] = &[ + (0.95, 0.0, 100, 0.5, Alpine), // high elevation dominates + (0.10, 0.0, 1, 0.5, Wetland), // saturated low ground + (0.35, 0.0, 4, 0.5, CoastalLowland), // near coast, low + (0.50, 0.0, 100, 0.10, Tundra), // cold pole + (0.50, 0.0, 100, 0.30, BorealForest), // cool + (0.50, 0.0, 10, 0.90, TropicalWet), // hot + moist + (0.50, 0.0, 30, 0.90, Savanna), // hot + mid-dry + (0.50, 0.0, 50, 0.90, Desert), // hot + dry + (0.50, 0.0, 10, 0.50, TemperateForest), // temperate + moist + (0.50, 0.0, 40, 0.50, TemperateGrassland),// temperate + mid + (0.50, 0.0, 70, 0.50, Desert), // temperate + arid + ]; + for &(e, s, w, t, expected) in cases { + let got = classify_variant(e, s, w, t); + assert_eq!(got, expected, "classify_variant({e},{s},{w},{t})"); + assert_ne!(got, Volcanic, "Volcanic must never be emitted (no L1 signal)"); + } + } } diff --git a/tooling/planet-gen/test_sim_determinism.py b/tooling/planet-gen/test_sim_determinism.py new file mode 100644 index 000000000..fd28fcfc6 --- /dev/null +++ b/tooling/planet-gen/test_sim_determinism.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Determinism guard for planet_simulation.simulate() (#963). + +The 1024×512 elevation bump (D-202 amended) claims to be deterministic — the +271-body heightmap bake cannot be re-run cheaply, so a silent non-determinism +means a full regeneration. This asserts that simulating the same body twice +yields a bit-identical elevation array. + +Run: uv run python tooling/planet-gen/test_sim_determinism.py +Exit: 0 = deterministic, 1 = drift detected, 2 = could not find a test body. +""" +import hashlib +import sqlite3 +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent.parent +sys.path.insert(0, str(REPO / "tooling" / "planet-gen")) + +import numpy as np # noqa: E402 + +from body_definition_parser import parse_system # noqa: E402 +from planet_simulation import GRID_H, GRID_W, simulate # noqa: E402 + + +def _elev_hash(terrain) -> str: + arr = np.ascontiguousarray(terrain["elevation"], dtype=np.float32) + return hashlib.sha256(arr.tobytes()).hexdigest() + + +def main() -> int: + conn = sqlite3.connect(str(REPO / "server" / "data" / "systems.db")) + rows = conn.execute( + "SELECT body_id, terrain_reference FROM bodies " + "WHERE inhabited=1 AND terrain_reference IS NOT NULL AND population>0 " + "AND system_id != 'GJ 0' ORDER BY body_id LIMIT 30" + ).fetchall() + + checked = 0 + for body_id, tref in rows: + if checked >= 3: + break + sys_index = tref.split("/bodies/")[0] + "/index.md" + try: + defs = parse_system(sys_index) + except Exception: + continue + bd = next((d for d in defs if d.get("id") == body_id), None) + if bd is None: + continue + t1 = simulate(bd) + t2 = simulate(bd) + if not t1 or "elevation" not in t1: + continue + h1, h2 = _elev_hash(t1), _elev_hash(t2) + ok = h1 == h2 and t1["elevation"].shape == (GRID_H, GRID_W) + print(f" {body_id}: {'OK' if ok else 'DRIFT'} {h1[:16]} shape={t1['elevation'].shape}") + if not ok: + print(f"FAIL: {body_id} elevation is non-deterministic ({h1[:16]} != {h2[:16]})") + return 1 + checked += 1 + + if checked == 0: + print("SKIP: no testable body found") + return 2 + print(f"PASS: {checked} bodies simulate deterministically at {GRID_W}×{GRID_H}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())