This is the third 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 map generation segment of the third part in Trystan's series. All the code for this part can be found at the part 3a tag of the jsrogue repository. At the time of writing, I have updated the rot.js version to the d4ea2ab commit, although there are no breaking changes in terms of what we've done.

Today we'll start working on our actual game! Roguelike games traditionally took place in a dark, monster-filled, multi-level dungeon, but we can let our imagination run free and build all sorts of environments! The great thing about using ASCII graphics is that you aren't limited by your graphical abilitites! No matter what you want to add, whether it be a tree, a dragon or a jetpack, all you have to do is decide what character and colors will represent it! For example may be a pine tree just like % may be a deadly two-headed dog. This is your game... have fun with it! If you want some inspiration and examples of how different games can look check out the background of the Dwarf Fortress page, these Dungeon Crawl screenshots and this screenshot of Rogue, the game that started it all.

To keep things simple and make sure we've got everything running properly we'll be keeping our environment simple in this post. We're going to build some nice caves for our hero to explore, plunder, and possibly stay in forever! For now our cave will be fairly simple and consist of walls, represented by #, and a simple floor, represented by a period. In this post we'll focus simply on building a map and drawing it on the screen, and in the next post we'll make it so we can scroll around the screen. Here's a small screenshot of what we'll have at the end of this post:

Demo Link

The results after this post can be seen here

assets/glyph.js

Almost everything in our game will be represented by some kind of glyph. If you are wondering what a glyph is, it is simply a combination of a character with a foreground and background color. Before we can do any kind of graphical work, we'll need to create a small class representing a glyph:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Game.Glyph = function(chr, foreground, background) {
    // Instantiate properties to default if they weren't passed
    this._char = chr || ' ';
    this._foreground = foreground || 'white';
    this._background = background || 'black';
};

// Create standard getters for glyphs
Game.Glyph.prototype.getChar = function(){ 
    return this._char; 
}
Game.Glyph.prototype.getBackground = function(){
    return this._background;
}
Game.Glyph.prototype.getForeground = function(){ 
    return this._foreground; 
}

As you can see a glyph simply wraps around a character, a foreground color, and a background color (with default values for all 3 properties). The properties can be fetched using getter methods.

assets/tile.js

Levels in ASCII games are usually described in terms of cells or tiles, where each character (for example our walls) represents a given cell. Each of these cells occupies the same width and height visually. A simple way to refer to the cells on a screen is to use 2D cartesian coordinates (x and y). In our case, we use the top left corner as the origin (x=0 and y=0). Therefore a given game level will contain a 2D array of tiles (with the first dimension representing the x and the second dimension representing the y). So what exactly is a tile? For now a tile simply contains a glyph, but in the future it will keep all sorts of useful information such as whether characters can walk on this tile and it will also describe how players can interact with the tile.

1
2
3
4
5
6
7
Game.Tile = function(glyph) {
    this._glyph = glyph;
};

Game.Tile.prototype.getGlyph = function() {
    return this._glyph;
};

Now we want to create different tiles for walls and floors. However because there is no differentiating between a wall in one cell and a wall in another, we can simply have a single instance of both tile types and pass that around! Note that we're also going to create a nullTile which will be returned whenever we try to access an out of bounds tiles. This will be useful as it will prevent us from having to do null checks all the time, and as mentioned by Trystan, follows the Null Object pattern.

1
2
3
Game.Tile.nullTile = new Game.Tile(new Game.Glyph());
Game.Tile.floorTile = new Game.Tile(new Game.Glyph('.'));
Game.Tile.wallTile = new Game.Tile(new Game.Glyph('#', 'goldenrod'));

assets/map.js

Now that we have the notion of a tile, we're ready to create our map! As mentioned before, our Map will simply contain a 2D array of tiles, and we use their index in the first and second dimension to represent the x and y coordinate respectively.

 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
Game.Map = function(tiles) {
    this._tiles = tiles;
    // cache the width and height based
    // on the length of the dimensions of
    // the tiles array
    this._width = tiles.length;
    this._height = tiles[0].length;
};

// Standard getters
Game.Map.prototype.getWidth = function() {
    return this._width;
};
Game.Map.prototype.getHeight = function() {
    return this._height;
};

// Gets the tile for a given coordinate set
Game.Map.prototype.getTile = function(x, y) {
    // Make sure we are inside the bounds. If we aren't, return
    // null tile.
    if (x < 0 || x >= this._width || y < 0 || y >= this._height) {
        return Game.Tile.nullTile;
    } else {
        return this._tiles[x][y] || Game.Tile.nullTile;
    }
};

assets/screens.js

