This is the twelfth post in the Building a Roguelike in Javascript series. I recommend you start at the beginning unless you've been following along. This part corresponds to the ninth part in Trystan's series. All the code for this part can be found at the part 9 tag of the jsrogue repository. At the time of writing, I am still using the d4ea2ab commit for rot.js.

In this post we are going to make some more interesting cave dwellers! We are going to add some monsters which wander randomly around the cave and attack our hero. Although this initial artifical intelligence is very primitive, it will be a good stepping stone for future posts! We're also going to do some refactoring to the way entities are stored in order to allow us to quickly look up if an entity is at a given position!

Demo Link

The results after this post can be seen here.

assets/map.js

Before we start creating some monsters, we are going to refactor the way entities are stored on a map. Currently the entities are just stored in an array. This causes problems when we want to find if an entity is at a given tile as we always have to search through the entire set of entities. As there are generally much less entities then tile cells, and we cannot have more than one entity at a given tile cell, we are going to store our entities in a hash table indexed by position (similar to how our visible tiles were stored in part 8b). This will allow us to quickly search for an entity at a given position. The downside of this is that we have slightly more maintenance to do when actually moving an entity but the benefits of quick lookup rapidly outweigh these costs.

The first thing we want to do is modify our constructor to remove the array definition and create an object instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Game.Map = function(tiles, player) {
    // ...
    // Setup the field of visions
    this._fov = [];
    this.setupFov();
    // Create a table which will hold the entities
    this._entities = {};
    // Create the engine and scheduler
    // ...
};

We now want to update our functions for getting entities at a given location. Entities will be indexed using a string of the form "x,y,z". We will also want to modify our function for searching entities within a given radius, alhough to keep it simple we will still iterate through all entities instead of generating all possible keys. The most important change to this functon is the way we iterate (using for key in instead of iterating through the length).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Game.Map.prototype.getEntityAt = function(x, y, z){
    // Get the entity based on position key 
    return this._entities[x + ',' + y + ',' + z];
};
Game.Map.prototype.getEntitiesWithinRadius = function(centerX, centerY,
                                                      centerZ, radius) {
    results = [];
    // Determine our bounds
    var leftX = centerX - radius;
    var rightX = centerX + radius;
    var topY = centerY - radius;
    var bottomY = centerY + radius;
    // Iterate through our entities, adding any which are within the bounds
    for (var key in this._entities) {
        var entity = this._entities[key];
        if (entity.getX() >= leftX && entity.getX() <= rightX && 
            entity.getY() >= topY && entity.getY() <= bottomY &&
            entity.getZ() == centerZ) {
            results.push(entity);
        }
    }
    return results;
};

Recall that in this blog post we want to create entities that move around! So we'll need to provide a public method which the Entity class can call when we move an entity. In fact we'll be able to re-use this method when adding an entity to the map as well! This method will allow us to signal that an entity has moved (or entered the map if we don't specify an old position) and take care of maintaining the entity's position in the table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Game.Map.prototype.updateEntityPosition = function(entity, oldX, oldY, oldZ) {
    // Delete the old key if it is the same entity and we have old positions.
    if (oldX) {
        var oldKey = oldX + ',' + oldY + ',' + oldZ;
        if (this._entities[oldKey] == entity) {
            delete this._entities[oldKey];
        }
    }
    // Make sure the entity's position is within bounds
    if (entity.getX() < 0 || entity.getX() >= this._width ||
        entity.getY() < 0 || entity.getY() >= this._height ||
        entity.getZ() < 0 || entity.getZ() >= this._depth) {
        throw new Error("Entity's position is out of bounds.");
    }
    // Sanity check to make sure there is no entity at the new position.
    var key = entity.getX() + ',' + entity.getY() + ',' + entity.getZ();
    if (this._entities[key]) {
        throw new Error('Tried to add an entity at an occupied position.');
    }
    // Add the entity to the table of entities
    this._entities[key] = entity;
};

The next step is to change our function for adding an entity. We can simply use the function we just created without specifying an old position. We also change our function for removing an entity to simply delete their position's entry from the entities table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Game.Map.prototype.addEntity = function(entity) {
    // Update the entity's map
    entity.setMap(this);
    // Update the map with the entity's position
    this.updateEntityPosition(entity);
    // Check if this entity is an actor, and if so add
    // them to the scheduler
    if (entity.hasMixin('Actor')) {
       this._scheduler.add(entity, true);
    }
};
// ...
Game.Map.prototype.removeEntity = function(entity) {
    // Remove the entity from the map
    var key = entity.getX() + ',' + entity.getY() + ',' + entity.getZ();
    if (this._entities[key] == entity) {
        delete this._entities[key];
    }
    // If the entity is an actor, remove them from the scheduler
    if (entity.hasMixin('Actor')) {
        this._scheduler.remove(entity);
    }
};

assets/entity.js

