krunker_maze_generator/
lib.rs

1// src/lib.rs
2
3use image::{Rgb, RgbImage};
4use rand::{rng, seq::SliceRandom};
5use serde_json::json;
6use std::{
7    cmp::Reverse,
8    collections::{BinaryHeap, HashMap},
9};
10
11/// The core maze data (cells & walls) and all operations on it.
12pub struct Maze {
13    pub width: usize,
14    pub height: usize,
15    pub vert_walls: Vec<Vec<bool>>,
16    pub hor_walls: Vec<Vec<bool>>,
17}
18
19impl Maze {
20    pub fn new(width: usize, height: usize) -> Self {
21        let vert_walls = vec![vec![true; width + 1]; height];
22        let hor_walls  = vec![vec![true; width]; height + 1];
23        Maze { width, height, vert_walls, hor_walls }
24    }
25
26    pub fn generate(&mut self) {
27        let mut visited = vec![vec![false; self.width]; self.height];
28        let mut stack   = Vec::new();
29        visited[0][0] = true;
30        stack.push((0, 0, 0));
31
32        while let Some((x, y, dir_idx)) = stack.pop() {
33            let mut dirs = vec![
34                (1isize, 0isize, 'R'),
35                (-1, 0, 'L'),
36                (0, 1, 'D'),
37                (0, -1, 'U'),
38            ];
39            dirs.shuffle(&mut rng());
40
41            for i in dir_idx..dirs.len() {
42                let (dx, dy, dir) = dirs[i];
43                let nx = x as isize + dx;
44                let ny = y as isize + dy;
45                if nx >= 0 && nx < self.width as isize && ny >= 0 && ny < self.height as isize {
46                    let (nx, ny) = (nx as usize, ny as usize);
47                    if !visited[ny][nx] {
48                        match dir {
49                            'R' => self.vert_walls[y][x + 1] = false,
50                            'L' => self.vert_walls[y][x]     = false,
51                            'D' => self.hor_walls[y + 1][x]  = false,
52                            'U' => self.hor_walls[y][x]      = false,
53                            _   => {}
54                        }
55                        stack.push((x, y, i + 1));
56                        visited[ny][nx] = true;
57                        stack.push((nx, ny, 0));
58                        break;
59                    }
60                }
61            }
62        }
63    }
64
65    /// Solve via A* from top‑left to bottom‑right
66    pub fn solve(&self) -> Vec<(usize, usize)> {
67        let total = self.width * self.height;
68        let start = 0;
69        let goal  = total - 1;
70
71        let mut g_score = vec![usize::MAX; total];
72        let mut came_from = HashMap::new();
73        let mut open = BinaryHeap::new();
74
75        // Heuristic: Manhattan to goal
76        let h = |idx: usize| {
77            let x = (idx % self.width)  as isize;
78            let y = (idx / self.width)  as isize;
79            let gx = (self.width - 1)   as isize;
80            let gy = (self.height - 1)  as isize;
81            ((gx - x).abs() + (gy - y).abs()) as usize
82        };
83
84        g_score[start] = 0;
85        open.push((Reverse(h(start)), start));
86
87        while let Some((_, current)) = open.pop() {
88            if current == goal { break }
89            let cx = current % self.width;
90            let cy = current / self.width;
91
92            let neighbors = [
93                (cx.wrapping_sub(1), cy,        cx > 0                    && !self.vert_walls[cy][cx]),
94                (cx + 1,         cy,        cx + 1 < self.width && !self.vert_walls[cy][cx + 1]),
95                (cx,             cy.wrapping_sub(1), cy > 0              && !self.hor_walls[cy][cx]),
96                (cx,             cy + 1,        cy + 1 < self.height && !self.hor_walls[cy + 1][cx]),
97            ];
98
99            for &(nx, ny, ok) in &neighbors {
100                if !ok || nx >= self.width || ny >= self.height { continue }
101                let neighbor = ny * self.width + nx;
102                let tentative = g_score[current] + 1;
103                if tentative < g_score[neighbor] {
104                    g_score[neighbor] = tentative;
105                    came_from.insert(neighbor, current);
106                    open.push((Reverse(tentative + h(neighbor)), neighbor));
107                }
108            }
109        }
110
111        // Reconstruct path
112        let mut path = Vec::new();
113        let mut cur  = goal;
114        while let Some(&p) = came_from.get(&cur) {
115            path.push((cur % self.width, cur / self.width));
116            cur = p;
117        }
118        path.push((0, 0));
119        path.reverse();
120        path
121    }
122
123    /// Draw maze + solution into an RGB image
124    pub fn draw(&self, cell_size: usize, wall_thick: usize) -> RgbImage {
125        let img_w = (self.width * cell_size + wall_thick) as u32;
126        let img_h = (self.height * cell_size + wall_thick) as u32;
127        let mut img = RgbImage::new(img_w, img_h);
128
129        // Colors
130        let white = Rgb([255, 255, 255]);
131        let black = Rgb([0,   0,   0  ]);
132        let red   = Rgb([255,   0,   0]);
133
134        // Fill background
135        for x in 0..img_w {
136            for y in 0..img_h {
137                img.put_pixel(x, y, white);
138            }
139        }
140
141        // Walls
142        for y in 0..self.height {
143            let y0 = (y * cell_size) as u32;
144            for x in 0..=self.width {
145                if self.vert_walls[y][x] {
146                    let x0 = (x * cell_size) as u32;
147                    for dx in 0..wall_thick as u32 {
148                        for dy in 0..cell_size as u32 {
149                            img.put_pixel(x0 + dx, y0 + dy, black);
150                        }
151                    }
152                }
153            }
154        }
155        for y in 0..=self.height {
156            let y0 = (y * cell_size) as u32;
157            for x in 0..self.width {
158                if self.hor_walls[y][x] {
159                    let x0 = (x * cell_size) as u32;
160                    for dx in 0..cell_size as u32 {
161                        for dy in 0..wall_thick as u32 {
162                            img.put_pixel(x0 + dx, y0 + dy, black);
163                        }
164                    }
165                }
166            }
167        }
168
169        // Solution path
170        let thickness = (cell_size as u32) / 2;
171        for window in self.solve().windows(2) {
172            let (x1, y1) = window[0];
173            let (x2, y2) = window[1];
174            let cx1 = x1 as u32 * cell_size as u32 + cell_size as u32 / 2;
175            let cy1 = y1 as u32 * cell_size as u32 + cell_size as u32 / 2;
176            let cx2 = x2 as u32 * cell_size as u32 + cell_size as u32 / 2;
177            let cy2 = y2 as u32 * cell_size as u32 + cell_size as u32 / 2;
178
179            if cx1 == cx2 {
180                let x0 = cx1.saturating_sub(thickness / 2);
181                let h = (cy2 as i32 - cy1 as i32).abs() as u32;
182                let y_min = cy1.min(cy2);
183                for dx in 0..thickness {
184                    for dy in 0..=h {
185                        img.put_pixel(x0 + dx, y_min + dy, red);
186                    }
187                }
188            } else {
189                let y0 = cy1.saturating_sub(thickness / 2);
190                let w  = (cx2 as i32 - cx1 as i32).abs() as u32;
191                let x_min = cx1.min(cx2);
192                for dy in 0..thickness {
193                    for dx in 0..=w {
194                        img.put_pixel(x_min + dx, y0 + dy, red);
195                    }
196                }
197            }
198        }
199
200        img
201    }
202
203    /// Build the JSON segments and full map structure
204    pub fn to_map_json(&self, cell_size: usize, wall_thick: usize) -> serde_json::Value {
205        let mut segments = Vec::new();
206        // vertical
207        for x in 0..=self.width {
208            let mut y = 0;
209            while y < self.height {
210                if self.vert_walls[y][x] {
211                    let y1 = y;
212                    let mut y2 = y + 1;
213                    while y2 < self.height && self.vert_walls[y2][x] {
214                        y2 += 1;
215                    }
216                    segments.push(json!({"type":"vertical","x":x,"y1":y1,"y2":y2}));
217                    y = y2;
218                } else {
219                    y += 1;
220                }
221            }
222        }
223        // horizontal
224        for y in 0..=self.height {
225            let mut x = 0;
226            while x < self.width {
227                if self.hor_walls[y][x] {
228                    let x1 = x;
229                    let mut x2 = x + 1;
230                    while x2 < self.width && self.hor_walls[y][x2] {
231                        x2 += 1;
232                    }
233                    segments.push(json!({"type":"horizontal","y":y,"x1":x1,"x2":x2}));
234                    x = x2;
235                } else {
236                    x += 1;
237                }
238            }
239        }
240        let fw = (self.width  * cell_size) as i32;
241        let fd = (self.height * cell_size) as i32;
242        let mut sizes  = vec![fw, 1, fd];
243        let mut objects = vec![json!({ "p":[fw/2, -1, fd/2], "si":0 })];
244
245        for (i, seg) in segments.iter().enumerate() {
246            let si = i + 1;
247            if seg["type"] == "vertical" {
248                let x  = seg["x"].as_i64().unwrap() as i32 * cell_size as i32;
249                let y1 = seg["y1"].as_i64().unwrap() as i32 * cell_size as i32;
250                let y2 = seg["y2"].as_i64().unwrap() as i32 * cell_size as i32;
251                let length = y2 - y1;
252                sizes.extend([wall_thick as i32, 20, length]);
253                objects.push(json!({"p":[x,0,(y1+y2)/2],"si":si}));
254            } else {
255                let y  = seg["y"].as_i64().unwrap() as i32 * cell_size as i32;
256                let x1 = seg["x1"].as_i64().unwrap() as i32 * cell_size as i32;
257                let x2 = seg["x2"].as_i64().unwrap() as i32 * cell_size as i32;
258                let length = x2 - x1;
259                sizes.extend([length,20, wall_thick as i32]);
260                objects.push(json!({"p":[(x1+x2)/2,0,y],"si":si}));
261            }
262        }
263
264        let half = (cell_size as i32) / 2;
265        let start_spawn = json!([half,0,half,0,0,0]);
266        let end_spawn   = json!([(fw-half),0,(fd-half),0,0,0]);
267
268        json!({
269            "name":    "GeneratedMaze",
270            "ambient": "#97a0a8",
271            "light":   "#f2f8fc",
272            "sky":     "#dce8ed",
273            "fog":     "#8d9aa0",
274            "fogD":    2000,
275            "xyz":     sizes,
276            "objects": objects,
277            "spawns":  [start_spawn, end_spawn],
278        })
279    }
280}