Skip to main content

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