Files
settled-reach/server/src/test_world/rooms/decay_observatory.rs
T
jpmschweitzerandClaude Opus 4.6 dd1dd0feec fix(simulation): address PR #46 review — zone tests, doc accuracy, TBD comment
Add 3 zone enrichment tests to observer pipeline (tile inside zone,
tile outside zone, absent ZoneMap resource). Fix misleading Decay
Observatory doc comment that claimed Direct start when test starts at
KnowsDetails. Add production population TBD note on ZoneMap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:22:48 +01:00

147 lines
5.1 KiB
Rust

//! Decay Observatory — Room 13 (24x14)
//!
//! Tests D-041 (knowledge graph decay). Observer sees an NPC (Direct), then
//! LOS is broken (drops to KnowsDetails). Tick advances verify confidence
//! degrades: KnowsDetails → KnowsOf → Suspects (floor). Separate test
//! confirms Stale state triggers when age exceeds stale_after threshold.
//!
//! Decay runs once per game-minute (every 10 ticks, D-031). Confidence floor
//! is Suspects — decay never removes an entity from the knowledge graph.
//!
//! Layout: Open room. Single observable NPC east of observer.
//! This room sits in the corridor-NW path (x=12-18, y=22-40); the corridor
//! creates natural north/south doorways through the room walls.
//!
//! Observer position: (10, 30) absolute, facing East.
//!
//! Entities (StableId 69):
//! npc_observe_target (16, 30) — 6 tiles east of observer, starts in LOS
use bevy_app::prelude::*;
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind};
use crate::simulation::interaction::Interactable;
use crate::simulation::movement::TilePosition;
use crate::simulation::path_follow::MovementSpeed;
use crate::simulation::tier::ActiveSim;
/// Room origin (top-left corner including walls).
const ORIGIN_X: i32 = 0;
const ORIGIN_Y: i32 = 24;
/// Observer position for Decay Observatory tests (absolute).
pub const OBSERVER_POS: TilePosition = TilePosition { x: 10, y: 30, z: 0 };
/// NPC position — 6 tiles east of observer, starts in direct LOS.
pub const NPC_OBSERVE_POS: TilePosition = TilePosition { x: 16, y: 30, z: 0 };
/// Spawn Decay Observatory entities in canonical order (StableId 69).
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
let pos = TilePosition::new(ORIGIN_X + 16, ORIGIN_Y + 6, 0);
let entity = app
.world_mut()
.spawn((
Npc,
ActiveSim,
Interactable,
pos,
Want {
primary: WantKind::Safety,
intensity: 4,
description: "Decay Observatory: observable NPC".to_string(),
},
Contentment { level: 5 },
ToleranceThreshold {
current_stress: 0,
threshold: 60,
},
MovementSpeed::default(),
))
.id();
let sid = registry.register(entity);
app.world_mut()
.entity_mut(entity)
.insert(StableEntityId(sid));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::knowledge::graph::KnowledgeGraph;
use crate::knowledge::types::{DecayThresholds, KnowledgeConfidence, KnowledgeState, StableId};
const TARGET: StableId = StableId(1);
fn observe_then_leave(tick_observe: u64, tick_leave: u64) -> KnowledgeGraph {
let mut g = KnowledgeGraph::new();
g.observe_entity(TARGET, NPC_OBSERVE_POS, tick_observe);
g.observe_entity_leaving_los(&TARGET, tick_leave);
g
}
/// After LOS loss, confidence degrades one level per decay pass:
/// KnowsDetails → KnowsOf → Suspects (floor).
#[test]
fn decay_observatory_confidence_degrades_after_los_loss() {
let mut g = observe_then_leave(100, 110);
// Verify starting state: KnowsDetails after LOS loss.
assert_eq!(
g.confidence_of(&TARGET),
Some(KnowledgeConfidence::KnowsDetails),
"confidence must be KnowsDetails immediately after LOS loss"
);
let thresholds = DecayThresholds {
decay_after: 10,
stale_after: 10_000,
};
// Tick 200: age = 200 - 100 = 100 > decay_after=10 → KnowsOf
g.decay(200, &thresholds);
assert_eq!(
g.confidence_of(&TARGET),
Some(KnowledgeConfidence::KnowsOf),
"KnowsDetails must decay to KnowsOf after one decay pass"
);
// Tick 300: age still > decay_after → Suspects
g.decay(300, &thresholds);
assert_eq!(
g.confidence_of(&TARGET),
Some(KnowledgeConfidence::Suspects),
"KnowsOf must decay to Suspects after second decay pass"
);
// Tick 400: Suspects is the floor — no further degradation.
g.decay(400, &thresholds);
assert_eq!(
g.confidence_of(&TARGET),
Some(KnowledgeConfidence::Suspects),
"Suspects is the confidence floor — decay must not go below Suspects"
);
}
/// After exceeding the stale threshold the entry state becomes Stale.
#[test]
fn decay_observatory_stale_after_threshold() {
let mut g = observe_then_leave(100, 110);
let thresholds = DecayThresholds {
decay_after: 10,
stale_after: 50,
};
// Tick 200: age = 200 - 100 = 100 > stale_after=50 → Stale
g.decay(200, &thresholds);
let entry = g.entity_knowledge(&TARGET).unwrap();
assert_eq!(
entry.state,
KnowledgeState::Stale,
"entry state must become Stale when age exceeds stale_after threshold"
);
}
}