use rand::Rng; // --------------- maze grid --------------- #[derive(Clone, Copy, PartialEq, Eq)] pub enum Cell { Wall, Passage, Exit, } pub struct Maze { cells: Vec, width: usize, height: usize, } impl Maze { /// Recursive-backtracker perfect maze. `room_w × room_h` is the logical /// room count; the actual grid will be `(room_w*2+1) × (room_h*2+1)`. pub fn generate(room_w: usize, room_h: usize, rng: &mut impl Rng) -> Self { let w = room_w * 2 + 1; let h = room_h * 2 + 1; let mut cells = vec![Cell::Wall; w * h]; cells[1 * w + 1] = Cell::Passage; let mut stack = vec![(1usize, 1usize)]; while let Some(&(cy, cx)) = stack.last() { let mut unvisited = Vec::new(); for (dy, dx) in [(-2isize, 0isize), (2, 0), (0, -2), (0, 2)] { let ny = cy as isize + dy; let nx = cx as isize + dx; if ny >= 1 && ny < (h - 1) as isize && nx >= 1 && nx < (w - 1) as isize { if cells[ny as usize * w + nx as usize] == Cell::Wall { unvisited.push((ny as usize, nx as usize)); } } } if unvisited.is_empty() { stack.pop(); } else { let idx = rng.gen_range(0..unvisited.len()); let (ny, nx) = unvisited[idx]; // carve the wall between current cell and chosen neighbour let wall_y = (cy + ny) / 2; let wall_x = (cx + nx) / 2; cells[wall_y * w + wall_x] = Cell::Passage; cells[ny * w + nx] = Cell::Passage; stack.push((ny, nx)); } } // ---- place exit in a far-away passage ---- let mut passages: Vec<(f64, usize, usize)> = Vec::new(); for y in (1..h).step_by(2) { for x in (1..w).step_by(2) { if cells[y * w + x] == Cell::Passage && !(y == 1 && x == 1) { let dist = ((y as f64 - 1.0).powi(2) + (x as f64 - 1.0).powi(2)).sqrt(); passages.push((dist, y, x)); } } } if !passages.is_empty() { passages.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); let far_count = (passages.len() / 4).max(1); let idx = rng.gen_range(0..far_count); let (_, ey, ex) = passages[idx]; cells[ey * w + ex] = Cell::Exit; } Self { cells, width: w, height: h, } } #[inline] pub fn is_wall(&self, y: usize, x: usize) -> bool { self.cells .get(y * self.width + x) .map_or(true, |&c| c == Cell::Wall) } /// Floating-point query — out-of-bounds is treated as wall. #[inline] pub fn is_passage_at_float(&self, x: f64, y: f64) -> bool { let xi = x as usize; let yi = y as usize; if xi >= self.width || yi >= self.height { return false; } self.cells[yi * self.width + xi] != Cell::Wall } #[inline] pub fn is_exit(&self, x: f64, y: f64) -> bool { let xi = x as usize; let yi = y as usize; if xi >= self.width || yi >= self.height { return false; } self.cells[yi * self.width + xi] == Cell::Exit } pub fn width(&self) -> usize { self.width } pub fn height(&self) -> usize { self.height } } // --------------- player --------------- pub struct Player { pub x: f64, pub y: f64, pub angle: f64, // radians, 0 = +x (east), π/2 = +y (south) } impl Player { pub fn new(x: f64, y: f64, angle: f64) -> Self { Self { x, y, angle } } pub fn rotate(&mut self, delta: f64) { self.angle = (self.angle + delta).rem_euclid(2.0 * std::f64::consts::PI); } /// Apply a raw (dx, dy) displacement with wall-sliding collision. /// Checks are done per-axis so the player slides along walls naturally. pub fn try_move(&mut self, dx: f64, dy: f64, maze: &Maze) { let m = 0.35; // collision margin — keeps the camera out of walls let nx = self.x + dx; if maze.is_passage_at_float(nx + m, self.y + m) && maze.is_passage_at_float(nx + m, self.y - m) && maze.is_passage_at_float(nx - m, self.y + m) && maze.is_passage_at_float(nx - m, self.y - m) { self.x = nx; } let ny = self.y + dy; if maze.is_passage_at_float(self.x + m, ny + m) && maze.is_passage_at_float(self.x - m, ny + m) && maze.is_passage_at_float(self.x + m, ny - m) && maze.is_passage_at_float(self.x - m, ny - m) { self.y = ny; } } } // --------------- top-level game --------------- pub struct Game { pub maze: Maze, pub player: Player, pub wall_texture: Vec, pub floor_texture: Vec, } impl Game { pub fn new() -> Self { let mut rng = rand::thread_rng(); let maze = Maze::generate(16, 16, &mut rng); // 33×33 grid let player = Player::new(1.5, 1.5, 0.0); // start centre of (1,1), facing east let wall_texture = crate::texture::generate_wall_texture(&mut rng); let floor_texture = crate::texture::generate_floor_texture(&mut rng); Self { maze, player, wall_texture, floor_texture, } } pub fn rotate(&mut self, delta: f64) { self.player.rotate(delta); } /// Apply forward + strafe as a single combined displacement so that /// diagonal wall-hugging is checked atomically (no two-step clip). pub fn apply_movement(&mut self, forward: f64, strafe: f64) { let a = self.player.angle; let dx = a.cos() * forward + (a + std::f64::consts::FRAC_PI_2).cos() * strafe; let dy = a.sin() * forward + (a + std::f64::consts::FRAC_PI_2).sin() * strafe; self.player.try_move(dx, dy, &self.maze); } pub fn check_win(&self) -> bool { self.maze.is_exit(self.player.x, self.player.y) } }