Populating the dungeon

We have an explorable dungeon now. Yeah! But when you actually go through it, it feels a bit…​ boring. It’s just empty rooms connected to more empty rooms. Let’s add some monsters!

This isn’t going to be as bad thanks to our object system. We create an object for each monster and add it to the objects list. So all we need to do is to create a few monsters in random locations for each room.

Let’s add a function that takes a room and does exactly that.

fn place_objects(room: Rect, objects: &mut Vec<Object>) {
    // choose random number of monsters
    let num_monsters = rand::thread_rng().gen_range(0, MAX_ROOM_MONSTERS + 1);

    for _ in 0..num_monsters {
        // choose random spot for this monster
        let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2);
        let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2);

        let mut monster = if rand::random::<f32>() < 0.8 {  // 80% chance of getting an orc
            // create an orc
            Object::new(x, y, 'o', colors::DESATURATED_GREEN)
        } else {
            Object::new(x, y, 'T', colors::DARKER_GREEN)
        };

        objects.push(monster);
    }
}

We’ll define the MAX_ROOM_MONSTERS constant at the top of the file:

const MAX_ROOM_MONSTERS: i32 = 3;

Calling rand::random::<f32>() will produce an f32 number between 0.0 and 1.0. 80% of that is 0.8.

As you can see, we’re defining orcs and trolls here, but you can do anything you want! And as we add more properties to Object, you can set them all here and create any monster (or npc or item) your heart desires.

later on, we’ll use a choice table to make the code cleaner but for now you can just extend the if random() …​ else block to add more variety.

To actually place the monsters in each room, we will call this function right after create_room in make_map:

// add some content to this room, such as monsters
place_objects(new_room, objects);

And update the signature of make_map to take in a mutable reference to our list of objects. We’ll also stop returning the starting_position. It can be accessed via objects[PLAYER].pos():

fn make_map(objects: &mut Vec<Object>) -> Map {
    ...

    map
}

Let’s set the player’s position when we generate the first room:

if rooms.is_empty() {
    // this is the first room, where the player starts at
    objects[PLAYER].set_pos(new_x, new_y);
} else {
    ...
}

When we call make_map pass it a mutable reference to the entire objects list:

let mut game = Game {
    // generate map (at this point it's not drawn to the screen)
    map: make_map(&mut objects),
};

Let’s also remove the dummy NPC from the initial objects list. We won’t need it anymore:

// the list of objects with just the player
let mut objects = vec![player];

Getting hold of the Player’s position

Before we move further, there are two things we can do to make working with the player object easier and also to get and set the position of an object.

We’ve already typed objects[0] a few times to refer to the player. To make it a bit clearer, we’ll define a new constant PLAYER with the value 0. Put this among the other constants:

// player will always be the first object
const PLAYER: usize = 0;

And now we can replace every usage of objects[0] with objects[PLAYER]. We have one in the main function:

while !root.window_closed() {
    ...

    // render the screen
    let fov_recompute = previous_player_position != (objects[PLAYER].pos());  (1)

    ...
}
1 objects[0]objects[PLAYER]

And one more in render_all:

if fov_recompute {
    // recompute FOV if needed (the player moved or something)
    let player = &objects[PLAYER];  (1)
    tcod.fov
        .compute_fov(player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO);
}
1 objects[0]objects[PLAYER]

It’s a bit longer to type, but the intent is much clearer.

Next, add these two methods to Object:

pub fn pos(&self) -> (i32, i32) {
    (self.x, self.y)
}

pub fn set_pos(&mut self, x: i32, y: i32) {
    self.x = x;
    self.y = y;
}

These give us a shorthand for getting or setting both coordinates (x and y) at once. This will again simplify some code in main, the move_by method of Object as well as setting the player’s initial position in make_map.

Blocking objects

If you tried to walk up to a monster, you’d see that the player would walk right through! That’s clearly not what we want to happen in general. Plus, we don’t want multiple monsters standing on the same tile.

But there are other options (scrolls, potions) that should not block the tile they’re on.

Does it block?

