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:
@@ -50,7 +50,10 @@ pub struct RawAttractor {
|
||||
pub row: u16,
|
||||
pub col: u16,
|
||||
pub attractor_type: AttractorType,
|
||||
pub strength: f32,
|
||||
/// Strength on a 0–100 integer scale (100 = strongest). Quantized here from
|
||||
/// the f32 flow-accumulation ratio — the single f32→integer boundary, after
|
||||
/// which every ranking/scoring decision is integer (D-010, #955).
|
||||
pub strength: i32,
|
||||
}
|
||||
|
||||
/// Precomputed per-cell terrain fields, shared by feature extraction (D-209)
|
||||
@@ -311,7 +314,8 @@ pub fn extract_attractors(
|
||||
row: r as u16,
|
||||
col: c as u16,
|
||||
attractor_type: at,
|
||||
strength: strength.clamp(0.0, 1.0),
|
||||
// The single f32→integer boundary: quantize the 0.0–1.0 ratio to 0–100.
|
||||
strength: (strength.clamp(0.0, 1.0) * 100.0).round() as i32,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -462,9 +466,11 @@ pub fn extract_attractors(
|
||||
}
|
||||
for group in by_type.values_mut() {
|
||||
group.sort_by(|a, b| {
|
||||
let sa = (a.strength * 1e6) as i64;
|
||||
let sb = (b.strength * 1e6) as i64;
|
||||
sb.cmp(&sa).then(a.row.cmp(&b.row)).then(a.col.cmp(&b.col))
|
||||
// strength is integer now — rank by it directly (descending).
|
||||
b.strength
|
||||
.cmp(&a.strength)
|
||||
.then(a.row.cmp(&b.row))
|
||||
.then(a.col.cmp(&b.col))
|
||||
});
|
||||
}
|
||||
let mut kept: Vec<RawAttractor> = Vec::with_capacity(MAX_ATTRACTORS);
|
||||
|
||||
Reference in New Issue
Block a user