Every monster in the dungeon is wide awake, all the time. Each turn, every one of them asks the same questions in the same order: is it badly wounded, can it see you, does it remember where you were? A goblin on the far side of the map is as alert on the first turn as one you've been fighting for ten. Nothing sleeps, nothing hears a fight, and a monster that loses you gives up the moment it reaches the place it saw you last.
This chapter gives each monster a state of mind: asleep, wandering, hunting or fleeing. What a monster does on its turn depends on its state. It changes state when something happens to it: it sees you, it's hit, it's badly hurt, or it hears a noise. Programmers call this a state machine. The next image shows the monsters' minds, as a new debugging aid on F4 draws them.

A blue outline is a monster asleep, a gray one is wandering, and a red one is hunting you. You've just killed a rat in a corridor. The noise of the fight woke the jackal beside it, which has stepped into the rat's place, hunting you. A goblin wandering along the corridor heard it as well, and its line shows it coming to see what the noise was. Farther off, the sleepers sleep on.
Chapter 11 in the book's repository holds this chapter's finished project. The chapter carries on in your own project from Chapter 10, or from the book's Chapter 10 folder.In this chapter, we will:
- Give every monster a state, asleep, wandering, hunting or fleeing, and a function for each
- Draw every monster's state with F4, a debugging aid
- Give kinds of monster traits, stored as bits, and add the fruit bat
- Let a hunter search where it lost you, and give you a key to wait
- Make fights loud, and add the shrieker, which calls for help
There are five checkpoints along the way. Let's start with the plan.
Planning the Chapter
This chapter changes three files, and the tile atlas:
| File | Status | What it's for |
|---|---|---|
src/Monster.h |
Changed | Each monster's state, and the traits that make kinds behave differently; the fruit bat and the shrieker |
src/Game.h, src/Game.cpp |
Changed | A function for each state, F4, searching, the wait key, and noise |
assets/tiles.png, assets/tiles.txt |
Changed | Pictures of the two new monsters |
| The other files | Unchanged |
The next image shows how the files fit together at the end of the chapter. Nothing is new. The monsters' minds live in Game, which already knows the map, the player and every monster.

A state machine is a small idea. Whatever it describes is always in exactly one of a few states, and it changes state only when something happens. For the monsters, the states and what moves a monster from one to another all fit in one picture. The next image shows it, as it will be at the end of the chapter.

