feat(simulation): wire D-199 economic read-set into generation context (#954)
Layer 2 clean half. GenerateSkeleton now builds a CityGenerationContext from a body's real economic read-set instead of stub defaults. - New CityContextReader (server/src/atlas/city_context_reader.rs): reads the 6-field D-199 set (economic_role, prosperity_baseline, population, dominant_faction, founding_age_years, settlement_class) via one JOIN; prosperity_baseline derived per D-197. - GenWorkItem::GenerateSkeleton carries the resolved context; run_work_item builds the real context instead of DistrictWorldState::default(). - Left at defaults (deferred, documented): dominant_bulk_class / dominant_production_ubiquity (#982, design-blocked), trait_selection (#1005), morphology_zone + Layer-3 fields (attractor placement / D-204 / D-214). 16 new tests; cargo check/clippy --all-targets -D warnings clean; 1279 lib tests pass; fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,9 @@ use crossbeam_channel::{Receiver, Sender};
|
||||
use crate::atlas::body_world_state::BodyWorldState;
|
||||
use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
|
||||
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
|
||||
use crate::atlas::skeleton_gen::generate_skeleton;
|
||||
use crate::seed::SeedChain;
|
||||
use crate::simulation::generator::DistrictWorldState;
|
||||
use crate::simulation::generator::{CityGenerationContext, DistrictWorldState};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Priority
|
||||
@@ -68,7 +69,35 @@ pub enum GenWorkItem {
|
||||
body_seed: SeedChain,
|
||||
},
|
||||
/// Generate a Phase 1 DistrictSkeleton for this city.
|
||||
GenerateSkeleton { city_id: u64 },
|
||||
///
|
||||
/// `context` is the D-199 economic read-set pre-resolved at dispatch time.
|
||||
/// All 6 required fields must be populated before this item is submitted
|
||||
/// (D-199: "Missing fields abort the task … generation does not proceed with
|
||||
/// partial context").
|
||||
///
|
||||
/// `body_id` routes the resulting `SkeletonGenerated` completion into the
|
||||
/// correct `BodyWorldState` cache entry (D-230).
|
||||
///
|
||||
/// `district_id` is the stable content-addressable id for the generated
|
||||
/// district (keyed by city position + world seed).
|
||||
///
|
||||
/// `economic_role`, `population`, and `founding_age_years` are D-199 fields
|
||||
/// carried alongside the context because `generate_skeleton` accepts them as
|
||||
/// separate parameters (its signature is not changed by this ticket).
|
||||
GenerateSkeleton {
|
||||
city_id: u64,
|
||||
body_id: String,
|
||||
/// D-199 economic read-set + all other context fields.
|
||||
context: Box<CityGenerationContext>,
|
||||
/// Stable content-addressable district id (D-194/D-230).
|
||||
district_id: u64,
|
||||
/// District-level seed chain (D-224).
|
||||
chain: SeedChain,
|
||||
// D-199 raw fields passed to generate_skeleton separately.
|
||||
economic_role: String,
|
||||
population: i64,
|
||||
founding_age_years: u32,
|
||||
},
|
||||
/// Pre-fill a chunk in an existing district.
|
||||
FillChunk {
|
||||
district_id: u64,
|
||||
@@ -339,13 +368,35 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
|
||||
reason: format!("heightmap load failed: {e}"),
|
||||
},
|
||||
},
|
||||
GenWorkItem::GenerateSkeleton { city_id } => {
|
||||
// Stub: real skeleton generation (#957) will populate `body_id` from
|
||||
// the CityGenerationContext and `state` from the plan phase (D-230).
|
||||
GenWorkItem::GenerateSkeleton {
|
||||
city_id,
|
||||
body_id,
|
||||
context,
|
||||
district_id,
|
||||
chain,
|
||||
economic_role,
|
||||
population,
|
||||
founding_age_years,
|
||||
} => {
|
||||
// Build the Phase 1 skeleton from the pre-resolved D-199 context.
|
||||
// `economic_role`, `population`, and `founding_age_years` are the
|
||||
// D-199 raw fields carried alongside the context because
|
||||
// `generate_skeleton` accepts them as separate parameters.
|
||||
let skeleton = generate_skeleton(
|
||||
context,
|
||||
*population,
|
||||
economic_role,
|
||||
*district_id,
|
||||
*founding_age_years,
|
||||
*chain,
|
||||
);
|
||||
GenCompletion::SkeletonGenerated {
|
||||
city_id: *city_id,
|
||||
body_id: String::new(),
|
||||
state: Box::new(DistrictWorldState::default()),
|
||||
body_id: body_id.clone(),
|
||||
state: Box::new(DistrictWorldState {
|
||||
skeleton,
|
||||
block_tags: std::collections::BTreeMap::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
GenWorkItem::FillChunk {
|
||||
@@ -401,6 +452,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a minimal `GenerateSkeleton` work item with a stub context.
|
||||
///
|
||||
/// The stub context uses Commission/Regional/Urban defaults — the same
|
||||
/// values the existing skeleton_gen tests use. These tests exercise queue
|
||||
/// mechanics (ordering, saturation, drain), not economic read-set content.
|
||||
fn gen_skeleton(city_id: u64) -> GenWorkItem {
|
||||
use crate::simulation::generator::{
|
||||
BulkClass, CityGenerationContext, FoundingOrientation, MorphologyZone,
|
||||
PoliticalArchetype, ProductionUbiquity, SettingType, WorldTier,
|
||||
};
|
||||
GenWorkItem::GenerateSkeleton {
|
||||
city_id,
|
||||
body_id: format!("TestBody{city_id}"),
|
||||
context: Box::new(CityGenerationContext {
|
||||
city_id,
|
||||
political_archetype: PoliticalArchetype::Commission,
|
||||
prosperity_baseline: 0.6,
|
||||
surrounding_biome: SettingType::Urban,
|
||||
road_entry_directions: vec![],
|
||||
footprint_radius_km: 5.0,
|
||||
founding_orientation: FoundingOrientation::Cardinal,
|
||||
world_tier: WorldTier::Regional,
|
||||
morphology_zone: MorphologyZone::AlluvialPlain,
|
||||
trait_selection: vec![],
|
||||
dominant_bulk_class: BulkClass::NonPhysical,
|
||||
dominant_production_ubiquity: ProductionUbiquity::Common,
|
||||
}),
|
||||
district_id: city_id * 10,
|
||||
chain: SeedChain::root(42 + city_id),
|
||||
economic_role: "service_mixed".to_string(),
|
||||
population: 500_000,
|
||||
founding_age_years: 200,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_and_drain() {
|
||||
let q = make_queue();
|
||||
@@ -437,18 +523,9 @@ mod tests {
|
||||
// Uses 3 threads so all items can dispatch without hitting saturation.
|
||||
let q = GenerationQueue::with_threads(3);
|
||||
// Using GenerateSkeleton (no dedup logic) to test ordering directly.
|
||||
q.submit(
|
||||
GenWorkItem::GenerateSkeleton { city_id: 1 },
|
||||
GenPriority::Low,
|
||||
);
|
||||
q.submit(
|
||||
GenWorkItem::GenerateSkeleton { city_id: 2 },
|
||||
GenPriority::Immediate,
|
||||
);
|
||||
q.submit(
|
||||
GenWorkItem::GenerateSkeleton { city_id: 3 },
|
||||
GenPriority::Medium,
|
||||
);
|
||||
q.submit(gen_skeleton(1), GenPriority::Low);
|
||||
q.submit(gen_skeleton(2), GenPriority::Immediate);
|
||||
q.submit(gen_skeleton(3), GenPriority::Medium);
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let completions = q.drain_completions();
|
||||
assert_eq!(completions.len(), 3);
|
||||
|
||||
Reference in New Issue
Block a user