docs(decisions): D-224 SeedChain deterministic seed-derivation contract (#952)
Single typed seed-derivation chain (server/src/seed.rs) descending from the master world seed via domain-separated splitmix64 mixing — the only sanctioned way to derive a child seed. Promotes the existing splitmix64 (EntityRng's mixer) to a shared pub(crate) function and removes ad-hoc wrapping_add pre-mixing. Pins the derivation formula (load-bearing) and documents the scope of effect: heightmaps (Python Layer 0) and Layer 1 (RNG-free) are unaffected; only RNG-using layers (skeleton_gen, future settlement) change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1444,4 +1444,36 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
|
||||
---
|
||||
|
||||
*83 decisions (D-001 through D-223, excluding gaps). Last updated: 2026-05-22 (D-223 — Authored content as a flavored name pool)*
|
||||
### D-224: SeedChain — deterministic seed-derivation contract
|
||||
- **Date:** 2026-05-23
|
||||
- **Decision:** All deterministic generation — the Phase-4 world cascade now, and NPC / storyteller / economic generation later — descends from a single master world seed through one shared type, **`SeedChain`**, living at **`server/src/seed.rs`** (top-level, because seeds are broader than the atlas). It is the **only** sanctioned way to derive a child seed; ad-hoc pre-mixing (`seed.wrapping_add(C)`, currently in `skeleton_gen.rs:248`) is removed, and the doc-comment references to a "SeedChain" in `skeleton_gen.rs:42` / `generator.rs:655` become real.
|
||||
|
||||
**Mixing primitive.** `splitmix64` — already used by `EntityRng::from_seed_and_id` (`simulation/rng.rs`), chosen there specifically to avoid the `wrapping_add` collision class where `(seed=0,id=N) == (seed=1,id=N-1)` — is promoted to a shared `pub(crate)` function in `server/src/seed.rs` and reused. One canonical mixer for the whole codebase.
|
||||
|
||||
**Contract:**
|
||||
```rust
|
||||
pub struct SeedChain(u64); // Copy
|
||||
pub enum SeedDomain { Body, Layer1Topography, Layer3Settlement, Layer4Quarter, Block, Npc /* … */ } // u64 tags
|
||||
impl SeedChain {
|
||||
pub fn root(world_seed: u64) -> Self; // top of the chain
|
||||
pub fn derive(self, domain: SeedDomain, id: u64) -> Self;
|
||||
pub fn atlas_rng(self) -> AtlasRng; // integer-only LCG stream (D-010)
|
||||
pub fn seed(self) -> u64; // raw — for SimRng/ChaCha or further derive()
|
||||
}
|
||||
```
|
||||
|
||||
**Derivation (load-bearing — pinned, because changing it changes every generated world):**
|
||||
`derive(domain, id) = splitmix64(self.0 ^ splitmix64(domain as u64)) ^ splitmix64(id)`.
|
||||
Properties: deterministic; domain-separated (distinct `SeedDomain` tags never share a stream); full avalanche (splitmix64 on each input); integer-only (D-010 #4); chainable (`root → Body → Layer3Settlement → Quarter → Block`). The output is well-distributed, so `AtlasRng::new` is fed the derived seed directly — no `| 1` or golden-ratio pre-mix guard.
|
||||
|
||||
**Scope of effect (verified 2026-05-23):** SeedChain changes only the RNG-*using* layers — the existing `skeleton_gen.rs` (Layer 4 block placement) and the future Layer-3 settlement placement (#955). It does **not** affect Layer 0 heightmaps (produced by the Python `planet_simulation` pipeline, seeded separately via `--seed`, committed as `heightmap.png` files) nor Layer 1 (`drainage`/`features`/`subbiome` are RNG-free — pure functions of the heightmap). The #952 Layer 0→1 golden fixtures are therefore SeedChain-independent and can be captured in any order relative to the SeedChain work.
|
||||
|
||||
- **Rationale:** Three seeding paths had drifted apart — `AtlasRng` (LCG, "callers pre-mix"), `EntityRng` (correct splitmix64 mixing), and ad-hoc `wrapping_add` in atlas callers — while the code already *named* a SeedChain that didn't exist. A single typed derivation chain with domain separation makes every sub-stream reproducible from one world seed, eliminates the `(seed,id)` collision class `wrapping_add` invites, and gives the determinism harness (#952) a stable contract to verify against. Promoting one mixer prevents two divergent implementations.
|
||||
- **Implementation:** #952 (Phase 4, epic #750) — add `server/src/seed.rs`, thread `SeedChain` through the existing atlas RNG callers, then capture the golden fixtures (SHA-256 of `heightmap.png` + msgpack of `Layer1Output`). Pre-Phase-5: no savegames exist, so the seed-stream change needs no migration; D-202's `schema_version` lineage covers future changes once saves exist.
|
||||
- **Raised by:** Jeroen + Claude, `/whats-next` refinement of #952, 2026-05-23.
|
||||
- **Cross-reference:** [D-010](#d-010) (determinism — integer-only, seed→identical output), [D-200](#d-200) (three-tier execution model), [D-208](#d-208) (RNG-free drainage), [D-223](#d-223) (names-only pool — placement uses seeded RNG), `simulation/rng.rs` (EntityRng / splitmix64 precedent)
|
||||
- **Dissent:** None
|
||||
|
||||
---
|
||||
|
||||
*84 decisions (D-001 through D-224, excluding gaps). Last updated: 2026-05-23 (D-224 — SeedChain seed-derivation contract)*
|
||||
|
||||
Reference in New Issue
Block a user