Every monster starts the run asleep or wandering. A sleeper wakes and hunts you when you come next to it, when you hit it, or now and then when it can see you. Wanderers walk to places they choose at random, until they see you. Hunters flee when they're badly wounded. One that loses you searches for a while, then gives up and wanders again.
The dashed arrows are noise, which comes in the chapter's last part. A noise that's loud enough wakes a sleeper, and draws a wanderer to see what it was.
The picture is the plan for the code, too. Each state gets a function that does what the state does. And each arrow is a line or two in one of those functions, where the monster's state changes. We'll build it in five steps: the states, F4 to see them, traits, searching, and noise.
States of Mind
The first step gives every monster a state, and turns Chapter 10's rules into one function for each state. You'll see the difference in the log, before F4 shows it on the map.
Four States
A monster's state is one of four values, so it's an enum class. Open Monster.h, and add it below the table of kinds, MONSTER_DEFS:
// What a monster is doing, which decides what it does on its turn
enum class MonsterState
{
Asleep, // doesn't move, until something wakes it
Wandering, // walks toward a cell it chose at random
Hunting, // goes after the player, or where it last saw them
Fleeing, // runs from the player
};
Each monster has a state now, and a wandering monster needs somewhere to wander to. Replace the comment above struct Monster : Entity with this:
// One monster in the dungeon: an Entity, the kind of monster it is, what
// it's doing, and the cells it's heading for and last saw the player on
Add the state and the target below def:
MonsterState state = MonsterState::Wandering;
std::optional<Point> target;
A monster that's made and given no other state is wandering. That's a safe default, since a wanderer does nothing surprising. The target is a std::optional<Point>, like lastSeen, since a monster may have nowhere in mind.
The include for <optional> is there already, from Chapter 10. Its comment should say what it's for now, so in Monster.h, change for where it last saw the player to for what a monster is heading for.
Asleep or Awake
Most monsters should start the run asleep, so that a careful player can creep past them, or get the first blow in. Open Game.cpp, and add two constants to the unnamed namespace, below FAR_DISTANCE:
// One monster in AWAKE_ONE_IN starts the run awake. An asleep monster
// that can see the player wakes with a chance of 1 in WAKE_ONE_IN each
// turn (both *tune*)
constexpr int AWAKE_ONE_IN = 3;
constexpr int WAKE_ONE_IN = 20;
Each monster has a chance of one in three of starting the run awake. A sleeper that can see you has a chance of one in twenty, each turn, of waking up. The tune in the comment marks both numbers as first guesses. From now on, a number marked this way is a guess at something that sets how hard the game is. In Chapter 27, a bot that plays thousands of games helps to settle such numbers.
Each monster's state is set when it's placed. In placeMonsters, add these lines below monster.defense = monster.def->defense;:
// Most start asleep, and the rest wandering
bool awake = rng_.roll(1, AWAKE_ONE_IN) == 1;
monster.state = awake ? MonsterState::Wandering
: MonsterState::Asleep;
One time in three, the roll is 1, and the monster starts awake. These rolls change what seed 42 gives, too. Each one uses up a number from the generator. Every roll after it, for the next monster's room, cell and kind, comes out differently. The dungeon is the same, since it's made first, but its monsters are new.
Design note: Let sleeping monsters lie. In Rogue, most monsters don't move at all until something sets them running: each turn, the game moves only the monsters that are already running. Step next to a mean one, and it starts after you with a chance of two in three, unless you wear a ring of stealth. Sleepers make a dungeon feel lived in, as if the monsters were there before you came. They also give a careful player a choice: fight now, or creep past and leave them be. Our monsters wake in much the same ways, and noise, at the end of this chapter, adds one more.
Next, the function that decides what each monster does on its turn.
A Function for Each State
Chapter 10's monstersAct asks its questions in one long chain of if and else. With states, each monster does only what its state says, so the chain becomes a switch, with a function for each state. Replace the comment above monstersAct with this:
// The monsters' turn. What each one does depends on its state. Once the
// player is dead, no monster acts
In monstersAct, delete everything from bool sees = seesPlayer(monster); down to monster.pos = wanderStep(monster);, and the closing brace below it, along with the blank line above it. That's all four of Chapter 10's branches. Their rules move into the new functions, and what changes is what a monster does once it can't see you: it no longer simply wanders, as you'll see.
In their place, add the switch below the check for the player's death, with a blank line in between:
switch (monster.state)
{
case MonsterState::Asleep:
actAsleep(monster);
break;
case MonsterState::Wandering:
actWandering(monster);
break;
case MonsterState::Hunting:
actHunting(monster);
break;
case MonsterState::Fleeing:
actFleeing(monster);
break;
}
Each case calls the function for the monster's state. A break ends the case, so the code of the next one doesn't run as well. Now declare the functions. In Game.h, add these below monstersAct's declaration:
void actAsleep(Monster& monster);
void actWandering(Monster& monster);
void actHunting(Monster& monster);
void actFleeing(Monster& monster);
bool walkToward(Monster& monster, Point goal);
void giveUp(Monster& monster);
std::optional<Point> randomFloorCell();
void announce(const Monster& monster, const std::string& what);
The four handlers take the monster by reference, since they change it. The other four are helpers they share: walkToward takes a step toward a cell, giveUp ends a hunt, randomFloorCell chooses somewhere to wander to, and announce tells you what a monster does.
Back in Game.cpp, the handlers go below monstersAct, in the order of the states. First, sleeping:
// An asleep monster doesn't move. It wakes if the player comes next to it,
// and, while it could see them, with a chance of 1 in WAKE_ONE_IN a turn.
// Woken, it hunts them
void Game::actAsleep(Monster& monster)
{
bool nextTo = isNextTo(monster.pos, player_.pos);
bool stirs = seesPlayer(monster) && rng_.roll(1, WAKE_ONE_IN) == 1;
if (nextTo || stirs)
{
monster.state = MonsterState::Hunting;
monster.lastSeen = player_.pos;
announce(monster, "wakes up!");
}
}
Each sleeper wakes if you're next to it, or if it can see you and wins its one-in-twenty chance. A woken monster hunts you at once. It has just seen you, or felt you beside it, so it knows where you are. Its lastSeen is your cell. Then announce tells you, if you can see it happen.
A wandering monster walks toward its target, and chooses another when it gets there. Add actWandering below actAsleep:
// A wandering monster walks toward a cell it chose at random, and chooses
// another when it gets there, or can't. If it sees the player, it starts
// hunting them, from its next turn
void Game::actWandering(Monster& monster)
{
if (seesPlayer(monster))
{
monster.state = MonsterState::Hunting;
monster.lastSeen = player_.pos;
announce(monster, "notices you!");
return;
}
if (!monster.target || monster.pos == *monster.target)
{
monster.target = randomFloorCell();
}
if (monster.target && !walkToward(monster, *monster.target))
{
monster.target.reset();
}
}
In the preceding code, seeing you comes first. The monster starts hunting, and remembers where you are. But noticing you takes its turn, which gives you a moment to react.
Otherwise, a monster with no target, or one that has arrived, chooses a new target with randomFloorCell, which can come back empty. Then walkToward takes a step. If it couldn't get any closer, perhaps because another monster is in the way, the target is forgotten. The monster chooses again on its next turn.
Hunting is Chapter 10's chase and memory, gathered into one function. Add its first part below actWandering:
// A hunting monster goes after the player, unless it's badly wounded, when
// it flees instead. Seeing them, it attacks if it's next to them, and
// otherwise comes closer by the shortest way, remembering where they are.
// Having lost them, it goes to where it last saw them, and if they're not
// there, it gives up
void Game::actHunting(Monster& monster)
{
if (isBadlyWounded(monster))
{
monster.state = MonsterState::Fleeing;
monster.lastSeen.reset();
announce(monster, "flees!");
actFleeing(monster);
return;
}
A badly wounded hunter becomes a fleeing one, and forgets where it saw you, as Chapter 10's did. It doesn't waste its turn changing its mind: it calls actFleeing right away, and runs. Add the rest of actHunting below the first part, with a blank line in between:
if (seesPlayer(monster))
{
monster.lastSeen = player_.pos;
if (isNextTo(monster.pos, player_.pos))
{
monsterAttacks(monster);
}
else
{
monster.pos = stepDown(monster, toPlayer_);
}
return;
}
// It goes to where it last saw them. If it can't get any closer, as
// when another monster is in the way, it gives up
bool onTheWay = monster.lastSeen && monster.pos != *monster.lastSeen;
if (onTheWay)
{
if (!walkToward(monster, *monster.lastSeen))
{
giveUp(monster);
}
return;
}
giveUp(monster);
}
In the preceding code, a hunter that can see you does what Chapter 10's monsters did. It remembers where you are, and attacks you if it's next to you, or steps down the map toward you. A hunter that can't see you goes to where it last saw you, with walkToward. When it gets there, giveUp sends it back to wandering, and so does a step that can't get it any closer, as when another monster is in the way.
Fleeing is Chapter 10's running away, with one change. Add the first part of actFleeing below actHunting:
// A fleeing monster runs from the player, down the flee map, while it can
// see them, and lies low while it can't. Cornered, with nowhere lower to
// go, it turns and fights. Healed past two thirds of its health, it hunts
// again
void Game::actFleeing(Monster& monster)
{
if (monster.hp * 3 > monster.maxHp * 2)
{
monster.state = MonsterState::Hunting;
actHunting(monster);
return;
}
if (!seesPlayer(monster))
{
return;
}
A monster that has healed past two thirds of its health turns and hunts you again. For now none can, since monsters don't heal until Chapter 15's forest troll. The test monster.hp * 3 > monster.maxHp * 2 is "more than two thirds" in whole numbers. Multiplying both sides by three keeps the fraction from being thrown away, as dividing whole numbers would.
Out of sight, a fleeing monster stays where it is, and hides. Add the rest below the first part, with a blank line in between:
Point next = stepDown(monster, fromPlayer_);
if (next != monster.pos)
{
monster.pos = next;
}
else if (isNextTo(monster.pos, player_.pos))
{
monsterAttacks(monster);
}
}
While it can see you, that's Chapter 10's running away: down the flee map, and a fight if it's cornered next to you. Out of sight is what's new. Chapter 10's monster forgot you then, and wandered off, but this one stays afraid, and lies low until it has healed past two thirds of its health. Nothing heals a monster yet, so one that gets away hides where it is until you find it, and then runs again.
Then come the helpers. Add walkToward and giveUp below actFleeing:
// Takes a step toward a cell, on a Dijkstra map of the monster's own.
// Returns false if it couldn't get any closer
bool Game::walkToward(Monster& monster, Point goal)
{
DijkstraMap toGoal;
toGoal.compute(map_, { goal });
Point next = stepDown(monster, toGoal);
bool moved = next != monster.pos;
monster.pos = next;
return moved;
}
// A hunter that's lost the player goes back to wandering
void Game::giveUp(Monster& monster)
{
monster.state = MonsterState::Wandering;
monster.lastSeen.reset();
monster.target.reset();
}
The function walkToward is Chapter 10's way to lastSeen, made general: a map of the monster's own, with one goal, and a step down it. It returns whether the monster moved, so that a monster that's stuck can give up. And giveUp puts a hunter back to wandering, with nothing remembered.
A wanderer needs somewhere to go. Add randomFloorCell below giveUp:
// A floor cell chosen at random, for a wandering monster to head for, or
// nothing, if none turns up in a few hundred tries
std::optional<Point> Game::randomFloorCell()
{
for (int tries = 0; tries < 500; ++tries)
{
Point cell = { rng_.roll(0, MAP_W - 1), rng_.roll(0, MAP_H - 1) };
if (map_.at(cell.x, cell.y) == Terrain::Floor)
{
return cell;
}
}
return std::nullopt;
}
A cell is chosen anywhere on the map, and kept if it's floor. Seed 42's map is just over a quarter floor, so a few tries may miss. But 500 misses in a row would take a broken map. Even then, the function says so honestly, rather than making up a cell. It returns std::nullopt, the empty value of a std::optional.
Last, announce, below randomFloorCell:
// Tells the player what a monster does, if they can see it do it
void Game::announce(const Monster& monster, const std::string& what)
{
if (map_.isVisible(monster.pos.x, monster.pos.y))
{
std::string name = monster.def->name;
log_.add("The " + name + " " + what);
}
}
A monster's doings go in the log only if you can see the monster. If a kobold wakes up in a room you've never seen, you shouldn't be told, since the message would tell you there's a kobold there.
A Blow Wakes Them
The most direct way to wake a monster is to hit it. A sleeper you hit should wake up hunting you, and so should a wanderer that hadn't noticed you. Replace the whole of playerAttacks with this:
// The player strikes a monster. A blow in melee always hits
void Game::playerAttacks(Monster& monster)
{
int damage = rollDamage(rng_, player_.minDamage, player_.maxDamage,
monster.defense);
monster.hp -= damage;
std::string name = monster.def->name;
if (!monster.isAlive())
{
log_.add("You kill the " + name + "!");
return;
}
log_.add("You hit the " + name + " for " + std::to_string(damage) + ".");
// A blow wakes a monster, and sets it hunting whoever struck it
bool unaware = monster.state == MonsterState::Asleep ||
monster.state == MonsterState::Wandering;
if (unaware)
{
monster.state = MonsterState::Hunting;
monster.lastSeen = player_.pos;
}
}
In the preceding code, a killing blow ends the function early, with return, since a dead monster needs no state. Otherwise, the message is the same as before, on one line now. Then a monster that was asleep or wandering becomes a hunter that knows where you are. A hunter stays a hunter, and a fleeing monster keeps fleeing: a blow doesn't make it any braver.
Checkpoint: Press F5, with seed 42 in Command Arguments, and press F1. The monsters are new. A goblin, g, lies in the corridor below your room, just to the left of the passage down. Walk down toward it, pressing j, and notice that it doesn't move: it's asleep.
On your sixth step, "The goblin wakes up!" It saw you coming, and won its one-in-twenty chance. Take one more step down, and the goblin hits you. Attack it with b, down and to the left, twice. The second blow leaves it badly wounded, and "The goblin flees!": it runs right past you, and away along the corridor.
Seeing Their Minds
The log tells you some of what the monsters are thinking, but only about the ones you can see. A debugging aid can show you all of them. F4 will outline every monster in its state's color, and draw a line to where it's heading.
A Color for Each State
Each state needs a color. Add them to the unnamed namespace, below WAKE_ONE_IN, with a helper that picks one:
// The monster minds overlay's colors, one for each state
constexpr SDL_Color ASLEEP_COLOR = { 90, 140, 255, 255 };
constexpr SDL_Color WANDERING_COLOR = { 170, 170, 170, 255 };
constexpr SDL_Color HUNTING_COLOR = { 255, 80, 70, 255 };
constexpr SDL_Color FLEEING_COLOR = { 255, 220, 60, 255 };
SDL_Color stateColor(MonsterState state)
{
switch (state)
{
case MonsterState::Asleep:
return ASLEEP_COLOR;
case MonsterState::Wandering:
return WANDERING_COLOR;
case MonsterState::Hunting:
return HUNTING_COLOR;
case MonsterState::Fleeing:
return FLEEING_COLOR;
}
return WANDERING_COLOR;
}
In the preceding code, blue is for sleep, gray for wandering, red for hunting and yellow for fleeing. The switch returns from every case, so the line after it can never be reached. But the compiler can't be sure of that. A cast can give an enum class variable a value that isn't one of its names. Without the last return, Visual Studio warns that not every path returns a value.
The overlay has a flag, as F1 and F3 do. In Game.h, add it below showDistances_:
bool showMinds_ = false; // and on F4
Then add a declaration below drawDistances's:
void drawMinds() const;
F4 works as F3 does. In Game.cpp, add its check to handleKey, below F2's:
if (key.key == SDLK_F4)
{
// A debugging aid: F4 shows what every monster is doing
if (!key.repeat)
{
showMinds_ = !showMinds_;
redraw_ = true;
}
return;
}
Now the drawing. Add drawMinds below drawDistances, in two parts. Here's the first:
// A debugging aid (F4): every monster's state, as an outline in the
// state's color, seen or not, and a line to where it's heading: the cell
// a wanderer chose, or where a hunter last saw the player
void Game::drawMinds() const
{
for (const Monster& monster : monsters_)
{
SDL_FRect from = view_->camera().cellRect(monster.pos);
setColor(stateColor(monster.state));
SDL_RenderRect(renderer_, &from);
Every monster is outlined, whether you can see it or not. This is a debugging aid, and it's allowed to cheat. The outline is the camera's rectangle for the monster's cell, drawn with SDL_RenderRect, as Chapter 9's highlight is. Add the rest below the first part, with a blank line in between:
std::optional<Point> goal;
if (monster.state == MonsterState::Wandering)
{
goal = monster.target;
}
else if (monster.state == MonsterState::Hunting)
{
goal = monster.lastSeen;
}
if (goal)
{
SDL_FRect to = view_->camera().cellRect(*goal);
SDL_RenderLine(renderer_, from.x + from.w / 2, from.y + from.h / 2,
to.x + to.w / 2, to.y + to.h / 2);
}
}
}
The variable goal is where the monster is heading, if anywhere: a wanderer's target, or the cell where a hunter last saw you. Sleepers and fleeing monsters have none. If there's a goal, SDL_RenderLine joins the middles of the two cells, in the color that setColor chose for the outline.
The overlay goes on top of the monsters and the player, so that nothing covers it. In draw, add these lines below view_->drawPlayer(player_.pos);:
if (showMinds_)
{
drawMinds();
}
Last, the side panel says when F4 is on, with a key to the colors. In drawSidePanel, add these lines below F3's reminder:
if (showMinds_)
{
// The note, and a key to the overlay's colors
float word = glyphs_.charWidth();
y += row;
glyphs_.drawText("DEBUG: minds (F4)", x, y, Palette::DEBUG);
y += row;
glyphs_.drawText("asleep", x, y, ASLEEP_COLOR);
glyphs_.drawText("wandering", x + 7 * word, y, WANDERING_COLOR);
y += row;
glyphs_.drawText("hunting", x, y, HUNTING_COLOR);
glyphs_.drawText("fleeing", x + 8 * word, y, FLEEING_COLOR);
}
Each state's name is drawn in its own color, two to a row. The second name in each row starts one character past the end of the first, measured with charWidth, so the key stays neat whatever the font's size.
Checkpoint: Press F5, with seed 42, and press F1 and F4. Every monster has an outline: blue for the sleepers, the goblin among them, and gray for the wanderers. Take a step, and each wanderer gets a line to the cell it's heading for, the kobold's right across the map. Walk down toward the goblin, as before. On your sixth step, its outline turns red, with a line to you, as the next image shows.

