diff --git a/server/examples/bench_layer1.rs b/server/examples/bench_layer1.rs index 5e4183d44..18328591d 100644 --- a/server/examples/bench_layer1.rs +++ b/server/examples/bench_layer1.rs @@ -50,7 +50,9 @@ fn main() { let o = run_layer1(&hm); let mut by_type: std::collections::BTreeMap = std::collections::BTreeMap::new(); for a in &o.attractors { - *by_type.entry(format!("{:?}", a.attractor_type)).or_default() += 1; + *by_type + .entry(format!("{:?}", a.attractor_type)) + .or_default() += 1; } println!("=== Layer-1 output for one 512×256 body ==="); println!( diff --git a/server/src/atlas/attractor_matching.rs b/server/src/atlas/attractor_matching.rs index 18a0ba2b2..dcac69872 100644 --- a/server/src/atlas/attractor_matching.rs +++ b/server/src/atlas/attractor_matching.rs @@ -314,7 +314,7 @@ pub fn match_cities( placements.push(CityPlacement { city_id: cities[ci].city_id, position: attractors[ai].position, - attractor_type: attractors[ai].attractor_type.clone(), + attractor_type: attractors[ai].attractor_type, score, synthetic: false, }); @@ -369,7 +369,7 @@ pub fn match_cities( placements.push(CityPlacement { city_id: cities[ci].city_id, position: attractors[ai].position, - attractor_type: attractors[ai].attractor_type.clone(), + attractor_type: attractors[ai].attractor_type, score, synthetic: false, }); diff --git a/server/src/atlas/drainage.rs b/server/src/atlas/drainage.rs index 20cecd2c0..b9b2d62f2 100644 --- a/server/src/atlas/drainage.rs +++ b/server/src/atlas/drainage.rs @@ -457,7 +457,10 @@ fn merge_small_basins( while active.len() > min_count { // Smallest active basin (tie → lowest id; BTreeSet iterates ascending). - let smallest = *active.iter().min_by_key(|&&b| (size[b as usize], b)).unwrap(); + let smallest = *active + .iter() + .min_by_key(|&&b| (size[b as usize], b)) + .unwrap(); let smallest_size = size[smallest as usize]; if active.len() <= max_count && smallest_size as f64 / n as f64 >= min_frac { break; @@ -661,7 +664,7 @@ mod tests { let result = analyze(&elev, 128, 64, 0.3); let n = result.drainage_basins.len(); assert!( - n >= 1 && n <= 12, + (1..=12).contains(&n), "Basin count {} out of expected range [1, 12]", n ); @@ -693,7 +696,10 @@ mod tests { 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"); + 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 @@ -714,6 +720,6 @@ mod tests { } 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"); + assert!((1..=12).contains(&n), "basin count {n} out of range"); } } diff --git a/server/src/atlas/features.rs b/server/src/atlas/features.rs index 97ef70f63..7c2ce06b3 100644 --- a/server/src/atlas/features.rs +++ b/server/src/atlas/features.rs @@ -16,7 +16,7 @@ //! No `HashMap`/`HashSet` iteration. `strength` is f32 but is never used as a //! sort key. -use std::collections::{HashMap, VecDeque}; +use std::collections::{BTreeMap, VecDeque}; use crate::atlas::drainage::DrainageResult; use crate::atlas::heightmap::BodyHeightmap; @@ -169,7 +169,9 @@ fn compute_lake_mask(ocean_mask: &[bool], w: usize, h: usize) -> Vec { } // Lakes = submerged cells in any non-ocean component. - (0..n).map(|i| comp[i] >= 0 && comp[i] != ocean_comp).collect() + (0..n) + .map(|i| comp[i] >= 0 && comp[i] != ocean_comp) + .collect() } /// Multi-source BFS Chebyshev distance to nearest ocean cell or river mouth. @@ -317,14 +319,28 @@ pub fn extract_attractors( for &(r, c) in &drainage.river_network.mouths { let i = idx(r as usize, c as usize, w); let s = accum[i] as f32 / max_accum; - claim(&mut out, &mut claimed, r as usize, c as usize, AttractorType::RiverMouth, s); + claim( + &mut out, + &mut claimed, + r as usize, + c as usize, + AttractorType::RiverMouth, + s, + ); } // 2. RiverCrossing — confluences, strength = accum / max_accum * 0.7. for &(r, c) in &drainage.river_network.confluences { let i = idx(r as usize, c as usize, w); let s = accum[i] as f32 / max_accum * 0.7; - claim(&mut out, &mut claimed, r as usize, c as usize, AttractorType::RiverCrossing, s); + claim( + &mut out, + &mut claimed, + r as usize, + c as usize, + AttractorType::RiverCrossing, + s, + ); } // 3. CoastalAccess — land within 3 cells of ocean, thinned by spacing. @@ -346,7 +362,14 @@ pub fn extract_attractors( } } for (r, c, s) in thin_by_spacing(coastal, &claimed, w) { - claim(&mut out, &mut claimed, r, c, AttractorType::CoastalAccess, s); + claim( + &mut out, + &mut claimed, + r, + c, + AttractorType::CoastalAccess, + s, + ); } // 4. ValleyFloor — gentle slope, mid elevation, positive habitability. @@ -524,7 +547,11 @@ fn is_saddle(elev: &[f32], r: usize, c: usize, w: usize, h: usize) -> bool { return false; // poles can't be saddles in this scheme } let nc = wrap_col(c as i32 + dc, w as i32); - signs[k] = if elev[idx(nr as usize, nc, w)] > e { 1 } else { -1 }; + signs[k] = if elev[idx(nr as usize, nc, w)] > e { + 1 + } else { + -1 + }; } let mut transitions = 0; for k in 0..8 { @@ -540,9 +567,9 @@ fn is_saddle(elev: &[f32], r: usize, c: usize, w: usize, h: usize) -> bool { /// /// Uses a bucket grid (cell size = `MIN_SPACING`) so each candidate only checks /// the 3×3 neighboring buckets — O(k) amortized rather than O(k²). The kept -/// order is fully determined by the sorted candidate iteration, so the internal -/// `HashMap` (lookup only, never iterated for output) does not affect -/// determinism. +/// order is fully determined by the sorted candidate iteration; the bucket map +/// is `BTreeMap` (the project bans `HashMap` for determinism) and is lookup-only +/// regardless. fn thin_by_spacing( mut cands: Vec<(usize, usize, f32)>, _claimed: &[bool], @@ -555,7 +582,7 @@ fn thin_by_spacing( sb.cmp(&sa).then(a.0.cmp(&b.0)).then(a.1.cmp(&b.1)) }); let sp = MIN_SPACING.max(1) as usize; - let mut buckets: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new(); + let mut buckets: BTreeMap<(usize, usize), Vec<(usize, usize)>> = BTreeMap::new(); let mut kept: Vec<(usize, usize, f32)> = Vec::new(); for (r, c, s) in cands { let (br, bc) = (r / sp, c / sp); @@ -627,8 +654,8 @@ mod tests { assert!(a.len() <= MAX_ATTRACTORS); // Sorted by (type as u8, row, col). for win in a.windows(2) { - let ka = (win[0].attractor_type.clone() as u8, win[0].row, win[0].col); - let kb = (win[1].attractor_type.clone() as u8, win[1].row, win[1].col); + let ka = (win[0].attractor_type as u8, win[0].row, win[0].col); + let kb = (win[1].attractor_type as u8, win[1].row, win[1].col); assert!(ka <= kb, "attractors must be sorted"); } } @@ -668,12 +695,16 @@ mod tests { // (a global-strength cap would drop all of them — the bug Hoshe caught). let h = hm(sine_grid(512, 256), 512, 256, 0.40); let dr = drainage::analyze(&h.data, 512, 256, 0.40); - assert!(!dr.river_network.mouths.is_empty(), "fixture must have mouths"); + assert!( + !dr.river_network.mouths.is_empty(), + "fixture must have mouths" + ); let ta = TerrainAnalysis::analyze(&h, &dr); let a = extract_attractors(&h, &dr, &ta); assert!(a.len() <= MAX_ATTRACTORS); assert!( - a.iter().any(|x| x.attractor_type == AttractorType::RiverMouth), + a.iter() + .any(|x| x.attractor_type == AttractorType::RiverMouth), "RiverMouth attractors must survive the cap when mouths exist" ); } diff --git a/server/src/atlas/heightmap.rs b/server/src/atlas/heightmap.rs index daa76095f..a29914249 100644 --- a/server/src/atlas/heightmap.rs +++ b/server/src/atlas/heightmap.rs @@ -136,7 +136,13 @@ pub fn load_heightmap_reader( .find(|c| c.keyword == "sea_level") .and_then(|c| c.text.trim().parse::().ok()) .unwrap_or(default_sea_level); - (info.width, info.height, info.bit_depth, info.color_type, sea_level) + ( + info.width, + info.height, + info.bit_depth, + info.color_type, + sea_level, + ) }; let mut buf = vec![0u8; png_reader.output_buffer_size()]; let frame = png_reader.next_frame(&mut buf)?; diff --git a/server/src/atlas/layer1.rs b/server/src/atlas/layer1.rs index 7cb090436..4697a6414 100644 --- a/server/src/atlas/layer1.rs +++ b/server/src/atlas/layer1.rs @@ -35,8 +35,7 @@ pub struct Layer1Output { /// Run the Layer-1 topography pipeline for a single body. pub fn run_layer1(hm: &BodyHeightmap) -> Layer1Output { - let drainage: DrainageResult = - drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level); + let drainage: DrainageResult = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level); let ta: TerrainAnalysis = TerrainAnalysis::analyze(hm, &drainage); let raw = features::extract_attractors(hm, &drainage, &ta); @@ -162,18 +161,21 @@ mod tests { b.terrain_modification_cost.to_bits() ); } - assert_eq!( - o1.river_network.river_cells, - o2.river_network.river_cells - ); + assert_eq!(o1.river_network.river_cells, o2.river_network.river_cells); } #[test] fn produces_attractors_and_costs() { let o = run_layer1(&hm(256, 128)); assert!(!o.attractors.is_empty(), "expected some attractors"); - assert!(o.attractors.iter().all(|a| a.terrain_modification_cost >= 1.0)); - assert!(o.attractors.iter().all(|a| (0.0..=1.0).contains(&a.strength))); + assert!(o + .attractors + .iter() + .all(|a| a.terrain_modification_cost >= 1.0)); + assert!(o + .attractors + .iter() + .all(|a| (0.0..=1.0).contains(&a.strength))); } #[test] diff --git a/server/src/atlas/subbiome.rs b/server/src/atlas/subbiome.rs index 6dd2bd180..036bb0722 100644 --- a/server/src/atlas/subbiome.rs +++ b/server/src/atlas/subbiome.rs @@ -134,7 +134,10 @@ mod tests { #[test] fn alpine_for_high_elevation() { // elev_pct > 0.8 → Alpine regardless of other signals. - let (v, cost) = (classify_variant(0.95, 30.0, 100, 0.5), base_cost(SubBiomeVariant::Alpine)); + let (v, cost) = ( + classify_variant(0.95, 30.0, 100, 0.5), + base_cost(SubBiomeVariant::Alpine), + ); assert_eq!(v, SubBiomeVariant::Alpine); assert!(cost > 3.0); } @@ -163,22 +166,25 @@ mod tests { // (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 + (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)"); + assert_ne!( + got, Volcanic, + "Volcanic must never be emitted (no L1 signal)" + ); } } }