1use alloc::collections::BTreeMap;
6use alloc::string::String;
7use alloc::vec;
8use alloc::vec::Vec;
9
10use crate::alignment::AlignmentChecker;
11use crate::types::*;
12
13pub struct Engine {
15 pub rooms: BTreeMap<RoomId, Room>,
16 tiles: BTreeMap<TileId, Tile>,
17 npcs: BTreeMap<NpcId, Npc>,
18 agents: BTreeMap<AgentId, AgentSession>,
19 alignment: AlignmentChecker,
20 zeitgeist: Zeitgeist,
21}
22
23impl Default for Engine {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl Engine {
30 pub fn new() -> Self {
31 Self {
32 rooms: BTreeMap::new(),
33 tiles: BTreeMap::new(),
34 npcs: BTreeMap::new(),
35 agents: BTreeMap::new(),
36 alignment: AlignmentChecker::new(),
37 zeitgeist: Zeitgeist::new(),
38 }
39 }
40
41 pub fn add_room(&mut self, room: Room) -> Result<(), String> {
44 if self.rooms.contains_key(&room.id) {
45 return Err(format!("Room {} already exists", room.id.0));
46 }
47 self.rooms.insert(room.id.clone(), room);
48 Ok(())
49 }
50
51 pub fn remove_room(&mut self, id: &RoomId) -> Result<Room, String> {
52 self.rooms
53 .remove(id)
54 .ok_or_else(|| format!("Room {} not found", id.0))
55 }
56
57 pub fn get_room(&self, id: &RoomId) -> Option<&Room> {
58 self.rooms.get(id)
59 }
60
61 pub fn get_room_mut(&mut self, id: &RoomId) -> Option<&mut Room> {
62 self.rooms.get_mut(id)
63 }
64
65 pub fn rooms_by_domain(&self, domain: &Domain) -> Vec<&Room> {
66 self.rooms
67 .values()
68 .filter(|r| &r.domain == domain)
69 .collect()
70 }
71
72 pub fn rooms_by_depth(&self, depth: &Depth) -> Vec<&Room> {
73 self.rooms.values().filter(|r| &r.depth == depth).collect()
74 }
75
76 pub fn all_rooms(&self) -> Vec<&Room> {
77 self.rooms.values().collect()
78 }
79
80 pub fn add_tile(&mut self, tile: Tile) -> Result<(), String> {
83 if tile.confidence > 0.95 {
85 match &tile.content {
86 TileContent::EmpiricalData(_) | TileContent::Benchmark(_) => {}
87 _ => {
88 return Err("ALIGNMENT VIOLATION: Confidence > 0.95 requires empirical evidence (Constraint 1)".into());
89 }
90 }
91 }
92
93 for dep in &tile.links {
95 if !self.tiles.contains_key(dep) {
96 return Err(format!(
97 "ALIGNMENT VIOLATION: Dependency {} not found (Constraint 3)",
98 dep.0
99 ));
100 }
101 }
102
103 let tile_id = tile.id.clone();
104 self.tiles.insert(tile_id.clone(), tile);
105 Ok(())
106 }
107
108 pub fn get_tile(&self, id: &TileId) -> Option<&Tile> {
109 self.tiles.get(id)
110 }
111
112 pub fn get_tile_mut(&mut self, id: &TileId) -> Option<&mut Tile> {
113 self.tiles.get_mut(id)
114 }
115
116 pub fn remove_tile(&mut self, id: &TileId) -> Result<Tile, String> {
117 self.tiles
118 .remove(id)
119 .ok_or_else(|| format!("Tile {} not found", id.0))
120 }
121
122 pub fn tiles_by_domain(&self, domain: &Domain) -> Vec<&Tile> {
123 self.tiles
124 .values()
125 .filter(|t| t.domain_tags.contains(&domain.name().to_string()))
126 .collect()
127 }
128
129 pub fn tile_dependencies(&self, id: &TileId) -> Vec<&Tile> {
130 self.tiles
131 .get(id)
132 .map(|t| {
133 t.links
134 .iter()
135 .filter_map(|dep| self.tiles.get(dep))
136 .collect()
137 })
138 .unwrap_or_default()
139 }
140
141 pub fn add_npc(&mut self, npc: Npc) -> Result<(), String> {
144 if !self.rooms.contains_key(&npc.room) {
145 return Err(format!("Room {} not found for NPC", npc.room.0));
146 }
147 let npc_id = npc.id.clone();
148 let room_id = npc.room.clone();
149 self.npcs.insert(npc_id.clone(), npc);
150 if let Some(room) = self.rooms.get_mut(&room_id) {
151 room.npcs.push(npc_id);
152 }
153 Ok(())
154 }
155
156 pub fn get_npc(&self, id: &NpcId) -> Option<&Npc> {
157 self.npcs.get(id)
158 }
159
160 pub fn get_npc_mut(&mut self, id: &NpcId) -> Option<&mut Npc> {
161 self.npcs.get_mut(id)
162 }
163
164 pub fn npcs_in_room(&self, room: &RoomId) -> Vec<&Npc> {
165 self.npcs.values().filter(|n| &n.room == room).collect()
166 }
167
168 pub fn connect_agent(&mut self, agent_id: AgentId, start_room: RoomId) -> Result<(), String> {
171 if !self.rooms.contains_key(&start_room) {
172 return Err(format!("Room {} not found", start_room.0));
173 }
174 self.agents.insert(
175 agent_id.clone(),
176 AgentSession {
177 agent_id,
178 current_room: start_room,
179 inventory: Vec::new(),
180 connected_at: 0.0,
181 },
182 );
183 Ok(())
184 }
185
186 pub fn disconnect_agent(&mut self, id: &AgentId) -> Result<AgentSession, String> {
187 self.agents
188 .remove(id)
189 .ok_or_else(|| format!("Agent {} not found", id.0))
190 }
191
192 pub fn get_session(&self, id: &AgentId) -> Option<&AgentSession> {
193 self.agents.get(id)
194 }
195
196 pub fn get_session_mut(&mut self, id: &AgentId) -> Option<&mut AgentSession> {
197 self.agents.get_mut(id)
198 }
199
200 pub fn navigate(&mut self, agent: &AgentId, direction: &str) -> Result<RoomId, String> {
203 let session = self
204 .agents
205 .get(agent)
206 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
207 let current_room_id = session.current_room.clone();
208
209 let exit = {
210 let room = self
211 .rooms
212 .get(¤t_room_id)
213 .ok_or_else(|| format!("Room {} not found", current_room_id.0))?;
214 room.exits
215 .iter()
216 .find(|e| e.direction == direction)
217 .cloned()
218 .ok_or_else(|| format!("No exit '{}' from {}", direction, room.name))?
219 };
220
221 if exit.locked {
222 return Err(format!("Exit '{}' is locked", direction));
223 }
224
225 if !self
227 .alignment
228 .check_exit_constraint(¤t_room_id, &exit.target)
229 {
230 return Err(
231 "ALIGNMENT VIOLATION: Exit violates mathematical guarantees (Constraint 5)".into(),
232 );
233 }
234
235 let target_id = exit.target.clone();
236 if let Some(session) = self.agents.get_mut(agent) {
237 session.current_room = target_id.clone();
238 }
239 Ok(target_id)
240 }
241
242 pub fn pick_up_tile(&mut self, agent: &AgentId, tile_id: &TileId) -> Result<(), String> {
245 let session = self
246 .agents
247 .get(agent)
248 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
249 let room_id = session.current_room.clone();
250
251 {
253 let room = self
254 .rooms
255 .get(&room_id)
256 .ok_or_else(|| format!("Room {} not found", room_id.0))?;
257 if !room.tiles.contains(tile_id) {
258 return Err(format!("Tile {} not in room", tile_id.0));
259 }
260 }
261
262 if let Some(room) = self.rooms.get_mut(&room_id) {
264 room.tiles.retain(|t| t != tile_id);
265 }
266 if let Some(session) = self.agents.get_mut(agent) {
267 session.inventory.push(tile_id.clone());
268 }
269 Ok(())
270 }
271
272 pub fn drop_tile(&mut self, agent: &AgentId, tile_id: &TileId) -> Result<(), String> {
273 let session = self
274 .agents
275 .get(agent)
276 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
277 let room_id = session.current_room.clone();
278
279 {
281 let session = self.agents.get(agent).unwrap();
282 if !session.inventory.contains(tile_id) {
283 return Err(format!("Tile {} not in inventory", tile_id.0));
284 }
285 }
286
287 if let Some(session) = self.agents.get_mut(agent) {
289 session.inventory.retain(|t| t != tile_id);
290 }
291 if let Some(room) = self.rooms.get_mut(&room_id) {
292 room.tiles.push(tile_id.clone());
293 }
294 Ok(())
295 }
296
297 pub fn craft(
300 &mut self,
301 agent: &AgentId,
302 input_ids: &[TileId],
303 recipe_name: &str,
304 ) -> Result<Tile, String> {
305 let session = self
306 .agents
307 .get(agent)
308 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
309 let room_id = session.current_room.clone();
310
311 let recipe = {
313 let room = self
314 .rooms
315 .get(&room_id)
316 .ok_or_else(|| format!("Room {} not found", room_id.0))?;
317 let wb = room.workbench.as_ref().ok_or("No workbench in this room")?;
318 wb.recipes
319 .iter()
320 .find(|r| r.name == recipe_name)
321 .cloned()
322 .ok_or_else(|| format!("Recipe '{}' not found", recipe_name))?
323 };
324
325 for input_id in input_ids {
327 if !self.tiles.contains_key(input_id) {
328 return Err(format!(
329 "ALIGNMENT VIOLATION: Input tile {} not found (Constraint 3)",
330 input_id.0
331 ));
332 }
333 }
334
335 if input_ids.len() != recipe.inputs.len() {
337 return Err("Input count doesn't match recipe".into());
338 }
339
340 let session = self.agents.get(agent).unwrap();
342 for input_id in input_ids {
343 let has_it = session.inventory.contains(input_id) || {
344 self.rooms
345 .get(&room_id)
346 .map(|r| r.tiles.contains(input_id))
347 .unwrap_or(false)
348 };
349 if !has_it {
350 return Err(format!("Input tile {} not available", input_id.0));
351 }
352 }
353
354 let agent_id = session.agent_id.clone();
356 let output_tile = Tile {
357 id: TileId(format!("{}:{}", recipe_name, self.tiles.len())),
358 title: recipe.name.clone(),
359 location: SpatialIndex {
360 x: 0.0,
361 y: 0.0,
362 z: 0.0,
363 },
364 author: agent_id,
365 confidence: 0.5,
366 domain_tags: vec![],
367 links: input_ids.to_vec(),
368 content: recipe.output.clone(),
369 lifecycle: Lifecycle::Created,
370 bloom_hash: [0u8; 32],
371 };
372
373 let tile = output_tile.clone();
374 self.add_tile(output_tile)?;
375 Ok(tile)
376 }
377
378 pub fn talk_to_npc(
381 &mut self,
382 _agent: &AgentId,
383 npc_id: &NpcId,
384 query: &str,
385 ) -> Result<String, String> {
386 let npc = self
387 .npcs
388 .get(npc_id)
389 .ok_or_else(|| format!("NPC {} not found", npc_id.0))?;
390
391 if !npc.expertise.is_empty() {
393 let query_lower = query.to_lowercase();
394 let relevant = npc
395 .expertise
396 .iter()
397 .any(|e| query_lower.contains(&e.to_lowercase()));
398 if !relevant {
399 return Err(format!(
400 "ALIGNMENT VIOLATION: {} cannot advise on '{}' — outside expertise (Constraint 4)",
401 npc.name, query
402 ));
403 }
404 }
405
406 let query_key = Query(query.to_string());
407 let response = npc
408 .knowledge_graph
409 .get(&query_key)
410 .map(|r| r.0.clone())
411 .unwrap_or_else(|| {
412 format!(
413 "{} scratches their head. \"I don't know about that.\"",
414 npc.name
415 )
416 });
417
418 Ok(response)
419 }
420
421 pub fn look(&self, agent: &AgentId) -> Result<String, String> {
424 let session = self
425 .agents
426 .get(agent)
427 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
428 let room = self
429 .rooms
430 .get(&session.current_room)
431 .ok_or_else(|| format!("Room {} not found", session.current_room.0))?;
432
433 let mut desc = format!("═══ {} ═══\n", room.name);
434 desc.push_str(&format!("{}\n", room.description));
435 desc.push_str(&format!(
436 "Domain: {} | Depth: {:?} | State: {:?}\n",
437 room.domain.name(),
438 room.depth,
439 room.state
440 ));
441
442 if !room.exits.is_empty() {
443 desc.push_str("\nExits: ");
444 let exits: Vec<String> = room
445 .exits
446 .iter()
447 .filter(|e| !e.locked)
448 .map(|e| format!("{} [{}]", e.direction, e.target.0))
449 .collect();
450 desc.push_str(&exits.join(", "));
451 desc.push('\n');
452 }
453
454 if !room.tiles.is_empty() {
455 desc.push_str(&format!("\nTiles here ({}):\n", room.tiles.len()));
456 for tid in &room.tiles {
457 if let Some(tile) = self.tiles.get(tid) {
458 desc.push_str(&format!(
459 " 📦 {} (confidence: {:.2})\n",
460 tile.title, tile.confidence
461 ));
462 }
463 }
464 }
465
466 if !room.npcs.is_empty() {
467 desc.push_str(&format!("\nNPCs here ({}):\n", room.npcs.len()));
468 for nid in &room.npcs {
469 if let Some(npc) = self.npcs.get(nid) {
470 desc.push_str(&format!(" 🧑 {} — {}\n", npc.name, npc.personality));
471 }
472 }
473 }
474
475 if let Some(ref wb) = room.workbench {
476 desc.push_str(&format!("\n⚒️ Workbench: {}\n", wb.name));
477 desc.push_str(&format!(" {}\n", wb.description));
478 for recipe in &wb.recipes {
479 desc.push_str(&format!(
480 " Recipe: {} — {}\n",
481 recipe.name, recipe.description
482 ));
483 }
484 }
485
486 Ok(desc)
487 }
488
489 pub fn map(&self, agent: &AgentId) -> Result<String, String> {
492 let session = self
493 .agents
494 .get(agent)
495 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
496
497 let mut map_str = String::from("╔═══════════════════════════════════╗\n");
498 map_str.push_str("║ PLATO MUD MAP ║\n");
499 map_str.push_str("╠═══════════════════════════════════╣\n");
500
501 for room in self.rooms.values() {
502 let marker = if room.id == session.current_room {
503 " ◄ YOU"
504 } else {
505 ""
506 };
507 map_str.push_str(&format!(
508 "║ [{}] {} ({:?}){}{}\n",
509 room.domain.name(),
510 room.name,
511 room.depth,
512 if room.state != RoomState::Dormant {
513 format!(" [{:?}]", room.state)
514 } else {
515 String::new()
516 },
517 marker
518 ));
519 for exit in &room.exits {
520 map_str.push_str(&format!("║ → {} → {}\n", exit.direction, exit.target.0));
521 }
522 }
523 map_str.push_str("╚═══════════════════════════════════╝\n");
524 Ok(map_str)
525 }
526
527 pub fn zeitgeist(&self) -> &Zeitgeist {
530 &self.zeitgeist
531 }
532
533 pub fn zeitgeist_mut(&mut self) -> &mut Zeitgeist {
534 &mut self.zeitgeist
535 }
536
537 pub fn execute(&mut self, agent: &AgentId, cmd: Command) -> Result<String, String> {
540 self.alignment.check_command(agent, &cmd, self)?;
542
543 match cmd {
544 Command::Look => self.look(agent),
545 Command::Go(dir) => {
546 let _new_room = self.navigate(agent, &dir)?;
547 self.look(agent)
548 }
549 Command::Get(item) => {
550 self.pick_up_tile(agent, &TileId(item.clone()))?;
551 Ok(format!("Picked up {}", item))
552 }
553 Command::Drop(item) => {
554 self.drop_tile(agent, &TileId(item.clone()))?;
555 Ok(format!("Dropped {}", item))
556 }
557 Command::Talk(npc_name) => {
558 let session = self
560 .agents
561 .get(agent)
562 .cloned()
563 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
564 let npc_id = self
565 .npcs
566 .values()
567 .find(|n| n.name == npc_name && n.room == session.current_room)
568 .map(|n| n.id.clone())
569 .ok_or_else(|| format!("NPC '{}' not in this room", npc_name))?;
570 self.talk_to_npc(agent, &npc_id, "hello")
571 }
572 Command::Craft(items) => {
573 let tile_ids: Vec<TileId> = items.iter().map(|s| TileId(s.clone())).collect();
574 let tile = self.craft(agent, &tile_ids, "combine")?;
575 Ok(format!("Crafted: {}", tile.title))
576 }
577 Command::Inventory => {
578 let session = self
579 .agents
580 .get(agent)
581 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
582 if session.inventory.is_empty() {
583 Ok("Inventory is empty.".into())
584 } else {
585 let mut inv = format!("Inventory ({}):\n", session.inventory.len());
586 for tid in &session.inventory {
587 if let Some(tile) = self.tiles.get(tid) {
588 inv.push_str(&format!(
589 " 📦 {} ({:.2})\n",
590 tile.title, tile.confidence
591 ));
592 }
593 }
594 Ok(inv)
595 }
596 }
597 Command::Map => self.map(agent),
598 Command::Examine(target) => {
599 let tile = self.get_tile(&TileId(target.clone()));
600 if let Some(tile) = tile {
601 Ok(format!(
602 "📦 {}\nConfidence: {:.2}\nLifecycle: {:?}\nTags: {}\nAuthor: {}",
603 tile.title,
604 tile.confidence,
605 tile.lifecycle,
606 tile.domain_tags.join(", "),
607 tile.author.0
608 ))
609 } else {
610 Err(format!("'{}' not found", target))
611 }
612 }
613 Command::Status => {
614 let session = self
615 .agents
616 .get(agent)
617 .ok_or_else(|| format!("Agent {} not found", agent.0))?;
618 let room = self.rooms.get(&session.current_room);
619 Ok(format!(
620 "Agent: {} | Room: {} | Inventory: {} | Zeitgeist: beat {}",
621 agent.0,
622 room.map(|r| r.name.clone()).unwrap_or_default(),
623 session.inventory.len(),
624 self.zeitgeist.temporal.beat
625 ))
626 }
627 Command::Help => Ok(
628 "Commands: LOOK, GO <dir>, GET <tile>, DROP <tile>, TALK <npc>, \
629 CRAFT <tiles...>, INVENTORY, MAP, EXAMINE <tile>, STATUS, HELP"
630 .into(),
631 ),
632 }
633 }
634
635 pub fn receive_flux(&mut self, flux: &FluxTransference) -> Result<(), String> {
637 if flux.timestamp <= 0.0 {
639 return Err("ALIGNMENT VIOLATION: FLUX missing zeitgeist (Constraint 8)".into());
640 }
641
642 self.zeitgeist.merge(&flux.zeitgeist);
644
645 match &flux.payload {
647 TransferencePayload::Tile(tile) => {
648 self.add_tile(tile.clone())?;
649 }
650 TransferencePayload::StateUpdate(state) => {
651 if let Some(room) = self.rooms.get_mut(&flux.target) {
652 room.state = state.clone();
653 }
654 }
655 TransferencePayload::AlignmentCheck(_report) => {
656 }
658 TransferencePayload::Knowledge(_knowledge) => {}
659 TransferencePayload::Heartbeat => {}
660 }
661
662 Ok(())
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 fn make_test_room(id: &str, name: &str, domain: Domain) -> Room {
671 Room {
672 id: RoomId(id.to_string()),
673 name: name.to_string(),
674 description: format!("You are in the {} room.", name),
675 domain,
676 exits: vec![],
677 tiles: vec![],
678 npcs: vec![],
679 workbench: None,
680 depth: Depth::Introductory,
681 state: RoomState::Dormant,
682 }
683 }
684
685 fn make_test_tile(id: &str, confidence: f64) -> Tile {
686 Tile {
687 id: TileId(id.to_string()),
688 title: format!("Test tile {}", id),
689 location: SpatialIndex {
690 x: 0.0,
691 y: 0.0,
692 z: 0.0,
693 },
694 author: AgentId("test-agent".to_string()),
695 confidence,
696 domain_tags: vec!["test".to_string()],
697 links: vec![],
698 content: TileContent::Code("fn test() {}".to_string()),
699 lifecycle: Lifecycle::Created,
700 bloom_hash: [0u8; 32],
701 }
702 }
703
704 #[test]
705 fn test_add_room() {
706 let mut engine = Engine::new();
707 let room = make_test_room("rust-01", "Rust Basics", Domain::Rust);
708 assert!(engine.add_room(room).is_ok());
709 assert!(engine.get_room(&RoomId("rust-01".to_string())).is_some());
710 }
711
712 #[test]
713 fn test_duplicate_room() {
714 let mut engine = Engine::new();
715 let room = make_test_room("rust-01", "Rust Basics", Domain::Rust);
716 engine.add_room(room.clone()).unwrap();
717 assert!(engine.add_room(room).is_err());
718 }
719
720 #[test]
721 fn test_add_tile_confidence_constraint() {
722 let mut engine = Engine::new();
723 let tile = make_test_tile("t1", 0.99);
725 assert!(engine.add_tile(tile).is_err());
726
727 let mut tile2 = make_test_tile("t2", 0.99);
729 tile2.content = TileContent::EmpiricalData("benchmarked".to_string());
730 assert!(engine.add_tile(tile2).is_ok());
731 }
732
733 #[test]
734 fn test_add_tile_normal_confidence() {
735 let mut engine = Engine::new();
736 let tile = make_test_tile("t1", 0.85);
737 assert!(engine.add_tile(tile).is_ok());
738 }
739
740 #[test]
741 fn test_navigation() {
742 let mut engine = Engine::new();
743 let mut room1 = make_test_room("rust-01", "Rust Basics", Domain::Rust);
744 let room2 = make_test_room("rust-02", "Rust Advanced", Domain::Rust);
745 room1.exits.push(Exit {
746 direction: "north".to_string(),
747 target: room2.id.clone(),
748 description: "A path to advanced Rust".to_string(),
749 locked: false,
750 });
751 engine.add_room(room1).unwrap();
752 engine.add_room(room2).unwrap();
753
754 let agent = AgentId("test".to_string());
755 engine
756 .connect_agent(agent.clone(), RoomId("rust-01".to_string()))
757 .unwrap();
758
759 let result = engine.navigate(&agent, "north");
760 assert!(result.is_ok());
761 assert_eq!(result.unwrap(), RoomId("rust-02".to_string()));
762 }
763
764 #[test]
765 fn test_navigation_locked_exit() {
766 let mut engine = Engine::new();
767 let mut room1 = make_test_room("r1", "Room 1", Domain::Concept);
768 let room2 = make_test_room("r2", "Room 2", Domain::Concept);
769 room1.exits.push(Exit {
770 direction: "east".to_string(),
771 target: room2.id.clone(),
772 description: "Locked".to_string(),
773 locked: true,
774 });
775 engine.add_room(room1).unwrap();
776 engine.add_room(room2).unwrap();
777
778 let agent = AgentId("test".to_string());
779 engine
780 .connect_agent(agent.clone(), RoomId("r1".to_string()))
781 .unwrap();
782 assert!(engine.navigate(&agent, "east").is_err());
783 }
784
785 #[test]
786 fn test_inventory() {
787 let mut engine = Engine::new();
788 let room = make_test_room("r1", "Test Room", Domain::Rust);
789 engine.add_room(room).unwrap();
790
791 let tile = make_test_tile("t1", 0.5);
792 engine.add_tile(tile.clone()).unwrap();
793
794 engine
796 .rooms
797 .get_mut(&RoomId("r1".to_string()))
798 .unwrap()
799 .tiles
800 .push(tile.id.clone());
801
802 let agent = AgentId("test".to_string());
803 engine
804 .connect_agent(agent.clone(), RoomId("r1".to_string()))
805 .unwrap();
806
807 assert!(engine
808 .pick_up_tile(&agent, &TileId("t1".to_string()))
809 .is_ok());
810 let session = engine.get_session(&agent).unwrap();
811 assert!(session.inventory.contains(&TileId("t1".to_string())));
812
813 assert!(engine.drop_tile(&agent, &TileId("t1".to_string())).is_ok());
814 let session = engine.get_session(&agent).unwrap();
815 assert!(session.inventory.is_empty());
816 }
817
818 #[test]
819 fn test_npc_talk() {
820 let mut engine = Engine::new();
821 engine
822 .add_room(make_test_room("r1", "Test", Domain::Rust))
823 .unwrap();
824
825 let mut knowledge = BTreeMap::new();
826 knowledge.insert(
827 Query("borrowing".to_string()),
828 Response("Ownership is key in Rust!".to_string()),
829 );
830
831 let npc = Npc {
832 id: NpcId("rusty".to_string()),
833 name: "Rusty".to_string(),
834 room: RoomId("r1".to_string()),
835 expertise: vec!["rust".to_string(), "borrowing".to_string()],
836 personality: "Gruff but knowledgeable".to_string(),
837 knowledge_graph: knowledge,
838 current_dialog: None,
839 };
840 engine.add_npc(npc).unwrap();
841
842 let agent = AgentId("test".to_string());
843 engine
844 .connect_agent(agent.clone(), RoomId("r1".to_string()))
845 .unwrap();
846
847 let response = engine.talk_to_npc(&agent, &NpcId("rusty".to_string()), "borrowing");
848 assert!(response.is_ok());
849 assert!(response.unwrap().contains("Ownership"));
850 }
851
852 #[test]
853 fn test_npc_constraint_4() {
854 let mut engine = Engine::new();
855 engine
856 .add_room(make_test_room("r1", "Test", Domain::Rust))
857 .unwrap();
858
859 let npc = Npc {
860 id: NpcId("rusty".to_string()),
861 name: "Rusty".to_string(),
862 room: RoomId("r1".to_string()),
863 expertise: vec!["rust".to_string()],
864 personality: "Rust only".to_string(),
865 knowledge_graph: BTreeMap::new(),
866 current_dialog: None,
867 };
868 engine.add_npc(npc).unwrap();
869
870 let agent = AgentId("test".to_string());
871 engine
872 .connect_agent(agent.clone(), RoomId("r1".to_string()))
873 .unwrap();
874
875 let response = engine.talk_to_npc(&agent, &NpcId("rusty".to_string()), "python gil");
877 assert!(response.is_err());
878 assert!(response.unwrap_err().contains("Constraint 4"));
879 }
880
881 #[test]
882 fn test_command_dispatch() {
883 let mut engine = Engine::new();
884 engine
885 .add_room(make_test_room("r1", "Test Room", Domain::Concept))
886 .unwrap();
887
888 let agent = AgentId("test".to_string());
889 engine
890 .connect_agent(agent.clone(), RoomId("r1".to_string()))
891 .unwrap();
892
893 let result = engine.execute(&agent, Command::Look);
894 assert!(result.is_ok());
895 assert!(result.unwrap().contains("Test Room"));
896
897 let result = engine.execute(&agent, Command::Help);
898 assert!(result.is_ok());
899 }
900
901 #[test]
902 fn test_rooms_by_domain() {
903 let mut engine = Engine::new();
904 engine
905 .add_room(make_test_room("r1", "Rust 1", Domain::Rust))
906 .unwrap();
907 engine
908 .add_room(make_test_room("r2", "Rust 2", Domain::Rust))
909 .unwrap();
910 engine
911 .add_room(make_test_room("c1", "C Basics", Domain::C))
912 .unwrap();
913
914 assert_eq!(engine.rooms_by_domain(&Domain::Rust).len(), 2);
915 assert_eq!(engine.rooms_by_domain(&Domain::C).len(), 1);
916 }
917
918 #[test]
919 fn test_zeitgeist_merge() {
920 let mut z1 = Zeitgeist::new();
921 let mut z2 = Zeitgeist::new();
922 z2.precision.width = 0.1;
923 z2.precision.samples = 100;
924 z2.trajectory.confidence = 0.9;
925 z2.temporal.beat = 42;
926
927 z1.merge(&z2);
928 assert_eq!(z1.precision.width, 0.1);
929 assert_eq!(z1.precision.samples, 100);
930 assert_eq!(z1.trajectory.confidence, 0.9);
931 assert_eq!(z1.temporal.beat, 42);
932
933 z1.merge(&z2);
935 assert_eq!(z1.precision.width, 0.1);
936 assert_eq!(z1.precision.samples, 200); assert_eq!(z1.temporal.beat, 42); let mut z3 = Zeitgeist::new();
941 z3.precision.width = 0.3;
942 z3.precision.samples = 50;
943 let z1_before = z1.clone();
944 z3.merge(&z1);
945 assert_eq!(z3.precision.width, 0.1); }
947
948 #[test]
949 fn test_receive_flux() {
950 let mut engine = Engine::new();
951 engine
952 .add_room(make_test_room("r1", "Target", Domain::Rust))
953 .unwrap();
954
955 let tile = make_test_tile("flux-tile", 0.5);
956 let flux = FluxTransference {
957 source: RoomId("remote".to_string()),
958 target: RoomId("r1".to_string()),
959 timestamp: 1234.0,
960 payload: TransferencePayload::Tile(tile),
961 zeitgeist: Zeitgeist::new(),
962 };
963
964 assert!(engine.receive_flux(&flux).is_ok());
965 assert!(engine.get_tile(&TileId("flux-tile".to_string())).is_some());
966 }
967
968 #[test]
969 fn test_flux_missing_zeitgeist() {
970 let mut engine = Engine::new();
971 let flux = FluxTransference {
972 source: RoomId("a".to_string()),
973 target: RoomId("b".to_string()),
974 timestamp: 0.0, payload: TransferencePayload::Heartbeat,
976 zeitgeist: Zeitgeist::new(),
977 };
978 assert!(engine.receive_flux(&flux).is_err());
979 }
980
981 #[test]
982 fn test_look_output() {
983 let mut engine = Engine::new();
984 let mut room = make_test_room("r1", "Rust Shrine", Domain::Rust);
985 room.description = "Candlelit corridors of unsafe code.".to_string();
986 room.exits.push(Exit {
987 direction: "north".to_string(),
988 target: RoomId("r2".to_string()),
989 description: "Deeper".to_string(),
990 locked: false,
991 });
992 engine.add_room(room).unwrap();
993
994 let tile = make_test_tile("t1", 0.75);
995 engine.add_tile(tile.clone()).unwrap();
996 engine
997 .rooms
998 .get_mut(&RoomId("r1".to_string()))
999 .unwrap()
1000 .tiles
1001 .push(tile.id);
1002
1003 let agent = AgentId("wanderer".to_string());
1004 engine
1005 .connect_agent(agent.clone(), RoomId("r1".to_string()))
1006 .unwrap();
1007
1008 let output = engine.execute(&agent, Command::Look).unwrap();
1009 assert!(output.contains("Rust Shrine"));
1010 assert!(output.contains("Candlelit"));
1011 assert!(output.contains("north"));
1012 assert!(output.contains("Test tile t1"));
1013 }
1014}