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:
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user