r/roguelikedev summer tutorial

from Red Blob Games
18 Jun 2020

Table of Contents

I had previously followed the r/roguelikedev summer tutorial series, but never finished. I’m trying again this year, using rot.js[1] this time.

Source code: roguelike-dev.js

 1  Getting started#

I’m going to follow the Roguelike Tutorial for Python[2] (2019 version, not the 2020 version, for various reasons) and adapt it for rot.js[3]. Part 0[4] of the tutorial covers setting up Python and libtcod. I’ll instead set up rot.js.

HTML:

<figure id="game"></figure>
<script src="https://unpkg.com/rot-js"></script>
<script src="game.js"></script>

Javascript:

const display = new ROT.Display({width: 60, height: 25});
document.getElementById('game')
        .appendChild(display.getContainer());
display.draw(5, 4, '@');

Set the fontFamily property if you want to override the default browser monospace font. For example, fontFamily: "Roboto Mono".

1.png

A warning about my coding style: I follow make it work before making the code nice. That means I’ll use plenty of global variables and hacks at first, and clean up some of them later. Don’t look at my code as an example of how to structure a program “properly”.

 2  Key input#

Part 1 of the roguelike tutorial[5] covers setting up a screen and keyboard handler. I already set up the screen in the previous section so now I need to set up a keyboard handler. Unlike Python, we don’t write an event loop in the browser. The browser already is running an event loop, and we add event handlers to it.

There are various places to attach a keyboard event handler.

I decided to instead make the game map focusable by adding the tabindex="1" attribute to its canvas. This way, clicking on the game map will give it keyboard focus. You can click away to add a comment and then come back to the game.

Javascript:

const canvas = display.getContainer();
canvas.addEventListener('keydown', handleKeyDown);
canvas.setAttribute('tabindex', "1");
canvas.focus();

The problem is that a canvas isn’t an obviously focusable element. What happens if it ever loses focus? I decided to add a message when the canvas loses focus:

HTML:

<figure>
  <div id="game"></div>
  <div id="focus-reminder">click game to focus</div>
</figure>

Javascript:

const focusReminder = document.getElementById('focus-reminder');
canvas.addEventListener('blur', () => { focusReminder.style.visibility = 'visible'; });
canvas.addEventListener('focus', () => { focusReminder.style.visibility = 'hidden'; });

CSS:

#game canvas { display: block; margin: auto; }

The CSS is not self-explanatory. I use display: block because a <canvas> element is inline by default, and that means it has some extra space below it matching the extra space a line of text has below it to separate it from the next line below. I don’t want that so I change it from an inline element to a block element. I use margin: auto to center the canvas in the parent element.

Here’s what it looks like if it does not have focus:

2.png

The next thing I need is an event handler:

function handleKeyDown(event) {
    console.log('keydown', event);
}

I often start out with a console.log to make sure that a function is getting called.

What’s next for Part 1? I need to make arrow keys move the player around. I can’t do that yet, because I don’t have a player position.

 3  Player movement#

I need to keep track of the player position and then change it when a key is pressed.

let player = {x: 5, y: 4, ch: '@'};

function drawCharacter(character) {
    let {x, y, ch} = character;
    display.draw(x, y, ch);
}

function draw() {
    drawCharacter(player);
}

function handleKeyDown(event) {
    if (event.keyCode === ROT.KEYS.VK_RIGHT) { player.x++; }
    if (event.keyCode === ROT.KEYS.VK_LEFT)  { player.x--; }
    if (event.keyCode === ROT.KEYS.VK_DOWN)  { player.y++; }
    if (event.keyCode === ROT.KEYS.VK_UP)    { player.y--; }
    draw();
}

draw();

3.png

Two problems:

  1. When using arrow keys, the page scrolls. I can fix this by adding event.preventDefault(). But if I do that, then browser hotkeys stop working. So I need to do something a little smarter. I’m going to prevent the default only if I handled the key.
  2. The @ character doesn’t get erased when I move. I need to either draw a space character over the old position, or I need to clear the game board and redraw everything. I’m going to redraw everything. I find it to be simpler and less error prone.

This would be a good time to mention that the rot.js interactive manual doesn’t cover all the functionality. You may also want to look at the non-interactive documentation[8] for a more complete list of methods. In this case, I looked at display/canvas→Canvas[9] to find the clear method.

Part 1[10] of the Python tutorial splits up keyboard handling into a function that generates an action and another function that performs the action. I’ll do the same.

function handleKeys(keyCode) {
    const actions = {
        [ROT.KEYS.VK_RIGHT]: () => ['move', +1, 0],
        [ROT.KEYS.VK_LEFT]:  () => ['move', -1, 0],
        [ROT.KEYS.VK_DOWN]:  () => ['move', 0, +1],
        [ROT.KEYS.VK_UP]:    () => ['move', 0, -1],
    };
    let action = actions[keyCode];
    return action ? action() : undefined;
}
    
function handleKeyDown(event) {
    let action = handleKeys(event.keyCode);
    if (action) {
        if (action[0] === 'move') {
            let [_, dx, dy] = action;
            player.x += dx;
            player.y += dy;
            draw();
        } else {
            throw `unhandled action ${action}`;
        }
        event.preventDefault();
    }
}

function draw() {
    display.clear();
    drawCharacter(player);
}

Ok, that’s better. It only captures keys that are being used for the game, and leaves browser hotkeys alone. And it erases the screen before drawing a new frame.

What else is in Part 1 of the tutorial?

I’m going to skip these two.

 4  Entities#

Part 2[11] of the tutorial covers entities. My design differs slightly from the tutorial:

  1. I include only “world” data in the entity. This is data about the entity in the world, but not the data about the entity being drawn.
  2. I include an entity type string instead. Normally this is “implicit” information in that each object belongs to a class. I prefer making game classes explicit.
  3. I also don’t put methods in this object. I’ve had too many methods that don’t “belong” in any one class, so I prefer to leave them as free functions.
  4. I give each entity an id. I find that useful in debugging. It may come in handy later for serialization or events or logging.
function createEntity(type, x, y) {
    let id = ++createEntity.id;
    return { id, type, x, y };
}
createEntity.id = 0;

let player = createEntity('player', 5, 4);

Here’s an example of how this design differs from the one in the Python tutorial:

function drawEntity(entity) {
    const visuals = {
        player: ['@', "hsl(60, 100%, 50%)"],
        troll: ['T', "hsl(120, 60%, 50%)"],
        orc: ['o', "hsl(100, 30%, 50%)"],
    };

    const [ch, fg, bg] = visuals[entity.type];
    display.draw(entity.x, entity.y, ch, fg, bg);
}

Instead of storing the character and the color in the object, I store a type in the object, and then store the character and color in a lookup table. There are some scenarios where I like this design better:

Ok, cool, I have a way to make entities. Let’s make a second one:

let troll = createEntity('troll', 20, 10);

Now I have to modify the drawing function to draw it too:

function draw() {
    display.clear();
    drawEntity(player);
    drawEntity(troll);
}

4.png

Looks good. The player and monster have different appearances.

I can’t keep adding a variable for each entity. Part 2 of the Roguelike Tutorial converts the individual entity variables into an set of entities. I was going to use an array or a Set but decided to use a Map instead.

let entities = new Map();
function createEntity(type, x, y) {
    let id = ++createEntity.id;
    let entity = { id, type, x, y };
    entities.set(id, entity);
    return entity;
}
createEntity.id = 0;

Then when I draw them, I can loop over entities:

function draw() {
    display.clear();
    for (let entity of entities.values()) {
        drawEntity(entity);
    }
}

 5  Map#

