chore(simulation): cargo fmt atlas modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 09:46:16 +02:00
co-authored by Claude Opus 4.6
parent 64a873fc1a
commit f1278f88fd
10 changed files with 247 additions and 137 deletions
+29 -25
View File
@@ -99,7 +99,11 @@ fn cell_score(
let row = role_row(&city.economic_role);
let col = attractor_col(&attractor.attractor_type);
let weight = matrix.weights[row][col];
let cost_factor = if terrain_cost > 0.0 { 1.0 / terrain_cost } else { 1.0 };
let cost_factor = if terrain_cost > 0.0 {
1.0 / terrain_cost
} else {
1.0
};
weight * attractor.strength * cost_factor
}
@@ -213,11 +217,7 @@ fn hungarian(cost: &[Vec<f32>]) -> Vec<usize> {
/// Minimum pixel separation between synthetic attractor positions.
const MIN_SPACING: u16 = 15;
fn synthetic_attractor(
placed: &[CityPlacement],
grid_w: u32,
grid_h: u32,
) -> GeographicAttractor {
fn synthetic_attractor(placed: &[CityPlacement], grid_w: u32, grid_h: u32) -> GeographicAttractor {
// Place at grid center as default, then walk until spacing is satisfied.
let mut row = (grid_h / 2) as u16;
let mut col = (grid_w / 4) as u16;
@@ -326,9 +326,7 @@ pub fn match_cities(
.iter()
.enumerate()
.filter(|(i, c)| {
!tier_a_indices.contains(i)
&& c.population >= 50_000
&& c.population < 1_000_000
!tier_a_indices.contains(i) && c.population >= 50_000 && c.population < 1_000_000
})
.map(|(i, _)| i)
.collect();
@@ -450,12 +448,12 @@ pub fn founding_orientation(
coastal_facing: u16,
) -> FoundingOrientation {
match attractor_type {
AttractorType::RiverMouth | AttractorType::CoastalAccess => {
FoundingOrientation::Coastal { facing_degrees: coastal_facing }
}
AttractorType::RiverCrossing => {
FoundingOrientation::RiverAligned { bearing_degrees: river_bearing }
}
AttractorType::RiverMouth | AttractorType::CoastalAccess => FoundingOrientation::Coastal {
facing_degrees: coastal_facing,
},
AttractorType::RiverCrossing => FoundingOrientation::RiverAligned {
bearing_degrees: river_bearing,
},
AttractorType::ValleyFloor => FoundingOrientation::TerrainFollowing,
AttractorType::PlainCenter => {
if matches!(territorial_status, TerritorialStatus::CommissionControlled) {
@@ -480,11 +478,17 @@ mod tests {
use crate::simulation::generator::{CompatibilityMatrix, GeographicAttractor};
fn uniform_matrix() -> CompatibilityMatrix {
CompatibilityMatrix { weights: [[1.0; 7]; 10] }
CompatibilityMatrix {
weights: [[1.0; 7]; 10],
}
}
fn make_attractor(row: u16, col: u16, at: AttractorType, strength: f32) -> GeographicAttractor {
GeographicAttractor { position: (row, col), attractor_type: at, strength }
GeographicAttractor {
position: (row, col),
attractor_type: at,
strength,
}
}
fn make_city(id: u64, class: SettlementClass, pop: i64) -> CityRecord {
@@ -517,7 +521,7 @@ mod tests {
make_city(2, SettlementClass::PopulationBudget, 200_000),
];
let attractors = vec![
make_attractor(5, 5, AttractorType::RiverMouth, 0.9), // best
make_attractor(5, 5, AttractorType::RiverMouth, 0.9), // best
make_attractor(10, 10, AttractorType::ValleyFloor, 0.4), // second
];
let matrix = uniform_matrix();
@@ -597,7 +601,12 @@ mod tests {
use crate::simulation::generator::TerritorialStatus;
let status = TerritorialStatus::FrontierUnclaimed;
let o = founding_orientation(&AttractorType::RiverMouth, &status, 90, 270);
assert!(matches!(o, FoundingOrientation::Coastal { facing_degrees: 270 }));
assert!(matches!(
o,
FoundingOrientation::Coastal {
facing_degrees: 270
}
));
let o2 = founding_orientation(
&AttractorType::PlainCenter,
@@ -607,12 +616,7 @@ mod tests {
);
assert!(matches!(o2, FoundingOrientation::Cardinal));
let o3 = founding_orientation(
&AttractorType::ValleyFloor,
&status,
0,
0,
);
let o3 = founding_orientation(&AttractorType::ValleyFloor, &status, 0, 0);
assert!(matches!(o3, FoundingOrientation::TerrainFollowing));
}
}
+14 -6
View File
@@ -74,21 +74,25 @@ mod tests {
fn pioneer_more_irregular_than_commission() {
let pioneer = block_irregularity(400, &PoliticalArchetype::Pioneer);
let commission = block_irregularity(400, &PoliticalArchetype::Commission);
assert!(pioneer > commission,
"Pioneer ({pioneer}) should be more irregular than Commission ({commission})");
assert!(
pioneer > commission,
"Pioneer ({pioneer}) should be more irregular than Commission ({commission})"
);
}
#[test]
fn age_increases_irregularity() {
let young = block_irregularity(50, &PoliticalArchetype::Industrial);
let old = block_irregularity(800, &PoliticalArchetype::Industrial);
assert!(old > young,
"Older settlement ({old}) should be more irregular than young ({young})");
assert!(
old > young,
"Older settlement ({old}) should be more irregular than young ({young})"
);
}
#[test]
fn max_offset_scales_with_irregularity() {
assert_eq!(max_offset_sim_tiles(0.05), 0); // 0.05 × 16 = 0.8 → 0
assert_eq!(max_offset_sim_tiles(0.05), 0); // 0.05 × 16 = 0.8 → 0
assert_eq!(max_offset_sim_tiles(1.0), 16);
assert_eq!(max_offset_sim_tiles(0.5), 8);
}
@@ -105,7 +109,11 @@ mod tests {
];
for a in &archetypes {
let v = block_irregularity(300, a);
assert!(v >= 0.05 && v <= 1.0, "archetype {:?} gave {v} out of [0.05, 1.0]", a);
assert!(
v >= 0.05 && v <= 1.0,
"archetype {:?} gave {v} out of [0.05, 1.0]",
a
);
}
}
}
+8 -2
View File
@@ -236,8 +236,14 @@ mod tests {
cache.get("A", 100);
// Insert D to trigger eviction; B (tick 2) is now LRU, not A (tick 100).
cache.insert(make_state("D", 4));
assert!(cache.contains("A"), "A was recently accessed — must survive");
assert!(!cache.contains("B"), "B had oldest access time — should be evicted");
assert!(
cache.contains("A"),
"A was recently accessed — must survive"
);
assert!(
!cache.contains("B"),
"B had oldest access time — should be evicted"
);
}
#[test]
+77 -27
View File
@@ -246,12 +246,15 @@ struct LcgRng {
impl LcgRng {
fn new(seed: u64) -> Self {
Self { state: seed.wrapping_add(1) }
Self {
state: seed.wrapping_add(1),
}
}
fn next_u64(&mut self) -> u64 {
// LCG parameters from Knuth
self.state = self.state
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.state
@@ -273,10 +276,10 @@ mod tests {
#[test]
fn population_tier_values() {
assert_eq!(population_tier(0), 0);
assert_eq!(population_tier(50_000), 0); // 0.05M → log10 < 0 → tier 0
assert_eq!(population_tier(1_000_000), 0); // 1M → log10(1) = 0 → tier 0
assert_eq!(population_tier(10_000_000), 1); // 10M → log10(10) = 1 → tier 1
assert_eq!(population_tier(100_000_000), 2); // 100M → tier 2
assert_eq!(population_tier(50_000), 0); // 0.05M → log10 < 0 → tier 0
assert_eq!(population_tier(1_000_000), 0); // 1M → log10(1) = 0 → tier 0
assert_eq!(population_tier(10_000_000), 1); // 10M → log10(10) = 1 → tier 1
assert_eq!(population_tier(100_000_000), 2); // 100M → tier 2
assert_eq!(population_tier(1_000_000_000_000), 5); // capped at 5
}
@@ -304,41 +307,81 @@ mod tests {
6,
7,
);
let transit = mix.districts.iter().filter(|d| matches!(d, DistrictType::Transit)).count();
let commercial = mix.districts.iter().filter(|d| matches!(d, DistrictType::Commercial)).count();
let residential = mix.districts.iter().filter(|d| matches!(d, DistrictType::Residential)).count();
let transit = mix
.districts
.iter()
.filter(|d| matches!(d, DistrictType::Transit))
.count();
let commercial = mix
.districts
.iter()
.filter(|d| matches!(d, DistrictType::Commercial))
.count();
let residential = mix
.districts
.iter()
.filter(|d| matches!(d, DistrictType::Residential))
.count();
assert!(transit >= 1, "transit guarantee not met: {transit}");
assert!(commercial >= 1, "commercial guarantee not met: {commercial}");
assert!(residential >= 2, "residential guarantee not met: {residential}");
assert!(
commercial >= 1,
"commercial guarantee not met: {commercial}"
);
assert!(
residential >= 2,
"residential guarantee not met: {residential}"
);
}
#[test]
fn determinism_same_seed() {
let mix1 = compute_district_mix(5_000_000, "financial", &PoliticalArchetype::Commission, 6, 99);
let mix2 = compute_district_mix(5_000_000, "financial", &PoliticalArchetype::Commission, 6, 99);
let mix1 = compute_district_mix(
5_000_000,
"financial",
&PoliticalArchetype::Commission,
6,
99,
);
let mix2 = compute_district_mix(
5_000_000,
"financial",
&PoliticalArchetype::Commission,
6,
99,
);
assert_eq!(mix1, mix2, "same inputs must produce identical output");
}
#[test]
fn different_archetypes_produce_different_mixes() {
let mix_corp = compute_district_mix(5_000_000, "financial", &PoliticalArchetype::Corporate, 8, 42);
let mix_pioneer = compute_district_mix(5_000_000, "financial", &PoliticalArchetype::Pioneer, 8, 42);
let mix_corp = compute_district_mix(
5_000_000,
"financial",
&PoliticalArchetype::Corporate,
8,
42,
);
let mix_pioneer =
compute_district_mix(5_000_000, "financial", &PoliticalArchetype::Pioneer, 8, 42);
// Should differ in at least one district type count.
assert_ne!(mix_corp.districts, mix_pioneer.districts,
"Corporate and Pioneer archetypes should produce different district mixes");
assert_ne!(
mix_corp.districts, mix_pioneer.districts,
"Corporate and Pioneer archetypes should produce different district mixes"
);
}
#[test]
fn military_archetype_has_administrative() {
let mix = compute_district_mix(
2_000_000,
"military",
&PoliticalArchetype::Military,
8,
10,
let mix = compute_district_mix(2_000_000, "military", &PoliticalArchetype::Military, 8, 10);
let admin = mix
.districts
.iter()
.filter(|d| matches!(d, DistrictType::Administrative))
.count();
assert!(
admin >= 1,
"military archetype should have Administrative districts"
);
let admin = mix.districts.iter().filter(|d| matches!(d, DistrictType::Administrative)).count();
assert!(admin >= 1, "military archetype should have Administrative districts");
}
#[test]
@@ -352,8 +395,15 @@ mod tests {
0,
);
for dt in &DIST_COLS {
let present = mix.districts.iter().any(|d| std::mem::discriminant(d) == std::mem::discriminant(dt));
assert!(present, "DistrictType {:?} never appeared in 50-district mix", dt);
let present = mix
.districts
.iter()
.any(|d| std::mem::discriminant(d) == std::mem::discriminant(dt));
assert!(
present,
"DistrictType {:?} never appeared in 50-district mix",
dt
);
}
}
}
+5 -13
View File
@@ -397,9 +397,7 @@ fn merge_small_basins(
let n_basins = sizes.len();
// Stop if within target range and all basins are large enough.
if n_basins <= max_count
&& sizes.values().all(|&s| s as f64 / n as f64 >= min_frac)
{
if n_basins <= max_count && sizes.values().all(|&s| s as f64 / n as f64 >= min_frac) {
break;
}
if n_basins <= min_count {
@@ -407,10 +405,7 @@ fn merge_small_basins(
}
// Find the smallest basin.
let (&smallest_id, &smallest_size) = sizes
.iter()
.min_by_key(|(_, &s)| s)
.unwrap();
let (&smallest_id, &smallest_size) = sizes.iter().min_by_key(|(_, &s)| s).unwrap();
if n_basins <= max_count && smallest_size as f64 / n as f64 >= min_frac {
break;
@@ -533,10 +528,8 @@ fn build_basins(labels: &[i32], w: usize, h: usize) -> Vec<DrainageBasin> {
// Sort boundary by angle from centroid for a coherent polygon.
if !boundary.is_empty() {
let cr = boundary.iter().map(|&(r, _)| r as f32).sum::<f32>()
/ boundary.len() as f32;
let cc = boundary.iter().map(|&(_, c)| c as f32).sum::<f32>()
/ boundary.len() as f32;
let cr = boundary.iter().map(|&(r, _)| r as f32).sum::<f32>() / boundary.len() as f32;
let cc = boundary.iter().map(|&(_, c)| c as f32).sum::<f32>() / boundary.len() as f32;
boundary.sort_by(|&(r1, c1), &(r2, c2)| {
let a1 = (r1 as f32 - cr).atan2(c1 as f32 - cc);
let a2 = (r2 as f32 - cr).atan2(c2 as f32 - cc);
@@ -644,8 +637,7 @@ mod tests {
let r1 = analyze(&elev, 64, 32, 0.3);
let r2 = analyze(&elev, 64, 32, 0.3);
assert_eq!(
r1.river_network.river_cells,
r2.river_network.river_cells,
r1.river_network.river_cells, r2.river_network.river_cells,
"River cells must be deterministic"
);
assert_eq!(
+56 -33
View File
@@ -55,7 +55,10 @@ pub enum GenWorkItem {
/// Generate a Phase 1 DistrictSkeleton for this city.
GenerateSkeleton { city_id: u64 },
/// Pre-fill a chunk in an existing district.
FillChunk { district_id: u64, block_pos: (u32, u32) },
FillChunk {
district_id: u64,
block_pos: (u32, u32),
},
}
impl GenWorkItem {
@@ -75,11 +78,21 @@ impl GenWorkItem {
/// Sent back to the main thread when a work item finishes (D-206).
#[derive(Debug)]
pub enum GenCompletion {
BodyAnalyzed { body_id: String },
SkeletonGenerated { city_id: u64 },
ChunkFilled { district_id: u64, block_pos: (u32, u32) },
BodyAnalyzed {
body_id: String,
},
SkeletonGenerated {
city_id: u64,
},
ChunkFilled {
district_id: u64,
block_pos: (u32, u32),
},
/// Work item failed — body_id or city_id for logging.
Failed { item: GenWorkItem, reason: String },
Failed {
item: GenWorkItem,
reason: String,
},
}
// ---------------------------------------------------------------------------
@@ -114,11 +127,7 @@ pub struct GenerationQueue {
impl std::fmt::Debug for GenerationQueue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pending_len = self
.pending
.lock()
.map(|p| p.len())
.unwrap_or(0);
let pending_len = self.pending.lock().map(|p| p.len()).unwrap_or(0);
f.debug_struct("GenerationQueue")
.field("pending_count", &pending_len)
.finish()
@@ -168,9 +177,7 @@ impl GenerationQueue {
drop(in_flight);
// Check pending list.
let pending = self.pending.lock().unwrap();
if pending.iter().any(|q| {
q.item.body_id() == Some(body_id)
}) {
if pending.iter().any(|q| q.item.body_id() == Some(body_id)) {
return;
}
drop(pending);
@@ -216,10 +223,7 @@ impl GenerationQueue {
// Mark body as in-flight.
if let Some(body_id) = item.body_id() {
self.in_flight
.lock()
.unwrap()
.insert(body_id.to_string());
self.in_flight.lock().unwrap().insert(body_id.to_string());
}
let tx = self.completion_tx.clone();
@@ -261,18 +265,19 @@ impl Default for GenerationQueue {
/// immediate success to allow the queue infrastructure to be tested independently.
fn run_work_item(item: &GenWorkItem) -> GenCompletion {
match item {
GenWorkItem::AnalyzeBody { body_id } => {
GenCompletion::BodyAnalyzed { body_id: body_id.clone() }
}
GenWorkItem::AnalyzeBody { body_id } => GenCompletion::BodyAnalyzed {
body_id: body_id.clone(),
},
GenWorkItem::GenerateSkeleton { city_id } => {
GenCompletion::SkeletonGenerated { city_id: *city_id }
}
GenWorkItem::FillChunk { district_id, block_pos } => {
GenCompletion::ChunkFilled {
district_id: *district_id,
block_pos: *block_pos,
}
}
GenWorkItem::FillChunk {
district_id,
block_pos,
} => GenCompletion::ChunkFilled {
district_id: *district_id,
block_pos: *block_pos,
},
}
}
@@ -293,7 +298,9 @@ mod tests {
fn submit_and_drain() {
let q = make_queue();
q.submit(
GenWorkItem::AnalyzeBody { body_id: "TestBody".to_string() },
GenWorkItem::AnalyzeBody {
body_id: "TestBody".to_string(),
},
GenPriority::Medium,
);
// Give Rayon time to complete the (stub) task.
@@ -311,11 +318,15 @@ mod tests {
let q = make_queue();
// Submit the same body twice before it can complete.
q.submit(
GenWorkItem::AnalyzeBody { body_id: "Dup".to_string() },
GenWorkItem::AnalyzeBody {
body_id: "Dup".to_string(),
},
GenPriority::Low,
);
q.submit(
GenWorkItem::AnalyzeBody { body_id: "Dup".to_string() },
GenWorkItem::AnalyzeBody {
body_id: "Dup".to_string(),
},
GenPriority::Low,
);
std::thread::sleep(Duration::from_millis(50));
@@ -329,9 +340,18 @@ mod tests {
// Submit three items rapidly; Immediate should be dispatched first.
let q = make_queue();
// Using GenerateSkeleton (no dedup logic) to test ordering directly.
q.submit(GenWorkItem::GenerateSkeleton { city_id: 1 }, GenPriority::Low);
q.submit(GenWorkItem::GenerateSkeleton { city_id: 2 }, GenPriority::Immediate);
q.submit(GenWorkItem::GenerateSkeleton { city_id: 3 }, GenPriority::Medium);
q.submit(
GenWorkItem::GenerateSkeleton { city_id: 1 },
GenPriority::Low,
);
q.submit(
GenWorkItem::GenerateSkeleton { city_id: 2 },
GenPriority::Immediate,
);
q.submit(
GenWorkItem::GenerateSkeleton { city_id: 3 },
GenPriority::Medium,
);
std::thread::sleep(Duration::from_millis(100));
let completions = q.drain_completions();
assert_eq!(completions.len(), 3);
@@ -348,7 +368,10 @@ mod tests {
fn pending_count_decreases_after_completion() {
let q = make_queue();
q.submit(
GenWorkItem::FillChunk { district_id: 99, block_pos: (0, 0) },
GenWorkItem::FillChunk {
district_id: 99,
block_pos: (0, 0),
},
GenPriority::High,
);
std::thread::sleep(Duration::from_millis(50));
+1 -3
View File
@@ -139,9 +139,7 @@ mod tests {
}
fn insert_heightmap(conn: &Connection, body_id: &str, w: u32, h: u32, sea_level: f32) {
let floats: Vec<f32> = (0..(w * h))
.map(|i| i as f32 / (w * h) as f32)
.collect();
let floats: Vec<f32> = (0..(w * h)).map(|i| i as f32 / (w * h) as f32).collect();
let bytes: &[u8] = bytemuck::cast_slice(&floats);
conn.execute(
"INSERT INTO atlas_body_heightmaps (body_id, width, height, data, sea_level)
+45 -21
View File
@@ -21,10 +21,9 @@
use crate::atlas::block_irregularity::block_irregularity;
use crate::atlas::district_mix::{compute_district_mix, population_tier};
use crate::simulation::generator::{
BlockPlacement, BlockSkeleton, ComplexityTier, DistrictId, DistrictLayoutMode,
DistrictSkeleton, DistrictType, MultiBlockReservation, PoliticalArchetype,
BlockPlacement, BlockSkeleton, CityGenerationContext, ComplexityTier, DistrictId,
DistrictLayoutMode, DistrictSkeleton, DistrictType, MultiBlockReservation, PoliticalArchetype,
ReservationFunction, ReservationId, SettingType, WorldTier, ZoningType,
CityGenerationContext,
};
// ---------------------------------------------------------------------------
@@ -76,8 +75,7 @@ pub fn generate_skeleton(
let reservations = derive_reservations(tier, economic_role, seed);
// Build the reservation lookup: block position → reservation id.
let mut block_reservation: [[Option<ReservationId>; 4]; 4] =
[[None, None, None, None]; 4];
let mut block_reservation: [[Option<ReservationId>; 4]; 4] = [[None, None, None, None]; 4];
for (idx, res) in reservations.iter().enumerate() {
let rid = idx as u64 + 1; // 1-based stable id within this district
for &(row, col) in &res.blocks {
@@ -92,7 +90,10 @@ pub fn generate_skeleton(
// ── Build 4×4 block grid ──────────────────────────────────────────────
// Flat district-mix list is already in deterministic order; assign
// row-major (row 0 col 0 → row 0 col 3 → row 1 col 0 …).
let primary_district_type = mix.districts.first().cloned()
let primary_district_type = mix
.districts
.first()
.cloned()
.unwrap_or(DistrictType::MixedUse);
let blocks = build_block_grid(&mix.districts, &block_reservation, &primary_district_type);
@@ -190,10 +191,18 @@ fn derive_complexity(world_tier: &WorldTier, pop_tier: u8, population: i64) -> C
match world_tier {
WorldTier::Epicenter | WorldTier::Regional => {
if pop_tier >= 1 { ComplexityTier::Full } else { ComplexityTier::Moderate }
if pop_tier >= 1 {
ComplexityTier::Full
} else {
ComplexityTier::Moderate
}
}
WorldTier::Backwater | WorldTier::Passage => {
if pop_tier >= 1 { ComplexityTier::Moderate } else { ComplexityTier::Minimal }
if pop_tier >= 1 {
ComplexityTier::Moderate
} else {
ComplexityTier::Minimal
}
}
WorldTier::Waypoint => ComplexityTier::Minimal,
}
@@ -245,7 +254,7 @@ fn organic_placements(irregularity: f32, seed: u64) -> [[BlockPlacement; 4]; 4]
let raw_x = (lcg.next_u32() % (2 * max_offset as u32 + 1)) as i16 - max_offset;
let raw_y = (lcg.next_u32() % (2 * max_offset as u32 + 1)) as i16 - max_offset;
let rot = (lcg.next_u32() % 4) as u8; // 03 (15° increments, max 45°)
// Street width 750020000 bps proportional to irregularity.
// Street width 750020000 bps proportional to irregularity.
let width_range = 12_500u32; // 20000 - 7500
let width = 7_500u32 + (lcg.next_u32() % (width_range + 1));
*item = BlockPlacement {
@@ -256,9 +265,7 @@ fn organic_placements(irregularity: f32, seed: u64) -> [[BlockPlacement; 4]; 4]
}
// Safety: BlockPlacement is Copy-able; reshape flat array to [[_; 4]; 4].
core::array::from_fn(|row| {
core::array::from_fn(|col| flat[row * 4 + col].clone())
})
core::array::from_fn(|row| core::array::from_fn(|col| flat[row * 4 + col].clone()))
}
// ---------------------------------------------------------------------------
@@ -311,9 +318,9 @@ fn build_block_grid(
position: (row as u8, col as u8),
zoning,
reservation,
chunk_layout: String::new(), // stub
chunk_layout: String::new(), // stub
hosted_sites: Vec::new(),
era: String::new(), // stub
era: String::new(), // stub
era_modifications: Vec::new(),
era_cause: None,
density_pct: density,
@@ -349,7 +356,11 @@ fn density_for_zoning(zoning: &ZoningType) -> u8 {
///
/// Phase 1 produces skeleton-only reservations — floor_zones and vertical_corridors
/// are deferred to Phase 2.
fn derive_reservations(pop_tier: u8, economic_role: &str, _seed: u64) -> Vec<MultiBlockReservation> {
fn derive_reservations(
pop_tier: u8,
economic_role: &str,
_seed: u64,
) -> Vec<MultiBlockReservation> {
let mut out = Vec::new();
// Park: large cities need open space.
@@ -398,11 +409,14 @@ struct SkeletonLcg {
impl SkeletonLcg {
fn new(seed: u64) -> Self {
// Mix seed to avoid degenerate state at 0.
Self { state: seed.wrapping_add(0x9e37_79b9_7f4a_7c15) }
Self {
state: seed.wrapping_add(0x9e37_79b9_7f4a_7c15),
}
}
fn next_u64(&mut self) -> u64 {
self.state = self.state
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.state
@@ -516,7 +530,9 @@ mod tests {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Epicenter);
// 100M pop → pop_tier 2 → park reservation.
let sk = generate_skeleton(&ctx, 100_000_000, "financial", 1, 300, 42);
let has_park = sk.reservations.iter()
let has_park = sk
.reservations
.iter()
.any(|r| matches!(r.function, ReservationFunction::Park));
assert!(has_park);
}
@@ -526,7 +542,9 @@ mod tests {
let ctx = make_context(PoliticalArchetype::Commission, WorldTier::Regional);
// pop_tier 0 but transit_hub → terminal reservation.
let sk = generate_skeleton(&ctx, 80_000, "transit_hub", 1, 200, 42);
let has_terminal = sk.reservations.iter()
let has_terminal = sk
.reservations
.iter()
.any(|r| matches!(r.function, ReservationFunction::Terminal));
assert!(has_terminal);
}
@@ -539,7 +557,10 @@ mod tests {
// All park blocks must reference the park reservation (id=1).
for &(row, col) in &[(1u8, 2u8), (1, 3), (2, 2), (2, 3)] {
let b = &sk.blocks[row as usize][col as usize];
assert!(b.reservation.is_some(), "block ({row},{col}) should be reserved");
assert!(
b.reservation.is_some(),
"block ({row},{col}) should be reserved"
);
}
}
@@ -553,7 +574,10 @@ mod tests {
for col in 0..4 {
assert_eq!(sk1.blocks[row][col].zoning, sk2.blocks[row][col].zoning);
assert_eq!(sk1.blocks[row][col].position, sk2.blocks[row][col].position);
assert_eq!(sk1.blocks[row][col].density_pct, sk2.blocks[row][col].density_pct);
assert_eq!(
sk1.blocks[row][col].density_pct,
sk2.blocks[row][col].density_pct
);
}
}
assert_eq!(sk1.reservations.len(), sk2.reservations.len());
+10 -4
View File
@@ -147,8 +147,11 @@ mod tests {
fn era_floor_decay_enforces_cracked_minimum() {
// Prosperous district in an EconomicDisruption-era block — still Cracked.
let cond = tile_condition(0.90, Some(&EraCause::EconomicDisruption));
assert_eq!(cond, TileCondition::Cracked,
"EconomicDisruption floor must prevent Intact/Worn");
assert_eq!(
cond,
TileCondition::Cracked,
"EconomicDisruption floor must prevent Intact/Worn"
);
}
#[test]
@@ -162,8 +165,11 @@ mod tests {
fn era_floor_does_not_improve_condition() {
// EconomicDisruption floor = Cracked; Broken score stays Broken.
let cond = tile_condition(0.10, Some(&EraCause::EconomicDisruption));
assert_eq!(cond, TileCondition::Broken,
"Era floor must not improve condition below score-derived value");
assert_eq!(
cond,
TileCondition::Broken,
"Era floor must not improve condition below score-derived value"
);
}
#[test]
+2 -3
View File
@@ -144,9 +144,8 @@ fn collect_names(conn: &Connection) -> rusqlite::Result<Vec<(String, String)>> {
// Star systems — include both system_name and proper_name as separate patterns
// so "Van Maanen's Star" and "GJ 35" both trigger if used in dialogue.
{
let mut stmt = conn.prepare(
"SELECT system_id, system_name, proper_name FROM star_systems",
)?;
let mut stmt =
conn.prepare("SELECT system_id, system_name, proper_name FROM star_systems")?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,