refactor(simulation): harden #952 cascade per QA + architecture review
Addresses the Hoshe (QA) + Tyre (architecture) review of the SeedChain/cascade work: - Golden was pinning an empty river network (128x64 produced 0 river cells). Bumped to 256x128, where GJ1c yields a real network (93 river cells, 19 mouths) — Layer 1's rivers are now actually guarded, not just attractors. - SeedChain::for_body(world_seed, body_id) + fnv1a_64: the single canonical body_id(String) -> u64 path (FNV-1a, the repo convention), so callers can't derive divergent worlds from the same seed via different ad-hoc hashes. The golden now uses it. - Stability guards: seed_domain_discriminants_are_pinned test (CI fails if a SeedDomain tag is renumbered); AttractorType gains #[repr(u8)] + explicit discriminants (it's cast as a sort key in features.rs). - Tests: SeedChain::root(0) non-degenerate; run_cascade error path (missing file -> Err, not panic). - Comments: clarified the id=0 derivations (sibling separation is caller-side via the per-district/quarter chain; #957 threads the index), tightened the all_district_types reachability comment (it pins the seed-0 sequence, not a probabilistic claim), and noted the golden's WORLD_SEED is cosmetic at Layers 0-1 + the x86_64 f32 capture caveat. Deferred with reason: the run_cascade -> CascadeInputs struct refactor (Tyre) is left for #954 — designing Layer-2's context shape now would be later-phase detail, and there's a single caller to migrate then. EntityRng keeps its domainless combine (migrating is stream-changing) — noted in D-224. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -167,4 +167,17 @@ mod tests {
|
||||
fn layers_are_ordered() {
|
||||
assert!(CascadeLayer::Heightmap < CascadeLayer::Topography);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_cascade_missing_file_is_err() {
|
||||
// The file-loading path returns an error (not a panic) for a bad path.
|
||||
let res = run_cascade(
|
||||
body_seed(),
|
||||
"missing",
|
||||
std::path::Path::new("/nonexistent/sr-test/heightmap.png"),
|
||||
0.3,
|
||||
CascadeLayer::Heightmap,
|
||||
);
|
||||
assert!(res.is_err(), "missing heightmap must Err, not panic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,9 @@ pub fn compute_district_mix(
|
||||
// We avoid f32 by using integer weighted random selection.
|
||||
let weight_sum: u32 = weights.iter().sum();
|
||||
let mut counts: [u32; 9] = [0; 9];
|
||||
// id = 0: one mix per district. Sibling districts/quarters are separated by
|
||||
// the distinct `chain` each receives (caller-derived; #957 threads a
|
||||
// per-quarter chain), so a fixed id here does not collide across siblings.
|
||||
let mut lcg = chain.derive(SeedDomain::Layer4Quarter, 0).atlas_rng();
|
||||
|
||||
for _ in 0..total_districts {
|
||||
@@ -371,10 +374,13 @@ mod tests {
|
||||
#[test]
|
||||
fn all_district_types_can_appear() {
|
||||
// Reachability check: every type has a clamped weight of at least 1
|
||||
// (compute_district_mix `.max(1)`), so all are reachable. A large draw
|
||||
// count makes every one appear regardless of the RNG sequence — a small
|
||||
// count can miss a low-weight type purely by luck, which is sequence- and
|
||||
// therefore seed-dependent (this is not a realistic city size).
|
||||
// (compute_district_mix `.max(1)`), so all are reachable. This asserts the
|
||||
// specific deterministic LCG sequence for SeedChain::root(0); 500 draws is
|
||||
// far beyond what's needed (the rarest type here, Administrative, has
|
||||
// weight ≈ 2.7%, so P(missed over 500 draws) ≈ 1e-6), so it stays green
|
||||
// across seeds — but it is the seed-0 sequence that's pinned, not a
|
||||
// probabilistic guarantee. Not a realistic city size; a unit test of the
|
||||
// allocator's reachability, not of a real settlement.
|
||||
let requested: u32 = 500;
|
||||
let mix = compute_district_mix(
|
||||
50_000_000,
|
||||
|
||||
@@ -245,6 +245,8 @@ fn derive_layout_mode(
|
||||
/// scaled to the ±16 sim tile maximum from `block_irregularity::max_offset_sim_tiles`.
|
||||
fn organic_placements(irregularity: f32, chain: SeedChain) -> [[BlockPlacement; 4]; 4] {
|
||||
let max_offset = (irregularity * 16.0) as i16;
|
||||
// id = 0: one placement pass per district. Sibling separation comes from the
|
||||
// distinct `chain` per district/quarter (caller-derived; #957), not the id.
|
||||
let mut lcg = chain.derive(SeedDomain::Block, 0).atlas_rng();
|
||||
|
||||
// Build the 2D array using a flat closure to keep things readable.
|
||||
|
||||
Reference in New Issue
Block a user