The second half of Part 2[12] creates a map data structure, and Part 3 generates a dungeon map. ROT.js includes dungeon map creation functions so I’ll use one of their algorithms. ROT will call a callback function for each map tile, 0 for walkable and 1 for wall. I’m going to store this data in a Map, indexed by a string x,y. For example at position x=3, y=5, I’ll use a string key "3,5".

function createMap(width, height) {
    let map = {
        width, height,
        tiles: new Map(),
        key(x, y) { return `${x},${y}`; },
        get(x, y) { return this.tiles.get(this.key(x, y)); },
        set(x, y, value) { this.tiles.set(this.key(x, y), value); },
    };

    const digger = new ROT.Map.Digger(width, height);
    digger.create((x, y, contents) => map.set(x, y, contents));
    return map;
}
let map = createMap(60, 25);

The next step is to draw the map. I want to draw it first, before the player or monsters.

function draw() {
    display.clear();
    for (let y = 0; y < map.height; y++) {
        for (let x = 0; x < map.width; x++) {
            if (map.get(x, y)) {
                display.draw(x, y, '⨉', "hsl(60, 10%, 40%)", "gray");
            } else {
                display.draw(x, y, '·', "hsl(60, 50%, 50%)", "black");
            }
        }
    }
    for (let entity of entities.values()) {
        drawEntity(entity);
    }
}

5.png

The final step is to make player movement not allow moving onto a wall. I modified the movement function to check if the map tile is 0. This is slightly different from checking that it’s not 1 in that this will automatically makes sure I don’t walk off the map, where the values are undefined.

function handleKeyDown(event) {
    let action = handleKeys(event.keyCode);
    if (action) {
        if (action[0] === 'move') {
            let [_, dx, dy] = action;
            let newX = player.x + dx,
                newY = player.y + dy;
            if (map.get(newX, newY) === 0) {
                player.x = newX;
                player.y = newY;
            }
            draw();
        } else {
            throw `unhandled action ${action}`;
        }
        event.preventDefault();
    }
}

The dungeon generation algorithm also generates a list of rooms and corridors. This might be useful later.

 6  Field of view#

ROT.js includes two field of view algorithms[13]. The field of view library is fairly easy to use. The input callback lets it ask you “can you see through x,y?” and the output callback lets it tell you “there’s this much light at x,y”. I saved the results in a Map and used it for calculating the light level at any point. The Python tutorial doesn’t use the light level but maybe I’ll find something to do with it later.

const fov = new ROT.FOV.PreciseShadowcasting((x, y) => map.get(x, y) === 0);

function draw() {
    display.clear();

    let lightMap = new Map();
    fov.compute(player.x, player.y, 10, (x, y, r, visibility) => {
        lightMap.set(map.key(x, y), visibility);
    });
                
    const colors = {
        [false]: {[false]: "rgb(50, 50, 150)", [true]: "rgb(0, 0, 100)"},
        [true]: {[false]: "rgb(200, 180, 50)", [true]: "rgb(130, 110, 50)"}
    };
    for (let y = 0; y < map.height; y++) {
        for (let x = 0; x < map.width; x++) {
            let lit = lightMap.get(map.key(x, y)) > 0.0,
                wall = map.get(x, y) !== 0;
            let color = colors[lit][wall];
            display.draw(x, y, ' ', "black", color);
        }
    }
    for (let entity of entities.values()) {
        if (lightMap.get(map.key(entity.x, entity.y)) > 0.0) {
            drawEntity(entity);
        }
    }
}

Ok, this seems like it’s not too hard. Looks cool:

6.png

But there’s a problem: the entities (@ and T) are getting drawn with a black background color, not with the map background. In libtcod, I can set the background and foreground separately, so in the official tutorial the map sets the background and the entity sets the foreground and character. In ROT.js, I have to set all three at once.

I need to merge my drawing loops somehow.

I’m going to remove the drawEntity() function and replace it with a lookup function. Instead of drawing to the screen it only tells the draw() function what to draw.