Attack the goblin twice, as before. When it flees, its outline turns yellow, and its line goes. A fleeing monster isn't heading anywhere in particular, only away. Watch the wanderers for a while, too: each walks to its cell, and chooses another when it gets there.
Traits
Every kind of monster follows the same rules. A good bestiary needs variety, though: animals that never sleep, things that can't move, bats that can't fly straight. Each of those is a trait, a yes or a no for each kind of monster, and a kind can have several.
One Bit Each
A kind's traits could be a bool each. But there will be more of them in later chapters, and most kinds have none. So they're kept in one unsigned int, a bit for each trait. An unsigned int is 32 bits, each a 0 or a 1, so one number can hold 32 yeses and noes. Open Monster.h, and add the traits below the includes:
// What makes a kind of monster behave differently. Each trait is one bit,
// so a kind can have several, joined with |, as in NEVER_SLEEPS | ERRATIC
namespace Trait
{
constexpr unsigned int NEVER_SLEEPS = 1 << 0;
// Half its moves are random
constexpr unsigned int ERRATIC = 1 << 1;
}
The << operator shifts a number's bits to the left. So 1 << 0 is 1, binary 0001, and 1 << 1 is 2, binary 0010. Each trait is a number with just one bit set, and a different bit for each trait.
The traits are in a namespace of their own, so their names are Trait::NEVER_SLEEPS and Trait::ERRATIC, and they can't clash with other names. They aren't an enum class, since the point is to combine them. An enum class won't let its values be joined with | unless you write that operator yourself.
Each kind of monster gets its traits. In MonsterDef, add these lines below defense:
unsigned int traits = 0;
// True if the kind has the trait. & keeps only the bits both have
bool has(unsigned int trait) const
{
return (traits & trait) != 0;
}
A new function, has, answers whether a kind has a trait. The & operator keeps only the bits that are set in both numbers. So traits & trait is the trait's bit if the kind has it, and 0 if it doesn't. The next image shows & at work, with |, which joins traits.

