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