1#![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
17pub 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 fn bootstrap_rooms(&mut self) {
45 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 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 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 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 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 "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, _ => None,
337 }
338 }
339
340 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 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}