fix(simulation): address PR #142 review — 11 issues resolved

Rust fixes: D-218 Backwater complexity (moved to Full arm), priority
queue dispatch gated on thread saturation, panic→soft-fail in
name_index, duplicated LCG unified into atlas::rng module, misleading
Safety comment removed, D-210 SubBiomeVariant deferred to #948.

Schema/data fixes: atlas_city_positions table (D-211 persistence),
settlement_class column on atlas_city_names, per-body transaction
boundaries in import_province_boundaries, import_city_names.py added
to GENERATOR_SOURCES, Python perf documented.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 15:30:50 +02:00
co-authored by Claude Opus 4.6
parent 1a2bc58e27
commit 332b9404be
13 changed files with 239 additions and 101 deletions
+74
View File
@@ -0,0 +1,74 @@
//! Seeded LCG for deterministic generation (D-010).
//!
//! Shared by all atlas generation modules that need seeded randomness.
//! Uses Knuth's LCG parameters — integer-only arithmetic, no f32, D-010 compliant.
//!
//! Two construction options:
//! - [`AtlasRng::new`] — adds 1 to seed (district_mix convention).
//! - [`AtlasRng::new_mixed`] — mixes seed with a Fibonacci hash constant to
//! avoid degenerate state at 0 (skeleton_gen convention).
/// Seeded linear congruential generator (D-010).
pub struct AtlasRng {
state: u64,
}
impl AtlasRng {
/// Seed by adding 1 — matches the district_mix convention.
pub fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(1),
}
}
/// Seed with Fibonacci hash mix — avoids degenerate state at seed=0.
pub fn new_mixed(seed: u64) -> Self {
Self {
state: seed.wrapping_add(0x9e37_79b9_7f4a_7c15),
}
}
fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.state
}
/// Next pseudorandom `u32` (top 31 bits of the LCG state).
pub fn next_u32(&mut self) -> u32 {
(self.next_u64() >> 33) as u32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deterministic_sequence() {
let mut a = AtlasRng::new(42);
let mut b = AtlasRng::new(42);
for _ in 0..100 {
assert_eq!(a.next_u32(), b.next_u32());
}
}
#[test]
fn different_seeds_differ() {
let mut a = AtlasRng::new(1);
let mut b = AtlasRng::new(2);
let vals_a: Vec<u32> = (0..10).map(|_| a.next_u32()).collect();
let vals_b: Vec<u32> = (0..10).map(|_| b.next_u32()).collect();
assert_ne!(vals_a, vals_b);
}
#[test]
fn mixed_seed_avoids_zero_state() {
let mut r = AtlasRng::new_mixed(0);
// Should not produce a constant zero sequence.
let vals: Vec<u32> = (0..5).map(|_| r.next_u32()).collect();
assert!(vals.iter().any(|&v| v != 0));
}
}