terrain_forge/
semantic.rs

1//! Semantic layers for procedural generation
2//!
3//! Provides region metadata, spawn markers, and connectivity information
4//! alongside tile generation for game integration.
5
6use crate::{Grid, Tile};
7use std::collections::HashMap;
8
9/// Configuration for semantic layer generation
10#[derive(Debug, Clone)]
11pub struct SemanticConfig {
12    /// Size thresholds for region classification
13    pub size_thresholds: Vec<(usize, String)>,
14    /// Marker types to generate with their weights
15    pub marker_types: Vec<(String, f32)>,
16    /// Maximum number of markers per region type
17    pub max_markers_per_region: usize,
18    /// Region size scaling factor for marker density (default: 100.0)
19    pub marker_scaling_factor: f32,
20    /// Connectivity analysis type
21    pub connectivity_type: ConnectivityType,
22    /// Advanced region analysis options
23    pub region_analysis: RegionAnalysisConfig,
24    /// Marker placement strategy
25    pub marker_placement: MarkerPlacementConfig,
26}
27
28/// Type of connectivity analysis to perform
29#[derive(Debug, Clone)]
30pub enum ConnectivityType {
31    /// 4-connected (orthogonal neighbors only)
32    FourConnected,
33    /// 8-connected (includes diagonal neighbors)
34    EightConnected,
35}
36
37/// Configuration for advanced region analysis
38#[derive(Debug, Clone)]
39pub struct RegionAnalysisConfig {
40    /// Enable shape analysis (aspect ratio, compactness)
41    pub analyze_shape: bool,
42    /// Enable connectivity pattern analysis
43    pub analyze_connectivity_patterns: bool,
44    /// Minimum region size for detailed analysis
45    pub min_analysis_size: usize,
46}
47
48/// Configuration for marker placement strategies
49#[derive(Debug, Clone)]
50pub struct MarkerPlacementConfig {
51    /// Placement strategy for markers
52    pub strategy: PlacementStrategy,
53    /// Minimum distance between markers of same type
54    pub min_marker_distance: usize,
55    /// Avoid placing markers near walls
56    pub avoid_walls: bool,
57}
58
59/// Marker placement strategies
60#[derive(Debug, Clone)]
61pub enum PlacementStrategy {
62    /// Random placement within region
63    Random,
64    /// Place at region center
65    Center,
66    /// Place near region edges
67    Edges,
68    /// Place in corners/extremes
69    Corners,
70}
71
72impl SemanticConfig {
73    /// Configuration optimized for cave systems (Cellular Automata)
74    pub fn cave_system() -> Self {
75        Self {
76            size_thresholds: vec![
77                (80, "Chamber".to_string()),
78                (25, "Tunnel".to_string()),
79                (5, "Alcove".to_string()),
80                (0, "Crevice".to_string()),
81            ],
82            marker_types: vec![
83                ("PlayerStart".to_string(), 1.0),
84                ("Exit".to_string(), 0.8),
85                ("Treasure".to_string(), 0.4),
86                ("Enemy".to_string(), 0.6),
87                ("Crystal".to_string(), 0.2),
88            ],
89            max_markers_per_region: 2,
90            marker_scaling_factor: 80.0, // Caves tend to be larger
91            connectivity_type: ConnectivityType::EightConnected, // Natural cave connections
92            region_analysis: RegionAnalysisConfig {
93                analyze_shape: true, // Cave shape matters
94                analyze_connectivity_patterns: true,
95                min_analysis_size: 15,
96            },
97            marker_placement: MarkerPlacementConfig {
98                strategy: PlacementStrategy::Random,
99                min_marker_distance: 5,
100                avoid_walls: true,
101            },
102        }
103    }
104
105    /// Configuration optimized for structured rooms
106    pub fn room_system() -> Self {
107        Self {
108            size_thresholds: vec![
109                (150, "Hall".to_string()),
110                (50, "Room".to_string()),
111                (15, "Chamber".to_string()),
112                (0, "Closet".to_string()),
113            ],
114            marker_types: vec![
115                ("PlayerStart".to_string(), 1.0),
116                ("Exit".to_string(), 1.0),
117                ("Treasure".to_string(), 0.3),
118                ("Enemy".to_string(), 0.4),
119                ("Furniture".to_string(), 0.7),
120            ],
121            max_markers_per_region: 4,
122            marker_scaling_factor: 60.0, // Rooms are more compact
123            connectivity_type: ConnectivityType::FourConnected, // Structured connections
124            region_analysis: RegionAnalysisConfig {
125                analyze_shape: true, // Room rectangularity matters
126                analyze_connectivity_patterns: false,
127                min_analysis_size: 8,
128            },
129            marker_placement: MarkerPlacementConfig {
130                strategy: PlacementStrategy::Center, // Furniture in room centers
131                min_marker_distance: 4,
132                avoid_walls: true,
133            },
134        }
135    }
136
137    /// Configuration optimized for maze systems
138    pub fn maze_system() -> Self {
139        Self {
140            size_thresholds: vec![
141                (50, "Junction".to_string()),
142                (10, "Corridor".to_string()),
143                (0, "DeadEnd".to_string()),
144            ],
145            marker_types: vec![
146                ("PlayerStart".to_string(), 1.0),
147                ("Exit".to_string(), 1.0),
148                ("Treasure".to_string(), 0.1),
149                ("Trap".to_string(), 0.3),
150            ],
151            max_markers_per_region: 1,
152            marker_scaling_factor: 30.0, // Mazes have smaller regions
153            connectivity_type: ConnectivityType::FourConnected, // Maze structure
154            region_analysis: RegionAnalysisConfig {
155                analyze_shape: false,
156                analyze_connectivity_patterns: true, // Junction analysis important
157                min_analysis_size: 5,
158            },
159            marker_placement: MarkerPlacementConfig {
160                strategy: PlacementStrategy::Corners, // Traps in corners
161                min_marker_distance: 8,
162                avoid_walls: false, // Maze walls are part of structure
163            },
164        }
165    }
166}
167
168impl Default for SemanticConfig {
169    fn default() -> Self {
170        Self {
171            size_thresholds: vec![
172                (100, "Large".to_string()),
173                (25, "Medium".to_string()),
174                (5, "Small".to_string()),
175                (0, "Tiny".to_string()),
176            ],
177            marker_types: vec![
178                ("PlayerStart".to_string(), 1.0),
179                ("Exit".to_string(), 1.0),
180                ("Treasure".to_string(), 0.3),
181                ("Enemy".to_string(), 0.5),
182            ],
183            max_markers_per_region: 3,
184            marker_scaling_factor: 100.0,
185            connectivity_type: ConnectivityType::FourConnected,
186            region_analysis: RegionAnalysisConfig {
187                analyze_shape: false,
188                analyze_connectivity_patterns: false,
189                min_analysis_size: 10,
190            },
191            marker_placement: MarkerPlacementConfig {
192                strategy: PlacementStrategy::Random,
193                min_marker_distance: 3,
194                avoid_walls: true,
195            },
196        }
197    }
198}
199
200/// A distinct region within the generated map
201#[derive(Debug, Clone)]
202pub struct Region {
203    pub id: u32,
204    pub kind: String,
205    pub cells: Vec<(u32, u32)>,
206    pub tags: Vec<String>,
207}
208
209/// Hierarchical marker types for different gameplay elements
210#[derive(Debug, Clone, PartialEq, Eq, Hash)]
211pub enum MarkerType {
212    /// Basic spawn points
213    Spawn,
214    Exit,
215
216    /// Quest-related markers
217    QuestObjective {
218        priority: u8,
219    },
220    QuestStart,
221    QuestEnd,
222
223    /// Loot and rewards
224    LootTier {
225        tier: u8,
226    },
227    Treasure,
228
229    /// Encounter zones
230    EncounterZone {
231        difficulty: u8,
232    },
233    BossRoom,
234    SafeZone,
235
236    /// Custom marker with string tag (backward compatibility)
237    Custom(String),
238}
239
240impl MarkerType {
241    /// Get the base category of this marker type
242    pub fn category(&self) -> &'static str {
243        match self {
244            MarkerType::Spawn | MarkerType::Exit => "spawn",
245            MarkerType::QuestObjective { .. } | MarkerType::QuestStart | MarkerType::QuestEnd => {
246                "quest"
247            }
248            MarkerType::LootTier { .. } | MarkerType::Treasure => "loot",
249            MarkerType::EncounterZone { .. } | MarkerType::BossRoom | MarkerType::SafeZone => {
250                "encounter"
251            }
252            MarkerType::Custom(_) => "custom",
253        }
254    }
255}
256
257/// A spawn marker for entity placement
258#[derive(Debug, Clone)]
259pub struct Marker {
260    pub x: u32,
261    pub y: u32,
262    pub marker_type: MarkerType,
263    pub weight: f32,
264    pub region_id: Option<u32>,
265    pub metadata: HashMap<String, String>,
266}
267
268impl Marker {
269    /// Create a new marker with the given type
270    pub fn new(x: u32, y: u32, marker_type: MarkerType) -> Self {
271        Self {
272            x,
273            y,
274            marker_type,
275            weight: 1.0,
276            region_id: None,
277            metadata: HashMap::new(),
278        }
279    }
280
281    /// Create a marker with custom tag (backward compatibility)
282    pub fn with_tag(x: u32, y: u32, tag: String) -> Self {
283        Self::new(x, y, MarkerType::Custom(tag))
284    }
285
286    /// Get the tag string for this marker (backward compatibility)
287    pub fn tag(&self) -> String {
288        match &self.marker_type {
289            MarkerType::Spawn => "spawn".to_string(),
290            MarkerType::Exit => "exit".to_string(),
291            MarkerType::QuestObjective { priority } => format!("quest_objective_{}", priority),
292            MarkerType::QuestStart => "quest_start".to_string(),
293            MarkerType::QuestEnd => "quest_end".to_string(),
294            MarkerType::LootTier { tier } => format!("loot_tier_{}", tier),
295            MarkerType::Treasure => "treasure".to_string(),
296            MarkerType::EncounterZone { difficulty } => format!("encounter_{}", difficulty),
297            MarkerType::BossRoom => "boss_room".to_string(),
298            MarkerType::SafeZone => "safe_zone".to_string(),
299            MarkerType::Custom(tag) => tag.clone(),
300        }
301    }
302}
303
304/// Constraints for marker placement
305#[derive(Debug, Clone)]
306pub struct MarkerConstraints {
307    /// Minimum distance from other markers of the same type
308    pub min_distance_same: Option<f32>,
309    /// Minimum distance from any other marker
310    pub min_distance_any: Option<f32>,
311    /// Maximum distance from specific marker types
312    pub max_distance_from: Vec<(MarkerType, f32)>,
313    /// Marker types that cannot coexist in the same region
314    pub exclude_types: Vec<MarkerType>,
315    /// Required marker types that must exist nearby
316    pub require_nearby: Vec<(MarkerType, f32)>,
317}
318
319impl MarkerConstraints {
320    /// Create constraints with no restrictions
321    pub fn none() -> Self {
322        Self {
323            min_distance_same: None,
324            min_distance_any: None,
325            max_distance_from: Vec::new(),
326            exclude_types: Vec::new(),
327            require_nearby: Vec::new(),
328        }
329    }
330
331    /// Create constraints for quest objectives (avoid clustering)
332    pub fn quest_objective() -> Self {
333        Self {
334            min_distance_same: Some(10.0),
335            min_distance_any: Some(3.0),
336            max_distance_from: Vec::new(),
337            exclude_types: vec![MarkerType::SafeZone],
338            require_nearby: Vec::new(),
339        }
340    }
341
342    /// Create constraints for loot (can cluster but not too close)
343    pub fn loot() -> Self {
344        Self {
345            min_distance_same: Some(5.0),
346            min_distance_any: Some(2.0),
347            max_distance_from: Vec::new(),
348            exclude_types: vec![MarkerType::SafeZone],
349            require_nearby: Vec::new(),
350        }
351    }
352}
353
354/// Spatial masks for gameplay logic
355#[derive(Debug, Clone)]
356pub struct Masks {
357    pub walkable: Vec<Vec<bool>>,
358    pub no_spawn: Vec<Vec<bool>>,
359    pub width: usize,
360    pub height: usize,
361}
362
363/// Region connectivity information
364#[derive(Debug, Clone)]
365pub struct ConnectivityGraph {
366    pub regions: Vec<u32>,
367    pub edges: Vec<(u32, u32)>,
368}
369
370/// Complete semantic information for a generated map
371#[derive(Debug, Clone)]
372pub struct SemanticLayers {
373    pub regions: Vec<Region>,
374    pub markers: Vec<Marker>,
375    pub masks: Masks,
376    pub connectivity: ConnectivityGraph,
377}
378
379/// Collect positions for markers of a given type.
380pub fn marker_positions(layers: &SemanticLayers, marker_type: &MarkerType) -> Vec<(usize, usize)> {
381    layers
382        .markers
383        .iter()
384        .filter(|marker| &marker.marker_type == marker_type)
385        .map(|marker| (marker.x as usize, marker.y as usize))
386        .collect()
387}
388
389impl Region {
390    pub fn new(id: u32, kind: impl Into<String>) -> Self {
391        Self {
392            id,
393            kind: kind.into(),
394            cells: Vec::new(),
395            tags: Vec::new(),
396        }
397    }
398
399    pub fn add_cell(&mut self, x: u32, y: u32) {
400        self.cells.push((x, y));
401    }
402
403    pub fn add_tag(&mut self, tag: impl Into<String>) {
404        self.tags.push(tag.into());
405    }
406
407    pub fn area(&self) -> usize {
408        self.cells.len()
409    }
410}
411
412impl Marker {
413    pub fn with_weight(mut self, weight: f32) -> Self {
414        self.weight = weight;
415        self
416    }
417
418    pub fn with_region(mut self, region_id: u32) -> Self {
419        self.region_id = Some(region_id);
420        self
421    }
422
423    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
424        self.metadata.insert(key.into(), value.into());
425        self
426    }
427}
428
429/// Requirements for semantic-driven generation
430#[derive(Debug, Clone)]
431pub struct SemanticRequirements {
432    /// Minimum number of regions of each type
433    pub min_regions: HashMap<String, usize>,
434    /// Maximum number of regions of each type
435    pub max_regions: HashMap<String, usize>,
436    /// Required connectivity between region types
437    pub required_connections: Vec<(String, String)>,
438    /// Minimum total walkable area
439    pub min_walkable_area: Option<usize>,
440    /// Required marker types and their minimum counts
441    pub required_markers: HashMap<MarkerType, usize>,
442}
443
444impl SemanticRequirements {
445    /// Create requirements with no constraints
446    pub fn none() -> Self {
447        Self {
448            min_regions: HashMap::new(),
449            max_regions: HashMap::new(),
450            required_connections: Vec::new(),
451            min_walkable_area: None,
452            required_markers: HashMap::new(),
453        }
454    }
455
456    /// Create basic dungeon requirements
457    pub fn basic_dungeon() -> Self {
458        let mut req = Self::none();
459        req.min_regions.insert("room".to_string(), 3);
460        req.required_connections
461            .push(("room".to_string(), "corridor".to_string()));
462        req.required_markers.insert(MarkerType::Spawn, 1);
463        req.required_markers.insert(MarkerType::Exit, 1);
464        req
465    }
466
467    /// Validate if semantic layers meet these requirements
468    pub fn validate(&self, layers: &SemanticLayers) -> bool {
469        // Check region counts
470        let mut region_counts: HashMap<String, usize> = HashMap::new();
471        for region in &layers.regions {
472            *region_counts.entry(region.kind.clone()).or_insert(0) += 1;
473        }
474
475        for (kind, min_count) in &self.min_regions {
476            if region_counts.get(kind).unwrap_or(&0) < min_count {
477                return false;
478            }
479        }
480
481        // Check marker counts
482        let mut marker_counts: HashMap<MarkerType, usize> = HashMap::new();
483        for marker in &layers.markers {
484            *marker_counts.entry(marker.marker_type.clone()).or_insert(0) += 1;
485        }
486
487        for (marker_type, min_count) in &self.required_markers {
488            if marker_counts.get(marker_type).unwrap_or(&0) < min_count {
489                return false;
490            }
491        }
492
493        true
494    }
495}
496
497impl Masks {
498    pub fn new(width: usize, height: usize) -> Self {
499        Self {
500            walkable: vec![vec![false; width]; height],
501            no_spawn: vec![vec![false; width]; height],
502            width,
503            height,
504        }
505    }
506
507    pub fn from_tiles(tiles: &Grid<Tile>) -> Self {
508        let mut masks = Self::new(tiles.width(), tiles.height());
509
510        for y in 0..tiles.height() {
511            for x in 0..tiles.width() {
512                let walkable = tiles.get(x as i32, y as i32).is_some_and(|t| t.is_floor());
513                masks.walkable[y][x] = walkable;
514            }
515        }
516
517        masks
518    }
519}
520
521impl ConnectivityGraph {
522    pub fn new() -> Self {
523        Self {
524            regions: Vec::new(),
525            edges: Vec::new(),
526        }
527    }
528
529    pub fn add_region(&mut self, id: u32) {
530        if !self.regions.contains(&id) {
531            self.regions.push(id);
532        }
533    }
534
535    pub fn add_edge(&mut self, from: u32, to: u32) {
536        self.add_region(from);
537        self.add_region(to);
538
539        if !self.edges.contains(&(from, to)) && !self.edges.contains(&(to, from)) {
540            self.edges.push((from, to));
541        }
542    }
543}
544
545/// Vertical connectivity analysis for multi-floor support
546#[derive(Debug, Clone)]
547pub struct VerticalConnectivity {
548    /// Potential stair locations (x, y, floor_from, floor_to)
549    pub stair_candidates: Vec<(u32, u32, u32, u32)>,
550    /// Confirmed stair placements
551    pub stairs: Vec<(u32, u32, u32, u32)>,
552    /// Regions accessible from each floor
553    pub floor_accessibility: HashMap<u32, Vec<u32>>,
554}
555
556impl VerticalConnectivity {
557    /// Create empty vertical connectivity analysis
558    pub fn new() -> Self {
559        Self {
560            stair_candidates: Vec::new(),
561            stairs: Vec::new(),
562            floor_accessibility: HashMap::new(),
563        }
564    }
565
566    /// Analyze potential stair placements between floors
567    /// This is a basic implementation that looks for suitable floor tiles
568    /// adjacent to walls that could support stairs
569    pub fn analyze_stair_candidates(&mut self, floor_grids: &[Grid<Tile>], min_clearance: usize) {
570        self.stair_candidates.clear();
571
572        for floor_idx in 0..floor_grids.len().saturating_sub(1) {
573            let current_floor = &floor_grids[floor_idx];
574            let next_floor = &floor_grids[floor_idx + 1];
575
576            // Find locations that are floor on both levels with wall support
577            for y in min_clearance..current_floor.height().saturating_sub(min_clearance) {
578                for x in min_clearance..current_floor.width().saturating_sub(min_clearance) {
579                    if self.is_valid_stair_location(
580                        current_floor,
581                        next_floor,
582                        x as i32,
583                        y as i32,
584                        min_clearance as i32,
585                    ) {
586                        self.stair_candidates.push((
587                            x as u32,
588                            y as u32,
589                            floor_idx as u32,
590                            (floor_idx + 1) as u32,
591                        ));
592                    }
593                }
594            }
595        }
596    }
597
598    /// Check if a location is suitable for stair placement
599    fn is_valid_stair_location(
600        &self,
601        floor1: &Grid<Tile>,
602        floor2: &Grid<Tile>,
603        x: i32,
604        y: i32,
605        clearance: i32,
606    ) -> bool {
607        // Both floors must have floor tiles at this location
608        let tile1 = floor1.get(x, y);
609        let tile2 = floor2.get(x, y);
610
611        if !tile1.is_some_and(|t| t.is_floor()) || !tile2.is_some_and(|t| t.is_floor()) {
612            return false;
613        }
614
615        // Check for adequate clearance around the stair location
616        for dy in -clearance..=clearance {
617            for dx in -clearance..=clearance {
618                let check_x = x + dx;
619                let check_y = y + dy;
620
621                let clear1 = floor1.get(check_x, check_y).is_some_and(|t| t.is_floor());
622                let clear2 = floor2.get(check_x, check_y).is_some_and(|t| t.is_floor());
623
624                if !clear1 || !clear2 {
625                    return false;
626                }
627            }
628        }
629
630        true
631    }
632
633    /// Place stairs at the best candidate locations
634    pub fn place_stairs(&mut self, max_stairs_per_floor: usize) {
635        self.stairs.clear();
636
637        // Group candidates by floor pair
638        let mut floor_candidates: HashMap<(u32, u32), Vec<(u32, u32)>> = HashMap::new();
639        for &(x, y, from_floor, to_floor) in &self.stair_candidates {
640            floor_candidates
641                .entry((from_floor, to_floor))
642                .or_default()
643                .push((x, y));
644        }
645
646        // Place limited number of stairs per floor pair
647        for ((from_floor, to_floor), candidates) in floor_candidates {
648            let stairs_to_place = candidates.len().min(max_stairs_per_floor);
649            for &(x, y) in candidates.iter().take(stairs_to_place) {
650                self.stairs.push((x, y, from_floor, to_floor));
651            }
652        }
653    }
654}
655
656impl Default for VerticalConnectivity {
657    fn default() -> Self {
658        Self::new()
659    }
660}
661
662impl Default for ConnectivityGraph {
663    fn default() -> Self {
664        Self::new()
665    }
666}