Pre-push surfaced fmt + clippy (-D warnings) failures in the new code: - rustfmt the 6 new/changed atlas files + the bench example. - features.rs: HashMap → BTreeMap (project bans HashMap for determinism via clippy disallowed_types; the bucket map is lookup-only either way). - attractor_matching.rs: drop now-redundant .clone() on AttractorType (it became Copy in #953) — clippy clone_on_copy. - drainage.rs tests: manual range → (1..=12).contains(&n). - features.rs test: drop .clone() on Copy AttractorType. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,9 @@ fn main() {
|
||||
let o = run_layer1(&hm);
|
||||
let mut by_type: std::collections::BTreeMap<String, usize> = 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!(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<bool> {
|
||||
}
|
||||
|
||||
// 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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,13 @@ pub fn load_heightmap_reader<R: Read>(
|
||||
.find(|c| c.keyword == "sea_level")
|
||||
.and_then(|c| c.text.trim().parse::<f32>().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)?;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user