So far, the dungeon has been empty. You can explore it, and the game remembers what you've seen, but nothing down there ever moves. A traditional roguelike is full of things that want to eat you, and it's time this one was too.
This chapter brings in the first monsters: giant white rats, jackals, kobolds, and goblins, each drawn as a letter in its own color. They're placed in the rooms when the dungeon is made, and they take a turn after every step you take. Most of them wander. But a monster that sees you comes for you, one step for each of yours, until it's standing beside you.
The next image shows the end of the chapter. A jackal spotted the player from across its room, and came to meet them. The player has just tried to walk into it.

The jackal can't hurt anyone yet. That's the next chapter's job.
Chapter 05 in the book's repository holds this chapter's finished project. The chapter carries on in your own project from Chapter 4, or from the book's Chapter 04 folder.In this chapter, we will:
- Gather the game's dice into one class,
Rng, that everything random in a run shares - Use Visual Studio's Find and Replace to change many lines at once
- Describe each kind of monster once, in a table, and point every monster at its kind
- Place monsters in the rooms, and draw the ones the player can see
- Stop the player from walking through them
- Give the monsters a turn, in which they wander, or come for you if they can see you
There are five checkpoints along the way. Let's wake something up.
Planning the Chapter
This chapter adds three files, and changes four:
| File | Status | What it's for |
|---|---|---|
src/Rng.h, src/Rng.cpp |
New | The dice: one random number engine for the whole run |
src/Monster.h |
New | The kinds of monster, and the monsters in the dungeon |
src/RoomGen.h, src/RoomGen.cpp |
Changed | The dungeon generator rolls the new dice |
src/Common.h |
Changed | Points can be compared, and the eight directions to a neighbor |
src/main.cpp |
Changed | Places the monsters, draws them, and moves them |
| The other files | Unchanged |
The next image shows how the files fit together at the end of the chapter, with the new ones filled in blue, and the changed ones outlined in blue.

A monster in this game is plain data: where it is, and what kind it is. Everything about a kind, such as its name, its letter, and its color, is written down once, in a table. Each monster points to its kind's row. That's a pattern called a type object.
A new kind of monster is a new row in the table, rather than a new class. In Chapter 16, the table moves out of the code altogether, into a text file.
For now, the code that places and moves the monsters lives in main.cpp, beside the code that moves the player. Chapter 7 gathers the whole game into a class, and they move there with everything else.
We'll build the chapter in four steps: the dice, the monsters themselves, their turn, and the chase.
One Set of Dice
Until now, only the dungeon generator has needed dice. It has used the engine that runGame makes, a std::mt19937, through a small helper of its own, roll. Now the monsters need dice too: to pick where each one goes, and what kind it is, and later, which way it wanders. The next chapter's combat needs them, and later chapters need them for items, traps, and treasure.
All of them should share one engine, started from the run's seed. Then the seed decides everything random in a run. Give the game the same seed, press the same keys, and the same things happen, every time. That's a great help when something goes wrong. You can play the run again and watch it go wrong again, as often as you need to.
The engine hands out its numbers in order, and each part of the game takes the next ones it needs. The next image shows how one seed serves a whole run.

