feat(simulation): integer-deterministic Layer-3 settlement placement (#955)
Wire the existing attractor-matching engine (#919/#925) into the generation cascade as Layer 3, and make the whole placement-scoring path integer-deterministic. Layer 3 (D-211): - CascadeLayer::Settlement + Layer3Output (placements) on the snapshot; BodyWorldState gains a `placements` field (the D-203 hot cache). - run_layer3 runs the five-phase match_cities against Layer-1 attractors via the authored D-195 compatibility matrix; pure function of (attractors, cities) — no RNG. cities are passed in by the caller so the cascade stays DB-free and testable. A `// cache seam` marks where a persistent cache wraps it later (#1021). - gen_queue passes &[] for now (Topography needs no cities); the runtime settlement read (gen_queue/layer_proxy) is the #955 follow-on. Integer determinism (D-010 / D-227 — D-195 amended): - Wiring match_cities into the deterministic cascade made its f32 scoring a live cross-platform divergence risk (a near-tie comparison or the Hungarian's f32 reductions can round differently per platform → a different world from the same seed). Converted the entire path to integers: CompatibilityMatrix is a 0-100 affinity table; attractor strength is 0-100 and terrain cost is a percent (100 = baseline), quantized once at the Layer-1 feature boundary; cell_score, the Hungarian, and CityPlacement.score are i64. No f32 in any placement or ranking decision. - Layer-1 golden fixture rebaked: confirmed selection/positions are unchanged (same 256 attractors, 93 river cells) — only the strength/cost representation changed. Tests: lib green (1292); new settlement_layer_places_cities_deterministically covers placement + determinism + propagation into BodyWorldState. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -439,27 +439,62 @@ pub struct GeographicAttractor {
|
||||
/// Pixel position in heightmap space [row, col].
|
||||
pub position: (u16, u16),
|
||||
pub attractor_type: AttractorType,
|
||||
/// Normalized strength 0.0–1.0. Derived from flow accumulation or habitability score.
|
||||
pub strength: f32,
|
||||
/// Strength on a 0–100 integer scale (100 = strongest). Quantized from flow
|
||||
/// accumulation / habitability at Layer-1 output. Integer for D-010
|
||||
/// determinism — every ranking/scoring decision is exact, no f32 (#955).
|
||||
pub strength: i32,
|
||||
/// Fine-grained terrain classification at this position (D-210).
|
||||
pub sub_biome: SubBiomeVariant,
|
||||
/// Infrastructure-build cost multiplier (1.0 = baseline grassland; higher =
|
||||
/// more expensive). Derived from `sub_biome` + local slope. Consumed by the
|
||||
/// attractor-matching pipeline (D-211) to penalize marginal cities. (D-210)
|
||||
pub terrain_modification_cost: f32,
|
||||
/// Infrastructure-build cost as a percent of baseline (100 = baseline
|
||||
/// grassland; >100 = more expensive, e.g. 200 = 2× cost). Derived from
|
||||
/// `sub_biome` + local slope; penalizes marginal cities in the matching
|
||||
/// pipeline (D-211). Integer for D-010 determinism (#955). (D-210)
|
||||
pub terrain_modification_cost: i32,
|
||||
}
|
||||
|
||||
/// Compatibility weights between economic roles and attractor types.
|
||||
/// A 10×7 matrix (10 economic_role values × 7 AttractorType variants).
|
||||
/// Each cell is a weight multiplier 0.0–3.0 applied during attractor-matching scoring.
|
||||
/// Source: D-195
|
||||
/// Each cell is an integer affinity 0–100 (100 = ideal terrain for that role,
|
||||
/// ~50 = neutral, low = poor) used during attractor-matching scoring.
|
||||
/// Integer for D-010 determinism (#955). Source: D-195
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct CompatibilityMatrix {
|
||||
/// Row order: manufacturing, financial, agricultural, extraction, service_mixed,
|
||||
/// institutional, transit_hub, research, military, residential.
|
||||
/// Column order: RiverMouth, CoastalAccess, RiverCrossing, ValleyFloor,
|
||||
/// PassEntrance, LakeShore, PlainCenter.
|
||||
pub weights: [[f32; 7]; 10],
|
||||
pub weights: [[i32; 7]; 10],
|
||||
}
|
||||
|
||||
impl CompatibilityMatrix {
|
||||
/// The canonical D-195 authored compatibility table (#955).
|
||||
///
|
||||
/// Integer affinity 0–100: 100 = ideal terrain for the role, ~50 = neutral,
|
||||
/// low (10–30) = poor fit (graduated, never a hard 0 — D-195). Re-authored
|
||||
/// on the 0–100 scale from D-195's relative examples (the agricultural→
|
||||
/// ValleyFloor 3.0 peak maps to the global max 100).
|
||||
///
|
||||
/// Rows: manufacturing, financial, agricultural, extraction, service_mixed,
|
||||
/// institutional, transit_hub, research, military, residential.
|
||||
/// Cols: RiverMouth, CoastalAccess, RiverCrossing, ValleyFloor,
|
||||
/// PassEntrance, LakeShore, PlainCenter.
|
||||
pub fn d195() -> Self {
|
||||
CompatibilityMatrix {
|
||||
weights: [
|
||||
// RMth Coast RXing Vall Pass Lake Plain
|
||||
[95, 80, 75, 60, 30, 35, 55], // manufacturing
|
||||
[70, 90, 65, 25, 40, 45, 70], // financial
|
||||
[85, 30, 55, 100, 10, 70, 90], // agricultural
|
||||
[30, 35, 40, 65, 95, 30, 25], // extraction
|
||||
[60, 65, 70, 50, 35, 50, 70], // service_mixed
|
||||
[65, 65, 60, 45, 20, 55, 90], // institutional
|
||||
[65, 75, 100, 35, 95, 25, 60], // transit_hub
|
||||
[40, 70, 35, 75, 40, 95, 60], // research
|
||||
[60, 75, 80, 55, 100, 25, 40], // military
|
||||
[70, 85, 50, 65, 20, 85, 70], // residential
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user