Since we generally move the entity using the Entity's setPosition method, we will notify the map from there that the entity's position has changed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Entity.prototype.setPosition = function(x, y, z) {
    var oldX = this._x;
    var oldY = this._y;
    var oldZ = this._z;
    // Update position
    this._x = x;
    this._y = y;
    this._z = z;
    // If the entity is on a map, notify the map that the entity has moved.
    if (this._map) {
        this._map.updateEntityPosition(this, oldX, oldY, oldZ);
    }
};

assets/screens.js

We're almost done the refactor! The last step is to change how we iterate through our entities when rendering them to the screen! This is a simple change in the play screen's rendering function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Game.Screen.playScreen = {
    // ...
    render: function(display) {
        // ...
        // Render the entities
        var entities = this._map.getEntities();
        for (var key in entities) {
            var entity = entities[key];
            // Only render the entitiy if they would show up on the screen
            if (entity.getX() >= topLeftX && entity.getY() >= topLeftY &&
                entity.getX() < topLeftX + screenWidth &&
                entity.getY() < topLeftY + screenHeight &&
                entity.getZ() == this._player.getZ()) {
                if (visibleCells[entity.getX() + ',' + entity.getY()]) {
                    display.draw(
                        entity.getX() - topLeftX, 
                        entity.getY() - topLeftY,    
                        entity.getChar(), 
                        entity.getForeground(), 
                        entity.getBackground()
                    );
                }
            }
        }
       // ...
    },
    // ..
};

assets/entity.js

Since we want to create entities that can wander around, they'll need to be able to move as well. Since every entity has the setPosition function, it seems a bit silly that we need to add a mixin simply for moving. To make our lives easier let's delete the Moveable mixin and make the Entity class have the tryMove function instead!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Game.Entity.prototype.tryMove = function(x, y, z, map) {
    var map = this.getMap();
    // Must use starting z
    var tile = map.getTile(x, y, this.getZ());
    var target = map.getEntityAt(x, y, this.getZ());
    // If our z level changed, check if we are on stair
    if (z < this.getZ()) {
        if (tile != Game.Tile.stairsUpTile) {
            Game.sendMessage(this, "You can't go up here!");
        } else {
            Game.sendMessage(this, "You ascend to level %d!", [z + 1]);
            this.setPosition(x, y, z);
        }
    } else if (z > this.getZ()) {
        if (tile != Game.Tile.stairsDownTile) {
            Game.sendMessage(this, "You can't go down here!");
        } else {
            this.setPosition(x, y, z);
            Game.sendMessage(this, "You descend to level %d!", [z + 1]);
        }
    // If an entity was present at the tile
    } else if (target) {
        // If we are an attacker, try to attack
        // the target
        if (this.hasMixin('Attacker')) {
            this.attack(target);
            return true;
        } else {
            // If not nothing we can do, but we can't 
            // move to the tile
            return false;
        }
    // Check if we can walk on the tile
    // and if so simply walk onto it
    } else if (tile.isWalkable()) {        
        // Update the entity's position
        this.setPosition(x, y, z);
        return true;
    // Check if the tile is diggable, and
    // if so try to dig it
    } else if (tile.isDiggable()) {
        map.dig(x, y, z);
        return true;
    }
    return false;
};

assets/entities.js

First things first remember to remove the Game.Mixins.Moveable code! We have to update our PlayerTemplate to no longer use that mixin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Game.PlayerTemplate = {
    character: '@',
    foreground: 'white',
    maxHp: 40,
    attackValue: 10,
    sightRadius: 6,
    mixins: [Game.Mixins.PlayerActor,
             Game.Mixins.Attacker, Game.Mixins.Destructible,
             Game.Mixins.Sight, Game.Mixins.MessageRecipient]
};

Now we want to create entities that can wander around! To do this we will make a new Actor mixin which will simply move by 1 unit in a random direction every time act is called.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Game.Mixins.WanderActor = {
    name: 'WanderActor',
    groupName: 'Actor',
    act: function() {
        // Flip coin to determine if moving by 1 in the positive or negative direction
        var moveOffset = (Math.round(Math.random()) === 1) ? 1 : -1;
        // Flip coin to determine if moving in x direction or y direction
        if (Math.round(Math.random()) === 1) {
            this.tryMove(this.getX() + moveOffset, this.getY(), this.getZ());
        } else {
            this.tryMove(this.getX(), this.getY() + moveOffset, this.getZ());
        }
    }
};

We are now ready to create our enemies! Let's create a bat (B) and a newt (:). Both of these will wander about, but the bat will be a bit stronger than the newt!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Game.BatTemplate = {
    name: 'bat',
    character: 'B',
    foreground: 'white',
    maxHp: 5,
    attackValue: 4,
    mixins: [Game.Mixins.WanderActor, 
             Game.Mixins.Attacker, Game.Mixins.Destructible]
};

Game.NewtTemplate = {
    name: 'newt',
    character: ':',
    foreground: 'yellow',
    maxHp: 3,
    attackValue: 2,
    mixins: [Game.Mixins.WanderActor, 
             Game.Mixins.Attacker, Game.Mixins.Destructible]
};