Let’s update Object with information whether it blocks the player or not. And give each object a name while we’re at it. Put this in the Object struct definition:

struct Object {
    x: i32,
    y: i32,
    char: char,
    color: Color,
    name: String,  (1)
    blocks: bool,  (2)
    alive: bool,  (3)
}
1 New field: name
2 New field: blocks
3 New field: alive

And change the new method on Object to:

pub fn new(x: i32, y: i32, char: char, name: &str, color: Color, blocks: bool) -> Self {
    Object {
        x: x,
        y: y,
        char: char,
        color: color,
        name: name.into(),
        blocks: blocks,
        alive: false,
    }
}
All our objects are alive at the moment, but soon we’ll add items, scrolls, stairs, etc. and the balance will shift. Better set things alive explicitly than turn it off. It’s easy to see when you’ve missed setting alive = true but hard to do the opposite.

Now we’ll create a function that tests if a tile is blocked — whether due to a wall or an object blocking it.

fn is_blocked(x: i32, y: i32, map: &Map, objects: &[Object]) -> bool {
    // first test the map tile
    if map[x as usize][y as usize].blocked {
        return true;
    }
    // now check for any blocking objects
    objects
        .iter()
        .any(|object| object.blocks && object.pos() == (x, y))
}

It takes the coordinates we want to check and we must also pass in the map and objects.

Ownership woes

Now we’d like to use is_blocked in the move_by method to make sure an object never moves onto a blocked tile.

If we just put the !is_blocked(self.x + dx, self.y + dy, map, objects) check into the method and add objects as a function parameter, Rust will not let us use it.

This method will compile just fine, but if you try calling it in handle_keys, the program will not compile:

objects[PLAYER].move_by(1, 0, &map, &objects);

Rust will complain that it cannot have a mutable and an immutable borrow at once.

To guarantee memory safety and no data races, Rust’s references (& and &mut) have a few rules. One of them is that when you have a mutable borrow, you can’t have any other mutable or immutable borrows into the same data.

And that is exactly what’s happening here. The signature of the move_by method is:

fn move_by(&mut self, dx: i32, dy: i32, map: &Map, objects: &[Object])

We need &Map and &[Object] because they both need to be passed to is_blocked. But, we also need the &mut self at the beginning to be able to modify the position of the object we’re moving.

And therein lies the problem, since all objects (including the one we’re calling move_by on) are in the objects vec, as soon as we mutably borrow one part of it, Rust locks the entire vec. The line above is essentially equivalent to this:

let player = &mut objects[PLAYER];   (1) (2)
let borrowed_objects = &objects;  (3)
player.move_by(1, 0, &map, objects)  (4)
1 Get a mutable borrow of the player object
2 That will treat the whole objects vec as mutably borrowed
3 Try to immutably borrow objects — fails because it’s already borrowed
4 We don’t even get here because of the double borrow issue

There’s multiple ways to solve this, but the easiest is to turn the method into a plain function and pass in the object index instead of a reference:

/// move by the given amount, if the destination is not blocked
fn move_by(id: usize, dx: i32, dy: i32, map: &Map, objects: &mut [Object]) {
    let (x, y) = objects[id].pos();
    if !is_blocked(x + dx, y + dy, map, objects) {
        objects[id].set_pos(x + dx, y + dy);
    }
}

Now we no longer have the problem, because we first get the object’s position (immutable borrow that ends immediately), then call is_blocked with objects (again, immutable borrow that ends right after the call) and finally, with no borrows to burden us, we set the position.

This is what Rust people sometimes refer to as "fighting the borrow checker". When you start with the language, you’ll likely encounter a lot of these situations. As you get more experienced, though, you’ll learn which patterns will cause trouble and structure your code differently.

Most of the time, Rust will catch things that could result in memory or threading issues in other languages. But sometimes (such as in our case here), it can’t tell whether the operation is okay or not and so it rather errs on the side of safety.

You can read more in the Rust book’s chapters on ownership and borrowing:

All’s well

So after this interlude, the objects (including the player) can no longer move into a tile occupied by another blocking object.

