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 }