Mapping the Mine pt 4

The bottom level of the map is meant to be a winding cavern, into which the hapless colonists drilled. They drilled too deep, monsters ate them - you know the drill. For this map, I decided to use Cellular Automata - also from Hands-on Rust (and the Roguelike tutorial).

Filling in the Edges

The entrance builder module includes a handy edge_filler function for preventing gaps at the edge of the map. This function is generally useful - and solves a problem with the Drunkard's Walk maps we made in the previous section. Sometimes, the edge of the map is open - and it's confusing that you can't walk off of it. So let's promote edge_filler to be a generally available function.

In src/map/layerbuilder/entrance.rs, delete the entire edge_filler function (or cut it into your clipboard). Then open src/map/layerbuilder/mod.rs and paste the function into there:


#![allow(unused)]
fn main() {
fn edge_filler(map: &mut Layer) {
    for y in 0..HEIGHT {
        let idx = map.point2d_to_index(Point::new(0, y));
        if map.tiles[idx].tile_type == TileType::Floor {
            map.tiles[idx] = Tile::wall();
        }
        let idx = map.point2d_to_index(Point::new(WIDTH - 1, y));
        if map.tiles[idx].tile_type == TileType::Floor {
            map.tiles[idx] = Tile::wall();
        }
    }
    for x in 0..WIDTH {
        let idx = map.point2d_to_index(Point::new(x, 0));
        if map.tiles[idx].tile_type == TileType::Floor {
            map.tiles[idx] = Tile::wall();
        }
        let idx = map.point2d_to_index(Point::new(x, HEIGHT - 1));
        if map.tiles[idx].tile_type == TileType::Floor {
            map.tiles[idx] = Tile::wall();
        }
    }
}
}

Going back to entrance.rs, add a use super::edge_filler to the imports. You also need to add use bracket_lib::prelude::{Point, Algorithm2D}; and use super::{HEIGHT, WIDTH, tile::TileType};.

We can now use edge_filler on all of our generators.

Adding Edge Filling to the Mine Middle

Open src/map/layerbuilder/mine_middle.rs and add a use super::edge_filler; line to the import declarations. Then find the end of the build_mine_middle function and right before you return layer add a call to the new function:


#![allow(unused)]
fn main() {
        drunkard(&mut layer);
    }

    edge_filler(&mut layer); // <-- This is new

    layer
}
}

The drunkard's walk map now won't have gaps at the edges. We'll use this function again when building the caverns.

Framework

The first thing to do is to open src/map/layer.rs and add a build_caverns call to the layer builder's match statement. Just like before, this will be a placeholder for a minute - but we'll dive straight into making it. So open the file, and adjust the match statement as follows:


#![allow(unused)]
fn main() {
impl Layer {
    pub fn new(depth: usize, ecs: &mut World) -> Self {
        let layer = match depth {
            0 => build_entrance(ecs),
            1 => build_mine_top(ecs),
            2 => build_mine_middle(ecs),
            3 => build_caverns(ecs),
            ...
}

You may also want to open map.rs and force the starting layer to 3 while you develop the level. That way, you'll start on the new level - and won't have to navigate much to see the fruits of your labor. Change src/map/map.rs:


#![allow(unused)]
fn main() {
impl Map {
    pub fn new(ecs: &mut World) -> Self {
        let mut layers = Vec::with_capacity(NUM_LAYERS);
        for i in 0..NUM_LAYERS {
            layers.push(Layer::new(i, ecs));
        }
        Self {
            current_layer: 3, // REMEMBER TO CHANGE ME BACK
            layers,
        }
    }
    ...
}

Now that we have the placeholder built, let's construct the caverns.

Building the caverns

We'll be making a new file inside the layerbuilder module, so we need to remember to tell the module to include the file in the project. Open src/map/layerbuilder/mod.rs and add:


#![allow(unused)]
fn main() {
mod caverns;
pub use caverns::build_caverns;
}

Create a new file, src/map/layerbuilder/caverns.rs. Add a header, containing what we will need for the module:


#![allow(unused)]
fn main() {
use super::{all_wall, edge_filler, colonists::spawn_first_colonist, spawn_face_eater, spawn_random_colonist};
use crate::{
    components::{Description, Door, Glyph, Position, TileTrigger},
    map::{tile::TileType, Layer, Tile, HEIGHT, WIDTH},
};
use bracket_lib::prelude::*;
use legion::*;
}

Cellular Automata

I adapted the Cellular Automata algorithm from Hands-on Rust to suit my needs. The first step for CA is to start with a completely random map. Add the following function:


#![allow(unused)]
fn main() {
fn random_noise_map(map: &mut Layer) {
    let mut rng_lock = crate::RNG.lock();
    let rng = rng_lock.as_mut().unwrap();

    map.tiles.iter_mut().for_each(|t| {
        let roll = rng.range(0, 100);
        if roll > 55 {
            *t = Tile::floor();
        } else {
            *t = Tile::wall();
        }
    });
}
}

This creates a random soup of walls and floors, with a slight bias towards floors. It's a good starting point for CA iterations. Each iteration needs a way to count neighbors, so add the following function:


#![allow(unused)]
fn main() {
fn count_neighbours(map: &Layer, x:i32, y:i32) -> usize {
    let mut neighbors = 0;
    for iy in -1 ..= 1 {
        for ix in -1 ..= 1 {
            let idx = map.point2d_to_index(Point::new(x+ix, y+iy));
            if !(ix==0 && iy == 0) && map.tiles[idx].tile_type == TileType::Wall
            {
                neighbors += 1;
            }
        }
    }
    neighbors
}
}

Finally, the iterations themselves. Each cell counts the number of neighboring walls, and adjusts the cell based on that count - just like in Hands-on Rust and the Roguelike Tutorial. Add the following function:


#![allow(unused)]
fn main() {
fn iteration(map: &mut Layer) {
    let mut new_tiles = map.tiles.clone();
    for y in 1 .. HEIGHT-1 {
        for x in 1 .. WIDTH-1 {
            let neighbors = count_neighbours(map, x as i32, y as i32);
            let idx = map.point2d_to_index(Point::new(x, y));
            if neighbors > 4 || neighbors == 0 {
                    new_tiles[idx] = Tile::wall();
                } else {
                    new_tiles[idx] = Tile::floor();
                }
        }
    }
    map.tiles = new_tiles;
}
}

Now that we've implemented the basics of a cellular automata generator, we can create the build_caverns function to actually use it:


#![allow(unused)]
fn main() {
pub fn build_caverns(ecs: &mut World) -> Layer {
    let mut layer = Layer::new(std::usize::MAX, ecs); // Gets a default layer
    // We're using Cellular Automata here, straight out of Hands-On Rust.
    random_noise_map(&mut layer);
    for _ in 0..15 {
        iteration(&mut layer);
    }
    edge_filler(&mut layer);

    layer
}
}

This is pretty much direct CA: it creates a random map, iterates the CA algorithm over it 15 times, and then fills in the edges. You have a passable cavern:

Adding a Staircase and Fixing some Bugs

The caverns need an exit - an upwards staircase. Otherwise, poor SecBot will venture in to the caverns and never return. After the call to edge_filler in build_caverns, add the following:


#![allow(unused)]
fn main() {
    edge_filler(&mut layer); // Start here

    let desired_start = Point::new(2, HEIGHT/2);
    let mut possible_starts : Vec<(usize, f32)> = layer
        .tiles
        .iter()
        .enumerate()
        .filter(|(_, t)| t.tile_type == TileType::Floor)
        .map(|(idx, _)| (idx, DistanceAlg::Pythagoras.distance2d(desired_start, layer.index_to_point2d(idx))))
        .collect();
    possible_starts.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap());
    layer.starting_point = layer.index_to_point2d(possible_starts[0].0);
    layer.colonist_exit = layer.starting_point;
    layer.tiles[possible_starts[0].0] = Tile::stairs_up();
    ... // End here
    layer
}
}

This works by finding the closest tile to the center that doesn't contain a wall, and inserts a staircase there.

Fixing Up Stairs on Other Levels

I made a mistake in my staircase code in previous iterations, so now's a good time to fix it. In src/map/layerbuilder/mine_middle.rs find layer.colonist_exit = down_pt and change it to read layer.colonist_exit = up_pt. It makes more sense for the colonists to exit via the UP stairs that leads towards the escape capsule.

The same error appears in src/map/layerbuilder/mine_top.rs. Once again, find layer.colonist_exit = down_pt and change it to read layer.colonist_exit = up_pt.

Finally, open src/map/tile.rs and fix the stairs_up function:


#![allow(unused)]
fn main() {
pub fn stairs_up() -> Self {
    Self {
        glyph: to_cp437('<'),
        color: ColorPair::new(YELLOW, BLACK),
        blocked: false,
        opaque: false,
        tile_type: TileType::StairsUp,
    }
}
}

Run the game now, and the caverns now feature an exit:

Wrap-Up

You can find the source code for mining_map4 here.

Next, we're going to start work on making staircases functional.