terrain_forge/algorithms/
room_accretion.rs

1use crate::{Algorithm, Grid, Rng, Tile};
2
3#[derive(Debug, Clone)]
4pub struct RoomAccretionConfig {
5    pub templates: Vec<RoomTemplate>,
6    pub max_rooms: usize,
7    pub loop_chance: f64,
8}
9
10#[derive(Debug, Clone)]
11pub enum RoomTemplate {
12    Rectangle { min: usize, max: usize },
13    Blob { size: usize, smoothing: usize },
14    Circle { min_radius: usize, max_radius: usize },
15}
16
17impl Default for RoomAccretionConfig {
18    fn default() -> Self {
19        Self {
20            templates: vec![
21                RoomTemplate::Rectangle { min: 5, max: 12 },
22                RoomTemplate::Blob { size: 8, smoothing: 2 },
23                RoomTemplate::Circle { min_radius: 3, max_radius: 6 },
24            ],
25            max_rooms: 15,
26            loop_chance: 0.1,
27        }
28    }
29}
30
31pub struct RoomAccretion {
32    config: RoomAccretionConfig,
33}
34
35impl RoomAccretion {
36    pub fn new(config: RoomAccretionConfig) -> Self {
37        Self { config }
38    }
39}
40
41impl Default for RoomAccretion {
42    fn default() -> Self {
43        Self::new(RoomAccretionConfig::default())
44    }
45}
46
47impl Algorithm<Tile> for RoomAccretion {
48    fn generate(&self, grid: &mut Grid<Tile>, seed: u64) {
49        let mut rng = Rng::new(seed);
50        let (w, h) = (grid.width(), grid.height());
51        
52        // Start with first room in center
53        let center_x = w / 2;
54        let center_y = h / 2;
55        let template = rng.pick(&self.config.templates).unwrap().clone();
56        place_room(grid, &template, center_x, center_y, &mut rng);
57        
58        // Add rooms by sliding until they fit adjacent to existing structure
59        for _ in 1..self.config.max_rooms {
60            let template = rng.pick(&self.config.templates).unwrap().clone();
61            
62            // Try multiple positions
63            let mut placed = false;
64            for _ in 0..50 {
65                let start_x = rng.range_usize(5, w - 5);
66                let start_y = rng.range_usize(5, h - 5);
67                
68                if let Some((final_x, final_y)) = slide_to_fit(grid, &template, start_x, start_y, &mut rng) {
69                    place_room(grid, &template, final_x, final_y, &mut rng);
70                    
71                    // Connect to existing structure
72                    connect_to_existing(grid, final_x, final_y, &template, &mut rng);
73                    placed = true;
74                    break;
75                }
76            }
77            
78            if !placed { break; }
79        }
80        
81        // Add loops
82        if self.config.loop_chance > 0.0 {
83            crate::effects::connect_regions_spanning(grid, self.config.loop_chance, &mut rng);
84        }
85    }
86
87    fn name(&self) -> &'static str {
88        "RoomAccretion"
89    }
90}
91
92fn place_room(grid: &mut Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize, rng: &mut Rng) {
93    match template {
94        RoomTemplate::Rectangle { min, max } => {
95            let size = rng.range_usize(*min, *max + 1);
96            let half = size / 2;
97            for y in cy.saturating_sub(half)..=(cy + half).min(grid.height() - 1) {
98                for x in cx.saturating_sub(half)..=(cx + half).min(grid.width() - 1) {
99                    grid.set(x as i32, y as i32, Tile::Floor);
100                }
101            }
102        }
103        RoomTemplate::Circle { min_radius, max_radius } => {
104            let radius = rng.range_usize(*min_radius, *max_radius + 1);
105            let r2 = (radius * radius) as f64;
106            for dy in -(radius as i32)..=(radius as i32) {
107                for dx in -(radius as i32)..=(radius as i32) {
108                    if (dx * dx + dy * dy) as f64 <= r2 {
109                        let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
110                        let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
111                        grid.set(x as i32, y as i32, Tile::Floor);
112                    }
113                }
114            }
115        }
116        RoomTemplate::Blob { size, smoothing } => {
117            // Create blob using cellular automata
118            let half = size / 2;
119            let mut temp_grid = Grid::new(size + 2, size + 2);
120            
121            // Random fill
122            for y in 1..size + 1 {
123                for x in 1..size + 1 {
124                    if rng.chance(0.45) {
125                        temp_grid.set(x as i32, y as i32, Tile::Floor);
126                    }
127                }
128            }
129            
130            // Smooth
131            for _ in 0..*smoothing {
132                let mut new_grid = temp_grid.clone();
133                for y in 1..size + 1 {
134                    for x in 1..size + 1 {
135                        let neighbors = [
136                            temp_grid[(x - 1, y)], temp_grid[(x + 1, y)],
137                            temp_grid[(x, y - 1)], temp_grid[(x, y + 1)],
138                            temp_grid[(x - 1, y - 1)], temp_grid[(x + 1, y - 1)],
139                            temp_grid[(x - 1, y + 1)], temp_grid[(x + 1, y + 1)],
140                        ];
141                        let floor_count = neighbors.iter().filter(|t| t.is_floor()).count();
142                        new_grid.set(x as i32, y as i32, if floor_count >= 4 { Tile::Floor } else { Tile::Wall });
143                    }
144                }
145                temp_grid = new_grid;
146            }
147            
148            // Copy to main grid
149            for y in 1..size + 1 {
150                for x in 1..size + 1 {
151                    if temp_grid[(x, y)].is_floor() {
152                        let gx = (cx as i32 + x as i32 - half as i32 - 1).max(0).min(grid.width() as i32 - 1);
153                        let gy = (cy as i32 + y as i32 - half as i32 - 1).max(0).min(grid.height() as i32 - 1);
154                        grid.set(gx, gy, Tile::Floor);
155                    }
156                }
157            }
158        }
159    }
160}
161
162fn slide_to_fit(grid: &Grid<Tile>, template: &RoomTemplate, start_x: usize, start_y: usize, rng: &mut Rng) -> Option<(usize, usize)> {
163    let directions = [(0, -1), (1, 0), (0, 1), (-1, 0)]; // N, E, S, W
164    let direction = rng.pick(&directions).unwrap();
165    
166    let mut x = start_x as i32;
167    let mut y = start_y as i32;
168    
169    // Slide until we hit existing floor or boundary
170    for _ in 0..50 {
171        if would_overlap(grid, template, x as usize, y as usize) {
172            // Back up one step and check if adjacent
173            x -= direction.0;
174            y -= direction.1;
175            if is_adjacent_to_floor(grid, template, x as usize, y as usize) {
176                return Some((x as usize, y as usize));
177            }
178            return None;
179        }
180        
181        x += direction.0;
182        y += direction.1;
183        
184        if x < 5 || y < 5 || x >= grid.width() as i32 - 5 || y >= grid.height() as i32 - 5 {
185            return None;
186        }
187    }
188    
189    None
190}
191
192fn would_overlap(grid: &Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize) -> bool {
193    let bounds = get_template_bounds(template);
194    for dy in -bounds.1..=bounds.1 {
195        for dx in -bounds.0..=bounds.0 {
196            let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
197            let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
198            if grid[(x, y)].is_floor() {
199                return true;
200            }
201        }
202    }
203    false
204}
205
206fn is_adjacent_to_floor(grid: &Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize) -> bool {
207    let bounds = get_template_bounds(template);
208    for dy in -(bounds.1 + 1)..=(bounds.1 + 1) {
209        for dx in -(bounds.0 + 1)..=(bounds.0 + 1) {
210            let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
211            let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
212            if grid[(x, y)].is_floor() {
213                return true;
214            }
215        }
216    }
217    false
218}
219
220fn get_template_bounds(template: &RoomTemplate) -> (i32, i32) {
221    match template {
222        RoomTemplate::Rectangle { max, .. } => ((*max / 2) as i32, (*max / 2) as i32),
223        RoomTemplate::Circle { max_radius, .. } => (*max_radius as i32, *max_radius as i32),
224        RoomTemplate::Blob { size, .. } => ((size / 2) as i32, (size / 2) as i32),
225    }
226}
227
228fn connect_to_existing(grid: &mut Grid<Tile>, cx: usize, cy: usize, template: &RoomTemplate, rng: &mut Rng) {
229    let bounds = get_template_bounds(template);
230    
231    // Find edge of room
232    let mut edge_points = Vec::new();
233    for dy in -bounds.1..=bounds.1 {
234        for dx in -bounds.0..=bounds.0 {
235            let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
236            let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
237            if grid[(x, y)].is_floor() {
238                // Check if on edge
239                let neighbors = [
240                    (x.wrapping_sub(1), y), (x + 1, y), (x, y.wrapping_sub(1)), (x, y + 1)
241                ];
242                for &(nx, ny) in &neighbors {
243                    if nx < grid.width() && ny < grid.height() && !grid[(nx, ny)].is_floor() {
244                        edge_points.push((x, y));
245                        break;
246                    }
247                }
248            }
249        }
250    }
251    
252    if let Some(&(start_x, start_y)) = rng.pick(&edge_points) {
253        // Carve a short corridor
254        let directions = [(0, -1), (1, 0), (0, 1), (-1, 0)];
255        let direction = rng.pick(&directions).unwrap();
256        
257        for i in 1..=3 {
258            let x = (start_x as i32 + direction.0 * i).max(0).min(grid.width() as i32 - 1);
259            let y = (start_y as i32 + direction.1 * i).max(0).min(grid.height() as i32 - 1);
260            grid.set(x, y, Tile::Floor);
261        }
262    }
263}