diff options
Diffstat (limited to 'src/game.rs')
| -rw-r--r-- | src/game.rs | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..4bedecc --- /dev/null +++ b/src/game.rs | |||
| @@ -0,0 +1,204 @@ | |||
| 1 | use rand::Rng; | ||
| 2 | |||
| 3 | // --------------- maze grid --------------- | ||
| 4 | |||
| 5 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 6 | pub enum Cell { | ||
| 7 | Wall, | ||
| 8 | Passage, | ||
| 9 | Exit, | ||
| 10 | } | ||
| 11 | |||
| 12 | pub struct Maze { | ||
| 13 | cells: Vec<Cell>, | ||
| 14 | width: usize, | ||
| 15 | height: usize, | ||
| 16 | } | ||
| 17 | |||
| 18 | impl Maze { | ||
| 19 | /// Recursive-backtracker perfect maze. `room_w × room_h` is the logical | ||
| 20 | /// room count; the actual grid will be `(room_w*2+1) × (room_h*2+1)`. | ||
| 21 | pub fn generate(room_w: usize, room_h: usize, rng: &mut impl Rng) -> Self { | ||
| 22 | let w = room_w * 2 + 1; | ||
| 23 | let h = room_h * 2 + 1; | ||
| 24 | |||
| 25 | let mut cells = vec![Cell::Wall; w * h]; | ||
| 26 | cells[1 * w + 1] = Cell::Passage; | ||
| 27 | |||
| 28 | let mut stack = vec![(1usize, 1usize)]; | ||
| 29 | |||
| 30 | while let Some(&(cy, cx)) = stack.last() { | ||
| 31 | let mut unvisited = Vec::new(); | ||
| 32 | |||
| 33 | for (dy, dx) in [(-2isize, 0isize), (2, 0), (0, -2), (0, 2)] { | ||
| 34 | let ny = cy as isize + dy; | ||
| 35 | let nx = cx as isize + dx; | ||
| 36 | if ny >= 1 && ny < (h - 1) as isize && nx >= 1 && nx < (w - 1) as isize { | ||
| 37 | if cells[ny as usize * w + nx as usize] == Cell::Wall { | ||
| 38 | unvisited.push((ny as usize, nx as usize)); | ||
| 39 | } | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | if unvisited.is_empty() { | ||
| 44 | stack.pop(); | ||
| 45 | } else { | ||
| 46 | let idx = rng.gen_range(0..unvisited.len()); | ||
| 47 | let (ny, nx) = unvisited[idx]; | ||
| 48 | |||
| 49 | // carve the wall between current cell and chosen neighbour | ||
| 50 | let wall_y = (cy + ny) / 2; | ||
| 51 | let wall_x = (cx + nx) / 2; | ||
| 52 | cells[wall_y * w + wall_x] = Cell::Passage; | ||
| 53 | cells[ny * w + nx] = Cell::Passage; | ||
| 54 | |||
| 55 | stack.push((ny, nx)); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | // ---- place exit in a far-away passage ---- | ||
| 60 | let mut passages: Vec<(f64, usize, usize)> = Vec::new(); | ||
| 61 | for y in (1..h).step_by(2) { | ||
| 62 | for x in (1..w).step_by(2) { | ||
| 63 | if cells[y * w + x] == Cell::Passage && !(y == 1 && x == 1) { | ||
| 64 | let dist = ((y as f64 - 1.0).powi(2) + (x as f64 - 1.0).powi(2)).sqrt(); | ||
| 65 | passages.push((dist, y, x)); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | if !passages.is_empty() { | ||
| 71 | passages.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); | ||
| 72 | let far_count = (passages.len() / 4).max(1); | ||
| 73 | let idx = rng.gen_range(0..far_count); | ||
| 74 | let (_, ey, ex) = passages[idx]; | ||
| 75 | cells[ey * w + ex] = Cell::Exit; | ||
| 76 | } | ||
| 77 | |||
| 78 | Self { | ||
| 79 | cells, | ||
| 80 | width: w, | ||
| 81 | height: h, | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | #[inline] | ||
| 86 | pub fn is_wall(&self, y: usize, x: usize) -> bool { | ||
| 87 | self.cells | ||
| 88 | .get(y * self.width + x) | ||
| 89 | .map_or(true, |&c| c == Cell::Wall) | ||
| 90 | } | ||
| 91 | |||
| 92 | /// Floating-point query — out-of-bounds is treated as wall. | ||
| 93 | #[inline] | ||
| 94 | pub fn is_passage_at_float(&self, x: f64, y: f64) -> bool { | ||
| 95 | let xi = x as usize; | ||
| 96 | let yi = y as usize; | ||
| 97 | if xi >= self.width || yi >= self.height { | ||
| 98 | return false; | ||
| 99 | } | ||
| 100 | self.cells[yi * self.width + xi] != Cell::Wall | ||
| 101 | } | ||
| 102 | |||
| 103 | #[inline] | ||
| 104 | pub fn is_exit(&self, x: f64, y: f64) -> bool { | ||
| 105 | let xi = x as usize; | ||
| 106 | let yi = y as usize; | ||
| 107 | if xi >= self.width || yi >= self.height { | ||
| 108 | return false; | ||
| 109 | } | ||
| 110 | self.cells[yi * self.width + xi] == Cell::Exit | ||
| 111 | } | ||
| 112 | |||
| 113 | pub fn width(&self) -> usize { | ||
| 114 | self.width | ||
| 115 | } | ||
| 116 | pub fn height(&self) -> usize { | ||
| 117 | self.height | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | // --------------- player --------------- | ||
| 122 | |||
| 123 | pub struct Player { | ||
| 124 | pub x: f64, | ||
| 125 | pub y: f64, | ||
| 126 | pub angle: f64, // radians, 0 = +x (east), π/2 = +y (south) | ||
| 127 | } | ||
| 128 | |||
| 129 | impl Player { | ||
| 130 | pub fn new(x: f64, y: f64, angle: f64) -> Self { | ||
| 131 | Self { x, y, angle } | ||
| 132 | } | ||
| 133 | |||
| 134 | pub fn rotate(&mut self, delta: f64) { | ||
| 135 | self.angle = (self.angle + delta).rem_euclid(2.0 * std::f64::consts::PI); | ||
| 136 | } | ||
| 137 | |||
| 138 | /// Apply a raw (dx, dy) displacement with wall-sliding collision. | ||
| 139 | /// Checks are done per-axis so the player slides along walls naturally. | ||
| 140 | pub fn try_move(&mut self, dx: f64, dy: f64, maze: &Maze) { | ||
| 141 | let m = 0.35; // collision margin — keeps the camera out of walls | ||
| 142 | |||
| 143 | let nx = self.x + dx; | ||
| 144 | if maze.is_passage_at_float(nx + m, self.y + m) | ||
| 145 | && maze.is_passage_at_float(nx + m, self.y - m) | ||
| 146 | && maze.is_passage_at_float(nx - m, self.y + m) | ||
| 147 | && maze.is_passage_at_float(nx - m, self.y - m) | ||
| 148 | { | ||
| 149 | self.x = nx; | ||
| 150 | } | ||
| 151 | |||
| 152 | let ny = self.y + dy; | ||
| 153 | if maze.is_passage_at_float(self.x + m, ny + m) | ||
| 154 | && maze.is_passage_at_float(self.x - m, ny + m) | ||
| 155 | && maze.is_passage_at_float(self.x + m, ny - m) | ||
| 156 | && maze.is_passage_at_float(self.x - m, ny - m) | ||
| 157 | { | ||
| 158 | self.y = ny; | ||
| 159 | } | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | // --------------- top-level game --------------- | ||
| 164 | |||
| 165 | pub struct Game { | ||
| 166 | pub maze: Maze, | ||
| 167 | pub player: Player, | ||
| 168 | pub wall_texture: Vec<u32>, | ||
| 169 | pub floor_texture: Vec<u32>, | ||
| 170 | } | ||
| 171 | |||
| 172 | impl Game { | ||
| 173 | pub fn new() -> Self { | ||
| 174 | let mut rng = rand::thread_rng(); | ||
| 175 | let maze = Maze::generate(16, 16, &mut rng); // 33×33 grid | ||
| 176 | let player = Player::new(1.5, 1.5, 0.0); // start centre of (1,1), facing east | ||
| 177 | let wall_texture = crate::texture::generate_wall_texture(&mut rng); | ||
| 178 | let floor_texture = crate::texture::generate_floor_texture(&mut rng); | ||
| 179 | |||
| 180 | Self { | ||
| 181 | maze, | ||
| 182 | player, | ||
| 183 | wall_texture, | ||
| 184 | floor_texture, | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | pub fn rotate(&mut self, delta: f64) { | ||
| 189 | self.player.rotate(delta); | ||
| 190 | } | ||
| 191 | |||
| 192 | /// Apply forward + strafe as a single combined displacement so that | ||
| 193 | /// diagonal wall-hugging is checked atomically (no two-step clip). | ||
| 194 | pub fn apply_movement(&mut self, forward: f64, strafe: f64) { | ||
| 195 | let a = self.player.angle; | ||
| 196 | let dx = a.cos() * forward + (a + std::f64::consts::FRAC_PI_2).cos() * strafe; | ||
| 197 | let dy = a.sin() * forward + (a + std::f64::consts::FRAC_PI_2).sin() * strafe; | ||
| 198 | self.player.try_move(dx, dy, &self.maze); | ||
| 199 | } | ||
| 200 | |||
| 201 | pub fn check_win(&self) -> bool { | ||
| 202 | self.maze.is_exit(self.player.x, self.player.y) | ||
| 203 | } | ||
| 204 | } | ||
