Skip to main content

terrain_forge/algorithms/
room_accretion.rs

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