diff --git a/demo/src/lib.rs b/demo/src/lib.rs index 1746e6f..be8e1bd 100644 --- a/demo/src/lib.rs +++ b/demo/src/lib.rs @@ -4,6 +4,7 @@ use mapgen::dungeon::{ MapBuilder, map::TileType, cellular_automata::CellularAutomataGen, + random_rooms::RandomRoomsGen, starting_point::{AreaStartingPosition, XStart, YStart}, cull_unreachable::CullUnreachable, distant_exit::DistantExit, @@ -29,11 +30,24 @@ pub struct World { impl World { pub fn new_cellular_automata(width: u32, height: u32, seed: u32) -> World { let mut rng = StdRng::seed_from_u64(seed as u64); - let map = MapBuilder::new(Box::new(CellularAutomataGen::new(width as usize, height as usize))) + let map = MapBuilder::new(Box::new(CellularAutomataGen::new())) .with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)) .with(CullUnreachable::new()) .with(DistantExit::new()) - .build_map_with_rng(&mut rng); + .build_map_with_rng(width as usize, height as usize, &mut rng); + let tiles = (0..map.tiles.len()) + .map(|i| if map.tiles[i] == TileType::Floor {Cell::Floor} else {Cell::Wall}) + .collect(); + World { + width, + height, + tiles } + } + + pub fn new_random_rooms(width: u32, height: u32, seed: u32) -> World { + let mut rng = StdRng::seed_from_u64(seed as u64); + let map = MapBuilder::new(Box::new(RandomRoomsGen::new())) + .build_map_with_rng(width as usize, height as usize, &mut rng); let tiles = (0..map.tiles.len()) .map(|i| if map.tiles[i] == TileType::Floor {Cell::Floor} else {Cell::Wall}) .collect(); diff --git a/demo/www/index.js b/demo/www/index.js index 3771b8a..4c95b2d 100644 --- a/demo/www/index.js +++ b/demo/www/index.js @@ -28,7 +28,10 @@ function newCellularAutomata() { } function newSimpleRooms() { - console.log("Simple rooms") + var seed = Date.now(); + world = World.new_random_rooms(width, height, seed); + requestAnimationFrame(renderLoop); + infoDiv.textContent = "Random Rooms with the seed: " + seed; } const renderLoop = () => { diff --git a/src/common/geometry.rs b/src/common/geometry.rs new file mode 100644 index 0000000..9895121 --- /dev/null +++ b/src/common/geometry.rs @@ -0,0 +1,70 @@ +//! Support function for 2D geometry +//! + +/// Position on the map +#[derive(Default, PartialEq, Copy, Clone, Debug, Eq, Hash)] +pub struct Point { + pub x: usize, + pub y: usize +} + +impl Point { + /// Create new point + pub fn new(x: usize, y: usize) -> Point { + Point {x, y} + } + + /// Euclidean distance to a given point + pub fn distance_to(self, point: &Point) -> f32 { + let a = (self.x as f32 - point.x as f32).powf(2.0); + let b = (self.y as f32 - point.y as f32).powf(2.0); + (a + b).sqrt() + } +} + +/// Rectangle region on the map +#[derive(PartialEq, Copy, Clone)] +pub struct Rect { + pub x1 : i32, + pub x2 : i32, + pub y1 : i32, + pub y2 : i32 +} + +impl Rect { + pub fn new(x:i32, y: i32, width:i32, height:i32) -> Rect { + Rect{x1:x, y1:y, x2:x+width, y2:y+height} + } + + /// Returns true if this overlaps with other + pub fn intersect(&self, other:&Rect) -> bool { + self.x1 <= other.x2 && self.x2 >= other.x1 && self.y1 <= other.y2 && self.y2 >= other.y1 + } + + pub fn center(&self) -> (i32, i32) { + ((self.x1 + self.x2)/2, (self.y1 + self.y2)/2) + } +} + +/// ------------------------------------------------------------------------------------------------ +/// Module unit tests +/// ------------------------------------------------------------------------------------------------ +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_distance() { + let p1 = Point::new(10, 10); + let p2 = Point::new(14, 7); + let distance = p1.distance_to(&p2); + assert_eq!(distance, 5.0); + } + + #[test] + fn test_intersect() { + let rect1 = Rect::new(10, 10, 40, 40); + let rect2 = Rect::new(30, 30, 60, 60); + assert!(rect1.intersect(&rect2)); + } +} \ No newline at end of file diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..a16983b --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,2 @@ +pub mod geometry; +pub mod random; \ No newline at end of file diff --git a/src/common/random.rs b/src/common/random.rs new file mode 100644 index 0000000..dd3c2fe --- /dev/null +++ b/src/common/random.rs @@ -0,0 +1,29 @@ +//! Helper function for random number generator +//! +use rand::prelude::*; + + +/// Generate random number between start and end inclusive on both ends +pub fn random_range(rng: &mut StdRng, start: usize, end: usize) -> usize { + let max = (end - start + 1) as u32; + ((rng.next_u32() % max) + start as u32) as usize +} + +/// ------------------------------------------------------------------------------------------------ +/// Module unit tests +/// ------------------------------------------------------------------------------------------------ +#[cfg(test)] +mod tests { + use std::time::{SystemTime, UNIX_EPOCH}; + use rand::prelude::*; + use super::*; + + #[test] + fn test_range() { + let system_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Can't access system time"); + let mut rng = StdRng::seed_from_u64(system_time.as_millis() as u64); + let x = random_range(&mut rng, 5, 8); + assert!(x >= 5 && x <= 8); + } + +} \ No newline at end of file diff --git a/src/dungeon/cellular_automata.rs b/src/dungeon/cellular_automata.rs index 107b98e..38a972f 100644 --- a/src/dungeon/cellular_automata.rs +++ b/src/dungeon/cellular_automata.rs @@ -15,8 +15,8 @@ //! }; //! //! let mut rng = StdRng::seed_from_u64(100); -//! let gen = CellularAutomataGen::new(80, 50); -//! let map = gen.generate_map(&mut rng); +//! let gen = CellularAutomataGen::new(); +//! let map = gen.generate_map(80, 50, &mut rng); //! //! assert_eq!(map.width, 80); //! assert_eq!(map.height, 50); @@ -29,29 +29,26 @@ use super::map::{Map, TileType}; /// Map generator and modifier -pub struct CellularAutomataGen { - width: usize, - height: usize -} +pub struct CellularAutomataGen {} impl MapGenerator for CellularAutomataGen { - fn generate_map(&self, rng : &mut StdRng) -> Map { - self.build(rng) + fn generate_map(&self, width: usize, height: usize, rng : &mut StdRng) -> Map { + self.build(width, height, rng) } } impl CellularAutomataGen { /// Create generator which will create map with the given dimension. - pub fn new(width: usize, height: usize) -> CellularAutomataGen { - CellularAutomataGen {width, height} + pub fn new() -> CellularAutomataGen { + CellularAutomataGen {} } /// Generate map - fn build(&self, rng : &mut StdRng) -> Map { - let mut map = Map::new(self.width, self.height); + fn build(&self, width: usize, height: usize, rng: &mut StdRng) -> Map { + let mut map = Map::new(width, height); // First we completely randomize the map, setting 55% of it to be floor. - for y in 1..self.height-1 { - for x in 1..self.width-1 { + for y in 1..height-1 { + for x in 1..width-1 { let roll = rng.next_u32() % 100; if roll > 55 { map.set_tile(x, y, TileType::Floor) } else { map.set_tile(x, y, TileType::Wall) } diff --git a/src/dungeon/cull_unreachable.rs b/src/dungeon/cull_unreachable.rs index 6f68a51..db34987 100644 --- a/src/dungeon/cull_unreachable.rs +++ b/src/dungeon/cull_unreachable.rs @@ -50,7 +50,8 @@ mod tests { use rand::prelude::*; use super::*; use super::MapModifier; - use crate::dungeon::map::{Point, Map}; + use crate::common::geometry::Point; + use crate::dungeon::map::Map; #[test] fn test_culling() { diff --git a/src/dungeon/dijkstra.rs b/src/dungeon/dijkstra.rs index f645977..5cb9eaa 100644 --- a/src/dungeon/dijkstra.rs +++ b/src/dungeon/dijkstra.rs @@ -8,9 +8,10 @@ //! Example generator usage: //! --- //! use rand::prelude::*; +//! use crate::common::geometry::Point; //! use mapgen::dungeon::{ //! MapModifier, -//! map::{Map, Point, TileType}, +//! map::{Map, TileType}, //! starting_point::{AreaStartingPosition, XStart, YStart} //! }; //! @@ -97,7 +98,8 @@ impl DijkstraMap { #[cfg(test)] mod tests { use super::*; - use crate::dungeon::map::{Point, Map}; + use crate::common::geometry::Point; + use crate::dungeon::map::Map; #[test] fn test_culling() { diff --git a/src/dungeon/distant_exit.rs b/src/dungeon/distant_exit.rs index a22f272..2bffe1d 100644 --- a/src/dungeon/distant_exit.rs +++ b/src/dungeon/distant_exit.rs @@ -6,8 +6,9 @@ use std::f32; use rand::prelude::StdRng; -use super::{MapModifier}; -use super::map::{Map, Point}; +use crate::common::geometry::Point; +use super::MapModifier; +use super::map::Map; use super::dijkstra::DijkstraMap; @@ -53,7 +54,8 @@ mod tests { use rand::prelude::*; use super::*; use super::MapModifier; - use crate::dungeon::map::{Point, Map}; + use crate::common::geometry::Point; + use crate::dungeon::map::Map; #[test] fn test_exit() { diff --git a/src/dungeon/map.rs b/src/dungeon/map.rs index ea3f2e3..e0830fc 100644 --- a/src/dungeon/map.rs +++ b/src/dungeon/map.rs @@ -8,30 +8,9 @@ //! use std::fmt; +use crate::common::geometry::{Point, Rect}; -/// Position on the map -#[derive(Default, PartialEq, Copy, Clone, Debug, Eq, Hash)] -pub struct Point { - pub x: usize, - pub y: usize -} - -impl Point { - /// Create new point - pub fn new(x: usize, y: usize) -> Point { - Point {x, y} - } - - /// Euclidean distance to a given point - pub fn distance_to(self, point: &Point) -> f32 { - let a = (self.x as f32 - point.x as f32).powf(2.0); - let b = (self.y as f32 - point.y as f32).powf(2.0); - (a + b).sqrt() - } -} - -/// Possible tile type on the map #[derive(PartialEq, Copy, Clone, Debug, Eq, Hash)] pub enum TileType { Wall, Floor @@ -123,6 +102,16 @@ impl Map { self.tiles[idx] = tile; } } + + /// Create room on the map at given location + /// Room is created by setting all tiles in the room to the Floor + pub fn create_room(&mut self, rect: &Rect) { + for x in rect.x1..rect.x2 { + for y in rect.y1..rect.y2 { + self.set_tile(x as usize, y as usize, TileType::Floor); + } + } + } } impl fmt::Display for Map { @@ -145,14 +134,6 @@ impl fmt::Display for Map { mod tests { use super::*; - #[test] - fn test_distance() { - let p1 = Point::new(10, 10); - let p2 = Point::new(14, 7); - let distance = p1.distance_to(&p2); - assert_eq!(distance, 5.0); - } - #[test] fn test_new_map() { let map = Map::new(10, 10); @@ -198,4 +179,20 @@ mod tests { let expected_exists = vec![(2, 1, 1.0), (1, 2, 1.0), (2, 2, 1.45)]; assert_eq!(exists, expected_exists); } + + #[test] + fn test_create_room() { + let mut map = Map::new(5, 5); + map.create_room(&Rect::new(1, 1, 3, 3)); + for x in 0..map.width { + for y in 0..map.height { + if x == 0 || y == 0 || x == 4 || y == 4 { + assert_eq!(map.at(x, y), TileType::Wall); + } else { + assert_eq!(map.at(x, y), TileType::Floor); + } + } + } + } + } \ No newline at end of file diff --git a/src/dungeon/mod.rs b/src/dungeon/mod.rs index 484d72b..b834d88 100644 --- a/src/dungeon/mod.rs +++ b/src/dungeon/mod.rs @@ -8,16 +8,17 @@ //! //! Example //! ``` +//! use mapgen::common::geometry::Point; //! use mapgen::dungeon::{ //! MapBuilder, -//! map::{Map, Point, TileType}, +//! map::{Map, TileType}, //! cellular_automata::CellularAutomataGen, //! starting_point::{AreaStartingPosition, XStart, YStart}, //! }; //! -//! let map = MapBuilder::new(Box::new(CellularAutomataGen::new(80, 50))) +//! let map = MapBuilder::new(Box::new(CellularAutomataGen::new())) //! .with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)) -//! .build_map(); +//! .build_map(80, 50); //! //! assert_eq!(map.width, 80); //! assert_eq!(map.height, 50); @@ -29,6 +30,7 @@ pub mod map; pub mod cellular_automata; pub mod cull_unreachable; pub mod distant_exit; +pub mod random_rooms; pub mod starting_point; mod dijkstra; @@ -40,7 +42,7 @@ use map::Map; /// Trait which should be implemented by any map generator which want to be used /// by MapBuilder pub trait MapGenerator { - fn generate_map(&self, rng: &mut StdRng) -> Map; + fn generate_map(&self, width: usize, height: usize, rng: &mut StdRng) -> Map; } /// Trait which should be implemented by map modifier. @@ -70,15 +72,15 @@ impl MapBuilder { } /// Build map using random number seeded with system time - pub fn build_map(&mut self) -> Map { + pub fn build_map(&mut self, width: usize, height: usize) -> Map { let system_time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Can't access system time"); let mut rng = StdRng::seed_from_u64(system_time.as_millis() as u64); - self.build_map_with_rng(&mut rng) + self.build_map_with_rng(width, height, &mut rng) } /// Build map using provided random number generator - pub fn build_map_with_rng(&mut self, rng: &mut StdRng) -> Map { - let mut map = self.generator.generate_map(rng); + pub fn build_map_with_rng(&mut self, width: usize, height: usize, rng: &mut StdRng) -> Map { + let mut map = self.generator.generate_map(width, height, rng); // Build additional layers in turn for modifier in self.modifiers.iter() { @@ -101,9 +103,9 @@ mod tests { #[test] fn test_ca_map() { - let map = MapBuilder::new(Box::new(CellularAutomataGen::new(80, 50))) + let map = MapBuilder::new(Box::new(CellularAutomataGen::new())) .with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)) - .build_map(); + .build_map(80, 50); assert_eq!(map.width, 80); assert_eq!(map.height, 50); diff --git a/src/dungeon/random_rooms.rs b/src/dungeon/random_rooms.rs new file mode 100644 index 0000000..e60118a --- /dev/null +++ b/src/dungeon/random_rooms.rs @@ -0,0 +1,75 @@ +//! Random rooms map generator. +//! +//! Try to generate rooms of different size to fille the whole map area. +//! +//! Example generator usage: +//! ``` +//! use rand::prelude::*; +//! use mapgen::dungeon::{ +//! MapGenerator, +//! random_rooms::RandomRoomsGen +//! }; +//! +//! let mut rng = StdRng::seed_from_u64(100); +//! let gen = RandomRoomsGen::new(); +//! let map = gen.generate_map(80, 50, &mut rng); +//! +//! assert_eq!(map.width, 80); +//! assert_eq!(map.height, 50); +//! ``` +//! + +use rand::prelude::*; +use super::MapGenerator; +use crate::common::geometry::Rect; +use crate::common::random; +use super::map::{Map}; + + +pub struct RandomRoomsGen { + max_rooms: usize, + min_room_size: usize, + max_room_size: usize, +} + +impl MapGenerator for RandomRoomsGen { + fn generate_map(&self, width: usize, height: usize, rng : &mut StdRng) -> Map { + self.build_rooms(width, height, rng) + } +} + + +impl RandomRoomsGen { + pub fn new() -> RandomRoomsGen { + RandomRoomsGen{ + max_rooms: 30, + min_room_size: 6, + max_room_size: 10 + } + } + + fn build_rooms(&self, width: usize, height: usize, rng : &mut StdRng) -> Map { + let mut map = Map::new(width, height); + let mut rooms : Vec = Vec::new(); + + // Create room dimensions + for _ in 0..self.max_rooms { + let w = random::random_range(rng, self.min_room_size, self.max_room_size); + let h = random::random_range(rng, self.min_room_size, self.max_room_size); + let x = random::random_range(rng, 0, width - w); + let y = random::random_range(rng, 0, height - h); + let new_room = Rect::new(x as i32, y as i32, w as i32, h as i32); + let intersects = rooms.iter().any(|r| new_room.intersect(r)); + if !intersects { + rooms.push(new_room); + } + } + + // Apply rooms to the map + for room in rooms { + map.create_room(&room); + } + + map + } +} \ No newline at end of file diff --git a/src/dungeon/starting_point.rs b/src/dungeon/starting_point.rs index 1007887..541cc59 100644 --- a/src/dungeon/starting_point.rs +++ b/src/dungeon/starting_point.rs @@ -6,9 +6,10 @@ //! Example modifier usage: //! ``` //! use rand::prelude::*; +//! use mapgen::common::geometry::Point; //! use mapgen::dungeon::{ //! MapModifier, -//! map::{Map, Point, TileType}, +//! map::{Map, TileType}, //! starting_point::{AreaStartingPosition, XStart, YStart} //! }; //! @@ -24,7 +25,8 @@ use rand::prelude::StdRng; use super::{MapModifier}; -use super::map::{Map, Point, TileType}; +use crate::common::geometry::Point; +use super::map::{Map, TileType}; /// Initial x region position diff --git a/src/lib.rs b/src/lib.rs index aba5d47..f2ed76a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,5 @@ //! * Dungeon maps //! +pub mod common; pub mod dungeon; \ No newline at end of file