//! Deterministic seed derivation and the generation RNG primitive (D-224). //! //! [`SeedChain`] is the single sanctioned path from the master world seed to any //! per-body / per-layer / per-district sub-stream. Derivation is domain-separated //! `splitmix64` mixing: deterministic, full-avalanche, integer-only (D-010 #4), //! and chainable (`root → Body → Layer3Settlement → Quarter → Block`). //! //! This module is the home of every deterministic-seeding primitive: //! - [`splitmix64`] — the one canonical mixer, shared with //! [`crate::simulation::rng`], where `EntityRng` first introduced it //! specifically to avoid the `wrapping_add` collision class //! (`(seed=0,id=N) == (seed=1,id=N-1)`). //! - [`AtlasRng`] — the integer-only LCG stream used by the generation cascade, //! normally produced via [`SeedChain::atlas_rng`]. //! - [`SeedChain`] / [`SeedDomain`] — the derivation contract itself. /// SplitMix64 finalizer — strong avalanche, applied to every seed input. /// /// Steele, Lea & Flood (2014). Integer-only (D-010 #4). `splitmix64(0)` returns /// the documented reference value `0xe220a8397b1dcdaf`. pub(crate) fn splitmix64(mut x: u64) -> u64 { x = x.wrapping_add(0x9e3779b97f4a7c15); x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb); x ^ (x >> 31) } /// FNV-1a (64-bit) hash of a string — the repo's deterministic `&str → u64` /// convention (matches `TemplateId`/`TriangleId` in `simulation::triangle` and /// the `observer` snapshot hash). The single sanctioned way to turn a string /// `body_id` into a `SeedDomain::Body` id (D-224), so two callers can never /// derive different worlds from the same seed via different ad-hoc hashes. pub(crate) fn fnv1a_64(s: &str) -> u64 { let mut hash: u64 = 0xcbf2_9ce4_8422_2325; // FNV-1a offset basis for byte in s.bytes() { hash ^= byte as u64; hash = hash.wrapping_mul(0x100_0000_01b3); // FNV-1a prime } hash } /// Seeded linear congruential generator (D-010). /// /// Knuth's LCG parameters — integer-only arithmetic, no `f32`, D-010 compliant. /// The generation cascade's pseudorandom stream. Normally seeded through /// [`SeedChain::atlas_rng`]; `SeedChain` output is well-distributed, so no /// caller-side pre-mixing is needed. pub struct AtlasRng { state: u64, } impl AtlasRng { /// Create a new RNG from a seed. /// /// A `SeedChain`-derived seed is already well-distributed. If seeding from a /// raw value that could realistically be 0, mix it first (or derive it /// through [`SeedChain`]). pub fn new(seed: u64) -> Self { Self { state: seed } } 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 } } /// Seed-derivation domain (D-224). /// /// Each variant is a distinct domain tag, so a per-body stream and a per-district /// stream derived with the same `id` never share a sequence. The discriminants /// are explicit and **stable**: changing one re-rolls every world that derives a /// seed through it, so they are append-only — never renumber an existing variant. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u64)] pub enum SeedDomain { /// Per-body generation (keyed by body StableId). Body = 1, /// Layer 1 topography (reserved — currently RNG-free; see D-224 scope note). Layer1Topography = 2, /// Layer 3 settlement placement (keyed by candidate/site index). Layer3Settlement = 3, /// Layer 4 quarter skeleton (keyed by quarter index). Layer4Quarter = 4, /// Block-level detail (keyed by block index within a quarter). Block = 5, /// NPC generation (keyed by NPC StableId). Npc = 6, /// Anti-squaring domain warp (D-239 §4, T-1026). /// Keyed by per-tile position id (see `atlas::domain_warp::pos_to_id`). DomainWarp = 7, /// ChunkContext derivation (64 m carrier, D-239 §1, T-1028). /// Keyed by region-scale position id (see `atlas::chunk_context::pos_to_id`). ChunkContext = 8, /// Per-voxel sub-chunk derivation (1 m, D-239 §10, T-1028). Keyed by the /// post-warp integer voxel-position id. Distinct from `ChunkContext` so the /// voxel stream can never collide with the region-scale meander seed (D-224 /// domain separation). Voxel = 9, /// Seasonal cover cluster scatter (D-239 §3, T-1030). Keyed by the coarse /// cluster-cell id (zigzag-encoded and Cantor-paired). Distinct from `Voxel` /// so the per-cluster cover hash can never collide with the per-voxel terrain /// stream — domain separation guarantees no freeze-to-terrain correlation. Cover = 10, } /// A position in the deterministic seed tree (D-224). /// /// `Copy` and cheap to pass by value. Construct with [`SeedChain::root`] from the /// master world seed, then descend with [`SeedChain::derive`]. Materialize an /// [`AtlasRng`] with [`SeedChain::atlas_rng`], or read the raw seed with /// [`SeedChain::seed`] to feed `SimRng::new` or to derive further. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SeedChain(u64); impl SeedChain { /// Root of the chain — the master world seed. pub fn root(world_seed: u64) -> Self { Self(world_seed) } /// This body's position in the seed tree, from the master `world_seed` and /// its string `body_id` (D-224). The one canonical `body_id → u64` path: /// `root(world_seed).derive(Body, fnv1a_64(body_id))`. Use this everywhere a /// body needs a seed, so all callers agree on the mapping. pub fn for_body(world_seed: u64, body_id: &str) -> Self { Self::root(world_seed).derive(SeedDomain::Body, fnv1a_64(body_id)) } /// Derive a child seed for `(domain, id)`. /// /// `splitmix64(self ^ splitmix64(domain)) ^ splitmix64(id)` (D-224, load-bearing). /// Deterministic, domain-separated, full-avalanche, integer-only. pub fn derive(self, domain: SeedDomain, id: u64) -> Self { Self(splitmix64(self.0 ^ splitmix64(domain as u64)) ^ splitmix64(id)) } /// Materialize the integer-only generation RNG at this point in the chain. pub fn atlas_rng(self) -> AtlasRng { AtlasRng::new(self.0) } /// The raw derived seed — feed to `SimRng::new` or a further /// [`SeedChain::derive`]. pub fn seed(self) -> u64 { self.0 } } #[cfg(test)] mod tests { use super::*; #[test] fn splitmix64_known_vector() { // Documented SplitMix64 output for seed 0. assert_eq!(splitmix64(0), 0xe220a8397b1dcdaf); } #[test] fn splitmix64_avalanche() { // Adjacent inputs produce uncorrelated outputs (the property that makes // it preferable to wrapping_add). let a = splitmix64(1); let b = splitmix64(2); assert_ne!(a, b); assert!((a ^ b).count_ones() > 16, "weak avalanche between 1 and 2"); } #[test] fn atlas_rng_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 atlas_rng_different_seeds_differ() { let mut a = AtlasRng::new(1); let mut b = AtlasRng::new(2); let vals_a: Vec = (0..10).map(|_| a.next_u32()).collect(); let vals_b: Vec = (0..10).map(|_| b.next_u32()).collect(); assert_ne!(vals_a, vals_b); } #[test] fn atlas_rng_from_seedchain_is_deterministic() { let chain = SeedChain::root(42).derive(SeedDomain::Body, 7); let mut a = chain.atlas_rng(); let mut b = chain.atlas_rng(); assert_eq!(a.next_u32(), b.next_u32()); } #[test] fn derive_is_deterministic() { let a = SeedChain::root(42).derive(SeedDomain::Body, 7); let b = SeedChain::root(42).derive(SeedDomain::Body, 7); assert_eq!(a.seed(), b.seed()); } #[test] fn domains_are_separated() { // Same root + same id, different domain → different stream. let root = SeedChain::root(42); let body = root.derive(SeedDomain::Body, 5).seed(); let quarter = root.derive(SeedDomain::Layer4Quarter, 5).seed(); let block = root.derive(SeedDomain::Block, 5).seed(); assert_ne!(body, quarter); assert_ne!(body, block); assert_ne!(quarter, block); } #[test] fn ids_are_separated() { let root = SeedChain::root(42); assert_ne!( root.derive(SeedDomain::Body, 1).seed(), root.derive(SeedDomain::Body, 2).seed() ); } #[test] fn different_world_seeds_diverge() { let a = SeedChain::root(1).derive(SeedDomain::Body, 7).seed(); let b = SeedChain::root(2).derive(SeedDomain::Body, 7).seed(); assert_ne!(a, b); } #[test] fn chains_compose_deterministically_and_order_matters() { let base = SeedChain::root(42).derive(SeedDomain::Body, 7); // Deterministic at each depth. assert_eq!( base.derive(SeedDomain::Block, 3).seed(), SeedChain::root(42) .derive(SeedDomain::Body, 7) .derive(SeedDomain::Block, 3) .seed() ); // A deeper chain differs from deriving the same leaf off the root. assert_ne!( base.derive(SeedDomain::Block, 3).seed(), SeedChain::root(42).derive(SeedDomain::Block, 3).seed() ); } #[test] fn seed_domain_discriminants_are_pinned() { // Load-bearing (D-224): renumbering a variant re-rolls every world that // derives through it. This guard fails CI the moment a tag changes — // append new variants, never renumber existing ones. assert_eq!(SeedDomain::Body as u64, 1); assert_eq!(SeedDomain::Layer1Topography as u64, 2); assert_eq!(SeedDomain::Layer3Settlement as u64, 3); assert_eq!(SeedDomain::Layer4Quarter as u64, 4); assert_eq!(SeedDomain::Block as u64, 5); assert_eq!(SeedDomain::Npc as u64, 6); assert_eq!(SeedDomain::DomainWarp as u64, 7); assert_eq!(SeedDomain::ChunkContext as u64, 8); assert_eq!(SeedDomain::Voxel as u64, 9); assert_eq!(SeedDomain::Cover as u64, 10); } #[test] fn root_zero_is_non_degenerate() { // World seed 0 must still produce a well-distributed stream. let derived = SeedChain::root(0).derive(SeedDomain::Body, 0); assert_ne!(derived.seed(), 0, "derived seed must not be zero"); let mut rng = derived.atlas_rng(); assert_ne!(rng.next_u32(), 0, "RNG from world seed 0 must not stall"); } #[test] fn for_body_deterministic_and_distinct() { assert_eq!( SeedChain::for_body(42, "GJ1c").seed(), SeedChain::for_body(42, "GJ1c").seed() ); assert_ne!( SeedChain::for_body(42, "GJ1c").seed(), SeedChain::for_body(42, "GJ1d").seed() ); // for_body is exactly root().derive(Body, fnv1a_64(id)). assert_eq!( SeedChain::for_body(42, "GJ1c").seed(), SeedChain::root(42) .derive(SeedDomain::Body, fnv1a_64("GJ1c")) .seed() ); } }