fix(simulation): address PR #10 review — plugin wiring, stale reset, warnings
- Register KnowledgePlugin in main.rs and game_loop test (Tyre critical) - Add KnowledgeGraph component to player spawn (Tyre critical) - Reset Stale -> Active on fresh direct observation (Hoshe warning) - Make registry/queue non-optional in emit_observation_events (Tyre/Hoshe) - Add tracing::warn for missing EntityRegistry entries (Hoshe warning) - Replace HashSet with Vec for small entity ID lookups (Hoshe suggestion) - Add const static assertion for KnowledgeConfidence ordering (Hoshe) - Add is_empty() and known_facts_iter() to KnowledgeGraph (Tyre) - Add decay_skips_non_minute_ticks and observe_resets_stale tests (Hoshe) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,11 +78,15 @@ pub fn process_knowledge_events(
|
||||
KnowledgeEventType::DirectObservation { target, position } => {
|
||||
if let Some(stable_id) = registry.to_stable(target) {
|
||||
observer_kg.observe_entity(stable_id, position, event.tick);
|
||||
} else {
|
||||
tracing::warn!("DirectObservation target {:?} not in EntityRegistry", target);
|
||||
}
|
||||
}
|
||||
KnowledgeEventType::LeftLOS { target } => {
|
||||
if let Some(stable_id) = registry.to_stable(target) {
|
||||
observer_kg.observe_entity_leaving_los(&stable_id, event.tick);
|
||||
} else {
|
||||
tracing::warn!("LeftLOS target {:?} not in EntityRegistry", target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,4 +234,55 @@ mod tests {
|
||||
schedule.add_systems(process_knowledge_events);
|
||||
schedule.run(&mut world); // Should not panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decay_skips_non_minute_ticks() {
|
||||
use crate::simulation::time::SimulationTime;
|
||||
|
||||
let mut world = World::new();
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
|
||||
let target_entity = world.spawn_empty().id();
|
||||
let target_sid = registry.register(target_entity);
|
||||
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
kg.observe_entity(target_sid, TilePosition::new(5, 5, 0), 0);
|
||||
kg.observe_entity_leaving_los(&target_sid, 1); // KnowsDetails
|
||||
|
||||
let observer = world.spawn(kg).id();
|
||||
registry.register(observer);
|
||||
world.insert_resource(registry);
|
||||
|
||||
let thresholds = DecayThresholds {
|
||||
decay_after: 5,
|
||||
stale_after: 100,
|
||||
};
|
||||
world.insert_resource(thresholds);
|
||||
|
||||
// Tick 7: not a multiple of 10, decay should NOT run
|
||||
world.insert_resource(SimulationTime { tick: 7, paused: false });
|
||||
let mut schedule = bevy_ecs::schedule::Schedule::default();
|
||||
schedule.add_systems(decay_knowledge);
|
||||
schedule.run(&mut world);
|
||||
|
||||
let kg = world.entity(observer).get::<KnowledgeGraph>().unwrap();
|
||||
assert_eq!(
|
||||
kg.confidence_of(&target_sid),
|
||||
Some(KnowledgeConfidence::KnowsDetails),
|
||||
"decay should not run on tick 7 (not a game-minute boundary)"
|
||||
);
|
||||
|
||||
// Tick 10: multiple of 10, decay SHOULD run (age = 10 > decay_after = 5)
|
||||
world.insert_resource(SimulationTime { tick: 10, paused: false });
|
||||
let mut schedule2 = bevy_ecs::schedule::Schedule::default();
|
||||
schedule2.add_systems(decay_knowledge);
|
||||
schedule2.run(&mut world);
|
||||
|
||||
let kg = world.entity(observer).get::<KnowledgeGraph>().unwrap();
|
||||
assert_eq!(
|
||||
kg.confidence_of(&target_sid),
|
||||
Some(KnowledgeConfidence::KnowsOf),
|
||||
"decay should run on tick 10 and downgrade confidence"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,16 @@ impl KnowledgeGraph {
|
||||
self.facts.len()
|
||||
}
|
||||
|
||||
/// Whether the knowledge graph has no entries at all.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entities.is_empty() && self.facts.is_empty()
|
||||
}
|
||||
|
||||
/// Iterate all known facts (deterministic order via BTreeMap).
|
||||
pub fn known_facts_iter(&self) -> impl Iterator<Item = (&FactId, &FactKnowledge)> {
|
||||
self.facts.iter()
|
||||
}
|
||||
|
||||
// --- Write Operations ---
|
||||
|
||||
/// Record a direct observation of another entity (entity is in LOS).
|
||||
@@ -119,8 +129,12 @@ impl KnowledgeGraph {
|
||||
entry.last_updated_tick = tick;
|
||||
entry.confidence = KnowledgeConfidence::Direct;
|
||||
entry.source = KnowledgeSource::DirectObservation { tick };
|
||||
// Do NOT reset state here -- a Contradicted entry stays Contradicted
|
||||
// even if you're looking at the entity right now.
|
||||
// Stale entries become Active again on fresh observation.
|
||||
// Contradicted entries stay Contradicted even if you're looking
|
||||
// at the entity right now — the contradiction is still unresolved.
|
||||
if entry.state == KnowledgeState::Stale {
|
||||
entry.state = KnowledgeState::Active;
|
||||
}
|
||||
}
|
||||
|
||||
/// Entity has left the observer's LOS. Downgrade from Direct.
|
||||
@@ -355,6 +369,21 @@ mod tests {
|
||||
assert_eq!(entry.last_known_position, Some(make_position(6, 10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observe_resets_stale_to_active() {
|
||||
let mut g = KnowledgeGraph::new();
|
||||
let target = StableId(1);
|
||||
g.observe_entity(target, make_position(5, 10), 100);
|
||||
// Mark as stale (would be done by decay system)
|
||||
g.entities.get_mut(&target).unwrap().state = KnowledgeState::Stale;
|
||||
|
||||
// Re-observe — stale should reset to Active
|
||||
g.observe_entity(target, make_position(6, 10), 200);
|
||||
let entry = g.entity_knowledge(&target).unwrap();
|
||||
assert_eq!(entry.state, KnowledgeState::Active);
|
||||
assert_eq!(entry.confidence, KnowledgeConfidence::Direct);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_background_creates_facts() {
|
||||
let facts = vec![
|
||||
|
||||
@@ -46,6 +46,14 @@ pub enum KnowledgeConfidence {
|
||||
Direct = 3,
|
||||
}
|
||||
|
||||
// Load-bearing ordering assertion — do not reorder variants.
|
||||
const _: () = {
|
||||
assert!(KnowledgeConfidence::Suspects as u8 == 0);
|
||||
assert!(KnowledgeConfidence::KnowsOf as u8 == 1);
|
||||
assert!(KnowledgeConfidence::KnowsDetails as u8 == 2);
|
||||
assert!(KnowledgeConfidence::Direct as u8 == 3);
|
||||
};
|
||||
|
||||
impl KnowledgeConfidence {
|
||||
/// Step down one confidence level (used by decay system).
|
||||
pub fn decayed(self) -> Self {
|
||||
|
||||
Reference in New Issue
Block a user