The dungeon takes the first rolls, the monsters take the next, and every turn takes a few more. So any change in what's rolled, or when, changes everything after it. That's why the monsters are placed after the dungeon is made. The dungeon's rolls still come first, so seed 7 still makes the dungeon it made in Chapter 2.
The Rng Class
An engine and a helper that rolls it belong together, so they become a class. In Solution Explorer, right-click src, choose Add > New Item, and add a Header File (.h) called Rng.h. Type the whole file, so that it reads like this:
#pragma once
#include <random> // std::mt19937 and std::uniform_int_distribution
// The dice. Everything random in a run comes from one Rng, started from
// the run's seed, so the same seed always plays out the same way
class Rng
{
public:
explicit Rng(unsigned int seed);
// A whole number from low to high, both included
int roll(int low, int high);
private:
std::mt19937 engine_;
};
An Rng holds the engine, engine_. Its one function, roll, gives a whole number from low to high, both included. So rng.roll(1, 6) rolls a die, and rng.roll(0, 1) tosses a coin.
The constructor takes the seed. It's explicit, which stops C++ from turning a number into an Rng by itself, wherever it expects one. Without it, a stray number could quietly become a whole new set of dice. With it, you have to ask for one, as in Rng rng(seed).
In the same way, add a C++ File (.cpp) called Rng.cpp, and type the whole file:
#include "Rng.h"
Rng::Rng(unsigned int seed)
: engine_(seed)
{
}
int Rng::roll(int low, int high)
{
return std::uniform_int_distribution<int>(low, high)(engine_);
}
The constructor starts the engine from the seed, in its initializer list. Then roll makes a std::uniform_int_distribution for the range it's asked for, and uses it once, on the engine. A distribution is cheap to make, since it only holds the range. So making a new one for each roll keeps roll simple. It's exactly what the generator's helper did, so every roll gives the same number as before.
Rolling the New Dice
The dungeon generator now takes an Rng, rather than the engine. Open RoomGen.h, and find the includes below #include <SDL3/SDL.h>:
#include <random> // std::mt19937, for the dungeon's dice
#include <vector> // std::vector, for the rooms
#include "Common.h"
#include "Map.h"
Replace them with these lines:
#include <vector> // std::vector, for the rooms
#include "Common.h"
#include "Map.h"
#include "Rng.h"
Now RoomGen.h includes Rng.h, rather than <random>, since it no longer names the engine itself. Then, in the declaration of generate, change std::mt19937& rng to Rng& rng, so that it takes the run's Rng, by reference, and rolls the same dice as the rest of the game.
Now open RoomGen.cpp. Its own roll helper isn't needed any more, since Rng has one. Find the roll helper, and delete it, along with the blank line below it:
int roll(std::mt19937& rng, int low, int high)
{
return std::uniform_int_distribution<int>(low, high)(rng);
}
The rest of the changes to RoomGen.cpp are the same few words, in many places. Every std::mt19937& rng becomes Rng& rng, and every call such as roll(rng, 0, 1) becomes rng.roll(0, 1). You could find each one by eye, but Visual Studio can change them all at once.
Find and Replace
Click anywhere in RoomGen.cpp, and press Ctrl+H to open Find and Replace. It appears at the top right of the file, with a box for the text to find, and below it, a box for what to put in its place. Type std::mt19937& rng in the top box, and Rng& rng in the box below it. Then press Alt+A, which is Replace All, or click the Replace All button to the right of the lower box.
That changes all four parameters, in addRoom, carveCorridor, split, and generate. Now the calls. Type roll(rng, in the top box, with a space after the comma, and rng.roll( in the box below it, and press Alt+A again, for Replace All. That changes all eight calls, so that roll(rng, MIN_ROOM, maxW), for instance, becomes rng.roll(MIN_ROOM, maxW). Press Esc to close the box.
Replace All is quick, but it's blind: it changes every match, including any you didn't mean. Here, those words appear nowhere else, so it's safe. Even so, it's worth reading over what it changed, before you build.
The Run's Rng
Last, runGame makes the run's Rng. Open main.cpp, and add its header below #include "Player.h":
#include "Rng.h"
Now main.cpp includes Rng.h, since it makes the Rng. In runGame, change std::mt19937 rng(seed); to Rng rng(seed);, so that rng is an Rng, made from the seed, just as the engine was, and generate takes it, as its new declaration says.
The Space key starts a new dungeon, from a new seed. In its code, change rng.seed(seed); to rng = Rng(seed);, so that it makes a new Rng from the new seed, and assigns it to rng, rather than reseeding the old engine. An Rng has no function to reseed it, and doesn't need one, since a new one is just as good.
Checkpoint: Press F5. The game plays exactly as it did. Type 7 into Command Arguments, as Chapter 2 showed, and the dungeon is the one in Figure 2.1, cell for cell. Every roll gives just what the old helper gave.
Monsters
Now for the monsters themselves. Each needs a kind, a place on the map, and a way to be drawn. And the player needs to know when one is in the way.
The Monster Table
In Solution Explorer, right-click src, choose Add > New Item, and add a Header File (.h) called Monster.h. Type its first part, the kinds of monster:
#pragma once
#include <SDL3/SDL.h>
#include "Common.h"
// What every monster of one kind has in common. Chapter 16 moves these
// into a text file, so new kinds need no recompiling
struct MonsterDef
{
const char* name;
char glyph;
SDL_Color color;
};
// Every kind of monster there is, so far. The inline makes it one table
// shared by every file that includes this header, not a copy in each
inline constexpr MonsterDef MONSTER_DEFS[] = {
{ "giant white rat", 'r', { 215, 215, 205, 255 } },
{ "jackal", 'j', { 205, 150, 80, 255 } },
{ "kobold", 'k', { 110, 185, 95, 255 } },
{ "goblin", 'g', { 160, 205, 60, 255 } },
};
In the preceding code, a MonsterDef is what every monster of one kind has in common: a name, for the messages; a glyph, the letter that's drawn; and a color. The table of kinds is MONSTER_DEFS, with a row for each. There are four so far, the plain monsters of the dungeon's first floors. The rest arrive in later chapters, along with whatever makes them different, such as the cave spider's venom in Chapter 14.
The name is a const char*, a plain string literal, rather than a std::string. The table is constexpr, so the compiler builds it before the game ever runs, and a std::string can't be built that way.
The table is also inline. A constexpr variable in a header is otherwise private to each .cpp file that includes it, so each file would get a copy of its own. With inline, there's one table for the whole game, shared by every file. That matters because monsters will point into the table, and a pointer to a row should mean the same row everywhere.
Now the monster itself. Add it below the table:
// One monster in the dungeon: where it is, and what kind it is
struct Monster
{
Point pos;
const MonsterDef* def = nullptr;
};
A Monster has a place, pos, and def, a pointer to its kind's row in the table. A pointer is all it needs, since everything about the kind is in the row. Every jackal points to the same row, and the row is stored once. The pointer starts as nullptr, which means no kind at all, until a monster is given one. The next image shows three monsters, each pointing to its kind.

So monster.def->name is a monster's name, and monster.def->glyph is its letter. In the next chapter, monsters get health. Each needs its own, since a hurt jackal mustn't hurt the others. So health will be copied into each monster, while what never changes stays in the table.
Comparing Points
The game will ask whether a monster stands on a particular cell, which means comparing two Points. C++ can write that comparison for us. Open Common.h, and add these lines to Point, below int y = 0;:
// Two points are equal when their x and y both are. C++20 writes this
// comparison for us, member by member
bool operator==(const Point& other) const = default;
The function operator== is the one that == calls. Saying = default, which is new in C++20, asks the compiler to write it: two points are equal if their xs are equal and their ys are equal. C++20 also makes != from it, so a != b works too, as !(a == b).
Where the Monsters Go
The rest of the monsters' code goes into main.cpp, in ten places. Some are new lines in slots between the code that's there, and some change lines that are there already. The next image maps them out.

As before, if a slot's place still isn't clear, the Chapter 05 folder in the repository holds main.cpp as it is at the end of the chapter.
At slot A, the includes. Find the three from the standard library:
#include <random> // std::random_device and std::mt19937, for the dice
#include <string> // std::string and std::to_string, for the panel
#include <vector> // std::vector, for the rooms and the log
Replace them with these lines:
#include <iterator> // std::size, to count the kinds of monster
#include <random> // std::random_device, for a random seed
#include <string> // std::string and std::to_string, for the panel
#include <vector> // std::vector, for the rooms, the log and monsters
One header, <iterator>, is new, for std::size, which counts the rows in the table. The comments line up again. The one for <random> says it's only for std::random_device now, since main.cpp no longer makes the engine itself. And the one for <vector> mentions the monsters. At slot B, add the monsters' header below #include "MessageLog.h":
#include "Monster.h"
With Monster.h included, main.cpp has the table and the monsters. At slot C, add how many monsters a new dungeon gets, below SIGHT_RADIUS:
// How many monsters a new dungeon starts with
constexpr int MONSTER_COUNT = 8;
The constant MONSTER_COUNT is eight, enough to meet a few as you explore, but not so many that the dungeon is crowded. Chapter 15 makes the count grow as you go deeper.
Placing the Monsters
Slot D holds two functions. The first finds the monster on a cell, if there is one. Add it below toggleFullscreen:
// The monster standing on a cell, or nullptr if there's none
Monster* monsterAt(std::vector<Monster>& monsters, Point pos)
{
for (Monster& monster : monsters)
{
if (monster.pos == pos)
{
return &monster;
}
}
return nullptr;
}
The function monsterAt looks at each monster in turn, by reference. It returns a pointer to the first one whose pos is the cell, using the == that Point now has. If none is there, it returns nullptr. Returning a pointer, rather than a copy, means the caller gets the monster itself, which matters once monsters can be hurt.
The second function puts the monsters in the dungeon. It's long, so it comes in two parts. Add the first below monsterAt:
// Puts MONSTER_COUNT monsters of random kinds on the floor of the
// rooms, one to a cell. The first room is the player's, so it's left
// empty
std::vector<Monster> placeMonsters(const std::vector<SDL_Rect>& rooms,
Rng& rng)
{
std::vector<Monster> monsters;
int lastRoom = static_cast<int>(rooms.size()) - 1;
int lastKind = static_cast<int>(std::size(MONSTER_DEFS)) - 1;
if (lastRoom < 1)
{
return monsters;
}
This function, placeMonsters, takes the rooms and the dice, and will return the monsters it makes. The first room, rooms[0], is the player's. So monsters go in the others, from room 1 to lastRoom. The kinds run from 0 to lastKind. Since std::size counts them, a new row in the table is picked up by itself.
A dungeon with only one room would have nowhere to put a monster, so the function returns no monsters at all. The generator always makes more rooms than that. But the check is cheap, and it means roll is never asked for a number from 1 to 0.
Add the second part below the if, with a blank line in between:
for (int i = 0; i < MONSTER_COUNT; ++i)
{
// A few tries each: a cell that's taken just means another roll
for (int tries = 0; tries < 20; ++tries)
{
const SDL_Rect& room = rooms[rng.roll(1, lastRoom)];
Point pos = { room.x + rng.roll(0, room.w - 1),
room.y + rng.roll(0, room.h - 1) };
if (monsterAt(monsters, pos) == nullptr)
{
Monster monster;
monster.pos = pos;
monster.def = &MONSTER_DEFS[rng.roll(0, lastKind)];
monsters.push_back(monster);
break;
}
}
}
return monsters;
}
Each monster gets up to 20 tries. A try picks a room, and a cell in it. When no monster is on that cell, the try makes a monster of a random kind there, and break ends the tries. If the cell is taken, the next try rolls again.
A monster whose 20 tries all hit taken cells is left out, which almost never happens with eight monsters and so many cells. Every cell a try picks is one a monster can stand on, since a room's rectangle covers just its floor.
Drawing the Monsters
At slot E, the monsters are drawn. Add this function below drawMap:
// The monsters the player can see. With revealMap, all of them
void drawMonsters(SDL_Renderer* renderer, const GlyphCache& glyphs,
const Map& map, const std::vector<Monster>& monsters,
bool revealMap)
{
for (const Monster& monster : monsters)
{
if (map.isVisible(monster.pos.x, monster.pos.y) || revealMap)
{
drawCell(renderer, glyphs, monster.pos.x, monster.pos.y,
monster.def->glyph, monster.def->color);
}
}
}
A monster is drawn only if its cell is visible, or the map is revealed with F1. Unlike walls, monsters aren't remembered. One you saw a moment ago may have moved, so drawing it where it was would be a lie. Each is drawn with its kind's glyph and color, from the table.
At slot F, drawFrame draws the monsters, and needs them to do it. Replace the whole of drawFrame with this:
void drawFrame(SDL_Renderer* renderer, const GlyphCache& glyphs,
const Map& map, const std::vector<Monster>& monsters,
const Player& player, const MessageLog& log, int turn,
unsigned int seed, bool revealMap)
{
setColor(renderer, Palette::BACKGROUND);
SDL_RenderClear(renderer);
drawMap(renderer, glyphs, map, revealMap);
drawMonsters(renderer, glyphs, map, monsters, revealMap);
drawCell(renderer, glyphs, player.pos.x, player.pos.y, '@',
Palette::PLAYER);
drawPanels(renderer);
drawSidePanel(glyphs, turn, seed, revealMap);
drawLog(glyphs, log);
SDL_RenderPresent(renderer);
}
Now drawFrame takes the monsters, as a const reference, since drawing doesn't change them. The map is drawn first, then the monsters, then the player. Whatever's drawn later covers what's beneath it, so the order works like layers. And drawFrame takes nine parameters now. That's the cost of passing everything around that Chapter 2 warned of, until Chapter 7 puts an end to it.
A Dungeon with Monsters
Now runGame makes the monsters. At slot G, the comment above the seed should mention them. Replace the comment above std::random_device entropy; with these lines:
// Everything random in a run comes from one Rng, started from one
// seed: the same seed always gives the same dungeon and monsters
At slot H, find the lines that make the dungeon:
// The dungeon, the player in the middle of its first room, and what
// the player can see from there
Map map;
std::vector<SDL_Rect> rooms = RoomGen::generate(map, rng);
Replace them with these lines:
// The dungeon, its monsters, the player in the middle of the first
// room, and what the player can see from there
Map map;
std::vector<SDL_Rect> rooms = RoomGen::generate(map, rng);
std::vector<Monster> monsters = placeMonsters(rooms, rng);
The monsters are placed as soon as the rooms exist, from the same rng, so their rolls come after the dungeon's. At slot I, a new dungeon needs new monsters too. Add this line to the Space key's code, below rooms = RoomGen::generate(map, rng);:
monsters = placeMonsters(rooms, rng);
The old monsters are replaced by a new set, in the new dungeon's rooms. At slot J, the loop passes the monsters to drawFrame. Find this call:
drawFrame(renderer, glyphs, map, player, log, turn, seed,
revealMap);
And change it to these lines:
drawFrame(renderer, glyphs, map, monsters, player, log,
turn, seed, revealMap);
The monsters go to drawFrame in the same place as in its parameters, after the map. That's every slot filled.
Checkpoint: Press F5. The first room is empty, as it always is, but walk into the others, and you'll find monsters standing in them. Press F1 to see where they all are.
The next image shows seed 42's monsters, with F1 pressed. There are eight, spread through the rooms, and none in the first.

Walk into a monster, and you'll walk right over it, since nothing stops you yet. The @ covers it, because the player is drawn last. That's next.
In the Way
A monster's cell isn't somewhere the player can step. For now, bumping into one just says so, and takes no turn. So the comment above the step should say that too. In the walking code, replace the comment above Point step; with these lines:
// A step takes a turn. Bumping into a wall or a monster
// doesn't
Then find the check for floor, a few lines below it:
if (map.at(to.x, to.y) == Terrain::Floor)
Replace it with these lines:
Monster* monster = monsterAt(monsters, to);
if (monster != nullptr)
{
std::string name = monster->def->name;
log.add("The " + name + " is in the way.");
}
else if (map.at(to.x, to.y) == Terrain::Floor)
The cell the player is stepping to is checked for a monster first. If there's one, the log says it's in the way, and nothing else happens. Otherwise, the floor check goes on as before, with else if.
The name is copied into a std::string first, for a reason. The text "The " is an array of characters, and monster->def->name is a const char*, a pointer. Written as "The " + monster->def->name, the + would try to add two pointers, which means nothing. So the compiler refuses. Once name is a std::string, + joins text, as it does for any std::string.
Checkpoint: Press F5, find a monster, and walk into it. The log says it's in the way, and the turn count doesn't change. Walk into it again, and the message counts up, as Chapter 3's log does for a repeated message. The next chapter turns the bump into an attack.
The Monsters' Turn
The monsters are there, but they're statues. In a roguelike, the dungeon moves when you do: you take a step, then every monster takes its turn, and then it's your move again. Nothing happens while you think, so you can take as long as you like over each step.
Eight Directions
A monster that wanders needs a direction. Chapter 3's KEY_STEPS holds the eight steps, but each belongs to a key. The monsters need just the steps. Open Common.h, and add them below Point:
// The steps from a cell to its eight neighbors, starting north and going
// clockwise
constexpr Point DIRECTIONS[] = {
{ 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 },
{ 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 },
};
The array DIRECTIONS holds the step to each of a cell's eight neighbors, starting north and going clockwise. Adding a step to a cell's x and y gives the neighbor. The next image shows them, with their places in the array.

It's in Common.h, rather than main.cpp, because more than the monsters will use it. Chapter 10's maps of distances look at every cell's neighbors too.
Wandering
Now the monsters' turn, as a function. Open main.cpp, and add it below placeMonsters:
// The monsters' turn: each takes a step in a random direction, if the
// cell there is free floor, and otherwise stays where it is
void moveMonsters(const Map& map, std::vector<Monster>& monsters,
const Player& player, Rng& rng)
{
for (Monster& monster : monsters)
{
Point step = DIRECTIONS[rng.roll(0, 7)];
Point to = { monster.pos.x + step.x, monster.pos.y + step.y };
if (map.at(to.x, to.y) == Terrain::Floor && to != player.pos &&
monsterAt(monsters, to) == nullptr)
{
monster.pos = to;
}
}
}
Each monster rolls one of the eight directions, from 0 to 7, and works out the cell that step leads to. It moves there only if the cell is floor, not the player's, and not another monster's. Otherwise, it stays where it is for this turn. So monsters never walk through walls, or each other. The test to != player.pos uses the != that C++20 made from Point's ==.
The monsters move one at a time, in order, and each sees where the others are now. So two monsters can never step into the same cell, since the second finds the first already there.
The function needs calling after each of the player's steps. In the walking code, find the lines that move the player:
player.pos = to;
Fov::compute(map, player.pos, SIGHT_RADIUS);
++turn;
Replace them with these lines:
// The player's step, then the monsters' turn
player.pos = to;
Fov::compute(map, player.pos, SIGHT_RADIUS);
moveMonsters(map, monsters, player, rng);
++turn;
A step goes in order: the player moves, the field of view is worked out from where they now stand, the monsters take their turn, and then the turn is counted. The field of view comes before the monsters, because in a moment, the monsters will use it to decide whether they can see the player.
A bump into a wall or a monster takes no turn, so the monsters don't move then. Neither do F1, Alt+Enter, or any key that isn't a step.
Checkpoint: Press F5, and walk into a room with a monster in it. Take a few steps, and watch it shuffle about at random. Then type 42 into Command Arguments, and walk down eight steps, and right ten. A jackal is in view, through a doorway above the corridor. Step left and right a few times, and watch it wander. Run the game again, take the same steps, and it wanders exactly the same way, because the seed decides every roll.
The Chase
Wandering monsters are a start, but they don't care that you're there. Now a monster that sees you will come for you.
How Far a Monster Can See
Each kind of monster sees as far as its own eyes allow. Open Monster.h, and add this line to MonsterDef, below SDL_Color color;:
int sight; // how far it can see, in cells
The new member, sight, is how many cells a monster of the kind can see. The table needs a value for it in every row. Find the table's rows:
{ "giant white rat", 'r', { 215, 215, 205, 255 } },
{ "jackal", 'j', { 205, 150, 80, 255 } },
{ "kobold", 'k', { 110, 185, 95, 255 } },
{ "goblin", 'g', { 160, 205, 60, 255 } },
Replace them with these lines:
{ "giant white rat", 'r', { 215, 215, 205, 255 }, 6 },
{ "jackal", 'j', { 205, 150, 80, 255 }, 7 },
{ "kobold", 'k', { 110, 185, 95, 255 }, 6 },
{ "goblin", 'g', { 160, 205, 60, 255 }, 7 },
Each row now ends with its kind's sight: 6 for the rat and the kobold, and 7 for the jackal and the goblin. All are less than the player's 9, so you'll usually see a monster before it sees you.
Seeing the Player
Three small functions help the chase along. The first measures distance. Open main.cpp, and add it below placeMonsters:
// The square of the distance between two cells. Squares compare the
// same way as the distances do, without any square roots
int distanceSquared(Point a, Point b)
{
int dx = a.x - b.x;
int dy = a.y - b.y;
return dx * dx + dy * dy;
}
Here, distanceSquared is the square of the straight-line distance between two cells. By Pythagoras, that's the difference across, squared, plus the difference down, squared. The real distance would need a square root, but the game only ever compares distances, to find which is nearer. If one distance is shorter than another, its square is smaller too. So the squares do the job, in whole numbers.
The second says whether a monster can see the player. Add it below distanceSquared:
// True if the monster can see the player. It can if the player can see
// its cell, since sight is symmetric, and the player is within its
// sight, measured as the field of view measures the player's
bool seesPlayer(const Map& map, const Monster& monster,
const Player& player)
{
int sight = monster.def->sight;
return map.isVisible(monster.pos.x, monster.pos.y) &&
distanceSquared(monster.pos, player.pos) <=
sight * (sight + 1);
}
The function seesPlayer is where Chapter 4's symmetry pays off. Sight is symmetric, so the monster can see the player exactly when the player can see the monster's cell. The field of view has already worked that out, and isVisible answers it. Then the player must be within the monster's own sight. The test is the one the field of view uses for the player: the squared distance is no more than sight * (sight + 1).
So the game works out one field of view a turn, the player's, and every monster uses it. Without symmetry, each monster would need a field of view of its own.
The third says whether a monster may step onto a cell. Add it below seesPlayer:
// True if a monster could step onto the cell: floor, with nobody on it
bool isFree(const Map& map, std::vector<Monster>& monsters,
const Player& player, Point cell)
{
return map.at(cell.x, cell.y) == Terrain::Floor &&
cell != player.pos && monsterAt(monsters, cell) == nullptr;
}
The function isFree gathers up the test that moveMonsters makes: floor, with neither the player nor a monster on it. Both the chase and the wandering will use it.
Stepping Closer
Now moveMonsters gives each monster a choice: a monster that sees the player chases them, and the others wander, as before. Its comment comes first. Replace the comment above moveMonsters with these lines:
// The monsters' turn. A monster that sees the player steps to the free
// cell nearest them, if that's nearer than where it stands. The others
// take a step in a random direction, if the cell there is free
The chase comes first. Add these lines at the top of the loop, below its opening brace:
if (seesPlayer(map, monster, player))
{
Point best = monster.pos;
int bestDistance = distanceSquared(monster.pos, player.pos);
for (Point step : DIRECTIONS)
{
Point to = { monster.pos.x + step.x,
monster.pos.y + step.y };
int distance = distanceSquared(to, player.pos);
if (distance < bestDistance &&
isFree(map, monsters, player, to))
{
best = to;
bestDistance = distance;
}
}
monster.pos = best;
}
else
{
In the preceding code, a monster that sees the player looks at all eight of its neighbors. For each, it works out how far that cell is from the player, and it keeps the nearest free one, starting from where it stands. Then it moves to the best cell it found. If no neighbor is nearer than where it stands, best is still its own cell, and it stays put. The next image shows one such choice, from the jackal in Figure 5.1, as the player stood in its room's doorway.

This is called a greedy step, because it takes whatever looks best right now, without planning ahead.
The lines you just added end with else, and an opening brace. The wandering code, below them, becomes that else's branch. It needs one more level of indentation, isFree for its test, and a closing brace. Find the wandering code, with the brace that ends the loop below it:
Point step = DIRECTIONS[rng.roll(0, 7)];
Point to = { monster.pos.x + step.x, monster.pos.y + step.y };
if (map.at(to.x, to.y) == Terrain::Floor && to != player.pos &&
monsterAt(monsters, to) == nullptr)
{
monster.pos = to;
}
}
Replace them with these lines:
Point step = DIRECTIONS[rng.roll(0, 7)];
Point to = { monster.pos.x + step.x, monster.pos.y + step.y };
if (isFree(map, monsters, player, to))
{
monster.pos = to;
}
}
}
A monster that doesn't see the player wanders, as it did before, and isFree does the test that was spelled out. The two closing braces end the else and the loop.
Once a monster reaches the player, it stays beside them, since the player's own cell isn't free. At most, it shuffles from a corner to a side, which is a little nearer. Then it waits, which is all it can do until the next chapter.
Checkpoint: Press F5, with seed 42 in Command Arguments. Walk down eight steps, out of the first room and into the corridor below it, and then right along the corridor. After ten steps to the right, a jackal comes into view, through a doorway just ahead, above the corridor. Step right once more, and then up, into the doorway and through it. The jackal comes to meet you, one step for each of yours, until it's standing beside you.
The next image shows the chase. The jackal saw the player as they stepped below its doorway, and came to meet them, one step for each of theirs.