/** return [char, fg, optional bg] for a given entity */
function entityGlyph(entityType) {
    const visuals = {
        player: ['@', "hsl(60, 100%, 70%)"],
        troll: ['T', "hsl(120, 60%, 30%)"],
        orc: ['o', "hsl(100, 30%, 40%)"],
    };
    return visuals[entityType];
}

Now the draw function has more logic, because it’s merging the entity glyph with the map background color:

function draw() {
    display.clear();

    let lightMap = new Map(); // map key to 0.0–1.0
    fov.compute(player.x, player.y, 10, (x, y, r, visibility) => {
        lightMap.set(map.key(x, y), visibility);
    });

    let glyphMap = new Map(); // map key to [char, fg, optional bg]
    for (let entity of entities.values()) {
        glyphMap.set(map.key(entity.x, entity.y), entityGlyph(entity.type));
    }
    
    const mapColors = {
        [false]: {[false]: "rgb(50, 50, 150)", [true]: "rgb(0, 0, 100)"},
        [true]: {[false]: "rgb(200, 180, 50)", [true]: "rgb(130, 110, 50)"}
    };
    for (let y = 0; y < map.height; y++) {
        for (let x = 0; x < map.width; x++) {
            let lit = lightMap.get(map.key(x, y)) > 0.0,
                wall = map.get(x, y) !== 0;
            let ch = ' ',
                fg = "black",
                bg = mapColors[lit][wall];
            let glyph = glyphMap.get(map.key(x, y));
            if (glyph) {
                ch = lit? glyph[0] : ch;
                fg = glyph[1];
                bg = glyph[2] || bg;
            }
            display.draw(x, y, ch, fg, bg);
        }
    }
}

Now the background colors behind entities look reasonable:

7.png

The background color comes from the map and the foreground color and character comes from the entity.

The next step is to implement the three states of the map:

  1. Unexplored: don’t show anything.
  2. Explored, but not currently visible: show in blue.
  3. Visible: show in yellow.

For this I’ll add a flag explored to the map. It will start out false and become true if the tile is ever visible. I realized that my map object isn’t great. It has a get and set but that is returning 0 for a floor and 1 for a tile. I also have other similar types of maps like lightMap and a glyphMap.

I’m going to make a wrapper around 2d maps from (x,y) to any value:

function createMap(initializer) {
    function key(x, y) { return `${x},${y}`; }
    return {
        _values: new Map(),
        at(x, y) {
            let k = key(x, y);
            if (!this._values.has(k)) { this._values.set(k, initializer()); }
            return this._values.get(k);
        },
    };
}

I replaced my game map data structure with the generic one:

function createTileMap(width, height) {
    let tileMap = createMap();
    const digger = new ROT.Map.Digger(width, height);
    digger.create((x, y, contents) =>
        tileMap.set(x, y, {
            walkable: contents === 0,
            wall: contents === 1,
            explored: false,
        })
    );
    return tileMap;
}

A note about data structure: I used to fall into a loop. I would put a lot of effort into the core data structures, figuring out class hierarchies, modules, extensibility, generics, patterns, etc. Then I would use it for a bit and realize something isn’t great. But I wouldn’t change it because I had put so much effort into it that it was really hard to justify throwing anything away.

These days I don’t start with the right data structures. Instead, I start with something and then plan to change it once I figure out what I want. I discover the best patterns while working on the project, instead of starting with the patterns and then making the project fit. Because I put so little effort into the initial code, it’s no big deal to throw it out and replace it with something better.

I changed the data structures for this project four times already, and it was still faster than if I had tried to figure out everything ahead of time. I’m optimizing for making it easy to make changes.

Now that I have a 2d sparse map data structure, I’ll reuse it for the light and glyph maps. While calculating the light map, I also update the explored flag in the tile map. Another possible design would be to keep a separate exploredMap instead of modifying the tile map; that would allow for multiple explored maps corresponding to different player characters. But this will do for now.

