fix(simulation): address PR #143 review (#1006)

- Add D-230 skeleton: DistrictSkeleton field to DistrictWorldState;
  cascade Default to DistrictSkeleton + contained enums/structs. Box the
  GenCompletion::SkeletonGenerated state to avoid large_enum_variant.
- Derive PartialEq on FloorExtent/FloorHeightProfile/DistrictWorldState/
  CityGenerationContext (+ minimal cascade) for downstream assert_eq tests.
- Add 4 unit tests for floor_at_voxel_z / voxel_range_for_floor (uniform,
  basement, variable heights, boundary) — the Q-104 deliverable.
- Drop unused smallvec direct dep (stays transitive via bevy_ecs).
- Key districts insert by skeleton.district_id, sharpen TODO(#957).

clippy --all-targets -D warnings clean; 1263 lib tests pass; fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 22:06:57 +02:00
co-authored by Claude Opus 4.8
parent 899f447eed
commit 99da796cde
5 changed files with 161 additions and 42 deletions
-4
View File
@@ -1492,7 +1492,6 @@ dependencies = [
"serde_json",
"serde_norway",
"sha2",
"smallvec",
"sysinfo",
"thiserror",
"toml",
@@ -1552,9 +1551,6 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "smol_str"
-1
View File
@@ -31,7 +31,6 @@ bytemuck = "1"
png = "0.17"
toml = "0.8"
aho-corasick = "1"
smallvec = { version = "1", features = ["serde"] }
# Economics simulation — Leontief + tâtonnement + D-180 event port (#821)
econ-sim = { path = "../tooling/econ-sim" }
+4 -3
View File
@@ -103,8 +103,9 @@ pub enum GenCompletion {
/// The body this skeleton belongs to — used to route state into
/// `BodyWorldState.districts` (D-230).
body_id: String,
/// District-level world state (block tags) produced by the plan phase (D-230).
state: DistrictWorldState,
/// District-level world state (skeleton + block tags) produced by the plan phase (D-230).
/// Boxed to keep `GenCompletion` variant sizes balanced (D-230 skeleton is ~2.7 KB).
state: Box<DistrictWorldState>,
},
ChunkFilled {
district_id: u64,
@@ -344,7 +345,7 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
GenCompletion::SkeletonGenerated {
city_id: *city_id,
body_id: String::new(),
state: DistrictWorldState::default(),
state: Box::new(DistrictWorldState::default()),
}
}
GenWorkItem::FillChunk {
+10 -3
View File
@@ -88,9 +88,16 @@ fn drain_generation_completions(
} => {
if !body_id.is_empty() {
if let Some(body_state) = cache.peek_mut(&body_id) {
// TODO(#957): keying by city_id is a stub — a city has multiple districts;
// use DistrictSkeleton.district_id as the DistrictId key when real skeleton gen lands.
body_state.districts.insert(city_id, state);
// Key by state.skeleton.district_id (D-194/D-230): a city has many
// districts, each with its own DistrictId. `city_id` is only the
// dispatch key used in the work item — the canonical insert key is
// the district's own stable id. TODO(#957): the stub GenerateSkeleton
// returns a default skeleton with district_id=0; real gen (#957) will
// populate it from CityGenerationContext.
let _ = city_id; // used as dispatch key only; district_id is the map key
body_state
.districts
.insert(state.skeleton.district_id, *state);
} else {
tracing::warn!(
city_id,
+147 -31
View File
@@ -103,7 +103,7 @@ pub type PlacedObject = String;
/// Determines simulation fidelity budget and NPC complexity ceiling.
///
/// Source: D-218, workshop-outcomes.md
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum WorldTier {
/// Hub system. Full simulation, high faction pressure.
Epicenter,
@@ -114,12 +114,13 @@ pub enum WorldTier {
/// Transit stop. Pass-through node. Moderate complexity ceiling.
Passage,
/// Not simulated until player approaches. Minimal complexity ceiling.
#[default]
Waypoint,
}
/// Generator content budget for a district.
/// Derived from WorldTier + SettingType at Phase 1.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum ComplexityTier {
/// Full social architecture. All Tier 1+2 guarantees. 20-80+ NPCs.
Full,
@@ -128,26 +129,34 @@ pub enum ComplexityTier {
/// Minimal social architecture. Tier 1 only. 1-8 NPCs.
Minimal,
/// No social architecture. Pure terrain. 0 NPCs. No guarantees.
#[default]
Empty,
}
/// Physical setting type for a district.
/// Merged from Gestalt's SettingGeometry and Tyre's TerrainType.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum SettingType {
Station,
#[default]
Urban,
Agricultural,
Maritime,
Wilderness { biome: Biome },
Water { water_type: WaterType },
Wilderness {
biome: Biome,
},
Water {
water_type: WaterType,
},
Transitional,
Orbital,
Specialized { function: SpecializedFunction },
Specialized {
function: SpecializedFunction,
},
}
/// Classification of a district (high-level function).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum DistrictType {
LogisticsHub,
Residential,
@@ -155,15 +164,17 @@ pub enum DistrictType {
Industrial,
Administrative,
Entertainment,
#[default]
MixedUse,
Transit,
Specialized,
}
/// How blocks are placed within the district's 512×512 footprint.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub enum DistrictLayoutMode {
/// Standard Cartesian grid — perpendicular streets.
#[default]
Grid,
/// Organic placement with per-block offsets and rotations.
Organic {
@@ -172,7 +183,7 @@ pub enum DistrictLayoutMode {
}
/// Zoning classification for a block or floor zone.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub enum ZoningType {
Commercial,
Residential,
@@ -181,11 +192,12 @@ pub enum ZoningType {
Transit,
Recreational,
Restricted,
#[default]
Mixed,
}
/// Reservation function — what purpose a multi-block reservation serves.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ReservationFunction {
Skyscraper,
Park,
@@ -217,7 +229,7 @@ pub enum ZoneAccessTier {
}
/// Vertical corridor type (how floors connect in a multi-level reservation).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum VerticalCorridorType {
Stairwell,
Elevator,
@@ -286,7 +298,7 @@ pub enum WallBackside {
}
/// Era cause — why a block has the era tag it has.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum EraCause {
Original,
CorporateMerger,
@@ -338,7 +350,7 @@ pub enum PoliticalArchetype {
/// Primary spatial axis of a city's original street grid.
/// Derived from the matched attractor type (D-211). Controls district grid rotation.
/// Source: D-213
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum FoundingOrientation {
/// Street grid perpendicular to coastline. `facing_degrees`: compass bearing toward water (0359).
Coastal { facing_degrees: u16 },
@@ -534,7 +546,7 @@ impl Default for FloorHeightProfile {
/// Floor/basement extent for a building, bridging D-110 floor-index addressing
/// and D-227 physical voxel-z (D-229, resolves Q-104).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct FloorExtent {
/// Index of the bottom floor (negative = basement, per D-110).
pub base_floor: i8,
@@ -682,7 +694,7 @@ pub struct ArchitectureFlavorRef {
///
/// Written once inside `GenerateSkeleton`; read-only thereafter by FillChunk
/// (D-230), the guarantee audit (D-097), and the Phase-6 interior generator (D-231).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct BuildingPropertyTag {
/// What the building is — matches a D-142 RON zone-type `id`.
pub zone_type_id: ZoneTypeId,
@@ -744,7 +756,7 @@ pub enum DoorInitialState {
}
/// Door credential requirement (D-231).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum DoorCredential {
None,
/// Door is only accessible during specified hours (023, inclusive range).
@@ -769,7 +781,7 @@ pub enum DoorCredential {
}
/// What a door connects to on the other side (D-231).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum DoorConnectsTo {
/// Opens onto a named street.
Street { street_id: String },
@@ -783,7 +795,7 @@ pub enum DoorConnectsTo {
///
/// A future interior generator produces a deterministic floor plan from this
/// descriptor + `SeedChain` with **no other system queried**.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct InteriorDescriptor {
pub zone_type_id: ZoneTypeId,
pub entry_class: BuildingEntryClass,
@@ -805,7 +817,7 @@ pub enum LayoutMode {
}
/// A single door on a building — the step3→step4 boundary (D-231).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct DoorSpec {
/// Which face of the building footprint this door is on.
pub facing: CardinalDirection,
@@ -897,8 +909,12 @@ pub enum ProductionUbiquity {
/// Output of `GenerateSkeleton` extended to include building-property tags.
/// Stored in `BodyWorldState.districts: BTreeMap<DistrictId, DistrictWorldState>`.
/// `BTreeMap` for D-010 determinism.
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
///
/// D-230: `{ skeleton: DistrictSkeleton, block_tags: BTreeMap<(u8,u8), Vec<BuildingPropertyTag>> }`
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub struct DistrictWorldState {
/// Phase 1 skeleton produced by `GenerateSkeleton` (D-230).
pub skeleton: DistrictSkeleton,
/// Building property tags keyed by block position (row, col) within the 4×4 grid.
/// Each entry is a Vec of one tag per building footprint placed in that block.
/// `BTreeMap` for D-010 determinism (no HashMap non-determinism).
@@ -912,7 +928,7 @@ pub struct DistrictWorldState {
///
/// Amended by D-229/D-232/D-233: adds `morphology_zone`, `trait_selection`,
/// `dominant_bulk_class`, `dominant_production_ubiquity`.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct CityGenerationContext {
/// Foreign key into atlas_city_names.id
pub city_id: u64,
@@ -945,7 +961,7 @@ pub struct CityGenerationContext {
// ---------------------------------------------------------------------------
/// Organic layout placement for a single block.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct BlockPlacement {
/// Offset from grid-aligned position (±16 sim tiles per axis max).
pub offset: (i16, i16),
@@ -958,7 +974,7 @@ pub struct BlockPlacement {
/// Single block within a district's 4×4 block grid.
/// Each block = 128×128 sim tiles = 2×2 chunks.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct BlockSkeleton {
/// Grid position (03, 03).
pub position: (u8, u8),
@@ -981,7 +997,7 @@ pub struct BlockSkeleton {
/// **D-110:** `z_level` is `i8` — negative values represent basements
/// (e.g. `z_level = -1` for a sub-basement). This differs from `z_levels: u8`
/// on the parent reservation which counts total floors (always ≥ 1).
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct FloorZone {
/// Absolute z-level of this floor. Negative for sub-ground levels (D-110).
pub z_level: i8,
@@ -991,7 +1007,7 @@ pub struct FloorZone {
}
/// Vertical connection spec within a multi-level reservation.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct VerticalCorridorSpec {
/// Which blocks this vertical corridor passes through.
pub block_coords: Vec<(u8, u8)>,
@@ -1003,14 +1019,14 @@ pub struct VerticalCorridorSpec {
}
/// Visual palette for a zone — base material + contextual modifiers.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct ZonePalette {
pub base: BasePalette,
pub modifiers: Vec<PaletteModifier>,
}
/// Contextual modifier applied on top of a base palette.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum PaletteModifier {
EconomicFunction(EconomicModifier),
Era(Era),
@@ -1020,7 +1036,7 @@ pub enum PaletteModifier {
}
/// Social site placement within a district.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct SocialSitePlacement {
pub site_id: SocialSiteId,
/// Which blocks this site spans.
@@ -1033,7 +1049,7 @@ pub struct SocialSitePlacement {
}
/// Triangle assignment within a social site.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct TriangleAssignment {
pub template: TriangleTemplate,
pub purposes: Vec<TrianglePurpose>,
@@ -1086,7 +1102,7 @@ pub struct StructuralChange {
/// `base_z: -3, z_levels: 24`
/// A deep mine shaft descending 30 floors below ground:
/// `base_z: -30, z_levels: 30`
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct MultiBlockReservation {
/// Which blocks (grid positions) are part of this reservation.
pub blocks: Vec<(u8, u8)>,
@@ -1128,7 +1144,7 @@ pub struct MultiBlockReservation {
///
/// All `Vec<_>` fields are stable-ordered at generation time (sorted by a
/// deterministic key). Generation reproduces identical output for the same seed.
#[derive(Serialize, Deserialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
pub struct DistrictSkeleton {
// ── Identity ──────────────────────────────────────────
pub district_id: DistrictId,
@@ -1178,6 +1194,106 @@ pub struct DistrictSkeleton {
mod tests {
use super::*;
// ── FloorExtent: floor_at_voxel_z and voxel_range_for_floor ─────────────
/// (a) Uniform building at ground level — round-trip voxel↔floor.
///
/// Uniform(3), base_floor=0, floor_count=2 → floors 0,1; heights 3 each.
/// Floor 0 occupies voxels 02; floor 1 occupies voxels 35.
#[test]
fn floor_extent_uniform_ground_round_trip() {
let extent = FloorExtent {
base_floor: 0,
floor_count: 2,
heights: FloorHeightProfile::Uniform(3),
};
// Range for each floor.
assert_eq!(extent.voxel_range_for_floor(0), Some((0, 2)));
assert_eq!(extent.voxel_range_for_floor(1), Some((3, 5)));
// Round-trip: voxel → floor → range contains voxel.
assert_eq!(extent.floor_at_voxel_z(0), Some(0));
assert_eq!(extent.floor_at_voxel_z(2), Some(0));
assert_eq!(extent.floor_at_voxel_z(3), Some(1));
assert_eq!(extent.floor_at_voxel_z(5), Some(1));
}
/// (b) Basement building — base_floor=-1, floor_count=3 (floors -1, 0, 1).
///
/// Uniform(3): floor -1 → voxels 02, floor 0 → 35, floor 1 → 68.
#[test]
fn floor_extent_basement_building() {
let extent = FloorExtent {
base_floor: -1,
floor_count: 3,
heights: FloorHeightProfile::Uniform(3),
};
assert_eq!(extent.voxel_range_for_floor(-1), Some((0, 2)));
assert_eq!(extent.voxel_range_for_floor(0), Some((3, 5)));
assert_eq!(extent.voxel_range_for_floor(1), Some((6, 8)));
assert_eq!(extent.floor_at_voxel_z(0), Some(-1));
assert_eq!(extent.floor_at_voxel_z(3), Some(0));
assert_eq!(extent.floor_at_voxel_z(6), Some(1));
assert_eq!(extent.floor_at_voxel_z(8), Some(1));
}
/// (c) Variable([5,3,3]) floor heights — confirm each floor's voxel range.
///
/// base_floor=0, floor_count=3: floor 0 → 5 voxels (04), floor 1 → 3 voxels (57),
/// floor 2 → 3 voxels (810).
#[test]
fn floor_extent_variable_heights() {
let extent = FloorExtent {
base_floor: 0,
floor_count: 3,
heights: FloorHeightProfile::Variable(vec![5, 3, 3]),
};
assert_eq!(extent.voxel_range_for_floor(0), Some((0, 4)));
assert_eq!(extent.voxel_range_for_floor(1), Some((5, 7)));
assert_eq!(extent.voxel_range_for_floor(2), Some((8, 10)));
assert_eq!(extent.floor_at_voxel_z(0), Some(0));
assert_eq!(extent.floor_at_voxel_z(4), Some(0));
assert_eq!(extent.floor_at_voxel_z(5), Some(1));
assert_eq!(extent.floor_at_voxel_z(7), Some(1));
assert_eq!(extent.floor_at_voxel_z(8), Some(2));
assert_eq!(extent.floor_at_voxel_z(10), Some(2));
}
/// (d) Boundary — one voxel above the top floor returns None; one below the base returns None.
///
/// Uniform(3), base_floor=0, floor_count=2: valid range [0, 5].
/// voxel_z=6 is one above the top; voxel_z=-1 is one below the base.
#[test]
fn floor_extent_boundary_returns_none() {
let extent = FloorExtent {
base_floor: 0,
floor_count: 2,
heights: FloorHeightProfile::Uniform(3),
};
// One voxel above the top floor (top floor ends at voxel 5).
assert_eq!(
extent.floor_at_voxel_z(6),
None,
"one voxel above top floor must return None"
);
// One voxel below the base floor (base maps to voxel 0).
assert_eq!(
extent.floor_at_voxel_z(-1),
None,
"one voxel below base floor must return None"
);
// Floor index outside extent also returns None from voxel_range_for_floor.
assert_eq!(
extent.voxel_range_for_floor(2),
None,
"floor index beyond floor_count must return None"
);
assert_eq!(
extent.voxel_range_for_floor(-1),
None,
"floor index below base_floor must return None"
);
}
/// D-110: FloorZone.z_level must be i8 (signed) to support negative sub-levels.
#[test]
fn floor_zone_z_level_is_signed() {