If your jackal isn't there, or goes somewhere else, compare your placeMonsters and moveMonsters with the book's. The rolls have to come in the same order, or seed 42 plays out differently.
A Simple Chase
The chase is simple, on purpose. A monster chases you only while it can see you. Step out of its sight, around a corner, and it forgets you at once, and goes back to wandering. And it only ever steps by straight-line distance. So once you're out of sight, it has no way to find you, around the walls in between.
Design note: who sees whom first. Every monster here sees less far than you, so you usually spot it before it spots you. That's a deliberate kindness. A roguelike is a game of decisions, and a moment's warning, such as a jackal at the edge of the light, gives you one. You can fight it where you stand, back into a corridor so that only one monster can reach you at a time, or slip away. Monsters that saw you first would make every fight an ambush. Chapter 11 adds noise, so a monster can sometimes hear you first, but this is the rule that sets the tone.
Real roguelike monsters do better, and this game's will too. Chapter 10 gives every monster a map of the distances to the player, through the whole dungeon, so it takes the shortest way to you around the walls. It also remembers where it last saw you, and goes there when you slip out of sight. Chapter 11 has it search around that spot before it gives up.
Playing the Game
There's something to meet now. Explore with a random seed, and see who's about. Rats and kobolds see six cells, and jackals and goblins seven. So watch for the moment a monster stops wandering and turns toward you. Then step around a corner, and see whether it gives up.
Try leading a monster back to the first room, through a doorway. Since it comes straight for you, it's easy to see where it will step next. That's worth remembering for the next chapter, when monsters can bite.
Understanding the Code
One Rng holds the run's engine, made from the seed. Every random choice in the run comes from it, in order. The dungeon takes the first rolls, then the monsters' places and kinds, and then each turn's wandering. So a seed and the same keys play out the same way every time.
Each Monster is a place and a pointer to its kind's row in MONSTER_DEFS, the table that holds each kind's name, glyph, color, and sight. The function placeMonsters puts eight in the rooms, any room but the player's. Then drawMonsters draws the ones the player can see.
After each step, the player's field of view is worked out, and then moveMonsters gives every monster a turn. A monster that sees the player, by the symmetry of the field of view, steps to its free neighbor nearest them. The others step in a random direction, if they can.
Experimenting
The monsters have a few numbers you can play with. Try each of these, and put each back afterward:
- A crowd. Change
MONSTER_COUNTto40, and the dungeon fills up. Watch the monsters in a crowded room get in each other's way. - A new kind. Add a row to
MONSTER_DEFS, such as{ "newt", ':', { 230, 200, 60, 255 }, 4 }, a yellow newt drawn as a colon. Nothing else needs to change, sinceplaceMonsterscounts the rows itself. - Sharp eyes. Give the jackal a sight of
20. It still can't see you from further than 9 cells away, since it sees you only when you can see it. - Blind monsters. Give every kind a sight of
0. Now none of them ever sees you, and the chase never starts. - Shy monsters. In the chase, change
distance < bestDistancetodistance > bestDistance. Now a monster that sees you steps to the free neighbor furthest from you, and runs away.
If you change the order of the rolls in placeMonsters, such as rolling the kind before the room, seed 42 places different monsters. That's the stream of rolls from Figure 5.3 at work.
Common Errors and Fixes
If the game didn't build, or doesn't behave as described, the problem is probably one of these.
LNK2019: unresolved external symbol "public: __cdecl Rng::Rng(unsigned int)", followed by more about Rng::roll, and then LNK1120: 2 unresolved externals. Rng.cpp isn't part of the project. With Show All Files on, right-click it in Solution Explorer, and choose Include In Project.
C3861: 'roll': identifier not found, in RoomGen.cpp. A call to the old helper is left over. Do the second Replace All again, from roll(rng, to rng.roll(, with the space after the comma.
C2039: 'seed': is not a member of 'Rng', in main.cpp. The Space key's code still reseeds the engine. It should read rng = Rng(seed);.
C2676: binary '==': 'Point' does not define this operator or a conversion to a type acceptable to the predefined operator, in monsterAt. The struct Point has no operator==. Add the three lines from Comparing Points to Point, in Common.h.
C2110: '+': cannot add two pointers, in the walking code. The message adds "The " straight to monster->def->name. Copy the name into a std::string first, as std::string name = monster->def->name;, and add name.
C2660: '`anonymous-namespace'::drawFrame': function does not take 8 arguments. The call to drawFrame in the loop doesn't pass monsters. It should pass them after map, as its parameters list them.
C2078: too many initializers, in Monster.h, and C2039: 'sight': is not a member of 'MonsterDef', in main.cpp. The table's rows have a sight, but MonsterDef has no sight member. Add int sight; below SDL_Color color;.
C2065: 'DIRECTIONS': undeclared identifier, in moveMonsters. The eight directions are missing from Common.h. Add them below Point.
The monsters never move. Check that the walking code calls moveMonsters, after Fov::compute and before ++turn.
A monster never chases you. Check its row in the table. If the row has no sight, it still compiles, since C++ fills a missing value with 0. But a monster with a sight of 0 never sees anyone.
FAQ
Q) Why do the monsters take turns one at a time, rather than all moving at once?
A) Moving one at a time keeps things simple and fair. Each monster sees where the others are now, so no two ever step into the same cell. The game never has to sort out a collision. Many roguelikes work this way. And since the whole turn happens between two of your keypresses, you never see it happen one monster at a time.
Q) Why doesn't each monster work out its own field of view?
A) It doesn't need to. Sight is symmetric, so a monster can see the player exactly when the player can see the monster's cell. And the player's field of view is worked out every turn anyway. One field of view a turn serves every monster, however many there are.
Q) Why is the monster table in the code, rather than in a file?
A) For now, it's simpler, and the compiler checks every row as it builds. Chapter 16 moves the table into a text file, along with the items and the character classes, so that new kinds of monster can be added without building the game again.
Q) Why does a monster point to its kind, rather than keep a copy of it?
A) Everything in a kind's row is the same for every monster of that kind, so one copy, in the table, is enough. A pointer is small, and it also tells you exactly which kind a monster is. When monsters get health, in the next chapter, health is copied into each monster, since each needs its own.
Q) Why don't the monsters take the shortest path to the player?
A) To take the shortest path, a monster needs to know the way around the walls, which takes more than one step's thought. Chapter 10 works out the distance to the player from every cell in the dungeon, all at once, and every monster follows it downhill. The greedy step in this chapter is the simplest chase there is, and a good one to start from.
AI Exercise (Optional)
Some monsters are restless, and some are lazy. In many roguelikes, a monster that doesn't see you might stay put on some turns, rather than wander on every one. If you'd like to try that with an AI's help, open your AI chatbot of choice, and try a prompt like this:
"I'm writing a roguelike in C++ with SDL 3. Each kind of monster is a row in a table, MONSTER_DEFS, of a struct called MonsterDef, which holds a name (const char*), a glyph (char), a color (SDL_Color), and a sight (int). A Monster holds a pos (a Point) and def, a pointer to its kind's row. In moveMonsters, a monster that can't see the player picks one of eight DIRECTIONS with rng.roll(0, 7), where rng.roll(low, high) gives a whole number from low to high, and steps there if isFree says it can. I'd like each kind to have a chance, out of 100, of wandering on its turn, so that rats mostly stay put and jackals roam. Show me the changes to MonsterDef, MONSTER_DEFS, and moveMonsters, with each curly brace on its own line, and explain them."
Notice what the preceding prompt does. It names the functions and the table, and says what rng.roll gives, so the AI can use the game's own dice rather than making new ones. The prompt also says where the change belongs: only a monster that can't see the player decides whether to wander.
When the answer comes back, check it against your code. Does it roll with rng, so the seed still decides everything? Does every row of the table get a value, in the right place? And does the chase still happen on every turn, for a monster that sees you?
Summary
The dungeon is alive: one set of dice rolls everything in the run, monsters come from a table of kinds, and those that see you come for you. In Chapter 6, a monster that reaches you stops waiting. Monsters and the player get health, a bump becomes a blow, and the dungeon becomes a place where you can die.
