feat(simulation): add server/src/atlas/ — full Phase 1 generation pipeline
Ten-module atlas package implementing the D-194–D-218 district generation stack: heightmap loader, BodyWorldState LRU cache, D8 drainage routing, background generation queue, five-phase attractor-matching, three-component district mix, block irregularity, tile condition thresholds, and the Phase 1 skeleton generator that wires them into DistrictSkeleton. Closes #916 #917 #918 #919 #920 #922 #923 #924 #899. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
//! Tile condition thresholds and derivation (D-217).
|
||||
//!
|
||||
//! A tile's visual condition is derived from the district's `prosperity_score`
|
||||
//! (0.0–1.0) using four threshold bands. The block's `EraCause` applies a
|
||||
//! minimum condition floor that prevents high-prosperity scores from masking
|
||||
//! historical decay.
|
||||
//!
|
||||
//! **Threshold bands (D-217):**
|
||||
//! | Band | Condition | prosperity_score |
|
||||
//! |------|-----------|-----------------|
|
||||
//! | 1 | Intact | > 0.63 |
|
||||
//! | 2 | Worn | 0.43 – 0.63 |
|
||||
//! | 3 | Cracked | 0.23 – 0.43 |
|
||||
//! | 4 | Broken | < 0.23 |
|
||||
//!
|
||||
//! **Era-based floor (D-217):**
|
||||
//! - `EconomicDisruption` (Decay-era): minimum Cracked.
|
||||
//! - `EmergencyExtension`: minimum Worn.
|
||||
//! - All other eras: no floor — condition follows prosperity_score freely.
|
||||
//!
|
||||
//! **Threshold crossing invalidation:** A tile's condition only changes when
|
||||
//! `prosperity_score` crosses a band boundary. Checked once per game-minute.
|
||||
//!
|
||||
//! Threshold values are authored constants (D-217): 0.63, 0.43, 0.23.
|
||||
|
||||
use crate::simulation::generator::EraCause;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TileCondition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Visual condition band for a tile, derived from prosperity_score (D-217).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum TileCondition {
|
||||
/// prosperity_score > 0.63. Clean, undamaged, well-maintained.
|
||||
Intact,
|
||||
/// prosperity_score 0.43–0.63. Scuff marks, minor discoloration, partial repairs.
|
||||
Worn,
|
||||
/// prosperity_score 0.23–0.43. Visible damage, incomplete repair, graffiti.
|
||||
Cracked,
|
||||
/// prosperity_score < 0.23. Structural damage, debris, derelict appearance.
|
||||
Broken,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Threshold constants (D-217 authored — do not compute at runtime)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub const THRESHOLD_INTACT: f32 = 0.63;
|
||||
pub const THRESHOLD_WORN: f32 = 0.43;
|
||||
pub const THRESHOLD_CRACKED: f32 = 0.23;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Derivation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Derive `TileCondition` from `prosperity_score` alone (no era floor).
|
||||
pub fn condition_from_score(prosperity_score: f32) -> TileCondition {
|
||||
if prosperity_score > THRESHOLD_INTACT {
|
||||
TileCondition::Intact
|
||||
} else if prosperity_score > THRESHOLD_WORN {
|
||||
TileCondition::Worn
|
||||
} else if prosperity_score > THRESHOLD_CRACKED {
|
||||
TileCondition::Cracked
|
||||
} else {
|
||||
TileCondition::Broken
|
||||
}
|
||||
}
|
||||
|
||||
/// Era-based minimum condition floor (D-217).
|
||||
///
|
||||
/// Returns the minimum `TileCondition` for a block with the given `EraCause`.
|
||||
/// `None` means no floor — condition follows prosperity_score freely.
|
||||
pub fn era_condition_floor(era_cause: Option<&EraCause>) -> Option<TileCondition> {
|
||||
match era_cause {
|
||||
Some(EraCause::EconomicDisruption) => Some(TileCondition::Cracked),
|
||||
Some(EraCause::EmergencyExtension) => Some(TileCondition::Worn),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive `TileCondition` with era-based floor applied.
|
||||
///
|
||||
/// If the era floor is stricter (lower condition) than the score-derived
|
||||
/// condition, the floor wins.
|
||||
pub fn tile_condition(prosperity_score: f32, era_cause: Option<&EraCause>) -> TileCondition {
|
||||
let from_score = condition_from_score(prosperity_score);
|
||||
match era_condition_floor(era_cause) {
|
||||
Some(floor) => {
|
||||
// Lower enum discriminant = better condition (Intact < Worn < Cracked < Broken).
|
||||
// Floor is a *minimum degradation* — we want the worse of the two.
|
||||
if floor > from_score {
|
||||
floor
|
||||
} else {
|
||||
from_score
|
||||
}
|
||||
}
|
||||
None => from_score,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a threshold crossing occurred between two prosperity scores.
|
||||
///
|
||||
/// Returns `true` if the tile's condition band changed between `old_score` and
|
||||
/// `new_score`. Used by the game-minute update loop to decide whether to
|
||||
/// apply a `ChunkMutation.tile_override`.
|
||||
pub fn threshold_crossed(old_score: f32, new_score: f32) -> bool {
|
||||
condition_from_score(old_score) != condition_from_score(new_score)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn intact_above_0_63() {
|
||||
assert_eq!(condition_from_score(0.64), TileCondition::Intact);
|
||||
assert_eq!(condition_from_score(1.0), TileCondition::Intact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worn_between_0_43_and_0_63() {
|
||||
assert_eq!(condition_from_score(0.63), TileCondition::Worn);
|
||||
assert_eq!(condition_from_score(0.50), TileCondition::Worn);
|
||||
assert_eq!(condition_from_score(0.44), TileCondition::Worn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cracked_between_0_23_and_0_43() {
|
||||
assert_eq!(condition_from_score(0.43), TileCondition::Cracked);
|
||||
assert_eq!(condition_from_score(0.30), TileCondition::Cracked);
|
||||
assert_eq!(condition_from_score(0.24), TileCondition::Cracked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broken_below_0_23() {
|
||||
assert_eq!(condition_from_score(0.23), TileCondition::Broken);
|
||||
assert_eq!(condition_from_score(0.10), TileCondition::Broken);
|
||||
assert_eq!(condition_from_score(0.0), TileCondition::Broken);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn era_floor_decay_enforces_cracked_minimum() {
|
||||
// Prosperous district in an EconomicDisruption-era block — still Cracked.
|
||||
let cond = tile_condition(0.90, Some(&EraCause::EconomicDisruption));
|
||||
assert_eq!(cond, TileCondition::Cracked,
|
||||
"EconomicDisruption floor must prevent Intact/Worn");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn era_floor_emergency_extension_enforces_worn_minimum() {
|
||||
// High prosperity EmergencyExtension block should never be Intact.
|
||||
let cond = tile_condition(0.80, Some(&EraCause::EmergencyExtension));
|
||||
assert_eq!(cond, TileCondition::Worn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn era_floor_does_not_improve_condition() {
|
||||
// EconomicDisruption floor = Cracked; Broken score stays Broken.
|
||||
let cond = tile_condition(0.10, Some(&EraCause::EconomicDisruption));
|
||||
assert_eq!(cond, TileCondition::Broken,
|
||||
"Era floor must not improve condition below score-derived value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_era_cause_follows_score() {
|
||||
let cond = tile_condition(0.90, None);
|
||||
assert_eq!(cond, TileCondition::Intact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn threshold_crossed_detects_band_change() {
|
||||
// 0.7 → 0.5 crosses the 0.63 boundary.
|
||||
assert!(threshold_crossed(0.70, 0.50));
|
||||
// 0.55 → 0.48 stays in Worn band.
|
||||
assert!(!threshold_crossed(0.55, 0.48));
|
||||
// 0.40 → 0.20 crosses 0.23 boundary.
|
||||
assert!(threshold_crossed(0.40, 0.20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn condition_ordering_intact_is_best() {
|
||||
assert!(TileCondition::Intact < TileCondition::Worn);
|
||||
assert!(TileCondition::Worn < TileCondition::Cracked);
|
||||
assert!(TileCondition::Cracked < TileCondition::Broken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user