summaryrefslogtreecommitdiff
path: root/dungeon/src/map.rs
blob: ade485e3aec15b8a99058ed426baa30a5731e28e (plain)
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
//! The `map` module contains structures of the dungeon game map
//! including the current `Floor`, and map `Tile`.

use rand::{Rng, SeedableRng, rngs::StdRng};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;

use std::{
	cell::RefCell,
	fmt::{Display, Write},
	hash::{DefaultHasher, Hash, Hasher},
};

use crate::bsp;
use crate::{const_pos, pos::Pos};

/// `MAP_SIZE` is the size of the size of the dungeon grid.
pub const MAP_SIZE: u16 = 48;

/// `MAP_SIZE` as a usize
pub const MAP_SIZE_USIZE: usize = MAP_SIZE as usize;

/// The number of tiles in the dungeon grid
pub const TILE_COUNT: usize = MAP_SIZE_USIZE * MAP_SIZE_USIZE;

/// The `Tile` enum represents what is (or is not) at
/// any given spot in the dungeon grid.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, EnumIter)]
pub enum Tile {
	/// `Wall` represents an impassible wall
	Wall,
	/// `Room` represents empty walkable space for a rectangular room
	Room,
	/// `Hallway` represents empty walkable space for a hallway
	Hallway,
	/// `Stairs` represents stairs to another floor
	Stairs,
}
impl Tile {
	/// Returns a list of all possible tiles
	pub fn values() -> impl Iterator<Item = Self> {
		Self::iter()
	}

	/// Returns if the tile is a wall
	#[must_use]
	pub const fn is_wall(self) -> bool {
		matches!(self, Self::Wall)
	}

	/// Returns if the tile is walkable
	#[must_use]
	pub const fn is_walkable(self) -> bool {
		matches!(self, Self::Room | Self::Hallway | Self::Stairs)
	}

	// Index by u16
}
impl Default for Tile {
	fn default() -> Self {
		Self::Wall
	}
}
impl Display for Tile {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		let char = match self {
			Self::Wall => '#',
			Self::Room => '.',
			Self::Hallway => ',',
			Self::Stairs => '>',
		};
		f.write_char(char)
	}
}

/// The `Floor` type represents the current playing
/// grid of the dungeon. It contains the tiles of the
/// grid, and the starting position of the player.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Floor {
	/// The dungeon grid
	tiles: Box<[Tile; TILE_COUNT]>,
	/// The position the player starts at
	player_start: Pos,
	/// The seed used when generating the dungeon grid
	seed: u64,
	/// The computed hash of the tile map
	hash: RefCell<u64>,
	/// If the tiles are dirty (hash needs to be recomputed)
	dirty: RefCell<bool>,
	/// Custom rng
	rng: StdRng,
}
impl Floor {
	/// Internal constructor for `Floor`
	fn new(tiles: Box<[Tile; TILE_COUNT]>, player_start: Pos, seed: u64) -> Self {
		let rng = rand::rngs::StdRng::seed_from_u64(seed);
		Self {
			tiles,
			player_start,
			seed,
			hash: RefCell::new(0),
			dirty: RefCell::new(true),
			rng,
		}
	}

	/// Generates a dungeon `Floor` using binary space partitioning.
	///
	/// # Examples
	///
	/// ```no_run
	/// use dungeon::Floor;
	///
	/// let floor = Floor::generate();
	/// ```
	#[must_use]
	pub fn generate() -> Self {
		let seed = rand::random();
		Self::generate_seeded(seed)
	}

	/// Generates a dungeon `Floor` using binary space partitioning provided with a seed.
	///
	/// The provided seed is used for randomness in the binary space partitioning
	/// algorithm.
	///
	/// # Examples
	///
	/// ```no_run
	/// use dungeon::Floor;
	///
	/// /// here is our very seedy seed
	/// let seed = 2893249402u64;
	/// let floor_1 = Floor::generate_seeded(seed);
	/// let floor_2 = Floor::generate_seeded(seed);
	/// assert_eq!(floor_1, floor_2); // both floors will be identical
	/// ```
	#[must_use]
	pub fn generate_seeded(seed: u64) -> Self {
		let (tiles, player_start) = bsp::generate(seed);

		Self::new(tiles, player_start, seed)
	}

	/// Returns the start position of the player
	#[must_use]
	pub const fn player_start(&self) -> Pos {
		self.player_start
	}

	/// Returns the seed used to generate the map
	#[must_use]
	pub const fn seed(&self) -> u64 {
		self.seed
	}

	/// Returns a `Tile` on the dungeon grid at `Pos`.
	#[must_use]
	pub const fn get(&self, pos: Pos) -> Tile {
		let idx = pos.idx();
		self.tiles[idx]
	}

	/// Returns a multable reference to a `Tile` on the dungeon grid at `Pos`.
	#[must_use]
	pub fn get_mut(&mut self, pos: Pos) -> &mut Tile {
		*self.dirty.get_mut() = true;
		let idx = pos.idx();
		&mut self.tiles[idx]
	}

	/// Returns a reference to all tiles in the `Floor`.
	/// The size of this lise will always be `TILE_COUNT` long.
	#[must_use]
	pub const fn tiles(&self) -> &[Tile] {
		&*self.tiles
	}

	/// Returns a mutable reference to all tiles in the `Floor`.
	/// The size of this lise will always be `TILE_COUNT` long.
	#[must_use]
	pub fn tiles_mut(&mut self) -> &mut [Tile] {
		*self.dirty.get_mut() = true;
		&mut *self.tiles
	}

	/// Returns the neighbors of a tile inside the floor, checking
	/// that the neighbor positions are the same tile type as in `pos`.
	pub fn neighbors(&self, pos: &Pos) -> impl Iterator<Item = Pos> {
		pos.neighbors().filter(|p| self.get(*p).is_walkable())
	}

	/// Computes the hash of the tile map
	#[must_use]
	pub fn hash(&self) -> u64 {
		// initial (immutable) dirty check
		let dirty = self.dirty.borrow();
		if !*dirty {
			return *self.hash.borrow();
		}
		drop(dirty);

		// recompute hash
		let mut dirty = self.dirty.borrow_mut();
		let mut hash = self.hash.borrow_mut();
		let mut s = DefaultHasher::new();
		self.tiles.hash(&mut s);
		*hash = s.finish();

		*dirty = false;
		*hash
	}

	/// Display the floor as a string for debugging
	///
	/// # Examples
	/// ```no_run
	/// use dungeon::Floor;
	/// let floor = Floor::generate();
	/// println!("{}", floor.display());
	/// ```
	#[must_use]
	pub fn display(&self) -> String {
		let mut output = String::new();
		for pos in Pos::values() {
			// If it's the player start, show 'P'
			if self.player_start == pos {
				output.push('P');
				continue;
			}
			// Otherwise, show the tile character
			let tile = self.get(pos);
			let char = match tile {
				Tile::Wall => '#',
				Tile::Room => '.',
				Tile::Hallway => ',',
				Tile::Stairs => '>',
			};
			output.push(char);
			// Newline at the end of each row
			if pos.xy().0 == MAP_SIZE - 1 {
				output.push('\n');
			}
		}
		output
	}

	/// Returns a random open (no wall) position
	pub fn random_pos(&mut self) -> Pos {
		loop {
			let pos = self.rand().random();
			if !self.get(pos).is_walkable() {
				continue;
			}
			break pos;
		}
	}

	/// Returns the random number gen for the `Floor`
	pub fn rand(&mut self) -> &mut impl Rng {
		&mut self.rng
	}
}
impl Default for Floor {
	/// Returns a floor with a single set of walls on the map border
	fn default() -> Self {
		let player_start = const_pos!(1, 1);
		let mut tiles = Box::new([Tile::Room; TILE_COUNT]);
		let seed = 0u64;

		for pos in Pos::values() {
			if pos.is_border() {
				tiles[pos.idx()] = Tile::Wall;
			}
		}

		Self::new(tiles, player_start, seed)
	}
}
impl Display for Floor {
	/// Display the floor as a string for debugging
	///
	/// # Examples
	/// ```no_run
	/// use dungeon::Floor;
	/// let floor = Floor::generate();
	/// println!("{floor}");
	/// ```
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		for pos in Pos::values() {
			// If it's the player start, show 'P'
			if self.player_start == pos {
				f.write_char('P')?;
				continue;
			}
			// Otherwise, show the tile character
			let tile = self.get(pos);
			write!(f, "{tile}")?;
			// Newline at the end of each row
			if pos.xy().0 == MAP_SIZE - 1 {
				writeln!(f)?;
			}
		}

		Ok(())
	}
}

/// Tests
#[cfg(test)]
mod tests {
	use super::*;

	// Test floor printing
	#[test]
	fn test_floor_display() {
		let floor = Floor::generate();
		// Print the display for visual inspection
		println!("{floor}");
	}
}