The parentheses in has matter. Without them, != would come first, since it binds more tightly than &, and the test would be a different one. The function is const, since it only looks. That matters too, since a monster's def points to a const MonsterDef, and only a const function can be called through it.
The table's rows give each kind its traits, as a last value. Replace the comment above MONSTER_DEFS with this:
// 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. A
// kind with no traits can leave them out, and gets 0
A row is filled in from the first member down. Any member it leaves out at the end gets its default, the one written in the struct. For traits, that's 0. So the rows that are there already don't need to change. In the comment above the rows, change damage from and to, defense to damage from and to, defense, traits.
Now the first monster with traits. Add the fruit bat at the end of the table:
{ "fruit bat", 'b', { 205, 120, 90, 255 }, "fruit-bat",
6, 3, 1, 2, 0, Trait::NEVER_SLEEPS | Trait::ERRATIC },
A fruit bat never sleeps, and half its moves are random, which is how a bat's flight looks. It's weak, with three health and a bite of one or two, but it's hard to pin down. Its two traits are joined with |, which keeps every bit that's set in either number, so 0001 and 0010 make 0011.
Using the Traits
The traits mean nothing until the game asks about them. A monster that never sleeps must start awake. In placeMonsters, find this line:
bool awake = rng_.roll(1, AWAKE_ONE_IN) == 1;
Replace it with these lines:
bool awake = monster.def->has(Trait::NEVER_SLEEPS) ||
rng_.roll(1, AWAKE_ONE_IN) == 1;
For a fruit bat, the dice aren't rolled at all: the left side of || is true, and that settles it. Half of an erratic monster's moves are random. In Game.h, add a declaration below walkToward's:
bool movesErratically(Monster& monster);
In Game.cpp, add the function below walkToward:
// Half of an erratic monster's moves are random. Returns true if this one
// is: the monster has made it, and has no other move this turn
bool Game::movesErratically(Monster& monster)
{
if (!monster.def->has(Trait::ERRATIC) || rng_.roll(0, 1) == 0)
{
return false;
}
monster.pos = wanderStep(monster);
return true;
}
Here, rng_.roll(0, 1) is 0 or 1, a coin toss. On a 1, an erratic monster takes a random step with wanderStep, as every wanderer did before this chapter. Then the function returns true: the monster's move is made, and it gets no other this turn. The three places that move a monster on purpose each call it first. At the start of walkToward, add these lines:
if (movesErratically(monster))
{
return true;
}
A random move counts as a move, so walkToward returns true, and a wanderer keeps its target. Then, in actHunting, change else to else if (!movesErratically(monster)), so that a hunter next to you always attacks, but one that's coming closer flits about half the time. And in actFleeing, change if (!seesPlayer(monster)) to if (!seesPlayer(monster) || movesErratically(monster)), so a fleeing bat flutters about half the time too.
The bat needs a picture for the tile view. From the book's repository, copy tiles.png and tiles.txt from the Chapter 11 folder's assets folder into your project's assets folder, in place of the old ones. They have the fruit bat's picture, and the shrieker's, which comes at the end of the chapter.
Checkpoint: Press F5, with seed 42, and press F1 and F4. The monsters have changed again, since a fifth kind changes the dice: the goblin has gone. On the far right of the map is a fruit bat, b, wandering.
Walk a few steps, and watch it. It heads for its target as the others do, but it zigzags, and now and then it doubles back. If it comes near you, press F2 to see its picture in the tile view.
Searching
A hunter that loses you goes to where it saw you last. If you're not there, it gives up at once. Real hunters look around first. You need a way to hide, too. At the moment, every key you press moves you somewhere, but sometimes the best thing to do is nothing.
Waiting a Turn
Many roguelikes wait a turn on the period key, and on the keypad's 5. Waiting is a step that goes nowhere, so it needs no new code at all, only a row in the key table. Open Game.cpp, and in the comment above struct KeyStep, change n go diagonally to n go diagonally. The period and the keypad's 5 wait a turn.
Then add the row at the end of KEY_STEPS, with a blank line in between:
// A step of nowhere: the player stays put, and the turn passes
{ SDL_SCANCODE_UNKNOWN, SDL_SCANCODE_KP_5, SDLK_PERIOD, { 0, 0 } },
The step is (0, 0), so the player steps onto their own cell. In playerStep, there's no monster on it, and it's floor, so the player moves to where they already are. Then the turn passes, with everything a turn brings: the monsters act, and now and then you heal. There's no arrow key for waiting, so the row's first value is SDL_SCANCODE_UNKNOWN, as the diagonals' are.
A Short Search
Now the search. A hunter that reaches the spot and finds you gone will look around nearby for a few turns. Add these constants below WAKE_ONE_IN:
// A hunter that finds the player gone searches nearby for this many
// turns, staying within SEARCH_RADIUS cells, before it gives up
constexpr int SEARCH_TURNS = 5;
constexpr int SEARCH_RADIUS = 3;
Each monster counts its own search. Open Monster.h, and replace the comment above struct Monster : Entity with this:
// One monster in the dungeon: an Entity, the kind of monster it is, what
// it's doing, the cells it's heading for and last saw the player on, and
// how long it's been searching for them there
Add the count below lastSeen:
int searchTurns = 0;
Back in Game.cpp, the search is part of hunting. In the comment above actHunting, change there, it gives up to there, it searches nearby for a while, then gives up.
A hunter that sees you isn't searching any more, whatever it was doing before. In actHunting, add this line below monster.lastSeen = player_.pos;:
monster.searchTurns = 0;
Then find the lines that decide whether the hunter is on its way, with their comment:
// It goes to where it last saw them. If it can't get any closer, as
// when another monster is in the way, it gives up
bool onTheWay = monster.lastSeen && monster.pos != *monster.lastSeen;
Replace it with these lines:
// It goes to where it last saw them, but not back there once it's
// searching. If it can't get any closer, as when another monster is in
// the way, it gives up
bool onTheWay = monster.lastSeen && monster.pos != *monster.lastSeen &&
monster.searchTurns == 0;
A searching monster wanders off the spot, a step at a time. Without the new test, on its next turn it would be on its way again, back to the spot. It would spend its search going to and fro. Now add the search itself, below the if that follows:
// The player isn't where it last saw them. It searches nearby, a step
// at a time in any direction, for a few turns
if (monster.lastSeen && monster.searchTurns < SEARCH_TURNS)
{
++monster.searchTurns;
Point next = wanderStep(monster);
int away = distanceSquared(next, *monster.lastSeen);
if (away <= SEARCH_RADIUS * SEARCH_RADIUS)
{
monster.pos = next;
}
return;
}
A hunter that has reached its last sighting, and has searched for fewer than SEARCH_TURNS turns, counts one more turn. Then it tries a random step. It keeps the step only if it stays within SEARCH_RADIUS cells of the spot, so the search stays near where you were. After five turns of that, the hunter falls through to giveUp. And if it sees you while it's searching, the branch at the top of the function finds you first, and the hunt is on again.
A monster that gives up should forget its search, and say so. In giveUp, add these lines below monster.target.reset();:
monster.searchTurns = 0;
announce(monster, "gives up the hunt.");
The hunt is over then, and the monster is wandering again, so on its next turn it chooses somewhere new to go.
A List on F4
You can't see a search on the map. A searching monster is still hunting, so its outline is red. So F4 gets a list as well: every monster's letter, cell and state, and how long it has been searching. First, each state needs a name. Add this helper to the unnamed namespace, below the colors:
std::string stateName(MonsterState state)
{
switch (state)
{
case MonsterState::Asleep:
return "asleep";
case MonsterState::Wandering:
return "wandering";
case MonsterState::Hunting:
return "hunting";
case MonsterState::Fleeing:
return "fleeing";
}
return "";
}
Then, in drawSidePanel, add the list below the key to the colors, with a blank line in between:
// Then every monster: its letter, its cell, and its state
y += row;
for (const Monster& monster : monsters_)
{
y += row;
std::string line = std::string(1, monster.def->glyph) + " " +
std::to_string(monster.pos.x) + "," +
std::to_string(monster.pos.y) + " " +
stateName(monster.state);
if (monster.searchTurns > 0)
{
line += ", searching " + std::to_string(monster.searchTurns);
}
glyphs_.drawText(line, x, y, stateColor(monster.state));
}
The list starts after an empty row, and each line is drawn in its state's color. A monster's glyph is a char, so std::string(1, monster.def->glyph) makes a string of one character from it, which + can then join to the rest.
Checkpoint: Press F5, with seed 42, and press F1 and F4. Walk eight steps down and eighteen to the right, along the corridor. A giant white rat is coming the other way, and it's about to see you. Turn back, seven steps to the left, and then four up, through the door into the room above. On your first step back, "The giant white rat notices you!", and it follows.
It never sees you again, though. A rat sees six cells, and you see nine. It noticed you from six cells away. Since you move first each turn, you stay one step beyond its sight, as the next image shows.

The rat goes to where it saw you, finds you gone, and searches. Watch its line in the side panel's list: "hunting, searching 1", then 2, and so on. Wait with the period key until it reaches 5, and the next turn it gives up, and wanders.
Noise
Monsters can see you now, and feel you next to them, but they can't hear a thing. A fight is loud, and anything asleep nearby should wake to it.
How Far a Noise Carries
A noise has a loudness, in cells. It carries that far in walking distance, the way a creature would walk. A wall stops it, and it goes around. What's left of it when it reaches a monster is its loudness, less the distance. Add the loudnesses below SEARCH_RADIUS:
// How loud things are, in cells (*tune*): a noise reaches as far as
// its loudness, by the ways a creature could walk. A monster asleep
// wakes if at least WAKE_LOUDNESS is left of a noise when it gets there
constexpr int FIGHT_LOUDNESS = 8;
constexpr int SHRIEK_LOUDNESS = 25;
constexpr int WAKE_LOUDNESS = 3;
A fight carries eight cells, and a sleeper wakes if three or more is left when the noise gets to it. So a fight wakes sleepers up to five steps away. The shriek, 25, is for the end of the chapter. Walking distance is what Chapter 10's Dijkstra maps measure, so a noise is one more map, with the noise's origin as its goal. In Game.h, add a declaration below announce's:
void makeNoise(Point origin, int loudness);
In Game.cpp, add makeNoise below randomFloorCell, in two parts. Here's the first:
// A noise spreads from where it's made, as far as its loudness, by the
// ways a creature could walk: a wall stops it, so it goes around. A monster
// asleep wakes if enough of the noise is left when it gets there, and goes
// to see what it was. One wandering goes to see, too. Hunters and those
// fleeing have better things to do
void Game::makeNoise(Point origin, int loudness)
{
DijkstraMap spread;
spread.compute(map_, { origin });
for (Monster& monster : monsters_)
{
// A monster the noise's own blow has killed hears nothing
if (!monster.isAlive())
{
continue;
}
int heard = loudness - spread.at(monster.pos);
if (heard < 0)
{
continue;
}
In the preceding code, spread.at gives each monster's walking distance from the noise. What the monster hears is the loudness less that distance, and below 0, it hears nothing. A dead monster hears nothing either. That matters, since your killing blow makes a noise too, before the dead are taken away. Add the rest below the first part:
if (monster.state == MonsterState::Asleep && heard >= WAKE_LOUDNESS)
{
monster.state = MonsterState::Wandering;
monster.target = origin;
announce(monster, "wakes up!");
}
else if (monster.state == MonsterState::Wandering)
{
monster.target = origin;
}
}
}
A sleeper that hears enough wakes up. But it doesn't know who made the noise, so it doesn't hunt. It wanders, with the noise's origin as its target, and goes to see. A wanderer that hears anything at all goes to see too.
Hunters ignore noise, since they're busy with you, and so do monsters that are fleeing. The next image shows how far a fight carries.

