summaryrefslogtreecommitdiffstats
path: root/src/game.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/game.rs204
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 @@
1use rand::Rng;
2
3// --------------- maze grid ---------------
4
5#[derive(Clone, Copy, PartialEq, Eq)]
6pub enum Cell {
7 Wall,
8 Passage,
9 Exit,
10}
11
12pub struct Maze {
13 cells: Vec<Cell>,
14 width: usize,
15 height: usize,
16}
17
18impl 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
123pub struct Player {
124 pub x: f64,
125 pub y: f64,
126 pub angle: f64, // radians, 0 = +x (east), π/2 = +y (south)
127}
128
129impl 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
165pub struct Game {
166 pub maze: Maze,
167 pub player: Player,
168 pub wall_texture: Vec<u32>,
169 pub floor_texture: Vec<u32>,
170}
171
172impl 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}