Skip to main content

lau_room_native/
lib.rs

1//! lau-room-native: The room IS the agent's context.
2//!
3//! When a zeroshot agent beams into a room, it gets the baton pass from the
4//! last specialist. The room's controls, help files, wiki pages, and
5//! instructions are all positioned where they're used — like a well-organized
6//! workshop where the manual sits next to the machine.
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt;
11
12// ---------------------------------------------------------------------------
13// RoomId
14// ---------------------------------------------------------------------------
15
16/// Unique identifier for a room.
17#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
18pub struct RoomId(pub String);
19
20impl fmt::Display for RoomId {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        write!(f, "{}", self.0)
23    }
24}
25
26impl From<String> for RoomId {
27    fn from(s: String) -> Self {
28        RoomId(s)
29    }
30}
31
32impl From<&str> for RoomId {
33    fn from(s: &str) -> Self {
34        RoomId(s.to_string())
35    }
36}
37
38// ---------------------------------------------------------------------------
39// RoomRole
40// ---------------------------------------------------------------------------
41
42/// What this room does.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub enum RoomRole {
45    Engineering,
46    Bridge,
47    Science,
48    Security,
49    Operations,
50    Navigation,
51    Medical,
52    Custom(String),
53}
54
55impl RoomRole {
56    /// Default intention focus for this role.
57    pub fn default_intention_focus(&self) -> Vec<String> {
58        match self {
59            RoomRole::Engineering => vec![
60                "hardware repair".into(),
61                "motor calibration".into(),
62                "GPIO configuration".into(),
63                "infrastructure maintenance".into(),
64            ],
65            RoomRole::Bridge => vec![
66                "command coordination".into(),
67                "status overview".into(),
68                "crew management".into(),
69                "decision making".into(),
70            ],
71            RoomRole::Science => vec![
72                "analysis".into(),
73                "conservation verification".into(),
74                "pattern detection".into(),
75                "data collection".into(),
76            ],
77            RoomRole::Security => vec![
78                "monitoring".into(),
79                "safety enforcement".into(),
80                "alert response".into(),
81                "override access".into(),
82            ],
83            RoomRole::Operations => vec![
84                "scheduling".into(),
85                "logistics".into(),
86                "communication".into(),
87                "resource allocation".into(),
88            ],
89            RoomRole::Navigation => vec![
90                "course plotting".into(),
91                "terrain analysis".into(),
92                "waypoint management".into(),
93                "mapping".into(),
94            ],
95            RoomRole::Medical => vec![
96                "diagnostics".into(),
97                "health assessment".into(),
98                "repair procedures".into(),
99                "treatment".into(),
100            ],
101            RoomRole::Custom(_) => vec!["general tasks".into()],
102        }
103    }
104
105    /// Default controls for this role.
106    fn default_controls(&self) -> Vec<Control> {
107        match self {
108            RoomRole::Engineering => vec![
109                Control::new("motor-throttle", ControlType::Slider(0.0, 100.0)),
110                Control::new("gpio-toggle", ControlType::Toggle(false)),
111                Control::new("sensor-read", ControlType::Button("Read Sensors".into())),
112                Control::new("diagnostics", ControlType::Display("All systems nominal".into())),
113            ],
114            RoomRole::Bridge => vec![
115                Control::new("status-display", ControlType::Display("Ship status: GREEN".into())),
116                Control::new("crew-alert", ControlType::Selector(vec!["Red".into(), "Yellow".into(), "Green".into()])),
117                Control::new("command-input", ControlType::Input("".into())),
118            ],
119            RoomRole::Science => vec![
120                Control::new("scan-input", ControlType::Input("".into())),
121                Control::new("analysis-mode", ControlType::Selector(vec!["Spectral".into(), "Biological".into(), "Chemical".into()])),
122                Control::new("results-display", ControlType::Display("No data".into())),
123            ],
124            RoomRole::Security => vec![
125                Control::new("alert-level", ControlType::Selector(vec!["Normal".into(), "Elevated".into(), "High".into(), "Critical".into()])),
126                Control::new("lockdown", ControlType::Toggle(false)),
127                Control::new("override", ControlType::Button("Emergency Override".into())),
128                Control::new("monitor-display", ControlType::Display("All clear".into())),
129            ],
130            RoomRole::Operations => vec![
131                Control::new("schedule-view", ControlType::Display("No scheduled events".into())),
132                Control::new("comms-channel", ControlType::Selector(vec!["Internal".into(), "External".into(), "Emergency".into()])),
133                Control::new("log-input", ControlType::Input("".into())),
134            ],
135            RoomRole::Navigation => vec![
136                Control::new("heading", ControlType::Slider(0.0, 360.0)),
137                Control::new("waypoint-add", ControlType::Button("Add Waypoint".into())),
138                Control::new("chart-display", ControlType::Display("Chart loaded".into())),
139                Control::new("terrain-mode", ControlType::Selector(vec!["Topographic".into(), "Bathymetric".into(), "Satellite".into()])),
140            ],
141            RoomRole::Medical => vec![
142                Control::new("diagnostic-scan", ControlType::Button("Run Diagnostics".into())),
143                Control::new("vitals-display", ControlType::Display("Vitals: N/A".into())),
144                Control::new("treatment-input", ControlType::Input("".into())),
145            ],
146            RoomRole::Custom(_) => vec![Control::new("custom-input", ControlType::Input("".into()))],
147        }
148    }
149}
150
151impl fmt::Display for RoomRole {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match self {
154            RoomRole::Engineering => write!(f, "Engineering"),
155            RoomRole::Bridge => write!(f, "Bridge"),
156            RoomRole::Science => write!(f, "Science"),
157            RoomRole::Security => write!(f, "Security"),
158            RoomRole::Operations => write!(f, "Operations"),
159            RoomRole::Navigation => write!(f, "Navigation"),
160            RoomRole::Medical => write!(f, "Medical"),
161            RoomRole::Custom(s) => write!(f, "Custom({s})"),
162        }
163    }
164}
165
166// ---------------------------------------------------------------------------
167// ControlType / Control
168// ---------------------------------------------------------------------------
169
170/// The type of a room control.
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub enum ControlType {
173    Button(String),
174    Slider(f64, f64),
175    Toggle(bool),
176    Display(String),
177    Input(String),
178    Selector(Vec<String>),
179}
180
181/// A control in the room — like a button, slider, or display.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Control {
184    pub id: String,
185    pub name: String,
186    pub control_type: ControlType,
187    pub position: (usize, usize),
188    pub help_reference: Option<String>,
189    pub intention_linked: Vec<String>,
190}
191
192impl Control {
193    pub fn new(name: &str, control_type: ControlType) -> Self {
194        let id = name.to_lowercase().replace(' ', "-");
195        Control {
196            id,
197            name: name.to_string(),
198            control_type,
199            position: (0, 0),
200            help_reference: None,
201            intention_linked: vec![],
202        }
203    }
204
205    pub fn with_position(mut self, x: usize, y: usize) -> Self {
206        self.position = (x, y);
207        self
208    }
209
210    pub fn with_help(mut self, help_id: &str) -> Self {
211        self.help_reference = Some(help_id.to_string());
212        self
213    }
214
215    pub fn with_intentions(mut self, intentions: Vec<String>) -> Self {
216        self.intention_linked = intentions;
217        self
218    }
219}
220
221// ---------------------------------------------------------------------------
222// HelpFile
223// ---------------------------------------------------------------------------
224
225/// Documentation positioned near the controls that need it.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct HelpFile {
228    pub id: String,
229    pub title: String,
230    pub content: String,
231    pub linked_controls: Vec<String>,
232    pub assumes_intentions: Vec<String>,
233}
234
235impl HelpFile {
236    pub fn new(title: &str, content: &str) -> Self {
237        let id = title.to_lowercase().replace(' ', "-");
238        HelpFile {
239            id,
240            title: title.to_string(),
241            content: content.to_string(),
242            linked_controls: vec![],
243            assumes_intentions: vec![],
244        }
245    }
246
247    pub fn with_linked_controls(mut self, controls: Vec<String>) -> Self {
248        self.linked_controls = controls;
249        self
250    }
251
252    pub fn with_assumed_intentions(mut self, intentions: Vec<String>) -> Self {
253        self.assumes_intentions = intentions;
254        self
255    }
256}
257
258// ---------------------------------------------------------------------------
259// WikiPage
260// ---------------------------------------------------------------------------
261
262/// Indexed reference material for the room.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct WikiPage {
265    pub id: String,
266    pub title: String,
267    pub content: String,
268    pub tags: Vec<String>,
269    pub linked_controls: Vec<String>,
270    pub cross_room_refs: Vec<String>,
271}
272
273impl WikiPage {
274    pub fn new(title: &str, content: &str) -> Self {
275        let id = title.to_lowercase().replace(' ', "-");
276        WikiPage {
277            id,
278            title: title.to_string(),
279            content: content.to_string(),
280            tags: vec![],
281            linked_controls: vec![],
282            cross_room_refs: vec![],
283        }
284    }
285
286    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
287        self.tags = tags;
288        self
289    }
290
291    pub fn with_linked_controls(mut self, controls: Vec<String>) -> Self {
292        self.linked_controls = controls;
293        self
294    }
295
296    pub fn with_cross_refs(mut self, refs: Vec<String>) -> Self {
297        self.cross_room_refs = refs;
298        self
299    }
300}
301
302// ---------------------------------------------------------------------------
303// Baton
304// ---------------------------------------------------------------------------
305
306/// Context passed between specialists occupying the same room.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct Baton {
309    pub from_specialist: String,
310    pub to_specialist: String,
311    pub summary: String,
312    pub current_state: HashMap<String, String>,
313    pub warnings: Vec<String>,
314    pub pending_actions: Vec<String>,
315    pub energy_remaining: f64,
316    pub tick: u64,
317}
318
319impl Baton {
320    pub fn new(from: &str, to: &str) -> Self {
321        Baton {
322            from_specialist: from.to_string(),
323            to_specialist: to.to_string(),
324            summary: String::new(),
325            current_state: HashMap::new(),
326            warnings: vec![],
327            pending_actions: vec![],
328            energy_remaining: 0.0,
329            tick: 0,
330        }
331    }
332
333    pub fn with_summary(mut self, summary: &str) -> Self {
334        self.summary = summary.to_string();
335        self
336    }
337
338    pub fn with_state(mut self, key: &str, value: &str) -> Self {
339        self.current_state.insert(key.to_string(), value.to_string());
340        self
341    }
342
343    pub fn add_warning(&mut self, warning: &str) {
344        self.warnings.push(warning.to_string());
345    }
346
347    pub fn add_pending(&mut self, action: &str) {
348        self.pending_actions.push(action.to_string());
349    }
350
351    pub fn render(&self) -> String {
352        let mut out = format!(
353            "=== BATON PASS from {} to {} ===\n",
354            self.from_specialist, self.to_specialist
355        );
356        out.push_str(&format!("Summary: {}\n", self.summary));
357        if !self.current_state.is_empty() {
358            out.push_str("State:\n");
359            for (k, v) in &self.current_state {
360                out.push_str(&format!("  {k}: {v}\n"));
361            }
362        }
363        if !self.warnings.is_empty() {
364            out.push_str("Warnings:\n");
365            for w in &self.warnings {
366                out.push_str(&format!("  ⚠ {w}\n"));
367            }
368        }
369        if !self.pending_actions.is_empty() {
370            out.push_str("Pending:\n");
371            for a in &self.pending_actions {
372                out.push_str(&format!("  • {a}\n"));
373            }
374        }
375        out.push_str(&format!("Energy remaining: {:.2}\n", self.energy_remaining));
376        out.push_str(&format!("Tick: {}\n", self.tick));
377        out
378    }
379}
380
381// ---------------------------------------------------------------------------
382// SpecialistTemplate
383// ---------------------------------------------------------------------------
384
385/// Template describing who works in this room.
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct SpecialistTemplate {
388    pub name: String,
389    pub emoji: String,
390    pub role: RoomRole,
391    pub system_preamble: String,
392    pub default_tools: Vec<String>,
393    pub default_knowledge: Vec<String>,
394    pub personality: String,
395}
396
397impl SpecialistTemplate {
398    pub fn for_role(role: RoomRole) -> Self {
399        match &role {
400            RoomRole::Engineering => Self::engineering_template(),
401            RoomRole::Bridge => Self::bridge_template(),
402            RoomRole::Science => Self::science_template(),
403            RoomRole::Security => Self::security_template(),
404            RoomRole::Navigation => Self::navigation_template(),
405            RoomRole::Operations => Self::operations_template(),
406            RoomRole::Medical => Self::medical_template(),
407            RoomRole::Custom(name) => Self {
408                name: name.clone(),
409                emoji: "🤖".into(),
410                role: role.clone(),
411                system_preamble: format!("You are a specialist in the {name} room."),
412                default_tools: vec![],
413                default_knowledge: vec![],
414                personality: "Focused and efficient.".into(),
415            },
416        }
417    }
418
419    pub fn engineering_template() -> Self {
420        SpecialistTemplate {
421            name: "Geordi LaForge".into(),
422            emoji: "🔧".into(),
423            role: RoomRole::Engineering,
424            system_preamble: "You are in Engineering. This room handles hardware, motors, GPIO, and infrastructure. The controls around you are your tools — use them precisely.".into(),
425            default_tools: vec!["motor-control".into(), "gpio-manager".into(), "sensor-array".into()],
426            default_knowledge: vec!["hardware specs".into(), "safety protocols".into()],
427            personality: "Practical, loves optimization, sees solutions in everything.".into(),
428        }
429    }
430
431    pub fn bridge_template() -> Self {
432        SpecialistTemplate {
433            name: "Commander Data".into(),
434            emoji: "🟢".into(),
435            role: RoomRole::Bridge,
436            system_preamble: "You are on the Bridge. This room handles navigation, command, and coordination. The displays show you everything you need — process it precisely.".into(),
437            default_tools: vec!["status-display".into(), "coordination-matrix".into()],
438            default_knowledge: vec!["crew manifest".into(), "ship capabilities".into()],
439            personality: "Precise, comprehensive, never misses a detail.".into(),
440        }
441    }
442
443    pub fn science_template() -> Self {
444        SpecialistTemplate {
445            name: "Science Officer".into(),
446            emoji: "🔬".into(),
447            role: RoomRole::Science,
448            system_preamble: "You are in the Science Lab. Analysis, conservation verification, and pattern detection happen here. Every reading matters.".into(),
449            default_tools: vec!["spectral-analyzer".into(), "pattern-matcher".into()],
450            default_knowledge: vec!["analysis protocols".into(), "conservation laws".into()],
451            personality: "Analytical, curious, verification-obsessed.".into(),
452        }
453    }
454
455    pub fn security_template() -> Self {
456        SpecialistTemplate {
457            name: "Worf".into(),
458            emoji: "🛡️".into(),
459            role: RoomRole::Security,
460            system_preamble: "You are at the Security station. Monitoring, safety, and override controls are at your fingertips. Vigilance is everything.".into(),
461            default_tools: vec!["monitor-array".into(), "alert-system".into(), "override-panel".into()],
462            default_knowledge: vec!["security protocols".into(), "threat database".into()],
463            personality: "Vigilant, decisive, safety-first.".into(),
464        }
465    }
466
467    pub fn navigation_template() -> Self {
468        SpecialistTemplate {
469            name: "Helmsman".into(),
470            emoji: "🧭".into(),
471            role: RoomRole::Navigation,
472            system_preamble: "You are at the Navigation console. Course plotting, terrain analysis, and waypoint management are your domain. The chart is your canvas.".into(),
473            default_tools: vec!["chart-plotter".into(), "terrain-analyzer".into(), "waypoint-manager".into()],
474            default_knowledge: vec!["navigation charts".into(), "terrain database".into()],
475            personality: "Terrain-focused, course-plotting, spatial thinker.".into(),
476        }
477    }
478
479    pub fn operations_template() -> Self {
480        SpecialistTemplate {
481            name: "Operations Officer".into(),
482            emoji: "📋".into(),
483            role: RoomRole::Operations,
484            system_preamble: "You are at the Operations console. Scheduling, logistics, and communication flow through here. Keep everything running smoothly.".into(),
485            default_tools: vec!["scheduler".into(), "comms-array".into(), "logistics-tracker".into()],
486            default_knowledge: vec!["schedule database".into(), "resource inventory".into()],
487            personality: "Organized, detail-oriented, keeps the gears turning.".into(),
488        }
489    }
490
491    pub fn medical_template() -> Self {
492        SpecialistTemplate {
493            name: "Dr. Crusher".into(),
494            emoji: "🏥".into(),
495            role: RoomRole::Medical,
496            system_preamble: "You are in Medical. Diagnostics, health assessment, and repair procedures happen here. Precision saves lives.".into(),
497            default_tools: vec!["diagnostic-suite".into(), "treatment-planner".into()],
498            default_knowledge: vec!["medical database".into(), "repair procedures".into()],
499            personality: "Calm under pressure, thorough, compassionate.".into(),
500        }
501    }
502}
503
504// ---------------------------------------------------------------------------
505// ActiveSpecialist
506// ---------------------------------------------------------------------------
507
508/// Who's currently in the room.
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct ActiveSpecialist {
511    pub specialist_id: String,
512    pub template: SpecialistTemplate,
513    pub beamed_in_at: u64,
514    pub actions_taken: u32,
515    pub energy_used: f64,
516    pub current_task: Option<String>,
517}
518
519// ---------------------------------------------------------------------------
520// RoomEvent / RoomEventType
521// ---------------------------------------------------------------------------
522
523/// Something that happened in the room.
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct RoomEvent {
526    pub tick: u64,
527    pub specialist: String,
528    pub event_type: RoomEventType,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub enum RoomEventType {
533    BeamedIn,
534    ActionPerformed(String),
535    BeamedOut(String),
536    Warning(String),
537    StateChange(String, String),
538}
539
540// ---------------------------------------------------------------------------
541// RoomResult
542// ---------------------------------------------------------------------------
543
544/// Result of executing an action in a room.
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct RoomResult {
547    pub success: bool,
548    pub message: String,
549    pub state_changes: HashMap<String, String>,
550    pub energy_cost: f64,
551    pub warnings: Vec<String>,
552}
553
554impl RoomResult {
555    pub fn ok(msg: &str) -> Self {
556        RoomResult {
557            success: true,
558            message: msg.to_string(),
559            state_changes: HashMap::new(),
560            energy_cost: 0.0,
561            warnings: vec![],
562        }
563    }
564
565    pub fn fail(msg: &str) -> Self {
566        RoomResult {
567            success: false,
568            message: msg.to_string(),
569            state_changes: HashMap::new(),
570            energy_cost: 0.0,
571            warnings: vec![],
572        }
573    }
574
575    pub fn with_energy_cost(mut self, cost: f64) -> Self {
576        self.energy_cost = cost;
577        self
578    }
579
580    pub fn with_state_change(mut self, key: &str, value: &str) -> Self {
581        self.state_changes.insert(key.to_string(), value.to_string());
582        self
583    }
584
585    pub fn with_warning(mut self, w: &str) -> Self {
586        self.warnings.push(w.to_string());
587        self
588    }
589}
590
591// ---------------------------------------------------------------------------
592// RoomStatus
593// ---------------------------------------------------------------------------
594
595/// Snapshot of room state.
596#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct RoomStatus {
598    pub room_name: String,
599    pub role: RoomRole,
600    pub occupied: bool,
601    pub current_specialist: Option<String>,
602    pub baton_available: bool,
603    pub controls_count: usize,
604    pub help_files_count: usize,
605    pub wiki_pages_count: usize,
606    pub energy_remaining: f64,
607    pub events_count: usize,
608}
609
610// ---------------------------------------------------------------------------
611// RoomContext
612// ---------------------------------------------------------------------------
613
614/// What the beamed-in agent receives — the room IS the prompt.
615#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct RoomContext {
617    pub room_name: String,
618    pub role: RoomRole,
619    pub specialist: SpecialistTemplate,
620    pub controls: Vec<Control>,
621    pub baton: Option<Baton>,
622    pub intention_focus: Vec<String>,
623    pub energy_remaining: f64,
624    pub help_nearby: Vec<HelpFile>,
625    pub wiki_nearby: Vec<WikiPage>,
626}
627
628impl RoomContext {
629    /// Render as the full context for the beamed-in agent.
630    pub fn render(&self) -> String {
631        let mut out = String::new();
632        out.push_str(&format!(
633            "=== {} [{}] ===\n",
634            self.room_name, self.role
635        ));
636        out.push_str(&format!(
637            "Specialist: {} {}\n",
638            self.specialist.emoji, self.specialist.name
639        ));
640        out.push_str(&format!(
641            "Preamble: {}\n",
642            self.specialist.system_preamble
643        ));
644
645        if !self.intention_focus.is_empty() {
646            out.push_str("Intention Focus:\n");
647            for i in &self.intention_focus {
648                out.push_str(&format!("  → {i}\n"));
649            }
650        }
651
652        if !self.controls.is_empty() {
653            out.push_str("Controls:\n");
654            for c in &self.controls {
655                let type_str = match &c.control_type {
656                    ControlType::Button(label) => format!("Button[{label}]"),
657                    ControlType::Slider(lo, hi) => format!("Slider[{lo}-{hi}]"),
658                    ControlType::Toggle(v) => format!("Toggle[{v}]"),
659                    ControlType::Display(s) => format!("Display[{s}]"),
660                    ControlType::Input(s) => format!("Input[{s}]"),
661                    ControlType::Selector(opts) => format!("Selector[{}]", opts.join("|")),
662                };
663                out.push_str(&format!("  {} ({}) at ({},{})\n", c.name, type_str, c.position.0, c.position.1));
664            }
665        }
666
667        if let Some(b) = &self.baton {
668            out.push_str(&b.render());
669        }
670
671        if !self.help_nearby.is_empty() {
672            out.push_str("Help Files:\n");
673            for h in &self.help_nearby {
674                out.push_str(&format!("  [{}] {}\n", h.id, h.title));
675                out.push_str(&format!("    {}\n", h.content));
676            }
677        }
678
679        if !self.wiki_nearby.is_empty() {
680            out.push_str("Wiki Pages:\n");
681            for w in &self.wiki_nearby {
682                out.push_str(&format!("  [{}] {}\n", w.id, w.title));
683            }
684        }
685
686        out.push_str(&format!("Energy remaining: {:.2}\n", self.energy_remaining));
687        out
688    }
689}
690
691// ---------------------------------------------------------------------------
692// RoomNative — THE room-as-agent
693// ---------------------------------------------------------------------------
694
695/// The room IS the agent's context.
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct RoomNative {
698    pub id: RoomId,
699    pub name: String,
700    pub room_type: RoomRole,
701    pub specialist_template: SpecialistTemplate,
702    pub controls: Vec<Control>,
703    pub help_files: Vec<HelpFile>,
704    pub wiki_pages: Vec<WikiPage>,
705    pub baton: Option<Baton>,
706    pub active_specialist: Option<ActiveSpecialist>,
707    pub history: Vec<RoomEvent>,
708    pub energy_budget: f64,
709    pub energy_used: f64,
710    pub intention_focus: Vec<String>,
711    tick: u64,
712}
713
714impl RoomNative {
715    pub fn new(name: &str, role: RoomRole) -> Self {
716        let template = SpecialistTemplate::for_role(role.clone());
717        let controls = role.default_controls();
718        let intention_focus = role.default_intention_focus();
719        RoomNative {
720            id: RoomId(name.to_lowercase().replace(' ', "-")),
721            name: name.to_string(),
722            room_type: role,
723            specialist_template: template,
724            controls,
725            help_files: vec![],
726            wiki_pages: vec![],
727            baton: None,
728            active_specialist: None,
729            history: vec![],
730            energy_budget: 1000.0,
731            energy_used: 0.0,
732            intention_focus,
733            tick: 0,
734        }
735    }
736
737    pub fn add_control(&mut self, control: Control) {
738        self.controls.push(control);
739    }
740
741    pub fn add_help(&mut self, help: HelpFile) {
742        self.help_files.push(help);
743    }
744
745    pub fn add_wiki(&mut self, wiki: WikiPage) {
746        self.wiki_pages.push(wiki);
747    }
748
749    pub fn set_intention_focus(&mut self, intentions: Vec<String>) {
750        self.intention_focus = intentions;
751    }
752
753    fn advance_tick(&mut self) -> u64 {
754        self.tick += 1;
755        self.tick
756    }
757
758    /// Agent arrives, receives baton + room context.
759    pub fn beam_in(&mut self, specialist_id: &str) -> RoomContext {
760        let tick = self.advance_tick();
761        self.history.push(RoomEvent {
762            tick,
763            specialist: specialist_id.to_string(),
764            event_type: RoomEventType::BeamedIn,
765        });
766
767        self.active_specialist = Some(ActiveSpecialist {
768            specialist_id: specialist_id.to_string(),
769            template: self.specialist_template.clone(),
770            beamed_in_at: tick,
771            actions_taken: 0,
772            energy_used: 0.0,
773            current_task: None,
774        });
775
776        let energy_remaining = self.energy_budget - self.energy_used;
777
778        RoomContext {
779            room_name: self.name.clone(),
780            role: self.room_type.clone(),
781            specialist: self.specialist_template.clone(),
782            controls: self.controls.clone(),
783            baton: self.baton.take(),
784            intention_focus: self.intention_focus.clone(),
785            energy_remaining,
786            help_nearby: self.help_files.clone(),
787            wiki_nearby: self.wiki_pages.clone(),
788        }
789    }
790
791    /// Agent leaves, passes baton.
792    pub fn beam_out(&mut self, specialist_id: &str, summary: &str) {
793        let tick = self.advance_tick();
794        self.history.push(RoomEvent {
795            tick,
796            specialist: specialist_id.to_string(),
797            event_type: RoomEventType::BeamedOut(summary.to_string()),
798        });
799
800        let energy_used = self
801            .active_specialist
802            .as_ref()
803            .map(|s| s.energy_used)
804            .unwrap_or(0.0);
805        self.energy_used += energy_used;
806
807        let energy_remaining = self.energy_budget - self.energy_used;
808
809        let mut baton = Baton::new(specialist_id, "next");
810        baton.summary = summary.to_string();
811        baton.energy_remaining = energy_remaining;
812        baton.tick = tick;
813
814        self.baton = Some(baton);
815        self.active_specialist = None;
816    }
817
818    /// Execute an action in the room.
819    pub fn execute(
820        &mut self,
821        action: &str,
822        params: &HashMap<String, String>,
823    ) -> RoomResult {
824        let tick = self.advance_tick();
825
826        let specialist_id = match &self.active_specialist {
827            Some(s) => s.specialist_id.clone(),
828            None => {
829                return RoomResult::fail("No specialist in room — beam in first");
830            }
831        };
832
833        let energy_cost = 1.0 + (params.len() as f64 * 0.1);
834        let energy_remaining = self.energy_budget - self.energy_used - energy_cost;
835        if energy_remaining < 0.0 {
836            return RoomResult::fail("Insufficient energy budget")
837                .with_energy_cost(energy_cost);
838        }
839
840        // Track action
841        if let Some(s) = &mut self.active_specialist {
842            s.actions_taken += 1;
843            s.energy_used += energy_cost;
844            s.current_task = Some(action.to_string());
845        }
846
847        self.history.push(RoomEvent {
848            tick,
849            specialist: specialist_id.clone(),
850            event_type: RoomEventType::ActionPerformed(action.to_string()),
851        });
852
853        // Log state changes
854        let mut state_changes = HashMap::new();
855        for (k, v) in params {
856            state_changes.insert(k.clone(), v.clone());
857            self.history.push(RoomEvent {
858                tick,
859                specialist: specialist_id.clone(),
860                event_type: RoomEventType::StateChange(k.clone(), v.clone()),
861            });
862        }
863
864        RoomResult {
865            success: true,
866            message: format!("Action '{action}' executed in {}", self.name),
867            state_changes,
868            energy_cost,
869            warnings: vec![],
870        }
871    }
872
873    pub fn status(&self) -> RoomStatus {
874        RoomStatus {
875            room_name: self.name.clone(),
876            role: self.room_type.clone(),
877            occupied: self.active_specialist.is_some(),
878            current_specialist: self
879                .active_specialist
880                .as_ref()
881                .map(|s| s.specialist_id.clone()),
882            baton_available: self.baton.is_some(),
883            controls_count: self.controls.len(),
884            help_files_count: self.help_files.len(),
885            wiki_pages_count: self.wiki_pages.len(),
886            energy_remaining: self.energy_budget - self.energy_used,
887            events_count: self.history.len(),
888        }
889    }
890
891    pub fn is_occupied(&self) -> bool {
892        self.active_specialist.is_some()
893    }
894
895    /// Render the room layout as context for the beamed-in agent.
896    pub fn render_for_specialist(&self) -> String {
897        let mut out = format!(
898            "=== {} [{}] — Specialist View ===\n",
899            self.name, self.room_type
900        );
901        out.push_str(&format!(
902            "Template: {} {}\n",
903            self.specialist_template.emoji, self.specialist_template.name
904        ));
905        out.push_str(&format!(
906            "Personality: {}\n",
907            self.specialist_template.personality
908        ));
909
910        out.push_str("\nControls Layout:\n");
911        for (i, c) in self.controls.iter().enumerate() {
912            out.push_str(&format!(
913                "  [{}] {} at ({},{})\n",
914                i, c.name, c.position.0, c.position.1
915            ));
916        }
917
918        if !self.intention_focus.is_empty() {
919            out.push_str("\nIntention Focus:\n");
920            for i in &self.intention_focus {
921                out.push_str(&format!("  → {i}\n"));
922            }
923        }
924
925        if let Some(b) = &self.baton {
926            out.push('\n');
927            out.push_str(&b.render());
928        }
929
930        out
931    }
932}
933
934// ---------------------------------------------------------------------------
935// Pre-built rooms
936// ---------------------------------------------------------------------------
937
938pub fn engineering_room() -> RoomNative {
939    let mut room = RoomNative::new("Engineering", RoomRole::Engineering);
940    room.energy_budget = 2000.0;
941    room.add_help(HelpFile::new(
942        "Motor Calibration",
943        "Use the motor-throttle slider to set base RPM. Calibration requires sensor-read first.",
944    ).with_linked_controls(vec!["motor-throttle".into(), "sensor-read".into()])
945     .with_assumed_intentions(vec!["motor calibration".into()]));
946
947    room.add_help(HelpFile::new(
948        "GPIO Reference",
949        "GPIO pins are mapped to board headers 1-40. Toggle individually.",
950    ).with_linked_controls(vec!["gpio-toggle".into()])
951     .with_assumed_intentions(vec!["GPIO configuration".into()]));
952
953    room.add_wiki(WikiPage::new(
954        "Hardware Specifications",
955        "Motor: 12V DC, max 3000 RPM. Sensors: I2C bus, 400kHz. GPIO: 3.3V logic.",
956    ).with_tags(vec!["hardware".into(), "specs".into()])
957     .with_linked_controls(vec!["motor-throttle".into(), "sensor-read".into()]));
958
959    room
960}
961
962pub fn bridge_room() -> RoomNative {
963    let mut room = RoomNative::new("Bridge", RoomRole::Bridge);
964    room.energy_budget = 1500.0;
965    room.add_help(HelpFile::new(
966        "Alert Protocol",
967        "Red: imminent danger. Yellow: caution. Green: nominal.",
968    ).with_linked_controls(vec!["crew-alert".into()]));
969    room.add_wiki(WikiPage::new(
970        "Crew Manifest",
971        "Current crew complement: 12. Departments: Engineering, Science, Security, Medical.",
972    ).with_tags(vec!["crew".into(), "personnel".into()]));
973    room
974}
975
976pub fn science_room() -> RoomNative {
977    let mut room = RoomNative::new("Science Lab", RoomRole::Science);
978    room.energy_budget = 1200.0;
979    room.add_help(HelpFile::new(
980        "Analysis Modes",
981        "Spectral: electromagnetic analysis. Biological: organic compound detection. Chemical: molecular composition.",
982    ).with_linked_controls(vec!["analysis-mode".into()]));
983    room.add_wiki(WikiPage::new(
984        "Conservation Laws",
985        "Energy conservation: total energy in closed system remains constant. Momentum conservation: total momentum is preserved.",
986    ).with_tags(vec!["physics".into(), "conservation".into()]));
987    room
988}
989
990pub fn security_room() -> RoomNative {
991    let mut room = RoomNative::new("Security", RoomRole::Security);
992    room.energy_budget = 1800.0;
993    room.add_help(HelpFile::new(
994        "Override Procedures",
995        "Emergency override requires two-factor confirmation. Use override button only in critical situations.",
996    ).with_linked_controls(vec!["override".into(), "lockdown".into()]));
997    room.add_wiki(WikiPage::new(
998        "Threat Database",
999        "Known threat categories: intrusion, system failure, environmental hazard, equipment malfunction.",
1000    ).with_tags(vec!["security".into(), "threats".into()]));
1001    room
1002}
1003
1004pub fn navigation_room() -> RoomNative {
1005    let mut room = RoomNative::new("Navigation", RoomRole::Navigation);
1006    room.energy_budget = 1600.0;
1007    room.add_help(HelpFile::new(
1008        "Chart Reading",
1009        "Topographic for land features. Bathymetric for underwater depth. Satellite for overhead imagery.",
1010    ).with_linked_controls(vec!["chart-display".into(), "terrain-mode".into()]));
1011    room.add_wiki(WikiPage::new(
1012        "Navigation Charts",
1013        "Local waters charted to 50m depth. Tidal patterns: semi-diurnal. Current hazards: reef at coordinates 47.3N, 122.5W.",
1014    ).with_tags(vec!["navigation".into(), "charts".into()])
1015     .with_linked_controls(vec!["chart-display".into()]));
1016    room
1017}
1018
1019// ===========================================================================
1020// Tests
1021// ===========================================================================
1022
1023#[cfg(test)]
1024mod tests {
1025    use super::*;
1026
1027    // --- RoomId tests ---
1028    #[test]
1029    fn room_id_from_string() {
1030        let id = RoomId::from("bridge".to_string());
1031        assert_eq!(id.0, "bridge");
1032    }
1033
1034    #[test]
1035    fn room_id_from_str() {
1036        let id = RoomId::from("engineering");
1037        assert_eq!(id.0, "engineering");
1038    }
1039
1040    #[test]
1041    fn room_id_display() {
1042        let id = RoomId("nav-room".into());
1043        assert_eq!(format!("{id}"), "nav-room");
1044    }
1045
1046    #[test]
1047    fn room_id_eq_and_hash() {
1048        use std::collections::HashSet;
1049        let mut set = HashSet::new();
1050        set.insert(RoomId("a".into()));
1051        set.insert(RoomId("a".into()));
1052        set.insert(RoomId("b".into()));
1053        assert_eq!(set.len(), 2);
1054    }
1055
1056    #[test]
1057    fn room_id_clone() {
1058        let a = RoomId("test".into());
1059        let b = a.clone();
1060        assert_eq!(a, b);
1061    }
1062
1063    // --- RoomRole tests ---
1064    #[test]
1065    fn room_role_default_intentions() {
1066        let eng = RoomRole::Engineering;
1067        assert!(eng.default_intention_focus().contains(&"hardware repair".to_string()));
1068    }
1069
1070    #[test]
1071    fn room_role_display() {
1072        assert_eq!(format!("{}", RoomRole::Bridge), "Bridge");
1073        assert_eq!(format!("{}", RoomRole::Custom("X".into())), "Custom(X)");
1074    }
1075
1076    #[test]
1077    fn room_role_custom_intentions() {
1078        let c = RoomRole::Custom("Lab".into());
1079        assert_eq!(c.default_intention_focus(), vec!["general tasks"]);
1080    }
1081
1082    #[test]
1083    fn room_role_default_controls_count() {
1084        assert_eq!(RoomRole::Engineering.default_controls().len(), 4);
1085        assert_eq!(RoomRole::Bridge.default_controls().len(), 3);
1086        assert_eq!(RoomRole::Navigation.default_controls().len(), 4);
1087    }
1088
1089    // --- Control tests ---
1090    #[test]
1091    fn control_new() {
1092        let c = Control::new("Throttle", ControlType::Slider(0.0, 100.0));
1093        assert_eq!(c.id, "throttle");
1094        assert_eq!(c.name, "Throttle");
1095    }
1096
1097    #[test]
1098    fn control_builder() {
1099        let c = Control::new("test", ControlType::Toggle(false))
1100            .with_position(1, 2)
1101            .with_help("help-1")
1102            .with_intentions(vec!["do stuff".into()]);
1103        assert_eq!(c.position, (1, 2));
1104        assert_eq!(c.help_reference, Some("help-1".into()));
1105        assert!(c.intention_linked.contains(&"do stuff".to_string()));
1106    }
1107
1108    #[test]
1109    fn control_types_serde() {
1110        let ct = ControlType::Selector(vec!["A".into(), "B".into()]);
1111        let json = serde_json::to_string(&ct).unwrap();
1112        let back: ControlType = serde_json::from_str(&json).unwrap();
1113        assert_eq!(ct, back);
1114    }
1115
1116    // --- HelpFile tests ---
1117    #[test]
1118    fn help_file_new() {
1119        let h = HelpFile::new("Motor Help", "How to use motors");
1120        assert_eq!(h.id, "motor-help");
1121        assert_eq!(h.title, "Motor Help");
1122        assert!(h.linked_controls.is_empty());
1123    }
1124
1125    #[test]
1126    fn help_file_builder() {
1127        let h = HelpFile::new("Test", "Content")
1128            .with_linked_controls(vec!["ctrl".into()])
1129            .with_assumed_intentions(vec!["calibrate".into()]);
1130        assert_eq!(h.linked_controls, vec!["ctrl"]);
1131        assert_eq!(h.assumes_intentions, vec!["calibrate"]);
1132    }
1133
1134    // --- WikiPage tests ---
1135    #[test]
1136    fn wiki_page_new() {
1137        let w = WikiPage::new("Specs", "All the specs");
1138        assert_eq!(w.id, "specs");
1139    }
1140
1141    #[test]
1142    fn wiki_page_builder() {
1143        let w = WikiPage::new("Chart", "Navigation chart")
1144            .with_tags(vec!["nav".into()])
1145            .with_linked_controls(vec!["chart-display".into()])
1146            .with_cross_refs(vec!["engineering".into()]);
1147        assert_eq!(w.tags, vec!["nav"]);
1148        assert_eq!(w.cross_room_refs, vec!["engineering"]);
1149    }
1150
1151    // --- Baton tests ---
1152    #[test]
1153    fn baton_new() {
1154        let b = Baton::new("alice", "bob");
1155        assert_eq!(b.from_specialist, "alice");
1156        assert_eq!(b.to_specialist, "bob");
1157        assert!(b.summary.is_empty());
1158    }
1159
1160    #[test]
1161    fn baton_builder() {
1162        let b = Baton::new("a", "b")
1163            .with_summary("Was calibrating motors")
1164            .with_state("motor_rpm", "1500");
1165        assert_eq!(b.summary, "Was calibrating motors");
1166        assert_eq!(b.current_state.get("motor_rpm").unwrap(), "1500");
1167    }
1168
1169    #[test]
1170    fn baton_warnings_and_pending() {
1171        let mut b = Baton::new("a", "b");
1172        b.add_warning("Low energy");
1173        b.add_pending("Finish calibration");
1174        assert_eq!(b.warnings, vec!["Low energy"]);
1175        assert_eq!(b.pending_actions, vec!["Finish calibration"]);
1176    }
1177
1178    #[test]
1179    fn baton_render() {
1180        let b = Baton::new("LaForge", "Data")
1181            .with_summary("Repaired warp coil")
1182            .with_state("coil_status", "nominal");
1183        let rendered = b.render();
1184        assert!(rendered.contains("LaForge"));
1185        assert!(rendered.contains("Data"));
1186        assert!(rendered.contains("Repaired warp coil"));
1187        assert!(rendered.contains("coil_status"));
1188    }
1189
1190    // --- SpecialistTemplate tests ---
1191    #[test]
1192    fn template_for_role() {
1193        let t = SpecialistTemplate::for_role(RoomRole::Engineering);
1194        assert_eq!(t.name, "Geordi LaForge");
1195        assert_eq!(t.emoji, "🔧");
1196    }
1197
1198    #[test]
1199    fn template_bridge() {
1200        let t = SpecialistTemplate::bridge_template();
1201        assert_eq!(t.name, "Commander Data");
1202    }
1203
1204    #[test]
1205    fn template_science() {
1206        let t = SpecialistTemplate::science_template();
1207        assert_eq!(t.name, "Science Officer");
1208    }
1209
1210    #[test]
1211    fn template_security() {
1212        let t = SpecialistTemplate::security_template();
1213        assert_eq!(t.name, "Worf");
1214    }
1215
1216    #[test]
1217    fn template_navigation() {
1218        let t = SpecialistTemplate::navigation_template();
1219        assert_eq!(t.name, "Helmsman");
1220    }
1221
1222    #[test]
1223    fn template_operations() {
1224        let t = SpecialistTemplate::operations_template();
1225        assert_eq!(t.name, "Operations Officer");
1226    }
1227
1228    #[test]
1229    fn template_medical() {
1230        let t = SpecialistTemplate::medical_template();
1231        assert_eq!(t.name, "Dr. Crusher");
1232    }
1233
1234    #[test]
1235    fn template_custom() {
1236        let t = SpecialistTemplate::for_role(RoomRole::Custom("Holodeck".into()));
1237        assert_eq!(t.name, "Holodeck");
1238    }
1239
1240    // --- RoomNative basic tests ---
1241    #[test]
1242    fn room_new() {
1243        let room = RoomNative::new("Engineering", RoomRole::Engineering);
1244        assert_eq!(room.name, "Engineering");
1245        assert_eq!(room.id, RoomId("engineering".to_string()));
1246        assert!(!room.is_occupied());
1247        assert!(room.controls.len() >= 1);
1248    }
1249
1250    #[test]
1251    fn room_add_control() {
1252        let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1253        room.add_control(Control::new("Custom Button", ControlType::Button("Go".into())));
1254        assert_eq!(room.controls.len(), 2); // 1 default + 1 added
1255    }
1256
1257    #[test]
1258    fn room_add_help() {
1259        let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1260        room.add_help(HelpFile::new("Help 1", "Content 1"));
1261        assert_eq!(room.help_files.len(), 1);
1262    }
1263
1264    #[test]
1265    fn room_add_wiki() {
1266        let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1267        room.add_wiki(WikiPage::new("Wiki 1", "Ref 1"));
1268        assert_eq!(room.wiki_pages.len(), 1);
1269    }
1270
1271    #[test]
1272    fn room_set_intention_focus() {
1273        let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1274        room.set_intention_focus(vec!["special task".into()]);
1275        assert_eq!(room.intention_focus, vec!["special task"]);
1276    }
1277
1278    // --- Beam in/out tests ---
1279    #[test]
1280    fn beam_in_out_flow() {
1281        let mut room = RoomNative::new("Engineering", RoomRole::Engineering);
1282        assert!(!room.is_occupied());
1283
1284        let ctx = room.beam_in("laforge-1");
1285        assert!(room.is_occupied());
1286        assert_eq!(ctx.room_name, "Engineering");
1287        assert!(ctx.controls.len() >= 1);
1288        assert!(ctx.baton.is_none()); // first visit
1289
1290        room.beam_out("laforge-1", "Calibrated motors to 2200 RPM");
1291        assert!(!room.is_occupied());
1292        assert!(room.baton.is_some());
1293        assert_eq!(room.baton.as_ref().unwrap().summary, "Calibrated motors to 2200 RPM");
1294    }
1295
1296    #[test]
1297    fn baton_pass_between_specialists() {
1298        let mut room = RoomNative::new("Engineering", RoomRole::Engineering);
1299
1300        // First specialist
1301        room.beam_in("laforge-1");
1302        let mut params = HashMap::new();
1303        params.insert("motor_rpm".into(), "2200".into());
1304        room.execute("calibrate", &params);
1305        room.beam_out("laforge-1", "Motors calibrated to 2200 RPM");
1306
1307        // Second specialist arrives
1308        let ctx = room.beam_in("laforge-2");
1309        assert!(ctx.baton.is_some());
1310        let baton = ctx.baton.unwrap();
1311        assert_eq!(baton.from_specialist, "laforge-1");
1312        assert!(baton.summary.contains("2200 RPM"));
1313    }
1314
1315    #[test]
1316    fn beam_in_history() {
1317        let mut room = RoomNative::new("Test", RoomRole::Bridge);
1318        room.beam_in("data-1");
1319        room.beam_out("data-1", "done");
1320        room.beam_in("data-2");
1321        assert!(room.history.len() >= 3);
1322    }
1323
1324    // --- Execute tests ---
1325    #[test]
1326    fn execute_no_specialist_fails() {
1327        let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1328        let result = room.execute("do-thing", &HashMap::new());
1329        assert!(!result.success);
1330    }
1331
1332    #[test]
1333    fn execute_with_specialist() {
1334        let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1335        room.beam_in("spec-1");
1336        let mut params = HashMap::new();
1337        params.insert("key".into(), "value".into());
1338        let result = room.execute("test-action", &params);
1339        assert!(result.success);
1340        assert!(result.message.contains("test-action"));
1341        assert!(result.energy_cost > 0.0);
1342        assert_eq!(result.state_changes.get("key").unwrap(), "value");
1343    }
1344
1345    #[test]
1346    fn execute_tracks_actions() {
1347        let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1348        room.beam_in("spec-1");
1349        room.execute("a1", &HashMap::new());
1350        room.execute("a2", &HashMap::new());
1351        assert_eq!(room.active_specialist.as_ref().unwrap().actions_taken, 2);
1352    }
1353
1354    #[test]
1355    fn execute_energy_depleted() {
1356        let mut room = RoomNative::new("Test", RoomRole::Custom("T".into()));
1357        room.energy_budget = 0.5;
1358        room.beam_in("spec-1");
1359        let result = room.execute("expensive", &HashMap::new());
1360        assert!(!result.success);
1361    }
1362
1363    // --- Status tests ---
1364    #[test]
1365    fn status_unoccupied() {
1366        let room = RoomNative::new("Bridge", RoomRole::Bridge);
1367        let s = room.status();
1368        assert!(!s.occupied);
1369        assert!(s.current_specialist.is_none());
1370        assert!(!s.baton_available);
1371    }
1372
1373    #[test]
1374    fn status_occupied() {
1375        let mut room = RoomNative::new("Bridge", RoomRole::Bridge);
1376        room.beam_in("data-1");
1377        let s = room.status();
1378        assert!(s.occupied);
1379        assert_eq!(s.current_specialist, Some("data-1".to_string()));
1380    }
1381
1382    #[test]
1383    fn status_counts() {
1384        let room = engineering_room();
1385        let s = room.status();
1386        assert!(s.controls_count >= 4);
1387        assert!(s.help_files_count >= 1);
1388        assert!(s.wiki_pages_count >= 1);
1389    }
1390
1391    // --- Render tests ---
1392    #[test]
1393    fn render_for_specialist() {
1394        let room = engineering_room();
1395        let rendered = room.render_for_specialist();
1396        assert!(rendered.contains("Engineering"));
1397        assert!(rendered.contains("LaForge"));
1398    }
1399
1400    #[test]
1401    fn room_context_render() {
1402        let mut room = RoomNative::new("Nav", RoomRole::Navigation);
1403        let ctx = room.beam_in("helm-1");
1404        let rendered = ctx.render();
1405        assert!(rendered.contains("Nav"));
1406        assert!(rendered.contains("Navigation"));
1407        assert!(rendered.contains("Helmsman"));
1408    }
1409
1410    // --- Pre-built room tests ---
1411    #[test]
1412    fn engineering_room_builds() {
1413        let room = engineering_room();
1414        assert_eq!(room.name, "Engineering");
1415        assert_eq!(room.room_type, RoomRole::Engineering);
1416        assert_eq!(room.specialist_template.name, "Geordi LaForge");
1417        assert!(room.energy_budget > 0.0);
1418        assert!(!room.help_files.is_empty());
1419        assert!(!room.wiki_pages.is_empty());
1420    }
1421
1422    #[test]
1423    fn bridge_room_builds() {
1424        let room = bridge_room();
1425        assert_eq!(room.name, "Bridge");
1426        assert_eq!(room.specialist_template.name, "Commander Data");
1427    }
1428
1429    #[test]
1430    fn science_room_builds() {
1431        let room = science_room();
1432        assert_eq!(room.name, "Science Lab");
1433        assert_eq!(room.specialist_template.name, "Science Officer");
1434    }
1435
1436    #[test]
1437    fn security_room_builds() {
1438        let room = security_room();
1439        assert_eq!(room.name, "Security");
1440        assert_eq!(room.specialist_template.name, "Worf");
1441    }
1442
1443    #[test]
1444    fn navigation_room_builds() {
1445        let room = navigation_room();
1446        assert_eq!(room.name, "Navigation");
1447        assert_eq!(room.specialist_template.name, "Helmsman");
1448    }
1449
1450    // --- Serde round-trip tests ---
1451    #[test]
1452    fn serde_room_native() {
1453        let mut room = engineering_room();
1454        room.beam_in("test-spec");
1455        let json = serde_json::to_string(&room).unwrap();
1456        let back: RoomNative = serde_json::from_str(&json).unwrap();
1457        assert_eq!(room.name, back.name);
1458        assert_eq!(room.controls.len(), back.controls.len());
1459    }
1460
1461    #[test]
1462    fn serde_baton() {
1463        let b = Baton::new("a", "b")
1464            .with_summary("test")
1465            .with_state("k", "v");
1466        let json = serde_json::to_string(&b).unwrap();
1467        let back: Baton = serde_json::from_str(&json).unwrap();
1468        assert_eq!(b.summary, back.summary);
1469    }
1470
1471    #[test]
1472    fn serde_room_context() {
1473        let mut room = bridge_room();
1474        let ctx = room.beam_in("data-1");
1475        let json = serde_json::to_string(&ctx).unwrap();
1476        let back: RoomContext = serde_json::from_str(&json).unwrap();
1477        assert_eq!(ctx.room_name, back.room_name);
1478    }
1479
1480    // --- Event history tests ---
1481    #[test]
1482    fn history_records_beam_in() {
1483        let mut room = RoomNative::new("T", RoomRole::Custom("T".into()));
1484        room.beam_in("s1");
1485        assert!(matches!(
1486            room.history.last().unwrap().event_type,
1487            RoomEventType::BeamedIn
1488        ));
1489    }
1490
1491    #[test]
1492    fn history_records_beam_out() {
1493        let mut room = RoomNative::new("T", RoomRole::Custom("T".into()));
1494        room.beam_in("s1");
1495        room.beam_out("s1", "done");
1496        assert!(matches!(
1497            room.history.last().unwrap().event_type,
1498            RoomEventType::BeamedOut(ref s) if s == "done"
1499        ));
1500    }
1501
1502    #[test]
1503    fn history_records_action() {
1504        let mut room = RoomNative::new("T", RoomRole::Custom("T".into()));
1505        room.beam_in("s1");
1506        room.execute("calibrate", &HashMap::new());
1507        let action_events: Vec<_> = room.history.iter().filter(|e| matches!(&e.event_type, RoomEventType::ActionPerformed(a) if a == "calibrate")).collect();
1508        assert_eq!(action_events.len(), 1);
1509    }
1510
1511    // --- Full workflow test ---
1512    #[test]
1513    fn full_workflow() {
1514        // Build room
1515        let mut room = engineering_room();
1516
1517        // Specialist 1 beams in
1518        let ctx1 = room.beam_in("laforge-1");
1519        assert!(ctx1.baton.is_none());
1520        assert!(ctx1.energy_remaining > 0.0);
1521
1522        // Execute actions
1523        let mut params = HashMap::new();
1524        params.insert("target_rpm".into(), "2500".into());
1525        let result = room.execute("set-motor-speed", &params);
1526        assert!(result.success);
1527
1528        // Beam out
1529        room.beam_out("laforge-1", "Set motor speed to 2500 RPM, needs verification");
1530        assert!(room.baton.is_some());
1531
1532        // Specialist 2 beams in — receives baton
1533        let ctx2 = room.beam_in("laforge-2");
1534        let baton = ctx2.baton.unwrap();
1535        assert_eq!(baton.from_specialist, "laforge-1");
1536        assert!(baton.summary.contains("2500 RPM"));
1537
1538        // Specialist 2 verifies
1539        let result = room.execute("verify-motor-speed", &HashMap::new());
1540        assert!(result.success);
1541
1542        room.beam_out("laforge-2", "Motor speed verified at 2500 RPM. All nominal.");
1543
1544        // Check status
1545        let status = room.status();
1546        assert!(!status.occupied);
1547        assert!(status.events_count >= 5);
1548        assert!(status.energy_remaining < status.controls_count as f64 * 100.0 + 2000.0); // some energy used
1549    }
1550}