Make MapBuilder modular with MapArchitect trait

This commit is contained in:
Daniel Lynn 2021-07-10 17:12:26 -05:00
parent 9cd0af9679
commit b807418601
7 changed files with 429 additions and 18 deletions

View File

@ -0,0 +1,94 @@
use super::MapArchitect;
use crate::prelude::*;
pub struct CellularAutomataArchitect {}
impl MapArchitect for CellularAutomataArchitect {
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder {
let mut mb = MapBuilder {
map: Map::new(),
rooms: Vec::new(),
monster_spawns: Vec::new(),
player_start: Point::zero(),
amulet_start: Point::zero(),
theme: super::themes::DungeonTheme::new(),
};
self.random_noise_map(rng, &mut mb.map);
for _ in 0..10 {
self.iteration(&mut mb.map);
}
let start = self.find_start(&mb.map);
mb.monster_spawns = mb.spawn_monsters(&start, rng);
mb.player_start = start;
mb.amulet_start = mb.find_most_distant(&mb.player_start);
mb
}
}
impl CellularAutomataArchitect {
fn random_noise_map(&mut self, rng: &mut RandomNumberGenerator, map: &mut Map) {
map.tiles.iter_mut().for_each(|t| {
let roll = rng.range(0, 100);
if roll > 55 {
*t = TileType::Floor;
} else {
*t = TileType::Wall;
}
});
}
fn count_neighbors(&self, x: i32, y: i32, map: &Map) -> usize {
let mut neighbors = 0;
for iy in -1..=1 {
for ix in -1..1 {
if !(ix == 0 && iy == 0) && map.tiles[map_idx(x + ix, y + iy)] == TileType::Wall {
neighbors += 1;
}
}
}
neighbors
}
fn iteration(&mut self, map: &mut Map) {
let mut new_tiles = map.tiles.clone();
for y in 1..SCREEN_HEIGHT - 1 {
for x in 1..SCREEN_WIDTH - 1 {
let neighbors = self.count_neighbors(x, y, map);
let idx = map_idx(x, y);
if neighbors > 4 || neighbors == 0 {
new_tiles[idx] = TileType::Wall;
} else {
new_tiles[idx] = TileType::Floor;
}
}
}
map.tiles = new_tiles;
}
fn find_start(&self, map: &Map) -> Point {
let center = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
let closest_point = map
.tiles
.iter()
.enumerate()
.filter(|(_, t)| **t == TileType::Floor)
.map(|(idx, _)| {
(
idx,
DistanceAlg::Pythagoras.distance2d(center, map.index_to_point2d(idx)),
)
})
.min_by(|(_, distance), (_, distance2)| distance.partial_cmp(&distance2).unwrap())
.map(|(idx, _)| idx)
.unwrap();
map.index_to_point2d(closest_point)
}
}

View File

@ -0,0 +1,87 @@
use super::MapArchitect;
use crate::prelude::*;
const STAGGER_DISTANCE: usize = 400;
const NUM_TILES: usize = (SCREEN_WIDTH * SCREEN_HEIGHT) as usize;
const DESIRED_FLOOR: usize = NUM_TILES / 3;
pub struct DrunkardsWalkArchitect {}
impl MapArchitect for DrunkardsWalkArchitect {
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder {
let mut mb = MapBuilder {
map: Map::new(),
rooms: Vec::new(),
monster_spawns: Vec::new(),
player_start: Point::zero(),
amulet_start: Point::zero(),
theme: super::themes::DungeonTheme::new(),
};
mb.fill(TileType::Wall);
let center = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
self.drunkard(&center, rng, &mut mb.map);
while mb
.map
.tiles
.iter()
.filter(|t| **t == TileType::Floor)
.count()
< DESIRED_FLOOR
{
self.drunkard(
&Point::new(rng.range(0, SCREEN_WIDTH), rng.range(0, SCREEN_HEIGHT)),
rng,
&mut mb.map,
);
let dijkstra_map = DijkstraMap::new(
SCREEN_WIDTH,
SCREEN_HEIGHT,
&[mb.map.point2d_to_index(center)],
&mb.map,
1024.0,
);
dijkstra_map
.map
.iter()
.enumerate()
.filter(|(_, distance)| *distance > &2000.0)
.for_each(|(idx, _)| mb.map.tiles[idx] = TileType::Wall);
}
mb.monster_spawns = mb.spawn_monsters(&center, rng);
mb.player_start = center;
mb.amulet_start = mb.find_most_distant(&mb.player_start);
mb
}
}
impl DrunkardsWalkArchitect {
fn drunkard(&mut self, start: &Point, rng: &mut RandomNumberGenerator, map: &mut Map) {
let mut drunkard_pos = start.clone();
let mut distance_staggered = 0;
loop {
let drunk_idx = map.point2d_to_index(drunkard_pos);
map.tiles[drunk_idx] = TileType::Floor;
match rng.range(0, 4) {
0 => drunkard_pos.x -= 1,
1 => drunkard_pos.x += 1,
2 => drunkard_pos.y -= 1,
_ => drunkard_pos.y += 1,
}
if !map.in_bounds(drunkard_pos) {
break;
}
distance_staggered += 1;
if distance_staggered > STAGGER_DISTANCE {
break;
}
}
}
}