We're now ready to display our map! The map will get drawn when we are currently on the Play screen. It's a good idea to keep all the screen logic and data in the screen's class, so we will contain our map in the Play screen. To do this, we must add the _map property to PlayScreen:

1
2
3
4
Game.Screen.playScreen = {
    _map : null,
    // ...
}

For now let's make it so that a map is randomly generated whenever we enter the screen. There are many algorithms for generating a variety of random maps, such as caves, dungeons, wilderness, etc. A great source of algorithms for this is the RogueBasin articles section and the Procedural Content Wiki. RogueBasin is a great resource overall and I highly recommend you check it out. I've also written a post on generating random dungeons which I encourage you to check out if you are interested! Conveniently, the rot.js library provides a plethora of map generating algorithms in the ROT.Map namespace. They usually return an array of 1s and 0s which can then be mapped to a tile type.

To generate caves as shown in the picture above, we're going to use the ROT.Map.Cellular generator. It uses a strategy called cellular automata to carve out realistic looking caves! The first thing we're going to do is create our empty array of tiles. For now we will make our map the same size as the screen, although this will be refactored in the next part when we introduce scrolling maps. So let's update our enter function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enter: function() {
    var map = [];
    for (var x = 0; x < 80; x++) {
        // Create the nested array for the y values
        map.push([]);
        // Add all the tiles
        for (var y = 0; y < 24; y++) {
            map[x].push(Game.Tile.nullTile);
        }
    }
    // ...
}

Now we're ready to generate our map! The first thing we have to do is create our instance of ROT.Map.Cellular and instantiate it with random values. The randomize function accepts the probability of a given cell starting out as a 1, so we use 0.5 to make it split equally:

1
2
var generator = new ROT.Map.Cellular(80, 24);
generator.randomize(0.5);

Once we've randomized our map, we want to actually apply the map generation method. Because of the way the algorithm works, re-applying it will generate smoother and smoother maps. By smoother I mean the caves are larger, more consistently shaped and generally more connected. In order to iterate, we have to use the ROT.Map.Cellular.create method. Note that in order to update our own map, we have to pass a callback to the create function which accepts an x, y, and either a 1 or a 0 depending on the value that is generated. We only need to to pass this callback on our last application though as that is the only time we actually want to update our map. When updating our map, we will consider a value of 1 to be a floor and a value of 0 to be a wall in order to determine which tile type to use. Here is our completed enter 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
enter: function() {  
    var map = [];
    for (var x = 0; x < 80; x++) {
        // Create the nested array for the y values
        map.push([]);
        // Add all the tiles
        for (var y = 0; y < 24; y++) {
            map[x].push(Game.Tile.nullTile);
        }
    }
    // Setup the map generator
    var generator = new ROT.Map.Cellular(80, 24);
    generator.randomize(0.5);
    var totalIterations = 3;
    // Iteratively smoothen the map
    for (var i = 0; i < totalIterations - 1; i++) {
        generator.create();
    }
    // Smoothen it one last time and then update our map
    generator.create(function(x,y,v) {
        if (v === 1) {
            map[x][y] = Game.Tile.floorTile;
        } else {
            map[x][y] = Game.Tile.wallTile;
        }
    });
    // Create our map from the tiles
    this._map = new Game.Map(map);
},

All that's left to do is actually render our map! Can you guess what function we'll modify next? Because of how we set up our system, all we have to do is iterate through our map tiles and render the glyph!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
render: function(display) {
    // Iterate through all map cells
    for (var x = 0; x < this._map.getWidth(); x++) {
        for (var y = 0; y < this._map.getHeight(); y++) {
            // Fetch the glyph for the tile and render it to the screen
            var glyph = this._map.getTile(x, y).getGlyph();
            display.draw(x, y,
                glyph.getChar(), 
                glyph.getForeground(), 
                glyph.getBackground());
        }
    }
},

index.html

We're all done! Now we can actually see our caves getting rendered, and each time we refresh our game it'll be a brand new cave. All we have to do is update our scripts:

1
2
3
4
5
6
<script src="assets/rot.min.js"></script>
<script src="assets/game.js"></script>
<script src="assets/screens.js"></script>
<script src="assets/glyph.js"></script>
<script src="assets/tile.js"></script>
<script src="assets/map.js"></script>

Conclusion

Go ahead and load up your game and enjoy your hard work so far! Our next post will make it so that we can actually make maps larger than the screen and scroll around!

Remember that all the code for this part can be found at the part 3a tag of the jsrogue repository. I hope you enjoyed this post and that you'll stick around for the next part!

Thanks for reading,

Dominic

Next Part

Part 3b - Exploring Caves