Next, make sure we don’t place two blocking objects onto the same tile. In place_objects, we’ll check whether the tile is free before placing a new monster:

// only place it if the tile is not blocked
if !is_blocked(x, y, map, objects) {
    // generate the monster
}

That means we now have to pass the map to place_objcets as well:

fn place_objects(room: Rect, map: &Map, objects: &mut Vec<Object>) {
    ...
}

And of course we have to pass map when we call place_objecs in make_map too:

// add some content to this room, such as monsters
place_objects(new_room, &map, objects);

Since objects have two new properties, we need to pass them along to any code that creates one. Update the player creation to:

// create object representing the player
let mut player = Object::new(0, 0, '@', "player", WHITE, true);
player.alive = true;

And update the code that creates the monsters:

let mut monster = if rand::random::<f32>() < 0.8 {
    // 80% chance of getting an orc
    // create an orc
    Object::new(x, y, 'o', "orc", DESATURATED_GREEN, true)
} else {
    // create a troll
    Object::new(x, y, 'T', "troll", DARKER_GREEN, true)
};

Let’s make the monsters alive as well. Right before objects.push(monster):

monster.alive = true;
objects.push(monster);

And in handle_keys, we’ll change the movement code from player.move_by(0, -1, game) to:

move_by(PLAYER, 0, -1, game, objects)

Player actions

Last stop before we get to the actual combat system! Our input system has a fatal flaw: player actions (movement, combat) and other keys (fullscreen, other options) are handled the same way. We need to separate them. This way, if the player pauses or dies he can’t move or fight, but can press other keys. We also want to know if the player’s input means he finished his turn or not; changing to fullscreen shouldn’t count as a turn. I know they’re just simple details - but the game would be incredibly annoying without them!

Let’s define high-level actions from the player that we can control the game loop with:

#[derive(Clone, Copy, Debug, PartialEq)]
enum PlayerAction {
    TookTurn,
    DidntTakeTurn,
    Exit,
}

(deriving PartialEq lets us use == and != to compare the enums together)

Change handle_keys to return PlayerAction instead of bool.

fn handle_keys(tcod: &mut Tcod, game: &Game, objects: &mut Vec<Object>) -> PlayerAction {
   ...
}

We’re going to be using the enum values heavily in handle_keys, so let’s import them on top of the function:

use PlayerAction::*;

And then, in the code for fullscreen, return DidntTakeTurn:

Key {
    code: Enter,
    alt: true,
    ..
} => {
    // Alt+Enter: toggle fullscreen
    let fullscreen = root.is_fullscreen();
    root.set_fullscreen(!fullscreen);
    DidntTakeTurn
}

Have the Escape code path return Exit:

Key { code: Escape, .. } => Exit,  // exit game

And PlayerAction::TookTurn to all the movement actions and PlayerAction::DidntTakeTurn to the catch-all at the end.

Key { code: Up, .. } => {
    move_by(PLAYER, 0, -1, map, objects);
    TookTurn
}

// and so on for Down, Left and Right

_ => DidntTakeTurn,

This will ensure that pressing an unknown key will not do anything and as we’ll add other actions, such as picking up items, accessing inventory, etc. we’ll have an easy way of saying whether they take a turn or not — or even being more dynamic than that — just opening an inventory may not cost anything but using an item from it could.

And now let’s only allow things like movement when the game is still going on. You wouldn’t want the player’s corpse to walk around after death (or maybe you would! There’s a game idea.), but you may still allow things like full screen, exiting the game or even a read-only view into the inventory.

Let’s update our match to include the player_alive, too:

let key = tcod.root.wait_for_keypress(true);
let player_alive = objects[PLAYER].alive;
match (key, key.text(), player_alive) {
    // key handling
}

We have also added key.text(). It’s a method that lets us read a textual representation of the pressed key. This will be helpful on when we want to handle letters or keys such as >. The position of these depends on the keyboard layout our player has set up. We can’t just check for Shift + , because many non-English keyboards have the greater-than key in a completely different place!

So we’ll pass the text in alongside the key and the player_alive and just ignore it for now.

Now instead of just matching on the key alone, we have to take these two values into consideration as well. Fullscreen and exit on Escape should work whether the player is alive or dead, so change them to:

(
    Key {
        code: Enter,
        alt: true,
        ..
    },
    _,
    _,
) => {
    // Alt+Enter: toggle fullscreen
    let fullscreen = tcod.root.is_fullscreen();
    tcod.root.set_fullscreen(!fullscreen);
    DidntTakeTurn
}
(Key { code: Escape, .. }, _, _) => Exit, // exit game

We’re taking three values in a tuple (key, text, player_alive) now and ignoring the latter two.

For movement, we only want it to work when the player is alive so:

// movement keys
(Key { code: Up, .. }, _, true) => {
    player_move_or_attack(0, -1, game, objects);
    TookTurn
}
(Key { code: Down, .. }, _, true) => {
    player_move_or_attack(0, 1, game, objects);
    TookTurn
}
(Key { code: Left, .. }, _, true) => {
    player_move_or_attack(-1, 0, game, objects);
    TookTurn
}
(Key { code: Right, .. }, _, true) => {
    player_move_or_attack(1, 0, game, objects);
    TookTurn
}
There are other ways to handle this. We could use use the if syntax in the match arm (so e.g. Key { code: Down, .. } if game_state == Playing ⇒ // move player) or even get rid of match entirely and use if/else statements just like in Python. However, I find this easier to read and it makes sure we never forget to handle the game state when we add a new key.

And now we need to go back to the main loop and handle PlayerAction there. Change the end of the loop to:

// handle keys and exit game if needed
previous_player_position = objects[PLAYER].pos();
let player_action = handle_keys(&mut tcod, &game, &mut objects);
if player_action == PlayerAction::Exit {
    break;
}

Fighting orderly

This part is already running long, so we won’t actually implement combat here (that will happen in the next part), but we’ll make sure that the player and the monsters take turns to act.

// let monsters take their turn
if objects[PLAYER].alive && player_action != PlayerAction::DidntTakeTurn {
    for object in &objects {
        // only if object is not player
        if (object as *const _) != (&objects[PLAYER] as *const _) {
            println!("The {} growls!", object.name);
        }
    }
}

The as *const _ bit is there to do a pointer comparison. Rust’s equality operators (== and !=) test for value equality, but we haven’t implemented that for Object and we don’t care anyway — we just want to make sure to not process player here.

The println! is just the debug message. You’ll see it in the console where you write cargo run --release to run your game. In the next part we’ll add an AI routine to move and attack and later on an in-game message log where we can print stuff to the player.

Right now, when a player tries to move (bump) into a monster, nothing happens. Let’s interpret that as an attack. We’ll add a new function called player_move_or_attack and use it instead of move_by in handle_keys.

Replace all calls to:

move_by(0, -1, map, objects);

With:

player_move_or_attack(1, 0, game, objects)

Now let’s write the function itself:

fn player_move_or_attack(dx: i32, dy: i32, game: &Game, objects: &mut [Object]) {
    // the coordinates the player is moving to/attacking
    let x = objects[PLAYER].x + dx;
    let y = objects[PLAYER].y + dy;

    // try to find an attackable object there
    let target_id = objects.iter().position(|object| object.pos() == (x, y));

    // attack if target found, move otherwise
    match target_id {
        Some(target_id) => {
            println!(
                "The {} laughs at your puny efforts to attack him!",
                objects[target_id].name
            );
        }
        None => {
            move_by(PLAYER, dx, dy, &game.map, objects);
        }
    }
}

The position method on an iterator runs a test on each object and as soon as it finds one, it returns its index in the collection (in our case a vec of Object).

It’s possible no match will be found, so it actually returns Option<usize> here.

We then test whether we have found a target at that position (in which case we know its index), and either print out a message or just move into that place.

And that’s it! Test it out. No one’s dealing any damage, but the game now detects when you’re trying to attack a monster. And you can see the monsters taking their turns after you.

Guess what’s next?

Continue to the next part.