27
src/map_builder/empty.rs Normal file
View File

@ -0,0 +1,27 @@
use super::MapArchitect;
use crate::prelude::*;
pub struct EmptyArchitect {}
impl MapArchitect for EmptyArchitect {
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder {
let mut mb = MapBuilder {
map: Map::new(),
rooms: Vec::new(),
monster_spawns: Vec::new(),
player_start: Point::zero(),
amulet_start: Point::zero(),
theme: super::themes::DungeonTheme::new(),
};
mb.fill(TileType::Floor);
mb.player_start = Point::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);
mb.amulet_start = mb.find_most_distant(&mb.player_start);
for _ in 0..50 {
mb.monster_spawns.push(Point::new(
rng.range(1, SCREEN_WIDTH),
rng.range(1, SCREEN_HEIGHT),
));
}
mb
}
}

View File

@ -1,35 +1,72 @@
mod automata;
mod drunkard;
mod empty;
mod prefab;
mod rooms;
mod themes;
use crate::prelude::*;
use automata::CellularAutomataArchitect;
use drunkard::DrunkardsWalkArchitect;
use empty::EmptyArchitect;
use prefab::apply_prefab;
use rooms::RoomsArchitect;
use themes::*;
const NUM_ROOMS: usize = 20;
trait MapArchitect {
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder;
}
pub trait MapTheme: Sync + Send {
fn tile_to_render(&self, tile_type: TileType) -> FontCharType;
}
pub struct MapBuilder {
pub map: Map,
pub rooms: Vec<Rect>,
pub monster_spawns: Vec<Point>,
pub player_start: Point,
pub amulet_start: Point,
pub theme: Box<dyn MapTheme>,
}
impl MapBuilder {
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let mut mb = MapBuilder {
map: Map::new(),
rooms: Vec::new(),
player_start: Point::zero(),
amulet_start: Point::zero(),
let mut architect: Box<dyn MapArchitect> = match rng.range(0, 3) {
0 => Box::new(DrunkardsWalkArchitect {}),
1 => Box::new(RoomsArchitect {}),
2 => Box::new(CellularAutomataArchitect {}),
_ => Box::new(EmptyArchitect {}),
};
mb.fill(TileType::Wall);
mb.build_random_rooms(rng);
mb.build_corridors(rng);
mb.player_start = mb.rooms[0].center();
let mut mb = architect.new(rng);
apply_prefab(&mut mb, rng);
mb.theme = match rng.range(0, 2) {
0 => DungeonTheme::new(),
_ => ForestTheme::new(),
};
mb
}
fn fill(&mut self, tile: TileType) {
self.map.tiles.iter_mut().for_each(|t| *t = tile);
}
fn find_most_distant(&self, point: &Point) -> Point {
let dijkstra_map = DijkstraMap::new(
SCREEN_WIDTH,
SCREEN_HEIGHT,
&[mb.map.point2d_to_index(mb.player_start)],
&mb.map,
&[self.map.point2d_to_index(*point)],
&self.map,
1024.0,
);
const UNREACHABLE: &f32 = &f32::MAX;
mb.amulet_start = mb.map.index_to_point2d(
self.map.index_to_point2d(
dijkstra_map
.map
.iter()
@ -38,12 +75,7 @@ impl MapBuilder {
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap()
.0,
);
mb
}
fn fill(&mut self, tile: TileType) {
self.map.tiles.iter_mut().for_each(|t| *t = tile);
)
}
fn build_random_rooms(&mut self, rng: &mut RandomNumberGenerator) {
@ -110,4 +142,28 @@ impl MapBuilder {
}
}
}
fn spawn_monsters(&self, start: &Point, rng: &mut RandomNumberGenerator) -> Vec<Point> {
const NUM_MONSTERS: usize = 50;
let mut spawnable_tiles: Vec<Point> = self
.map
.tiles
.iter()
.enumerate()
.filter(|(idx, t)| {
**t == TileType::Floor
&& DistanceAlg::Pythagoras.distance2d(*start, self.map.index_to_point2d(*idx))
> 10.0
})
.map(|(idx, _)| self.map.index_to_point2d(idx))
.collect();
let mut spawns = Vec::new();
for _ in 0..NUM_MONSTERS {
let target_index = rng.random_slice_index(&spawnable_tiles).unwrap();
spawns.push(spawnable_tiles[target_index]);
spawnable_tiles.remove(target_index);
}
spawns
}
}

