From 91160edb3fafe82ec512da41d2b7b31c02eeda62 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sun, 19 Nov 2023 09:59:19 +0100 Subject: [PATCH] minimal spanning tree implemented no disconnected rooms --- Cargo.lock | 33 +++++++++ Cargo.toml | 3 +- src/level_generator.rs | 164 +++++++++++++++++++++++++++-------------- 3 files changed, 144 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 940067d..3fc26bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,10 +86,23 @@ 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" @@ -117,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" @@ -210,6 +233,16 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index c2ffe71..5caa701 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ edition = "2021" [dependencies] ratatui = "0.24.0" crossterm = "0.27.0" -rand = "0.8.5" \ No newline at end of file +rand = "0.8.5" +petgraph = "0.6.4" diff --git a/src/level_generator.rs b/src/level_generator.rs index 6aff68f..3f8069b 100644 --- a/src/level_generator.rs +++ b/src/level_generator.rs @@ -1,5 +1,10 @@ use rand::prelude::SliceRandom; use rand::Rng; +use petgraph::graph::UnGraph; +use petgraph::graph::Graph; +use petgraph::data::*; +use petgraph::algo::min_spanning_tree; +use rand::rngs::ThreadRng; use crate::level::{Level, LevelElement, RoomType}; @@ -15,9 +20,9 @@ struct Room { pub offset_y: usize, pub width: usize, pub height: usize, - pub connection_down: bool, - pub connection_right: bool, - pub connected: bool, + // pub connection_down: bool, + // pub connection_right: bool, + // pub connected: bool, } impl Room { @@ -28,11 +33,20 @@ impl Room { offset_y: 0, width: 0, height: 0, - connection_down: false, - connection_right: false, - connected: false, + // connection_down: false, + // connection_right: false, + // connected: false, } } + /// 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)); + } } #[derive(Debug)] @@ -42,74 +56,114 @@ pub struct LevelGenerator { impl LevelGenerator { pub fn generate(level: usize) -> Self { - let mut room_types: Vec = Vec::with_capacity(ROOMS_HORIZONTAL * ROOMS_VERTICAL); - if level > 0 { - room_types.push(RoomType::StairUp); - } else { - room_types.push(RoomType::Start); - } - if level < 24 { - room_types.push(RoomType::StairDown); - } else { - room_types.push(RoomType::End); - } let mut rng = rand::thread_rng(); - for _ in room_types.len()..ROOMS_HORIZONTAL * ROOMS_VERTICAL { - match rng.gen_range(1..=6) { - 1..=3 => { room_types.push(RoomType::EmptyRoom) } - _ => { room_types.push(RoomType::BasicRoom) } - } - } - room_types.shuffle(&mut rng); - let mut rooms = [[Room::new(); ROOMS_HORIZONTAL]; ROOMS_VERTICAL]; - for r in 0..ROOMS_VERTICAL { - for c in 0..ROOMS_HORIZONTAL { - rooms[r][c].kind = room_types.pop().unwrap(); - if rooms[r][c].kind != RoomType::EmptyRoom { - let width = rng.gen_range(3..6); - let height = rng.gen_range(3..5); - rooms[r][c].width = width; - rooms[r][c].height = height; - rooms[r][c].offset_x = rng.gen_range(0..(ROOM_WIDTH - width)); - rooms[r][c].offset_y = rng.gen_range(0..(ROOM_HEIGHT - height)); + 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); + } + // generate a random set of rooms and shuffle them + for _ in room_types.len()..ROOMS_HORIZONTAL * ROOMS_VERTICAL { + match rng.gen_range(1..=6) { + // TODO tune room type distribution + 1..=3 => { room_types.push(RoomType::EmptyRoom) } + _ => { room_types.push(RoomType::BasicRoom) } } - if r == ROOMS_HORIZONTAL - 1 || c == ROOMS_HORIZONTAL - 1 { - continue; - } - match rng.gen_range(0..3) { - 0 => { rooms[r][c].connection_down = true } - 1 => { rooms[r][c].connection_right = true } - 2 => { - rooms[r][c].connection_down = true; - rooms[r][c].connection_down = true + } + 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)); } - _ => {} } } } - let mut unconnected_rooms = 0;//ROOMS_HORIZONTAL * ROOMS_VERTICAL - 1; - rooms[0][0].connected = true; - for r in 0..ROOMS_VERTICAL { - for c in 0..ROOMS_HORIZONTAL { - if !rooms[r][c].connected { + // 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; } - if rooms[r][c].connection_right { - rooms[r][c + 1].connected = true; + 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; + } } - if rooms[r][c].connection_down { - rooms[r + 1][c].connected = true; + 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; + } } } } - println!("Unconnected: {}", unconnected_rooms); + + // 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()]; + println!("{:?} {:?}", src, tgt); + } + // println!("Level {}\n{}", level, Dot::new(&mst)); LevelGenerator { rooms } } + /// 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 s = [[LevelElement::Wall; 1 + ROOMS_HORIZONTAL * ROOM_HEIGHT]; 1 + ROOMS_VERTICAL * ROOM_WIDTH];