terrain_forge/
semantic_extractor.rs

1//! Standalone semantic extraction system
2//!
3//! Extracts semantic information from any generated grid, completely decoupled
4//! from the generation algorithms. This allows semantic analysis of maps from
5//! any source - TerrainForge algorithms, pipelines, or external systems.
6
7use crate::semantic::{ConnectivityGraph, Marker, Masks, Region, SemanticConfig, SemanticLayers};
8use crate::{Grid, Rng, Tile};
9use std::collections::HashMap;
10
11/// Standalone semantic extractor that analyzes any grid
12pub struct SemanticExtractor {
13    config: SemanticConfig,
14}
15
16impl SemanticExtractor {
17    /// Create a new semantic extractor with the given configuration
18    pub fn new(config: SemanticConfig) -> Self {
19        Self { config }
20    }
21
22    /// Create extractor optimized for cave systems
23    pub fn for_caves() -> Self {
24        Self::new(SemanticConfig::cave_system())
25    }
26
27    /// Create extractor optimized for room-based dungeons
28    pub fn for_rooms() -> Self {
29        Self::new(SemanticConfig::room_system())
30    }
31
32    /// Create extractor optimized for maze systems
33    pub fn for_mazes() -> Self {
34        Self::new(SemanticConfig::maze_system())
35    }
36
37    /// Extract semantic layers from any grid
38    pub fn extract(&self, grid: &Grid<Tile>, rng: &mut Rng) -> SemanticLayers {
39        // 1. Extract regions using flood fill
40        let mut regions = self.extract_regions(grid);
41
42        // 2. Classify regions based on configuration
43        self.classify_regions(&mut regions);
44
45        // 3. Generate markers based on configuration
46        let markers = self.generate_markers(&regions, rng);
47
48        // 4. Create spatial masks
49        let masks = Masks::from_tiles(grid);
50
51        // 5. Build connectivity graph
52        let connectivity = self.build_connectivity(grid, &regions);
53
54        SemanticLayers {
55            regions,
56            markers,
57            masks,
58            connectivity,
59        }
60    }
61
62    /// Extract regions using flood fill algorithm
63    fn extract_regions(&self, grid: &Grid<Tile>) -> Vec<Region> {
64        let (labels, count) = crate::effects::label_regions(grid);
65        let mut regions = Vec::new();
66        let width = grid.width();
67
68        for region_id in 1..=count {
69            let mut region = Region::new(region_id, "Unknown");
70
71            // Collect all cells belonging to this region
72            for (x, y, _) in grid.iter() {
73                let index = y * width + x;
74                if labels[index] == region_id {
75                    region.add_cell(x as u32, y as u32);
76                }
77            }
78
79            if !region.cells.is_empty() {
80                regions.push(region);
81            }
82        }
83
84        regions
85    }
86
87    /// Classify regions based on size thresholds
88    fn classify_regions(&self, regions: &mut [Region]) {
89        for region in regions {
90            let size = region.cells.len();
91
92            // Find the first threshold that matches (thresholds should be sorted descending)
93            region.kind = self
94                .config
95                .size_thresholds
96                .iter()
97                .find(|(threshold, _)| size >= *threshold)
98                .map(|(_, name)| name.clone())
99                .unwrap_or_else(|| "Unknown".to_string());
100        }
101    }
102
103    /// Generate markers based on configuration
104    fn generate_markers(&self, regions: &[Region], rng: &mut Rng) -> Vec<Marker> {
105        let mut markers = Vec::new();
106
107        for region in regions {
108            let marker_count = (self.config.max_markers_per_region as f32
109                * (region.cells.len() as f32 / self.config.marker_scaling_factor).min(1.0))
110                as usize;
111
112            for _ in 0..marker_count {
113                if let Some((marker_type, weight)) = rng.pick(&self.config.marker_types) {
114                    if rng.random() < (*weight as f64) {
115                        if let Some(position) = self.find_marker_position(region, &markers, rng) {
116                            markers.push(
117                                Marker::new(position.0, position.1, marker_type)
118                                    .with_region(region.id)
119                                    .with_weight(*weight),
120                            );
121                        }
122                    }
123                }
124            }
125        }
126
127        markers
128    }
129
130    /// Find appropriate position for marker based on placement strategy
131    fn find_marker_position(
132        &self,
133        region: &Region,
134        existing_markers: &[Marker],
135        rng: &mut Rng,
136    ) -> Option<(u32, u32)> {
137        use crate::semantic::PlacementStrategy;
138
139        let candidates: Vec<(u32, u32)> = match self.config.marker_placement.strategy {
140            PlacementStrategy::Random => region.cells.clone(),
141            PlacementStrategy::Center => {
142                if let Some(center) = self.find_region_center(region) {
143                    vec![center]
144                } else {
145                    region.cells.clone()
146                }
147            }
148            PlacementStrategy::Edges => self.find_edge_positions(region),
149            PlacementStrategy::Corners => self.find_corner_positions(region),
150        };
151
152        // Filter candidates based on distance constraints
153        let valid_candidates: Vec<_> = candidates
154            .into_iter()
155            .filter(|&pos| self.is_valid_marker_position(pos, existing_markers))
156            .collect();
157
158        rng.pick(&valid_candidates).copied()
159    }
160
161    /// Check if position is valid for marker placement
162    fn is_valid_marker_position(&self, pos: (u32, u32), existing_markers: &[Marker]) -> bool {
163        let min_dist = self.config.marker_placement.min_marker_distance as f32;
164
165        for marker in existing_markers {
166            let dx = pos.0 as f32 - marker.x as f32;
167            let dy = pos.1 as f32 - marker.y as f32;
168            let distance = (dx * dx + dy * dy).sqrt();
169
170            if distance < min_dist {
171                return false;
172            }
173        }
174
175        true
176    }
177
178    /// Find center position of region
179    fn find_region_center(&self, region: &Region) -> Option<(u32, u32)> {
180        if region.cells.is_empty() {
181            return None;
182        }
183
184        let sum_x: u32 = region.cells.iter().map(|(x, _)| x).sum();
185        let sum_y: u32 = region.cells.iter().map(|(_, y)| y).sum();
186        let count = region.cells.len() as u32;
187
188        Some((sum_x / count, sum_y / count))
189    }
190
191    /// Find edge positions in region
192    fn find_edge_positions(&self, region: &Region) -> Vec<(u32, u32)> {
193        // Simplified: return cells that have fewer neighbors
194        // In a real implementation, this would check actual grid boundaries
195        region.cells.clone() // Placeholder
196    }
197
198    /// Find corner positions in region  
199    fn find_corner_positions(&self, region: &Region) -> Vec<(u32, u32)> {
200        if region.cells.len() < 4 {
201            return region.cells.clone();
202        }
203
204        // Find extremes
205        let min_x = region.cells.iter().map(|(x, _)| x).min().unwrap();
206        let max_x = region.cells.iter().map(|(x, _)| x).max().unwrap();
207        let min_y = region.cells.iter().map(|(_, y)| y).min().unwrap();
208        let max_y = region.cells.iter().map(|(_, y)| y).max().unwrap();
209
210        vec![
211            (*min_x, *min_y),
212            (*max_x, *min_y),
213            (*min_x, *max_y),
214            (*max_x, *max_y),
215        ]
216        .into_iter()
217        .filter(|pos| region.cells.contains(pos))
218        .collect()
219    }
220
221    /// Build connectivity graph between regions
222    fn build_connectivity(&self, grid: &Grid<Tile>, regions: &[Region]) -> ConnectivityGraph {
223        let mut graph = ConnectivityGraph::new();
224
225        // Add all regions to graph
226        for region in regions {
227            graph.add_region(region.id);
228        }
229
230        // Find adjacencies by checking region boundaries
231        let region_map = self.create_region_map(grid, regions);
232
233        for region in regions {
234            for &(x, y) in &region.cells {
235                // Use configurable connectivity type
236                let neighbors = match self.config.connectivity_type {
237                    crate::semantic::ConnectivityType::FourConnected => {
238                        vec![(0, 1), (1, 0), (0, -1), (-1, 0)]
239                    }
240                    crate::semantic::ConnectivityType::EightConnected => {
241                        vec![
242                            (0, 1),
243                            (1, 0),
244                            (0, -1),
245                            (-1, 0),
246                            (1, 1),
247                            (1, -1),
248                            (-1, 1),
249                            (-1, -1),
250                        ]
251                    }
252                };
253
254                for (dx, dy) in neighbors {
255                    let nx = x as i32 + dx;
256                    let ny = y as i32 + dy;
257
258                    if let Some(neighbor_region) = region_map.get(&(nx, ny)) {
259                        if *neighbor_region != region.id {
260                            graph.add_edge(region.id, *neighbor_region);
261                        }
262                    }
263                }
264            }
265        }
266
267        graph
268    }
269
270    /// Create a map from coordinates to region IDs for fast lookup
271    fn create_region_map(
272        &self,
273        _grid: &Grid<Tile>,
274        regions: &[Region],
275    ) -> HashMap<(i32, i32), u32> {
276        let mut map = HashMap::new();
277
278        for region in regions {
279            for &(x, y) in &region.cells {
280                map.insert((x as i32, y as i32), region.id);
281            }
282        }
283
284        map
285    }
286}
287
288impl Default for SemanticExtractor {
289    fn default() -> Self {
290        Self::new(SemanticConfig::default())
291    }
292}
293
294/// Convenience function for quick semantic extraction
295pub fn extract_semantics(grid: &Grid<Tile>, config: SemanticConfig, seed: u64) -> SemanticLayers {
296    let mut rng = Rng::new(seed);
297    let extractor = SemanticExtractor::new(config);
298    extractor.extract(grid, &mut rng)
299}
300
301/// Extract semantics with default configuration
302pub fn extract_semantics_default(grid: &Grid<Tile>, seed: u64) -> SemanticLayers {
303    extract_semantics(grid, SemanticConfig::default(), seed)
304}