84
src/map_builder/prefab.rs Normal file
View File

@ -0,0 +1,84 @@
use crate::prelude::*;
const FORTRESS: (&str, i32, i32) = (
"
------------
---######---
---#----#---
---#-M--#---
-###----###-
--M------M--
-###----###-
---#----#---
---#----#---
---######---
------------
",
12,
11,
);
pub fn apply_prefab(mb: &mut MapBuilder, rng: &mut RandomNumberGenerator) {
let mut placement = None;
let dijkstra_map = DijkstraMap::new(
SCREEN_WIDTH,
SCREEN_HEIGHT,
&[mb.map.point2d_to_index(mb.player_start)],
&mb.map,
1024.0,
);
let mut attempts = 0;
while placement.is_none() && attempts < 10 {
let dimensions = Rect::with_size(
rng.range(0, SCREEN_WIDTH - FORTRESS.1),
rng.range(0, SCREEN_HEIGHT - FORTRESS.2),
FORTRESS.1,
FORTRESS.2,
);
let mut can_place = false;
dimensions.for_each(|pt| {
let idx = mb.map.point2d_to_index(pt);
let distance = dijkstra_map.map[idx];
if distance < 2000.0 && distance > 20.0 && mb.amulet_start != pt {
can_place = true;
}
});
if can_place {
placement = Some(Point::new(dimensions.x1, dimensions.y1));
let points = dimensions.point_set();
mb.monster_spawns.retain(|pt| !points.contains(pt));
}
attempts += 1;
}
if let Some(placement) = placement {
let string_vec: Vec<char> = FORTRESS
.0
.chars()
.filter(|a| *a != '\r' && *a != '\n')
.collect();
let mut i = 0;
for ty in placement.y..placement.y + FORTRESS.2 {
for tx in placement.x..placement.x + FORTRESS.1 {
let idx = map_idx(tx, ty);
let c = string_vec[i];
match c {
'M' => {
mb.map.tiles[idx] = TileType::Floor;
mb.monster_spawns.push(Point::new(tx, ty));
}
'-' => mb.map.tiles[idx] = TileType::Floor,
'#' => mb.map.tiles[idx] = TileType::Wall,
_ => println!("No idea what to do with [{}]", c),
}
i += 1;
}
}
}
}

28
src/map_builder/rooms.rs Normal file
View File

@ -0,0 +1,28 @@
use super::MapArchitect;
use crate::prelude::*;
pub struct RoomsArchitect {}
impl MapArchitect for RoomsArchitect {
fn new(&mut self, rng: &mut RandomNumberGenerator) -> MapBuilder {
let mut mb = MapBuilder {
map: Map::new(),
rooms: Vec::new(),
monster_spawns: Vec::new(),
player_start: Point::zero(),
amulet_start: Point::zero(),
theme: super::themes::DungeonTheme::new(),
};
mb.fill(TileType::Wall);
mb.build_random_rooms(rng);
mb.build_corridors(rng);
mb.player_start = mb.rooms[0].center();
mb.amulet_start = mb.find_most_distant(&mb.player_start);
for room in mb.rooms.iter().skip(1) {
mb.monster_spawns.push(room.center());
}
mb
}
}

35
src/map_builder/themes.rs Normal file
View File

@ -0,0 +1,35 @@
use super::MapTheme;
use crate::prelude::*;
pub struct DungeonTheme {}
pub struct ForestTheme {}
impl DungeonTheme {
pub fn new() -> Box<dyn MapTheme> {
Box::new(Self {})
}
}
impl MapTheme for DungeonTheme {
fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
match tile_type {
TileType::Floor => to_cp437('.'),
TileType::Wall => to_cp437('#'),
}
}
}
impl ForestTheme {
pub fn new() -> Box<dyn MapTheme> {
Box::new(Self {})
}
}
impl MapTheme for ForestTheme {
fn tile_to_render(&self, tile_type: TileType) -> FontCharType {
match tile_type {
TileType::Floor => to_cp437(';'),
TileType::Wall => to_cp437('"'),
}
}
}