function computeLightMap(center, tileMap) {
    let lightMap = createMap(); // 0.0–1.0
    fov.compute(center.x, center.y, 10, (x, y, r, visibility) => {
        lightMap.set(x, y, visibility);
        if (visibility > 0.0) {
            if (tileMap.has(x, y))
            tileMap.get(x, y).explored = true;
        }
    });
    return lightMap;
}

function computeGlyphMap(entities) {
    let glyphMap = createMap(); // [char, fg, optional bg]
    for (let entity of entities.values()) {
        glyphMap.set(entity.x, entity.y, entityGlyph(entity.type));
    }
    return glyphMap;
}

Here’s the new draw() function:

const mapColors = {
    [false]: {[false]: "rgb(50, 50, 150)", [true]: "rgb(0, 0, 100)"},
    [true]: {[false]: "rgb(200, 180, 50)", [true]: "rgb(130, 110, 50)"}
};
function draw() {
    display.clear();

    let lightMap = computeLightMap(player, tileMap);
    let glyphMap = computeGlyphMap(entities);
    
    for (let y = 0; y < HEIGHT; y++) {
        for (let x = 0; x < WIDTH; x++) {
            let tile = tileMap.get(x, y);
            if (!tile || !tile.explored) { continue; }
            let lit = lightMap.get(x, y) > 0.0;
            let ch = ' ',
                fg = "black",
                bg = mapColors[lit][tile.wall];
            let glyph = glyphMap.get(x, y);
            if (glyph) {
                ch = lit? glyph[0] : ch;
                fg = glyph[1];
                bg = glyph[2] || bg;
            }
            display.draw(x, y, ch, fg, bg);
        }
    }
}

And hey, it works!

8.png

 7  Enemies#

Part 5 of the Python tutorial adds monsters to rooms.

One of the things the Python tutorial uses is the Python randint() function. ROT.js’s manual[14] shows that it has getUniform(), which I can wrap to make a randint() function. However if you dig deeper, ROT.js actually has the randint function[15], called getUniformInt(). There seem to be a lot of things that aren’t covered in the manual.

I made a shortcut for it:

const randint = ROT.RNG.getUniformInt.bind(ROT.RNG);

and then used it for the monster creating function:

function createMonsters(room, maxMonstersPerRoom) {
    let numMonsters = randint(0, maxMonstersPerRoom);
    for (let i = 0; i < numMonsters; i++) {
        let x = randint(room.getLeft(), room.getRight()),
            y = randint(room.getTop(), room.getBottom());
        if (!entityAt(x, y)) {
            let type = randint(0, 3) === 0? 'troll' : 'orc';
            createEntity(type, x, y);
        }
    }
}

But what is a room? The ROT.js dungeon digger records room objects in addition to tiles. I stored these in the tileMap for now.

function createTileMap(width, height) {
    let tileMap = createMap();
    const digger = new ROT.Map.Digger(width, height);
    digger.create(…);
    tileMap.rooms = digger.getRooms();
    tileMap.corridors = digger.getCorridors();
    return tileMap;
}

and then used them to make monsters in each room:

for (let room of tileMap.rooms) {
    createMonsters(room, 3);
}

Cool, it works! (Note: I disabled FOV for this screenshot)

9.png

Or … does it? Why are they all orcs?! I thought there must be a bug in my code, but no, it’s just random luck. If I change the seed I get both trolls and orcs.

10.png

The next step is that they add a blocks flag to each Entity. I decided to make that a property of the entity type.

const ENTITY_PROPERTIES = {
    player: {blocks: true, visuals: ['@', "hsl(60, 100%, 70%)"],},
    troll: {blocks: true, visuals: ['T', "hsl(120, 60%, 30%)"],},
    orc: {blocks: true, visuals: ['o', "hsl(100, 30%, 40%)"],},
};

As I mentioned earlier, I’ll often do something and then change how it works later. I’m replacing the entityGlyph() function with this table.