assets/map.js

Now that we've created our new templates, we want these creepy creatures to show up in the cave! For now let's just randomly spawn creatures throughout the cave!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Game.Map = function(tiles, player) {
    // ...
    // Add the player
    this.addEntityAtRandomPosition(player, 0);
    // Add random enemies to each floor.
    var templates = [Game.FungusTemplate, Game.BatTemplate, Game.NewtTemplate];
    for (var z = 0; z < this._depth; z++) {
        for (var i = 0; i < 15; i++) {
            // Randomly select a template
            var template = templates[Math.floor(Math.random() * templates.length)];
            // Place the entity
            this.addEntityAtRandomPosition(new Game.Entity(template), z);
        }
    }
    // Setup the explored array
    // ...
};

assets/entity.js

Before you go any further, I encourage you to check out your game! It's really starting to look like a real roguelike! We've got bats and newts flying around and fungus spreading throughout the cave. If you play around for long enough, you may notice two strange things - the first being that wandering enemies can dig, and the second being that enemies can attack each other! We're going to modify the digging system to make it so only the player can dig. We also want to make it so that enemies will only attack the player! For now let's use the fact that the player has the PlayerActor mixin to do this in the tryMove function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Game.Entity.prototype.tryMove = function(x, y, z, map) {
    // ...
    // If an entity was present at the tile
    } else if (target) {
        // An entity can only attack if the entity has the Attacker mixin and 
        // either the entity or the target is the player.
        if (this.hasMixin('Attacker') && 
            (this.hasMixin(Game.Mixins.PlayerActor) ||
             target.hasMixin(Game.Mixins.PlayerActor))) {
            this.attack(target);
            return true;
        } 
        // If not nothing we can do, but we can't 
        // move to the tile
        return false;        
    // Check if we can walk on the tile
    // and if so simply walk onto it
    } else if (tile.isWalkable()) {        
        // Update the entity's position
        this.setPosition(x, y, z);
        return true;
    // Check if the tile is diggable
    } else if (tile.isDiggable()) {
        // Only dig if the the entity is the player
        if (this.hasMixin(Game.Mixins.PlayerActor)) {
            map.dig(x, y, z);
            return true;
        }
        // If not nothing we can do, but we can't 
        // move to the tile
        return false;
    }
    return false;
};

assets/screens.js

We've now got a much more functional game! However you may notice that enemies can now actually attack your hero, and eventually kill them! We had created a losing screen, but we don't currently switch to it once the hero's health drops down to 0! What we're going to do when the player dies is send them a message and notifying them to press enter to move on to the lose screen. We're going to add a state to the Game screen to now when the game is over.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Game.Screen.playScreen = {
    _map: null,
    _player: null,
    _gameEnded: false,
    // ...
    handleInput: function(inputType, inputData) {
        // If the game is over, enter will bring the user to the losing screen.
        if (this._gameEnded) {
            if (inputType === 'keydown' && inputData.keyCode === ROT.VK_RETURN) {
                Game.switchScreen(Game.Screen.loseScreen);
            }
            // Return to make sure the user can't still play
            return;
        }
        // ...
    },
    // ...
    setGameEnded: function(gameEnded) {
        this._gameEnded = gameEnded;
    }
};

assets/entities.js

Now we've got to actually detect that the game is over and notify the player!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Mixins.PlayerActor = {
    // ...
    act: function() {
        // Detect if the game is over
        if (this.getHp() < 1) {
            Game.Screen.playScreen.setGameEnded(true);
            // Send a last message to the player
            Game.sendMessage(this, 'You have died... Press [Enter] to continue!');
        }
        // Re-render the screen
        // ...
    }
};

If you die now, the game will be stuck in an infinite loop and this will never actually work! The reason is that when an entity dies, they are removed from the map and thus never act! So let's modify the code in the Destructible mixin to simply call the player's act method if it is a player that dies!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Game.Mixins.Destructible = {
    // ...
    takeDamage: function(attacker, damage) {
        this._hp -= damage;
        // If have 0 or less HP, then remove ourseles from the map
        if (this._hp <= 0) {
            Game.sendMessage(attacker, 'You kill the %s!', [this.getName()]);
            // Check if the player died, and if so call their act method to prompt the user.
            if (this.hasMixin(Game.Mixins.PlayerActor)) {
                this.act();
            } else {
                this.getMap().removeEntity(this);
            }
        }
    }
};

Conclusion

Our cave is now populated by actual wandering creatures that can hurt us! The hero can die! If you want a fun game, set the player's health points to 1 and see how far into the cave you can get. What about when you start spawning more enemies? The next post is going to be a big one - we're going to add items to the game and an inventory to the hero... so stick around!

I hope you enjoyed this post! Remember that all the code for this part can be found at the part 9 tag of the jsrogue repository. As always please feel free to post any comments whether questions, clarifications or criticism!

Thanks for reading,

Dominic

Next Part

Part 10a - Items on the Cave Floor