notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }