diff --git a/Cargo.lock b/Cargo.lock index 317f825..3fc26bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,9 +86,34 @@ name = "el_diabolo" version = "0.1.0" dependencies = [ "crossterm", + "petgraph", + "rand", "ratatui", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.14.2" @@ -105,6 +130,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indoc" version = "2.0.4" @@ -198,6 +233,22 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.69" @@ -216,6 +267,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ratatui" version = "0.24.0" diff --git a/Cargo.toml b/Cargo.toml index f6b1637..efafe13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,5 @@ edition = "2021" [dependencies] ratatui = "0.24.0" crossterm = "0.27.0" +rand = "0.8.5" +petgraph = "0.6.4" \ No newline at end of file diff --git a/src/level.rs b/src/level.rs index 91839f1..d4af42f 100644 --- a/src/level.rs +++ b/src/level.rs @@ -8,6 +8,8 @@ pub const LEVEL_HEIGHT: usize = 25; #[derive(Copy, Clone, Debug, PartialEq)] pub enum StructureElement { + Start, + End, Wall, Floor, StairDown, @@ -21,6 +23,10 @@ pub struct Level { discovered: [[bool; LEVEL_HEIGHT]; LEVEL_WIDTH], monsters: Vec, artifacts: Vec>, + /// the position of the start in the level (eiter stair up or start point) + start: (usize, usize), + /// the position of the end in the level (eiter stair down or end point) + end: (usize, usize), } impl Level { @@ -37,6 +43,8 @@ impl Level { discovered: [[false; LEVEL_HEIGHT]; LEVEL_WIDTH], monsters: Vec::with_capacity(10), artifacts: Vec::with_capacity(10), + start: (0,0), + end: (0,0), } } pub fn get_element(&mut self, x: i16, y: i16) -> (Option, Option<&mut Monster>, Option<&mut Box<(dyn Artifact + 'static)>>) { diff --git a/src/level_generator.rs b/src/level_generator.rs new file mode 100644 index 0000000..8ed67cb --- /dev/null +++ b/src/level_generator.rs @@ -0,0 +1,339 @@ +use std::cmp::{max, min}; +use std::ops::Range; + +use petgraph::algo::min_spanning_tree; +use petgraph::data::*; +use petgraph::graph::Graph; +use petgraph::graph::UnGraph; +use rand::prelude::SliceRandom; +use rand::Rng; +use rand::rngs::ThreadRng; + +use crate::level::{Level, StructureElement}; +use crate::monster::Monster; +use crate::artifacts::Artifact; + +const ROOMS_VERTICAL: usize = 7; +const ROOMS_HORIZONTAL: usize = 4; + +const ROOM_WIDTH: usize = 7; +const ROOM_HEIGHT: usize = 6; + +#[derive(PartialEq, Copy, Clone)] +enum RoomType { + Start, + End, + StairUp, + StairDown, + BasicRoom, + TreasureRoom, + MonsterRoom, + EmptyRoom, +} + +#[derive(Copy, Clone)] +struct ConnectionInfo { + offset: usize, + distance: usize, +} + +#[derive(Copy, Clone)] +struct Room { + pub kind: RoomType, + pub offset_x: usize, + pub offset_y: usize, + pub width: usize, + pub height: usize, + pub connection_down: Option, + pub connection_right: Option, +} + +impl Room { + fn new() -> Self { + Self { + kind: RoomType::EmptyRoom, + offset_x: 0, + offset_y: 0, + width: 0, + height: 0, + connection_down: None, + connection_right: None, + } + } + /// change the size and position of a room randomly within its bounds + fn random(&mut self, rng: &mut ThreadRng) { + let width = rng.gen_range(3..6); + let height = rng.gen_range(3..5); + self.width = width; + self.height = height; + self.offset_x = rng.gen_range(0..(ROOM_WIDTH - width)); + self.offset_y = rng.gen_range(0..(ROOM_HEIGHT - height)); + } + fn get_x_range(&self) -> Range { + self.offset_x..self.offset_x + self.width + } + fn get_y_range(&self) -> Range { + self.offset_y..self.offset_y + self.height + } +} + +pub struct LevelGenerator { + rooms: [[Room; ROOMS_HORIZONTAL]; ROOMS_VERTICAL], +} + +impl LevelGenerator { + pub fn generate(level: usize) -> Self { + let mut rng = rand::thread_rng(); + let mut rooms = [[Room::new(); ROOMS_HORIZONTAL]; ROOMS_VERTICAL]; + let mut graph = UnGraph::<(usize, usize), u16>::default(); + + // trick the room_connectable function into failing on the first iteration + rooms[0][0].kind = RoomType::BasicRoom; + + while !LevelGenerator::rooms_connectable(&rooms) { + let mut room_types: Vec = Vec::with_capacity(ROOMS_HORIZONTAL * ROOMS_VERTICAL); + // level 0 contains a start room, all others contain a stair up + if level == 0 { + room_types.push(RoomType::Start); + } else { + room_types.push(RoomType::StairUp); + } + // level 24 (the last) contains an end room, all others a stair down + if level == 24 { + room_types.push(RoomType::End); + } else { + room_types.push(RoomType::StairDown); + } + room_types.push(RoomType::MonsterRoom); + // generate a random set of rooms and shuffle them + for _ in room_types.len()..ROOMS_HORIZONTAL * ROOMS_VERTICAL { + match rng.gen_range(1..=100) { + // TODO tune room type distribution + 1..=33 => { room_types.push(RoomType::EmptyRoom) } + 34..=66 => { room_types.push(RoomType::TreasureRoom) } + 67..=90 => { room_types.push(RoomType::MonsterRoom) } + _ => { room_types.push(RoomType::BasicRoom) } + } + } + room_types.shuffle(&mut rng); + + graph.clear(); + // place the rooms in the array an add nodes to the graph for every non empty room + for c in 0..ROOMS_VERTICAL { + for r in 0..ROOMS_HORIZONTAL { + rooms[c][r].kind = room_types.pop().unwrap(); + if rooms[c][r].kind != RoomType::EmptyRoom { + rooms[c][r].random(&mut rng); + graph.add_node((c, r)); + } + } + } + } + + // add edges to the graph connecting each room to all of its neighbours (max 4 of them) + for c in 0..ROOMS_VERTICAL { + for r in 0..ROOMS_HORIZONTAL { + if rooms[c][r].kind == RoomType::EmptyRoom { + continue; + } + let src_index = graph.node_indices().find(|i| graph[*i] == (c, r)).unwrap(); + for r_1 in r + 1..ROOMS_HORIZONTAL { + if rooms[c][r_1].kind != RoomType::EmptyRoom { + let tgt_index = graph.node_indices().find(|i| graph[*i] == (c, r_1)).unwrap(); + // todo use random weight for edge + graph.add_edge(src_index, tgt_index, 1); + break; + } + } + for c_1 in c + 1..ROOMS_VERTICAL { + if rooms[c_1][r].kind != RoomType::EmptyRoom { + let tgt_index = graph.node_indices().find(|i| graph[*i] == (c_1, r)).unwrap(); + // todo use random weight for edge + graph.add_edge(src_index, tgt_index, 1); + break; + } + } + } + } + + // calculate a minimum spanning tree + let mst: Graph<(usize, usize), u16, petgraph::Undirected> = Graph::from_elements(min_spanning_tree(&graph)); + for edge in mst.raw_edges() { + let src = mst[edge.source()]; + let tgt = mst[edge.target()]; + + let src_room = rooms[src.0][src.1]; + let mut tgt_room = rooms[tgt.0][tgt.1]; + + // cols are the same, either up or down + if src.0 == tgt.0 { + let range = LevelGenerator::range_overlap(src_room.get_x_range(), tgt_room.get_x_range()); + let position: usize; + if range.is_empty() { + position = range.start; + } else { + position = rng.gen_range(range); + } + if src.1 < tgt.1 { + // src to tgt + rooms[src.0][src.1].connection_down = Some(ConnectionInfo { offset: position, distance: tgt.1 - src.1 }); + } else { + // tgt to src + tgt_room.connection_down = Some(ConnectionInfo { offset: position, distance: src.1 - tgt.1 }); + } + } + // rows are the same, either left or right + if src.1 == tgt.1 { + let range = LevelGenerator::range_overlap(src_room.get_y_range(), tgt_room.get_y_range()); + let mut position: usize; + if range.is_empty() { + position = range.start; + } else { + position = rng.gen_range(range); + } + if src.1 == 0 && position == 0 { + position = 1; + } + if src.0 < tgt.0 { + // src to tgt + rooms[src.0][src.1].connection_right = Some(ConnectionInfo { offset: position, distance: tgt.0 - src.0 }); + } else { + // tgt to src + tgt_room.connection_right = Some(ConnectionInfo { offset: position, distance: src.1 - tgt.1 }); + } + } + } + + LevelGenerator { + rooms + } + } + + fn range_overlap(r1: Range, r2: Range) -> Range { + max(r1.start, r2.start)..min(r1.end, r2.end) + } + + /// Verifies that for a given matrix of rooms each room has at least one other room in the + /// same row or column. + fn rooms_connectable(rooms: &[[Room; ROOMS_HORIZONTAL]; ROOMS_VERTICAL]) -> bool { + for c in 0..ROOMS_VERTICAL { + for r in 0..ROOMS_HORIZONTAL { + if rooms[c][r].kind != RoomType::EmptyRoom { + let mut connected = 0; + for c1 in 0..ROOMS_VERTICAL { + if rooms[c1][r].kind != RoomType::EmptyRoom { + connected += 1; + } + } + for r1 in 0..ROOMS_HORIZONTAL { + if rooms[c][r1].kind != RoomType::EmptyRoom { + connected += 1; + } + } + if connected <= 2 { + return false; + } + } + } + } + return true; + } + pub fn render(&self) -> Level { + let mut rng = rand::thread_rng(); + let mut structure = [[StructureElement::Wall; 1 + ROOMS_HORIZONTAL * ROOM_HEIGHT]; 1 + ROOMS_VERTICAL * ROOM_WIDTH]; + let mut artifacts: Vec> = Vec::with_capacity(10); + let mut enemies: Vec = Vec::with_capacity(10); + let mut start_x: usize = 0; + let mut start_y: usize = 0; + let mut end_x: usize = 0; + let mut end_y: usize = 0; + for c in 0..ROOMS_VERTICAL { + for r in 0..ROOMS_HORIZONTAL { + let top = 1 + r * ROOM_HEIGHT; + let left = 1 + c * ROOM_WIDTH; + let room = self.rooms[c][r]; + for x in 0..room.width { + for y in 0..room.height { + structure[left + room.offset_x + x][top + room.offset_y + y] = StructureElement::Floor; + } + } + if room.kind == RoomType::TreasureRoom { + let t_x = left + room.offset_x + rng.gen_range(0..room.width); + let t_y = top + room.offset_y + rng.gen_range(0..room.height); + // artifacts[t_x][t_y] = Some(Artifact::Chest { gold: rng.gen_range(2..30) }); + } + if room.kind == RoomType::MonsterRoom { + let t_x = left + room.offset_x + rng.gen_range(0..room.width); + let t_y = top + room.offset_y + rng.gen_range(0..room.height); + // TODO randomize enemies here + // enemies[t_x][t_y] = Some(Monster::new(2)); + } + if room.kind == RoomType::StairDown { + end_x = left + room.offset_x + rng.gen_range(0..room.width); + end_y = top + room.offset_y + rng.gen_range(0..room.height); + } + if room.kind == RoomType::StairDown { + structure[end_x][end_y] = StructureElement::StairDown; + } + if room.kind == RoomType::End { + structure[end_x][end_y] = StructureElement::End; + } + if room.kind == RoomType::Start || room.kind == RoomType::StairUp { + start_x = left + room.offset_x + rng.gen_range(0..room.width); + start_y = top + room.offset_y + rng.gen_range(0..room.height); + } + if room.kind == RoomType::StairUp { + structure[start_x][start_y] = StructureElement::StairUp; + } + if room.kind == RoomType::Start { + structure[start_x][start_y] = StructureElement::Start; + } + } + } + // + for c in 0..ROOMS_VERTICAL { + for r in 0..ROOMS_HORIZONTAL { + let src_room = self.rooms[c][r]; + if let Some(x_conn) = src_room.connection_down { + let tgt_room = self.rooms[c][r + x_conn.distance]; + let top = 1 + r * ROOM_HEIGHT + src_room.offset_y; + let left = 1 + c * ROOM_WIDTH + x_conn.offset; + let bottom = 1 + (r + x_conn.distance) * ROOM_HEIGHT + tgt_room.offset_y + tgt_room.height; + for i in top..bottom { + if structure[left][i] == StructureElement::Wall { + structure[left][i] = StructureElement::Floor; + } + } + } + if let Some(y_conn) = src_room.connection_right { + let tgt_room = self.rooms[c + y_conn.distance][r]; + + let top = 1 + r * ROOM_HEIGHT + src_room.offset_y + y_conn.offset - 1; + let left = 1 + c * ROOM_WIDTH + src_room.offset_x; + let right = 1 + (c + y_conn.distance) * ROOM_WIDTH + tgt_room.offset_x + tgt_room.width; + for i in left..right { + if structure[i][top] == StructureElement::Wall { + structure[i][top] = StructureElement::Floor; + } + } + } + } + } + Level { + level: 0, + structure, + discovered: [[false; 1 + ROOMS_HORIZONTAL * ROOM_HEIGHT]; 1 + ROOMS_VERTICAL * ROOM_WIDTH], + monsters: enemies, + artifacts, + start: (start_x, start_y), + end: (end_x, end_y), + } + } +} + +// #[test] +// fn test_level_gen() { +// let level = LevelGenerator::generate(0); +// println!("{:#?}", level); +// assert_eq!(1, 2); +// } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index c529ea5..203fb06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod level; mod position; mod monster; mod artifacts; +mod level_generator; fn main() { let mut p = Player::new("Teddy Tester", 10);