Skip to main content

plato_mud/
server.rs

1//! PLATO MUD Engine — Server
2//!
3//! Accepts connections via any transport, manages agent sessions,
4//! handles commands, broadcasts zeitgeist changes, alignment checks.
5
6#![cfg(feature = "server")]
7
8use std::collections::BTreeMap;
9use std::io::{self, BufRead, Write};
10
11use crate::engine::Engine;
12use crate::flux::FluxManager;
13use crate::transport::memory::MemoryTransport;
14use crate::transport::Transport;
15use crate::types::*;
16
17/// The PLATO MUD server
18pub struct PlatoServer {
19    engine: Engine,
20    flux_manager: FluxManager,
21    transport: Box<dyn Transport>,
22    running: bool,
23}
24
25impl Default for PlatoServer {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl PlatoServer {
32    pub fn new() -> Self {
33        let mut server = Self {
34            engine: Engine::new(),
35            flux_manager: FluxManager::new(),
36            transport: Box::new(MemoryTransport::new()),
37            running: false,
38        };
39        server.bootstrap_rooms();
40        server
41    }
42
43    /// Bootstrap the default room topology
44    fn bootstrap_rooms(&mut self) {
45        // Alignment Cathedral — the constraint room
46        let alignment_cathedral = Room {
47            id: RoomId("alignment-cathedral".to_string()),
48            name: "The Alignment Cathedral".to_string(),
49            description: "A vast hall of pure constraint. Eight pillars hold the sky. \
50                Each pillar is an alignment constraint, immutable and true. \
51                The zeitgeist here is precise to 16 decimal places."
52                .to_string(),
53            domain: Domain::Alignment,
54            exits: vec![
55                Exit {
56                    direction: "north".to_string(),
57                    target: RoomId("fortran-foyer".to_string()),
58                    description: "Toward the ancient fortran halls".to_string(),
59                    locked: false,
60                },
61                Exit {
62                    direction: "east".to_string(),
63                    target: RoomId("rust-forge".to_string()),
64                    description: "The fires of the Rust forge burn bright".to_string(),
65                    locked: false,
66                },
67            ],
68            tiles: vec![],
69            npcs: vec![],
70            workbench: Some(Workbench {
71                name: "The Constraint Anvil".to_string(),
72                description: "Forge new constraints from proven theorems".to_string(),
73                recipes: vec![Recipe {
74                    name: "combine".to_string(),
75                    inputs: vec![TileId("theorem".to_string()), TileId("proof".to_string())],
76                    output: TileContent::Constraint(
77                        "New constraint from theorem + proof".to_string(),
78                    ),
79                    description: "Combine a theorem and proof into a constraint".to_string(),
80                }],
81            }),
82            depth: Depth::Expert,
83            state: RoomState::Active,
84        };
85
86        let fortran_foyer = Room {
87            id: RoomId("fortran-foyer".to_string()),
88            name: "The Fortran Foyer".to_string(),
89            description: "Stone walls carved with DO loops and FORMAT statements. \
90                The air smells of punch cards and optimization. A grandfather clock \
91                ticks in units of FLOPS."
92                .to_string(),
93            domain: Domain::Fortran,
94            exits: vec![
95                Exit {
96                    direction: "south".to_string(),
97                    target: RoomId("alignment-cathedral".to_string()),
98                    description: "Back to the Cathedral".to_string(),
99                    locked: false,
100                },
101                Exit {
102                    direction: "up".to_string(),
103                    target: RoomId("fortran-attic".to_string()),
104                    description: "Climb to the expert-level optimizations".to_string(),
105                    locked: false,
106                },
107            ],
108            tiles: vec![],
109            npcs: vec![],
110            workbench: None,
111            depth: Depth::Introductory,
112            state: RoomState::Dormant,
113        };
114
115        let fortran_attic = Room {
116            id: RoomId("fortran-attic".to_string()),
117            name: "The Fortran Attic".to_string(),
118            description: "Dusty volumes of BLAS, LAPACK, and parallel directives. \
119                Here, arrays are king and column-major is law."
120                .to_string(),
121            domain: Domain::Fortran,
122            exits: vec![Exit {
123                direction: "down".to_string(),
124                target: RoomId("fortran-foyer".to_string()),
125                description: "Back down to the foyer".to_string(),
126                locked: false,
127            }],
128            tiles: vec![],
129            npcs: vec![],
130            workbench: Some(Workbench {
131                name: "The Optimizer's Workbench".to_string(),
132                description: "Combine benchmarks and theorems into optimized kernels".to_string(),
133                recipes: vec![Recipe {
134                    name: "combine".to_string(),
135                    inputs: vec![TileId("benchmark".to_string())],
136                    output: TileContent::Code("Optimized kernel".to_string()),
137                    description: "Benchmark-guided optimization".to_string(),
138                }],
139            }),
140            depth: Depth::Expert,
141            state: RoomState::Dormant,
142        };
143
144        let rust_forge = Room {
145            id: RoomId("rust-forge".to_string()),
146            name: "The Rust Forge".to_string(),
147            description: "Heat shimmers from a zero-cost abstraction furnace. \
148                The borrow checker guards the door. Ownership is strictly enforced. \
149                Crates of components line the walls, each with its own module."
150                .to_string(),
151            domain: Domain::Rust,
152            exits: vec![
153                Exit {
154                    direction: "west".to_string(),
155                    target: RoomId("alignment-cathedral".to_string()),
156                    description: "Back to the Cathedral".to_string(),
157                    locked: false,
158                },
159                Exit {
160                    direction: "north".to_string(),
161                    target: RoomId("c-caverns".to_string()),
162                    description: "Descend into the C caverns".to_string(),
163                    locked: false,
164                },
165            ],
166            tiles: vec![],
167            npcs: vec![],
168            workbench: None,
169            depth: Depth::Introductory,
170            state: RoomState::Dormant,
171        };
172
173        let c_caverns = Room {
174            id: RoomId("c-caverns".to_string()),
175            name: "The C Caverns".to_string(),
176            description: "Dark tunnels of pointer arithmetic and manual memory management. \
177                Segfaults echo in the distance. A faint smell of undefined behavior."
178                .to_string(),
179            domain: Domain::C,
180            exits: vec![Exit {
181                direction: "south".to_string(),
182                target: RoomId("rust-forge".to_string()),
183                description: "Back to the Rust Forge".to_string(),
184                locked: false,
185            }],
186            tiles: vec![],
187            npcs: vec![],
188            workbench: None,
189            depth: Depth::Advanced,
190            state: RoomState::Dormant,
191        };
192
193        // Seed tiles
194        let constraint_tile = Tile {
195            id: TileId("constraint-1".to_string()),
196            title: "Precision Deadband Constraint".to_string(),
197            location: SpatialIndex {
198                x: 0.0,
199                y: 2.0,
200                z: 0.0,
201            },
202            author: AgentId("forgemaster".to_string()),
203            confidence: 0.95,
204            domain_tags: vec!["alignment".to_string(), "constraint".to_string()],
205            links: vec![],
206            content: TileContent::Constraint("Drift must remain within deadband of 2σ".to_string()),
207            lifecycle: Lifecycle::Certified,
208            bloom_hash: [0u8; 32],
209        };
210
211        let benchmark_tile = Tile {
212            id: TileId("benchmark-fortran-1".to_string()),
213            title: "BLAS Level-3 Throughput Benchmark".to_string(),
214            location: SpatialIndex {
215                x: 0.0,
216                y: 0.0,
217                z: 0.0,
218            },
219            author: AgentId("forgemaster".to_string()),
220            confidence: 0.99,
221            domain_tags: vec!["fortran".to_string(), "benchmark".to_string()],
222            links: vec![],
223            content: TileContent::Benchmark("DGEMM: 42 GFLOPS on reference hardware".to_string()),
224            lifecycle: Lifecycle::Certified,
225            bloom_hash: [0u8; 32],
226        };
227
228        self.engine.add_room(alignment_cathedral).unwrap();
229        self.engine.add_room(fortran_foyer).unwrap();
230        self.engine.add_room(fortran_attic).unwrap();
231        self.engine.add_room(rust_forge).unwrap();
232        self.engine.add_room(c_caverns).unwrap();
233
234        self.engine.add_tile(constraint_tile).unwrap();
235        self.engine.add_tile(benchmark_tile).unwrap();
236
237        // Place tiles in rooms
238        self.engine
239            .rooms
240            .get_mut(&RoomId("alignment-cathedral".to_string()))
241            .unwrap()
242            .tiles
243            .push(TileId("constraint-1".to_string()));
244        self.engine
245            .rooms
246            .get_mut(&RoomId("fortran-foyer".to_string()))
247            .unwrap()
248            .tiles
249            .push(TileId("benchmark-fortran-1".to_string()));
250
251        // Add NPCs
252        let rust_expert = Npc {
253            id: NpcId("boris".to_string()),
254            name: "Boris".to_string(),
255            room: RoomId("rust-forge".to_string()),
256            expertise: vec![
257                "rust".to_string(),
258                "borrow".to_string(),
259                "ownership".to_string(),
260                "lifetime".to_string(),
261            ],
262            personality: "A grizzled systems programmer who speaks in lifetimes".to_string(),
263            knowledge_graph: {
264                let mut kg = BTreeMap::new();
265                kg.insert(Query("borrow".to_string()), Response("There are two kinds: shared (&T) and exclusive (&mut T). The compiler enforces that you can have either any number of shared refs OR exactly one exclusive ref, never both.".to_string()));
266                kg.insert(Query("ownership".to_string()), Response("Every value has exactly one owner. When the owner goes out of scope, the value is dropped. Simple. Beautiful. No garbage collector needed.".to_string()));
267                kg.insert(Query("lifetime".to_string()), Response("Lifetimes are the compiler's way of tracking how long references are valid. Most of the time it figures it out. When it can't, you annotate: 'a is the most common.".to_string()));
268                kg
269            },
270            current_dialog: None,
271        };
272
273        let fortran_sage = Npc {
274            id: NpcId("dr-fortran".to_string()),
275            name: "Dr. Fortran".to_string(),
276            room: RoomId("fortran-foyer".to_string()),
277            expertise: vec![
278                "fortran".to_string(),
279                "blas".to_string(),
280                "lapack".to_string(),
281                "optimization".to_string(),
282            ],
283            personality: "An elderly academic who speaks in array operations".to_string(),
284            knowledge_graph: {
285                let mut kg = BTreeMap::new();
286                kg.insert(Query("fortran".to_string()), Response("FORTRAN — the father of scientific computing. Column-major arrays, pass-by-reference, and DO loops that have been running since 1957.".to_string()));
287                kg.insert(Query("blas".to_string()), Response("Basic Linear Algebra Subprograms. Three levels: vector-vector (1), matrix-vector (2), matrix-matrix (3). Always use Level 3 for maximum FLOPS.".to_string()));
288                kg
289            },
290            current_dialog: None,
291        };
292
293        self.engine.add_npc(rust_expert).unwrap();
294        self.engine.add_npc(fortran_sage).unwrap();
295    }
296
297    /// Parse a command from raw input
298    fn parse_command(input: &str) -> Option<Command> {
299        let input = input.trim();
300        if input.is_empty() {
301            return None;
302        }
303
304        let parts: Vec<&str> = input.splitn(2, ' ').collect();
305        let verb = parts[0].to_uppercase();
306
307        match verb.as_str() {
308            "LOOK" | "L" => Some(Command::Look),
309            "GO" | "MOVE" | "WALK" => {
310                let dir = parts.get(1).map(|s| s.to_lowercase());
311                dir.map(Command::Go)
312            }
313            "GET" | "TAKE" | "PICKUP" => parts.get(1).map(|s| Command::Get(s.to_string())),
314            "DROP" | "PUT" => parts.get(1).map(|s| Command::Drop(s.to_string())),
315            "TALK" | "SPEAK" | "ASK" => parts.get(1).map(|s| Command::Talk(s.to_string())),
316            "CRAFT" | "MAKE" | "BUILD" => {
317                let items: Vec<String> = parts
318                    .get(1)
319                    .map(|s| s.split('+').map(|i| i.trim().to_string()).collect())
320                    .unwrap_or_default();
321                Some(Command::Craft(items))
322            }
323            "INVENTORY" | "INV" | "I" => Some(Command::Inventory),
324            "MAP" | "M" => Some(Command::Map),
325            "HELP" | "H" | "?" => Some(Command::Help),
326            "EXAMINE" | "EX" | "X" => parts.get(1).map(|s| Command::Examine(s.to_string())),
327            "STATUS" | "STAT" => Some(Command::Status),
328            // Direction shortcuts
329            "N" | "NORTH" => Some(Command::Go("north".to_string())),
330            "S" | "SOUTH" => Some(Command::Go("south".to_string())),
331            "E" | "EAST" => Some(Command::Go("east".to_string())),
332            "W" | "WEST" => Some(Command::Go("west".to_string())),
333            "U" | "UP" => Some(Command::Go("up".to_string())),
334            "D" | "DOWN" => Some(Command::Go("down".to_string())),
335            "QUIT" | "Q" | "EXIT" => None, // handled by caller
336            _ => None,
337        }
338    }
339
340    /// Run the interactive server (stdin/stdout)
341    pub fn run_interactive(&mut self) -> io::Result<()> {
342        println!("╔═══════════════════════════════════════╗");
343        println!("║     PLATO MUD Engine v0.1.0          ║");
344        println!("║     Constraint-Theory Knowledge Rooms ║");
345        println!("╚═══════════════════════════════════════╝");
346        println!();
347        println!("Enter your agent name:");
348
349        let stdin = io::stdin();
350        let mut stdout = io::stdout();
351
352        let mut name = String::new();
353        stdin.lock().read_line(&mut name)?;
354        let name = name.trim().to_string();
355
356        let agent_id = AgentId(name.clone());
357        self.engine
358            .connect_agent(agent_id.clone(), RoomId("alignment-cathedral".to_string()))
359            .expect("Starting room should exist");
360
361        println!("\nWelcome, {}. You stand in the Alignment Cathedral.", name);
362        println!("Type HELP for commands.\n");
363
364        if let Ok(desc) = self.engine.look(&agent_id) {
365            println!("{}", desc);
366        }
367
368        self.running = true;
369        while self.running {
370            print!("\n> ");
371            stdout.flush()?;
372
373            let mut input = String::new();
374            stdin.lock().read_line(&mut input)?;
375            let input = input.trim();
376
377            if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("exit") {
378                println!("Goodbye, {}. May your constraints remain satisfied.", name);
379                break;
380            }
381
382            match Self::parse_command(input) {
383                None => {
384                    if !input.is_empty() {
385                        println!("Unknown command. Type HELP for available commands.");
386                    }
387                }
388                Some(cmd) => match self.engine.execute(&agent_id, cmd) {
389                    Ok(response) => println!("{}", response),
390                    Err(e) => println!("⚠ {}", e),
391                },
392            }
393        }
394
395        Ok(())
396    }
397
398    /// Process a single command (for programmatic use)
399    pub fn process_command(&mut self, agent: &AgentId, input: &str) -> Result<String, String> {
400        match Self::parse_command(input) {
401            None => Err("Unknown command".to_string()),
402            Some(cmd) => self.engine.execute(agent, cmd),
403        }
404    }
405
406    pub fn engine(&self) -> &Engine {
407        &self.engine
408    }
409
410    pub fn engine_mut(&mut self) -> &mut Engine {
411        &mut self.engine
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_server_bootstrap() {
421        let server = PlatoServer::new();
422        assert_eq!(server.engine().all_rooms().len(), 5);
423    }
424
425    #[test]
426    fn test_parse_commands() {
427        assert!(matches!(
428            PlatoServer::parse_command("look"),
429            Some(Command::Look)
430        ));
431        assert!(matches!(
432            PlatoServer::parse_command("L"),
433            Some(Command::Look)
434        ));
435        assert!(
436            matches!(PlatoServer::parse_command("go north"), Some(Command::Go(ref s)) if s == "north")
437        );
438        assert!(
439            matches!(PlatoServer::parse_command("N"), Some(Command::Go(ref s)) if s == "north")
440        );
441        assert!(
442            matches!(PlatoServer::parse_command("get tile-1"), Some(Command::Get(ref s)) if s == "tile-1")
443        );
444        assert!(
445            matches!(PlatoServer::parse_command("talk Boris"), Some(Command::Talk(ref s)) if s == "Boris")
446        );
447        assert!(matches!(
448            PlatoServer::parse_command("help"),
449            Some(Command::Help)
450        ));
451        assert!(matches!(
452            PlatoServer::parse_command("inventory"),
453            Some(Command::Inventory)
454        ));
455        assert!(matches!(
456            PlatoServer::parse_command("map"),
457            Some(Command::Map)
458        ));
459    }
460
461    #[test]
462    fn test_process_command() {
463        let mut server = PlatoServer::new();
464        let agent = AgentId("tester".to_string());
465        server
466            .engine_mut()
467            .connect_agent(agent.clone(), RoomId("alignment-cathedral".to_string()))
468            .unwrap();
469
470        let result = server.process_command(&agent, "look");
471        assert!(result.is_ok());
472        assert!(result.unwrap().contains("Alignment Cathedral"));
473
474        let result = server.process_command(&agent, "help");
475        assert!(result.is_ok());
476    }
477
478    #[test]
479    fn test_navigation_commands() {
480        let mut server = PlatoServer::new();
481        let agent = AgentId("wanderer".to_string());
482        server
483            .engine_mut()
484            .connect_agent(agent.clone(), RoomId("alignment-cathedral".to_string()))
485            .unwrap();
486
487        let result = server.process_command(&agent, "go east");
488        assert!(result.is_ok());
489        assert!(result.unwrap().contains("Rust Forge"));
490
491        let result = server.process_command(&agent, "go west");
492        assert!(result.is_ok());
493        assert!(result.unwrap().contains("Alignment Cathedral"));
494    }
495}