I modified the handleKeyDown() function to check if there’s already an entity there:

let newX = player.x + dx,
        newY = player.y + dy;
    if (tileMap.get(newX, newY).walkable) {
        let target = entityAt(newX, newY);
        if (target && ENTITY_PROPERTIES[target.type].blocks) {
            console.log(`You kick the ${target.type} in the shins, much to its annoyance!`);
            // TODO: draw this to the screen
        } else {
            player.x = newX;
            player.y = newY;
        }
    }
…

I tested this and it worked. Moving into a monster prints an message to the console.

The next section in the Python tutorial sets up a state PLAYER_TURN and ENEMY_TURN. I didn’t like the way it worked, because it will ignore the player keypress during the enemy turn. I don’t quite know what I want to do about it.

I think for now I’ll have the enemies move after each player move. I moved the above code into its own function:

function enemiesMove() {
    for (let entity of entities) {
        if (entity !== player) {
            console.log(`The ${entity.type} ponders the meaning of its existence.`);
        }
    }
}

Since my random number generator produced all orcs, I get a lot of console messages:

The orc ponders the meaning of its existence.

Great! Before I move on to the next part of the tutorial, I wanted to add a way to see the messages under the game screen.

 8  Console#

I updated the UI to have an extra div for messages, and put the instructions box below it:

<figure>
  <div id="game"></div>
  <pre id="messages"></pre>
  <div id="instructions"/>
</figure>

I gave it some style:

#messages {
    box-sizing: border-box;
    font-size: 0.8em;
    height: 6em; /* see explanation below */
    line-height: 1.0;
    background: black;
    color: white;
    margin: 0;
    padding: 0.5em 1em;
    text-align: left;
}

The size calculation was a little tricky. I want the height to be 5 lines tall. A line is typically line-height times font-size. I set the line height to 1.0 so it seems like the height will be 1.0 * 0.8em * 5 = 4em. But it’s not! The css for <pre> em is relative to the <pre>’s font size, except for font-size: 0.8em which is relative to the parent <figure>’s font size. So it’s really 1.0 * 1em = 5em. Plus, with box-sizing: border-box I need to include the size of the padding. Both the top and bottom padding are 0.5em here so that means the total height of the box is 6em.

Ok, and here’s the Javascript to print a line of text to the message area:

function print(message) {
    const MAX_LINES = 5;
    let messages = document.querySelector("#messages");
    let lines = messages.textContent.split("\n");
    lines.push(message);
    while (lines.length > MAX_LINES) { lines.shift(); }
    messages.textContent = lines.join("\n");
}

And here’s the updated code for the instructions box, which used to hide/show “Click game to focus” but now replaces that text with “Arrow keys to move”:

function setupKeyboardHandler(display, handler) {
    const canvas = display.getContainer();
    const instructions = document.getElementById('instructions');
    canvas.setAttribute('tabindex', "1");
    canvas.addEventListener('keydown', handleKeyDown);
    canvas.addEventListener('blur', () => { instructions.textContent = "Click game for keyboard focus"; });
    canvas.addEventListener('focus', () => { instructions.textContent = "Arrow keys to move"; });
    canvas.focus();
}

Here’s what it looks like:

11.png

Back to the Python tutorial.

 9  Combat#

Part 6[16] of the Python roguelike tutorial adds a “fighter” component with hp, max_hp, defense, power, and an “ai” component that tells the monster how to move.

My own coding style, as you may have noticed from earlier on the page, is to separate out “static” properties into their own structure. I want to be able to save the game, change the static properties, and load the game to get the new values. I treat differently properties like position and color. The position is a “per object” property of the troll. It is different for each troll. The color is a “class” property of the troll. The color is the same for all trolls. These static class properties went into ENTITY_PROPERTIES.

I think I’ll treat defense and power and max_hp as static properties, and hp as a per object property.

Email me , or tweet @redblobgames, or comment: