notedeck

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

describe.rs (5369B)


      1 use crate::ast::*;
      2 
      3 /// Generate a natural language description of a space.
      4 pub fn describe(space: &Space) -> String {
      5     describe_from(space, space.root, 10)
      6 }
      7 
      8 /// Generate a description starting from a specific cell with depth limit.
      9 pub fn describe_from(space: &Space, root: CellId, max_depth: usize) -> String {
     10     let mut buf = String::new();
     11     describe_cells(space, root, max_depth, 0, &mut buf);
     12     buf
     13 }
     14 
     15 fn describe_cells(space: &Space, id: CellId, max_depth: usize, depth: usize, buf: &mut String) {
     16     if depth > max_depth {
     17         return;
     18     }
     19 
     20     if !describe_cell(space, id, buf) {
     21         return;
     22     }
     23 
     24     buf.push_str(".\n");
     25 
     26     let children = space.children(id);
     27     if children.is_empty() {
     28         return;
     29     }
     30 
     31     let cell = space.cell(id);
     32     if matches!(cell.cell_type, CellType::Room | CellType::Space) {
     33         push_word(buf, "It contains");
     34     }
     35 
     36     // Recurse into first child (matches C behavior)
     37     describe_cells(space, children[0], max_depth, depth + 1, buf);
     38 }
     39 
     40 fn describe_cell(space: &Space, id: CellId, buf: &mut String) -> bool {
     41     let cell = space.cell(id);
     42     match &cell.cell_type {
     43         CellType::Room => describe_area(space, id, "room", buf),
     44         CellType::Space => describe_area(space, id, "space", buf),
     45         CellType::Group => describe_group(space, id, buf),
     46         CellType::Tilemap => false,
     47         CellType::Object(_) => false, // unimplemented in C reference
     48     }
     49 }
     50 
     51 fn describe_area(space: &Space, id: CellId, area_name: &str, buf: &mut String) -> bool {
     52     buf.push_str("There is a(n)");
     53 
     54     push_adjectives(space, id, buf);
     55     push_shape(space, id, buf);
     56     push_word(buf, area_name);
     57     push_made_of(space, id, buf);
     58     push_named(space, id, buf);
     59 
     60     true
     61 }
     62 
     63 fn describe_group(space: &Space, id: CellId, buf: &mut String) -> bool {
     64     let children = space.children(id);
     65     let nobjs = children.len();
     66 
     67     describe_amount(nobjs, buf);
     68     push_word(buf, "object");
     69 
     70     if nobjs > 1 {
     71         buf.push_str("s:");
     72     } else {
     73         buf.push(':');
     74     }
     75 
     76     push_word(buf, "a");
     77 
     78     for (i, &child_id) in children.iter().enumerate() {
     79         if i > 0 {
     80             if i == nobjs - 1 {
     81                 push_word(buf, "and");
     82             } else {
     83                 buf.push(',');
     84             }
     85         }
     86         describe_object_name(space, child_id, buf);
     87     }
     88 
     89     true
     90 }
     91 
     92 fn describe_object_name(space: &Space, id: CellId, buf: &mut String) {
     93     if let Some(name) = space.name(id) {
     94         push_word(buf, name);
     95     }
     96 
     97     let cell = space.cell(id);
     98     let type_str = match &cell.cell_type {
     99         CellType::Object(obj) => obj.to_string(),
    100         other => other.to_string(),
    101     };
    102     push_word(buf, &type_str);
    103 }
    104 
    105 fn describe_amount(n: usize, buf: &mut String) {
    106     let word = match n {
    107         1 => "a single",
    108         2 => "a couple",
    109         3 => "three",
    110         4 => "four",
    111         5 => "five",
    112         _ => "many",
    113     };
    114     push_word(buf, word);
    115 }
    116 
    117 // --- Helper functions ---
    118 
    119 /// Push a word with automatic space separation.
    120 /// Adds a space before the word if the previous character is not whitespace.
    121 fn push_word(buf: &mut String, word: &str) {
    122     if let Some(last) = buf.as_bytes().last() {
    123         if !last.is_ascii_whitespace() {
    124             buf.push(' ');
    125         }
    126     }
    127     buf.push_str(word);
    128 }
    129 
    130 fn push_adjectives(space: &Space, id: CellId, buf: &mut String) {
    131     let attrs = space.attrs(id);
    132     let conditions: Vec<&str> = attrs
    133         .iter()
    134         .filter_map(|a| match a {
    135             Attribute::Condition(s) => Some(s.as_str()),
    136             _ => None,
    137         })
    138         .collect();
    139 
    140     let adj_count = conditions.len();
    141 
    142     for (i, cond) in conditions.iter().enumerate() {
    143         if i > 0 {
    144             if i == adj_count - 1 {
    145                 push_word(buf, "and");
    146             } else {
    147                 buf.push(',');
    148             }
    149         }
    150         push_word(buf, cond);
    151     }
    152 }
    153 
    154 fn push_shape(space: &Space, id: CellId, buf: &mut String) {
    155     let shape = space.attrs(id).iter().find_map(|a| match a {
    156         Attribute::Shape(s) => Some(s),
    157         _ => None,
    158     });
    159 
    160     if let Some(shape) = shape {
    161         let adj = match shape {
    162             Shape::Rectangle => "rectangular",
    163             Shape::Circle => "circular",
    164             Shape::Square => "square",
    165         };
    166         push_word(buf, adj);
    167     }
    168 }
    169 
    170 fn push_made_of(space: &Space, id: CellId, buf: &mut String) {
    171     let material = space.attrs(id).iter().find_map(|a| match a {
    172         Attribute::Material(s) => Some(s.as_str()),
    173         _ => None,
    174     });
    175 
    176     if let Some(mat) = material {
    177         push_word(buf, "made of");
    178         push_word(buf, mat);
    179     }
    180 }
    181 
    182 fn push_named(space: &Space, id: CellId, buf: &mut String) {
    183     if let Some(name) = space.name(id) {
    184         push_word(buf, "named");
    185         push_word(buf, name);
    186     }
    187 }
    188 
    189 #[cfg(test)]
    190 mod tests {
    191     use super::*;
    192     use crate::parser::parse;
    193 
    194     #[test]
    195     fn test_describe_simple_room() {
    196         let space =
    197             parse("(room (shape rectangle) (name \"Test Room\") (material \"wood\"))").unwrap();
    198         let desc = describe(&space);
    199         assert!(desc.contains("There is a(n)"));
    200         assert!(desc.contains("rectangular"));
    201         assert!(desc.contains("room"));
    202         assert!(desc.contains("made of wood"));
    203         assert!(desc.contains("named Test Room"));
    204     }
    205 }