fix(simulation): address PR #50 review — determinism, overflow, invariants
- Sort eligible NPCs by StableId for deterministic conversation pairing (D-010) - Replace single_mut() with per-observer iteration (D-010 principle 3, D-027) - Widen current_stress * 100 to i32 in mood derivation to prevent i16 overflow - Promote line_interval to named constant LINE_INTERVAL_TICKS - Add 4 Sprint 14 component invariants (MoodState, InteractionMemory, ActivityState+PathRequest exclusion, double-tagged tier detection) - Attach MoodState to content spawn pipeline and gauntlet fixup - VecDeque for InteractionMemory.notable_events (O(1) pop_front) - Rename who_knows() to who_knows_full_scan() to communicate O(N) cost - Add O(N^2) growth limit comment on conversation pair scan - Document Suspicious/Focused as externally-set moods on enum variants Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,7 @@ pub struct InteractionMemory {
|
||||
pub last_interaction_tick: u64,
|
||||
/// Notable events: walk-aways and confrontations.
|
||||
/// Bounded by `MAX_NOTABLE_EVENTS` — oldest entries dropped when full.
|
||||
pub notable_events: Vec<InteractionEvent>,
|
||||
pub notable_events: std::collections::VecDeque<InteractionEvent>,
|
||||
}
|
||||
|
||||
/// Maximum number of notable events retained per NPC pair.
|
||||
@@ -65,9 +65,9 @@ impl InteractionMemory {
|
||||
/// Append a notable event, dropping the oldest if at capacity.
|
||||
pub fn push_event(&mut self, event: InteractionEvent) {
|
||||
if self.notable_events.len() >= MAX_NOTABLE_EVENTS {
|
||||
self.notable_events.remove(0);
|
||||
self.notable_events.pop_front();
|
||||
}
|
||||
self.notable_events.push(event);
|
||||
self.notable_events.push_back(event);
|
||||
}
|
||||
|
||||
/// Returns `true` if this is the first meeting (count == 0).
|
||||
@@ -145,7 +145,7 @@ mod tests {
|
||||
});
|
||||
assert_eq!(mem.notable_events.len(), MAX_NOTABLE_EVENTS);
|
||||
assert_eq!(mem.notable_events[0].tick, 1); // tick=0 dropped
|
||||
assert_eq!(mem.notable_events.last().unwrap().tick, 99);
|
||||
assert_eq!(mem.notable_events.back().unwrap().tick, 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -47,12 +47,14 @@ pub enum NpcMood {
|
||||
/// Low stress, positive recent context — settled and cooperative.
|
||||
Content,
|
||||
/// Observing unusual or off-script behavior — targeted wariness.
|
||||
/// Not reachable from `derive_mood()` — set externally by observation pipeline.
|
||||
Suspicious,
|
||||
/// Recent positive player interaction within memory window.
|
||||
Warm,
|
||||
/// Stress at or above threshold — confrontational or withdrawn.
|
||||
Hostile,
|
||||
/// Actively engaged in a scheduled activity — task-focused.
|
||||
/// Not reachable from `derive_mood()` — set externally by activity scheduler (#101).
|
||||
Focused,
|
||||
}
|
||||
|
||||
@@ -145,7 +147,9 @@ pub fn derive_mood(
|
||||
// 2. Anxious: above 60% of threshold.
|
||||
// Guard: skip if threshold == 0 (divide-by-zero equivalent — entity
|
||||
// has no tolerance and is already Hostile from rule 1).
|
||||
if threshold > 0 && current_stress * 100 >= threshold * ANXIOUS_STRESS_NUMERATOR {
|
||||
if threshold > 0
|
||||
&& (current_stress as i32) * 100 >= (threshold as i32) * (ANXIOUS_STRESS_NUMERATOR as i32)
|
||||
{
|
||||
return NpcMood::Anxious;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,8 +141,8 @@ impl RelationshipGraph {
|
||||
}
|
||||
|
||||
/// Get all entities who have feelings about a target.
|
||||
/// Full scan — use for event detection, not per-tick queries.
|
||||
pub fn who_knows(&self, target: &StableId) -> Vec<(&StableId, &RelationshipEdge)> {
|
||||
/// O(N) full scan of all edges — use for event detection, not per-tick queries.
|
||||
pub fn who_knows_full_scan(&self, target: &StableId) -> Vec<(&StableId, &RelationshipEdge)> {
|
||||
self.edges
|
||||
.iter()
|
||||
.filter(|((_, t), _)| t == target)
|
||||
@@ -374,7 +374,7 @@ mod tests {
|
||||
make_edge(RelationshipKind::Family, 8),
|
||||
);
|
||||
|
||||
let knowers = graph.who_knows(&target);
|
||||
let knowers = graph.who_knows_full_scan(&target);
|
||||
assert_eq!(knowers.len(), 3);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user