summaryrefslogtreecommitdiffstats
path: root/src/game.rs
blob: 4bedecce463825df8abca26639b57b1f50559eb2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
use rand::Rng;

// --------------- maze grid ---------------

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Cell {
    Wall,
    Passage,
    Exit,
}

pub struct Maze {
    cells: Vec<Cell>,
    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<u32>,
    pub floor_texture: Vec<u32>,
}

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)
    }
}