The sleeper five steps away wakes, with three left. The one six steps away hears two, and sleeps on. The one on the other side of the wall is nearer as the crow flies. But the noise has to go the long way around, and it's spent before it gets there.
Fights Are Loud
Every blow in a fight is a noise, made where you stand. In playerAttacks, add this line below log_.add("You kill the " + name + "!");:
makeNoise(player_.pos, FIGHT_LOUDNESS);
Then add these lines below the line that tells of a blow, which starts log_.add("You hit the ":
// The noise comes after the blow is told, so the log keeps the order
makeNoise(player_.pos, FIGHT_LOUDNESS);
The order matters for the log. The blow comes first, and then whatever its noise wakes, so the log reads in the order things happened. A monster's blows are loud too. In monsterAttacks, add the same line below the message that the monster hits you:
makeNoise(player_.pos, FIGHT_LOUDNESS);
Now a fight is heard from both sides. Every blow, yours or a monster's, wakes whatever sleeps within five steps.
The Shrieker
Last, a monster that makes a noise on purpose. A shrieker is a fungus. It can't move, and it can't hurt you. But when it sees you, it shrieks, and the shriek carries 25 cells.
The shrieker needs two new traits. In Monster.h, add them below ERRATIC:
// It never moves, and a shrieker calls for help while it sees the
// player
constexpr unsigned int STATIONARY = 1 << 2;
constexpr unsigned int SHRIEKS = 1 << 3;
They're the next two bits, 4 and 8. Add the shrieker at the end of the table:
{ "shrieker", ',', { 210, 130, 220, 255 }, "shrieker",
5, 4, 0, 0, 0, Trait::STATIONARY | Trait::SHRIEKS },
Its glyph is a comma, a small thing on the floor that's easy to miss, and its picture came with the atlas you copied for the fruit bat. The damage in its row is 0 to 0, since it never attacks. In Game.cpp, add how often it shrieks, below WAKE_LOUDNESS:
// A shrieker shrieks every SHRIEK_EVERY turns while it sees the player
constexpr int SHRIEK_EVERY = 3;
A monster that can't move needs a function of its own, since three of the four handlers are about moving. In Game.h, add a declaration below announce's:
void actStationary(Monster& monster);
In Game.cpp, in monstersAct, add these lines below the check for the player's death:
// A monster that can't move, once awake, does only what it can
// do where it is
if (monster.def->has(Trait::STATIONARY) &&
monster.state != MonsterState::Asleep)
{
actStationary(monster);
continue;
}
A stationary monster that's awake does only what actStationary says, and continue skips the switch, on to the next monster. Asleep, it goes through the switch as any sleeper does, so it wakes in the same ways. Add actStationary below randomFloorCell:
// A monster that can't move. While it sees the player, a shrieker shrieks
// every SHRIEK_EVERY turns: a noise that brings other monsters to it
void Game::actStationary(Monster& monster)
{
bool shrieks = monster.def->has(Trait::SHRIEKS) && seesPlayer(monster);
if (shrieks && turn_ % SHRIEK_EVERY == 0)
{
announce(monster, "shrieks!");
makeNoise(monster.pos, SHRIEK_LOUDNESS);
}
}
The expression turn_ % SHRIEK_EVERY is the remainder when the turn count is divided by three, and it's 0 every third turn. So a shrieker that sees you shrieks every third turn, and each shriek wakes every sleeper within 22 steps.
Checkpoint: Press F5, with seed 42, and press F1 and F4. Walk eight steps down, and then to the right along the corridor, until a giant white rat comes to meet you, and bites. A jackal is asleep in the doorway just beyond the rat, two steps from the fight, and the bite wakes it: "The jackal wakes up!" Then it notices you, and you have two to fight. Hit the rat, and it dies, and the jackal steps into its place. Watch the goblins farther along the corridor, too: one is near enough to hear, and its line turns toward the fight.
Then try seed 2, for the shrieker. Change the seed in Command Arguments to 2, press F5, and press F1 and F4. The shrieker, a purple comma, is in the room below yours.
Walk one step down and to the right, with n, and twelve down, with j, through the passage. Then you stand beside it, and it wakes. Wait a few turns with the period key, and "The shrieker shrieks!" The next image shows that moment.

Everything within earshot heads for the shrieker: two kobolds and a goblin that were asleep, and the fruit bat, which wasn't. Even the other shrieker woke, though it can't go anywhere. A kobold farther off sleeps on, out of earshot.
Keep waiting, and over the next few turns they arrive, and notice you. The shrieker has four health and no attack. Back up the passage, they can reach you only one at a time.
Design note: The shrieker. Our shrieker comes from NetHack, where a shrieker is a purple fungus, an
F, that can barely move. Stand next to one, and it shrieks, which wakes every monster on the level. One shriek in ten brings a new monster too, now and then a purple worm. Ours is gentler. Its shriek is a noise like any other, so it wakes only the monsters near enough to hear it, by the ways they could walk. The rest of the floor sleeps on. Either way, the lesson for the player is the same: kill it first, and quickly.
That's the last of the monsters' minds for this chapter. Chapter 15 adds traits for monsters that act every other turn, never run, heal themselves, or feed on the blows they deal. Each is a bit, and a line or two in one of these functions.
Playing the Game
The dungeon has moods now. Most monsters are asleep when you arrive, and you can creep past them, or creep up on one and strike first. A sleeper that wakes as you arrive doesn't attack until its next turn. Fights are loud, though, and anything asleep within five steps wakes to them.
You can lose a hunter now. Go around a corner and keep going, and it will search where it last saw you, and give up. A shrieker is worth killing quickly, before it calls the neighbors. And a fruit bat that's after you is easier to wait for than to chase: stand still with the period key, and let it come to you.
Understanding the Code
Every monster has a MonsterState, and monstersAct calls the function for it: actAsleep, actWandering, actHunting or actFleeing. States change inside those functions, and in playerAttacks and makeNoise, where something happens to a monster. F4 draws every monster's state, and lists them.
Sleepers wake when you're next to them, or with a chance of one in twenty a turn while they can see you, or when you hit them. Wanderers walk to random floor cells with walkToward, each on a Dijkstra map of its own. Hunters do Chapter 10's chase, flee when they're badly wounded, and search for five turns where they lost you before they give up.
A kind's traits are bits in one unsigned int, each made with <<, joined with | and tested with & in has. Rows that leave their traits out get the member's default, 0. The fruit bat never sleeps, and half its moves are random, in movesErratically.
A noise is a Dijkstra map from where it's made. Each monster hears its loudness, less its walking distance. A sleeper that hears three or more wakes and goes to see, as a wanderer does. Every blow in a fight is a noise of 8.
The shrieker is STATIONARY and SHRIEKS. Awake, it skips the handlers, and while it sees you, it shrieks every third turn, a noise of 25.
Experimenting
The numbers marked tune are made for trying. Try each of these, and put each back afterward:
- Wide awake. Set
AWAKE_ONE_INto1. Every roll comes out 1, so no monster starts asleep, and the dungeon is as alert as it was in Chapter 10. - Light sleepers. Set
WAKE_LOUDNESSto0. A fight now wakes every sleeper within eight steps, and a shriek, everything within 25. - A patient hunter. Set
SEARCH_TURNSto30, andSEARCH_RADIUSto8. Lose a hunter, and watch it in the F4 list as it hunts about for you. - A plain bat. In the fruit bat's row, change
Trait::NEVER_SLEEPS | Trait::ERRATICtoTrait::NEVER_SLEEPS. It flies as straight as the others, and is much easier to catch.
The last one shows how much one bit can change a monster. In Chapter 16, the kinds of monster move into a text file, and there a kind's traits are a list of words, such as never_sleeps erratic.
Common Errors and Fixes
If the game didn't build, or doesn't behave as described, the problem is probably one of these.
C2065: 'Hunting': undeclared identifier. A state needs its enum's name in front of it: MonsterState::Hunting. The same goes for the traits, so C2065: 'ERRATIC': undeclared identifier means Trait::ERRATIC.
C4715: '`anonymous namespace'::stateColor': not all control paths return a value, a warning, so the game still builds. The return after the switch in stateColor is missing.
LNK2019: unresolved external symbol "private: void __cdecl Game::actFleeing(struct Monster &)", and then LNK1120: 1 unresolved externals. A function is declared in Game.h but not defined in Game.cpp, or its name is spelled differently in the two. The message names the function.
C2662: 'bool MonsterDef::has(unsigned int)': cannot convert 'this' pointer from 'const MonsterDef' to 'MonsterDef &'. The function has is missing its const. A monster's def points to a const MonsterDef, and only a const function can be called through it.
C4554: '&': check operator precedence for possible error; use parentheses to clarify precedence, a warning. The parentheses around traits & trait in has are missing, so trait != 0 is worked out first. Then has tests the first bit for every trait. You'd notice it at the end of the chapter, when the shrieker walks about.
Monsters that F4 shows asleep walk about. The break after actAsleep(monster); in monstersAct is missing, so a sleeper's turn runs on into the wandering code.
The fruit bat flies as straight as anything else. Its row has || where it should have |. The || gives true, which is stored as 1. So the bat never sleeps, but it isn't erratic.
The bat is a pink square in the tile view. The old tiles.txt has no picture called fruit-bat, so the tile view draws the debugging color. Copy tiles.png and tiles.txt from the Chapter 11 folder.
Fights wake nothing. The loop in makeNoise takes each monster by value, as Monster monster, so it wakes copies. It should be Monster& monster.
FAQ
Q) Why does a monster woken by your blow hunt you, but one woken by a noise only wanders?
A) A monster you hit knows exactly where you are, since you're standing next to it. A noise says only that something happened somewhere, so a monster that hears one goes to look. If it sees you when it gets there, then it hunts you.
Q) Isn't it slow for every wandering monster to work out a Dijkstra map every turn?
A) Less than you'd think. Each map sets 2,304 distances, and spreads over the few hundred cells that are floor. So a turn's maps come to some tens of thousands of simple steps. On seed 42, in a Debug build, the monsters' whole turn takes about two thousandths of a second, and a turn passes only when you press a key. If it ever mattered, monsters heading for the same cell could share a map, as hunters share toPlayer_.
Q) Why does a stationary monster get its own branch, rather than a fifth state?
A) A state is what a monster is doing, and the same monster can be in different ones. Being unable to move is part of what a kind of monster is, so it's a trait. A shrieker is asleep or awake like any monster, and the branch only says what an awake one does instead of moving.
Q) Why is the wait key a step of (0, 0), rather than a key of its own?
A) Because a step onto your own cell already does everything waiting should: no monster is there, the cell is floor, and the turn passes. A key of its own would need its own code to do the same things, and could drift from it.
AI Exercise (Optional)
A state machine grows by adding a state, and the arrows into and out of it. If you'd like to add one 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 monster, a struct Monster, has a MonsterState state (an enum class: Asleep, Wandering, Hunting, Fleeing), a std::optional<Point> target, a std::optional<Point> lastSeen, and a Point pos. A class, Game, has monstersAct(), which switches on each monster's state and calls actAsleep, actWandering, actHunting or actFleeing (each takes Monster&), and helpers: seesPlayer(const Monster&), walkToward(Monster&, Point goal), which takes a step toward a cell and returns false if it couldn't get closer, and player_.pos. I'd like a fifth state, Guarding: a guard stays within three cells of a cell of its own, home, and hunts the player if it sees them, but when it gives up the hunt, it walks back home instead of wandering. Show me the code, with each curly brace on its own line, and explain it."
Notice what the preceding prompt does. It names the types and functions exactly, so the AI can use them as they are. And it says what the new state does in the same words this chapter uses, including where its arrows go.
When the answer comes back, check every place the new state must appear: the enum class, the switch in monstersAct, stateColor and stateName for F4, giveUp, which should send a guard home, playerAttacks's test of unaware, so that a guard you hit hunts you, and makeNoise, which must decide whether a guard goes to see. A state that's missing from one of them is the kind of mistake the compiler can't always catch. Then give one monster on seed 42 the new state, and watch it with F4.
Summary
Every monster now has a mind: asleep, wandering, hunting or fleeing, with a function for each state, and arrows between them where something happens. Traits, stored as bits, make kinds behave differently: the fruit bat never sleeps and flies erratically, and the shrieker can't move but calls for help. Hunters search before they give up, you can wait a turn, and fights and shrieks are noises that wake what's near enough to hear. F4 shows every mind at once. In Chapter 12, the dungeon gets things to pick up: potions and scrolls whose colors and titles keep what they do a secret, until you try one.
