lib.rs (9313B)
1 //! Protoverse: S-expression parser for spatial world descriptions 2 //! 3 //! Parses protoverse `.space` format — an s-expression language for 4 //! describing rooms, objects, and their attributes. Designed for 5 //! progressive LOD: text descriptions, 2D maps, and 3D rendering 6 //! can all be derived from the same source. 7 //! 8 //! # Example 9 //! 10 //! ``` 11 //! use protoverse::{parse, serialize, describe}; 12 //! 13 //! let input = r#"(room (name "My Room") (shape rectangle) (width 10) (depth 8) 14 //! (group 15 //! (table (name "desk") (material "wood")) 16 //! (chair (name "office chair"))))"#; 17 //! 18 //! let space = parse(input).unwrap(); 19 //! let description = describe(&space); 20 //! let roundtrip = serialize(&space); 21 //! ``` 22 23 pub mod ast; 24 pub mod describe; 25 pub mod parser; 26 pub mod serializer; 27 pub mod tokenizer; 28 29 pub use ast::*; 30 pub use describe::{describe, describe_from}; 31 pub use parser::parse; 32 pub use serializer::{serialize, serialize_from}; 33 34 #[cfg(test)] 35 mod tests { 36 use super::*; 37 38 const SATOSHIS_CITADEL: &str = r#"(space (shape rectangle) 39 (condition "clean") 40 (condition "shiny") 41 (material "solid gold") 42 (name "Satoshi's Den") 43 (width 10) (depth 10) (height 100) 44 (group 45 (table (id welcome-desk) 46 (name "welcome desk") 47 (material "marble") 48 (condition "clean") 49 (condition "new") 50 (width 1) (depth 2) (height 1) 51 (location center) 52 (light (name "desk"))) 53 54 (chair (id welcome-desk-chair) 55 (name "fancy")) 56 57 (chair (name "throne") (material "invisible")) 58 59 (light (location ceiling) 60 (name "ceiling") 61 (state off) 62 (shape circle))))"#; 63 64 const EXAMPLE_ROOM: &str = r#"(room (shape rectangle) 65 (condition "clean") 66 (material "gold") 67 (name "Satoshi's Den") 68 (width 10) (depth 10) (height 100) 69 (group 70 (table (id welcome-desk) 71 (name "welcome desk") 72 (material "marble") 73 (condition "new") 74 (width 1) (depth 2) (height 1) 75 (light (name "desk"))) 76 77 (chair (id welcome-desk-chair) 78 (name "fancy")) 79 80 (light (location ceiling) 81 (name "ceiling") 82 (state off) 83 (shape circle))))"#; 84 85 #[test] 86 fn test_parse_satoshis_citadel() { 87 let space = parse(SATOSHIS_CITADEL).unwrap(); 88 89 // Root is a space cell 90 let root = space.cell(space.root); 91 assert_eq!(root.cell_type, CellType::Space); 92 assert_eq!(space.name(space.root), Some("Satoshi's Den")); 93 94 // Root has 8 attributes 95 let attrs = space.attrs(space.root); 96 assert_eq!(attrs.len(), 8); 97 98 // Root has one child (group) 99 let root_children = space.children(space.root); 100 assert_eq!(root_children.len(), 1); 101 let group_id = root_children[0]; 102 let group = space.cell(group_id); 103 assert_eq!(group.cell_type, CellType::Group); 104 105 // Group has 4 children: table, chair, chair, light 106 let group_children = space.children(group_id); 107 assert_eq!(group_children.len(), 4); 108 109 assert_eq!( 110 space.cell(group_children[0]).cell_type, 111 CellType::Object(ObjectType::Table) 112 ); 113 assert_eq!( 114 space.cell(group_children[1]).cell_type, 115 CellType::Object(ObjectType::Chair) 116 ); 117 assert_eq!( 118 space.cell(group_children[2]).cell_type, 119 CellType::Object(ObjectType::Chair) 120 ); 121 assert_eq!( 122 space.cell(group_children[3]).cell_type, 123 CellType::Object(ObjectType::Light) 124 ); 125 126 // Table has a child light 127 let table_children = space.children(group_children[0]); 128 assert_eq!(table_children.len(), 1); 129 assert_eq!( 130 space.cell(table_children[0]).cell_type, 131 CellType::Object(ObjectType::Light) 132 ); 133 assert_eq!(space.name(table_children[0]), Some("desk")); 134 135 // Check object names 136 assert_eq!(space.name(group_children[0]), Some("welcome desk")); 137 assert_eq!(space.name(group_children[1]), Some("fancy")); 138 assert_eq!(space.name(group_children[2]), Some("throne")); 139 assert_eq!(space.name(group_children[3]), Some("ceiling")); 140 } 141 142 #[test] 143 fn test_parse_example_room() { 144 let space = parse(EXAMPLE_ROOM).unwrap(); 145 let root = space.cell(space.root); 146 assert_eq!(root.cell_type, CellType::Room); 147 assert_eq!(space.name(space.root), Some("Satoshi's Den")); 148 } 149 150 #[test] 151 fn test_round_trip() { 152 let space1 = parse(SATOSHIS_CITADEL).unwrap(); 153 let serialized = serialize(&space1); 154 155 // Re-parse the serialized output 156 let space2 = parse(&serialized).unwrap(); 157 158 // Same structure 159 assert_eq!(space1.cells.len(), space2.cells.len()); 160 assert_eq!(space1.attributes.len(), space2.attributes.len()); 161 assert_eq!(space1.child_ids.len(), space2.child_ids.len()); 162 163 // Same root type 164 assert_eq!( 165 space1.cell(space1.root).cell_type, 166 space2.cell(space2.root).cell_type 167 ); 168 169 // Same name 170 assert_eq!(space1.name(space1.root), space2.name(space2.root)); 171 172 // Same group children count 173 let g1 = space1.children(space1.root)[0]; 174 let g2 = space2.children(space2.root)[0]; 175 assert_eq!(space1.children(g1).len(), space2.children(g2).len()); 176 } 177 178 #[test] 179 fn test_describe_satoshis_citadel() { 180 let space = parse(SATOSHIS_CITADEL).unwrap(); 181 let desc = describe(&space); 182 183 // Check the area description 184 assert!(desc.contains("There is a(n)")); 185 assert!(desc.contains("clean")); 186 assert!(desc.contains("shiny")); 187 assert!(desc.contains("rectangular")); 188 assert!(desc.contains("space")); 189 assert!(desc.contains("made of solid gold")); 190 assert!(desc.contains("named Satoshi's Den")); 191 192 // Check the group description 193 assert!(desc.contains("It contains")); 194 assert!(desc.contains("four")); 195 assert!(desc.contains("objects:")); 196 assert!(desc.contains("welcome desk table")); 197 assert!(desc.contains("fancy chair")); 198 assert!(desc.contains("throne chair")); 199 assert!(desc.contains("ceiling light")); 200 201 // Exact match against C reference output 202 let expected = "There is a(n) clean and shiny rectangular space made of solid gold named Satoshi's Den.\nIt contains four objects: a welcome desk table, fancy chair, throne chair and ceiling light.\n"; 203 assert_eq!(desc, expected); 204 } 205 206 #[test] 207 fn test_parse_real_space_file() { 208 // Parse the actual .space file from the protoverse repo 209 let path = "/home/jb55/src/c/protoverse/satoshis-citadel.space"; 210 if let Ok(content) = std::fs::read_to_string(path) { 211 let space = parse(&content).unwrap(); 212 assert_eq!(space.cell(space.root).cell_type, CellType::Space); 213 assert_eq!(space.name(space.root), Some("Satoshi's Den")); 214 215 // Verify round-trip 216 let serialized = serialize(&space); 217 let space2 = parse(&serialized).unwrap(); 218 assert_eq!(space.cells.len(), space2.cells.len()); 219 } 220 } 221 222 #[test] 223 fn test_parent_references() { 224 let space = parse(SATOSHIS_CITADEL).unwrap(); 225 226 // Root has no parent 227 assert_eq!(space.cell(space.root).parent, None); 228 229 // Group's parent is root 230 let group_id = space.children(space.root)[0]; 231 assert_eq!(space.cell(group_id).parent, Some(space.root)); 232 233 // Table's parent is group 234 let table_id = space.children(group_id)[0]; 235 assert_eq!(space.cell(table_id).parent, Some(group_id)); 236 237 // Desk light's parent is table 238 let light_id = space.children(table_id)[0]; 239 assert_eq!(space.cell(light_id).parent, Some(table_id)); 240 } 241 242 #[test] 243 fn test_attribute_details() { 244 let space = parse(SATOSHIS_CITADEL).unwrap(); 245 246 // Check root shape 247 let shape = space 248 .find_attr(space.root, |a| matches!(a, Attribute::Shape(_))) 249 .unwrap(); 250 assert_eq!(*shape, Attribute::Shape(Shape::Rectangle)); 251 252 // Check root dimensions 253 let width = space 254 .find_attr(space.root, |a| matches!(a, Attribute::Width(_))) 255 .unwrap(); 256 assert_eq!(*width, Attribute::Width(10.0)); 257 258 // Check table material 259 let table_id = space.children(space.children(space.root)[0])[0]; 260 let material = space 261 .find_attr(table_id, |a| matches!(a, Attribute::Material(_))) 262 .unwrap(); 263 assert_eq!(*material, Attribute::Material("marble".to_string())); 264 265 // Check light state 266 let light_id = space.children(space.children(space.root)[0])[3]; 267 let state = space 268 .find_attr(light_id, |a| matches!(a, Attribute::State(_))) 269 .unwrap(); 270 assert_eq!(*state, Attribute::State(CellState::Off)); 271 } 272 }