So far, the dungeon has been one floor. You can explore all of it, fight everything on it, and pick it clean. Then there's nowhere left to go. A traditional roguelike is a descent: each floor has a way down, and each floor down is more dangerous than the one above, until the bottom, where the prize is.
This chapter makes the dungeon nine floors deep. Each floor gets stairs down, placed as far from where you arrive as the floor allows, and each new floor is made fresh when you take them. Monsters belong to bands of floors, so jackals and kobolds give way to wolves and cave orcs, and then to ogres, trolls and vampires. Some come in packs. The prize itself comes in Chapter 20.
Items turn up as often as they should, rather than all alike, and there's gold to find. The next image shows the end of the chapter: a third floor, with the whole map shown by F1.

The side panel says "Floor 3", and the player has picked up 105 gold pieces on the way down. The stairs to floor 4 are in the room at the top right, as far as the floor's corridors go from where the player came in. Gold lies about in piles, $. The room beside the player's holds a pack of three jackals, and a cave spider. And the red S in a room on the right is a giant spider, a kind from deeper down.
Chapter 15 in the book's repository holds this chapter's finished project. The chapter carries on in your own project from Chapter 14, or from the book's Chapter 14 folder.In this chapter, we will:
- Gather a floor's map, monsters and items into one struct,
Level, and change nothing else - Put stairs down on every floor, at the far end of the longest walk, and go down them with
> - Choose each item by how common its kind should be
- Choose each monster from the kinds found on its floor, with packs that sleep and wake together
- Add the middle band of monsters, slow ones and fearless ones among them, and then the deep band
- End the dungeon at floor 9, and scatter gold for the player to collect
There are five checkpoints along the way. Let's start with the plan.
Planning the Chapter
This chapter adds one file, and changes eleven, along with the tile atlas:
| File | Status | What it's for |
|---|---|---|
src/Level.h |
New | One floor: how deep it is, its map, its monsters, its items, and its stairs |
src/Map.h, src/Map.cpp |
Changed | The stairs, and walkable ground |
src/DijkstraMap.cpp |
Changed | Distances across the stairs, as across floor |
src/Item.h |
Changed | How common each kind is, and gold |
src/Monster.h |
Changed | The floors each kind is found on, packs, new traits, and nine new kinds |
src/Player.h |
Changed | The player's gold |
src/Common.h |
Changed | The colors of the stairs and of gold |
src/GlyphView.cpp, src/TileView.cpp |
Changed | Drawing the stairs |
src/Game.h, src/Game.cpp |
Changed | Making floors, going down, choosing kinds, packs, and the panel |
assets/tiles.png, assets/tiles.txt |
Changed | Pictures of the stairs, the new monsters, and gold |
| The other files | Unchanged |
The next image shows how the files fit together at the end of the chapter. The new file, Level.h, holds a floor. So it uses the map, the monsters and the items. Its arrows go around the outside of the picture to reach them.

The descent is one way. There are no stairs up, so a floor you leave is a floor you'll never see again. The game only ever needs to keep the one you're on.
That makes the design simple: Game holds one Level, and going down replaces it with a new one. The player, with their health, pack and gold, belongs to Game, not to the floor. So they come down the stairs unchanged.
Depth works through two numbers in each kind of monster: the first floor it's found on, and the last. A floor chooses its monsters from the kinds whose range includes it. Once in ten, it chooses from the kinds three floors further down, so a deeper monster can turn up early as a nasty surprise. Every floor also has one more monster than the floor above, and every second floor one more item.
We'll build the chapter in five steps. First the floor moves into a Level, which changes nothing you can see. Then come the stairs, then items by how common they are, then monsters by floor and in packs, and last of all the deep band, the bottom of the dungeon, and gold.
A Floor of Its Own
The first step gathers what makes up a floor into one place. It's a change to how the code is organized, not to what it does, so the game should play exactly as it did at the end of Chapter 14.
The Level
In Solution Explorer, right-click src, choose Add > New Item, and add a Header File (.h) called Level.h. Type it in:
#pragma once
#include <vector> // std::vector, for the monsters and the items
#include "Common.h"
#include "Item.h"
#include "Map.h"
#include "Monster.h"
// One floor of the dungeon: how deep it is, its map, the monsters and the
// items on it, and where its stairs down are. Only the floor the player is
// on is kept, since the way down is one way
struct Level
{
int depth = 1; // 1 for the first floor
Map map;
std::vector<Monster> monsters;
std::vector<FloorItem> items;
Point stairs;
};
The new Level is a plain struct, with no functions of its own. It holds what changes from floor to floor: how deep the floor is, its map, its monsters, and the items lying on it. The vectors of monsters and items move here from Game, along with the map. They're named for what they are now, monsters and items, since level.items can only be the items on the level.
The struct has a place for the stairs too, though they come in the next step. Nothing in this chapter reads stairs once it's set, since the map's own terrain says where they are. Chapter 19 will, when it makes sure a secret door never hides the only way down.
One Member in Place of Three
Open Game.h, and add the new header below #include "Item.h":
#include "Level.h"
The comment on #include <vector> says the vectors are for the monsters and the rooms, and the monsters' vector is leaving. In Game.h, change for the monsters and the rooms to for the rooms.
Now find these lines, the run's members, near the end of the class:
// The run: its seed and dice, the dungeon, who's in it, and what's
// been said
unsigned int seed_;
Rng rng_;
Map map_;
std::vector<Monster> monsters_;
std::vector<FloorItem> floorItems_;
Change them to these:
// The run: its seed and dice, the floor the player is on, what the
// player knows of the items, the player, and what's been said
unsigned int seed_;
Rng rng_;
Level level_;
Three members have become one. Everything that was map_ is now level_.map, monsters_ is level_.monsters, and floorItems_ is level_.items, as the next image shows.

The seed, the dice and the player stay where they are. They belong to the run, and they go on from floor to floor. A new floor needs a function to make it. Add its declaration below // Setting up:
void newLevel(int depth);
The rest of the change is in Game.cpp, where the old names are used dozens of times. Rather than finding each by hand, use Find and Replace, which Chapter 5 introduced.
Click anywhere in Game.cpp, and press Ctrl+H to open Find and Replace. Below the boxes are three small buttons, and the first, marked Ab, is Match case. Click it, so it's on, and leave the box beside the buttons at Current document. Type map_ in the top box, and level_.map in the box below it, and click Replace All.
Match case matters here. Without it, map_ also matches the MAP_ in MAP_W and MAP_H, and the Map_ at the end of revealMap_, and Replace All would turn them into nonsense such as level_.mapW. With it, Replace All changes the twenty-nine uses of the map, and nothing else.
Now type monsters_ in the top box, and level_.monsters in the box below it, and click Replace All, which changes eleven.
Last, type floorItems_ in the top box, and level_.items in the box below it, and click Replace All once more, for eight. Press Esc to close the box.
Making a Floor
The constructor made the dungeon itself, and now a function does, so that going down can make the next. Replace the comment above Game::Game with this:
// A new run: the first floor, the looks of this run's potions and
// scrolls, and the player, with a short sword in hand
In the constructor, delete everything from std::vector<SDL_Rect> rooms down to toPlayer_.compute(, and type these lines in their place:
newLevel(1);
identification_.randomize(rng_);
player_.weapon = Item{ findItemDef("short sword") };
player_.hp = PLAYER_HP;
player_.maxHp = PLAYER_HP;
Four of those five lines are as they were. What the deleted lines did is now the job of newLevel. It makes the floor, places the player, the monsters and the items, and works out what the player can see. Add it below run:
// Makes the floor at a depth, in place of the one before: its rooms, the
// player in the middle of the first, and its monsters and items
void Game::newLevel(int depth)
{
level_ = Level();
level_.depth = depth;
std::vector<SDL_Rect> rooms = RoomGen::generate(level_.map, rng_);
player_.pos = RoomGen::centerOf(rooms.front());
placeMonsters(rooms);
placeItems(rooms);
updateSight();
toPlayer_.compute(level_.map, { player_.pos });
}
The line level_ = Level(); throws the old floor away, with its monsters and items, and starts from an empty one. That's the whole of forgetting a floor. The rest is what the constructor used to do, in the same order, so the dice roll exactly as they did before.
The order matters more than it looks. The dungeon, the monsters and the items all come from rng_, one roll after another. So a seed's floor depends on every roll before it. The constructor now makes the floor first, and then shuffles the looks of the potions and scrolls, just as it did. Swap those two lines, and every seed would give a different dungeon.
Checkpoint: Press F5, with the seed in Command Arguments still 42. The game is exactly as it was at the end of Chapter 14: the same rooms, the same monsters in the same places, and the same items. Nothing you can see has changed, which is what this step was for.
Stairs Down
The second step puts stairs on each floor, and lets the player take them. It ends with a new floor for every trip down.
A New Kind of Ground
The stairs are a kind of terrain. Open Map.h, and add StairsDown to Terrain, below Floor:
StairsDown,
Until now, the game has asked "is this floor?" whenever it meant "can this be walked on?", since only floor could be. Now the stairs can be walked on too, so the map needs a better question. Add it below at's declaration, with a blank line on each side:
// True for terrain that can be walked on: floor, and the stairs
bool isWalkable(int x, int y) const;
The walls worth drawing are the ones beside ground you can walk on, which now includes the stairs. Replace the comment above bordersFloor with this:
// True for a wall with walkable ground beside it, diagonals included.
// Those are the only walls worth drawing. The rest is solid rock nobody
// can see
Open Map.cpp, and add the function below at:
bool Map::isWalkable(int x, int y) const
{
Terrain terrain = at(x, y);
return terrain == Terrain::Floor || terrain == Terrain::StairsDown;
}
It uses at, so a cell off the map is wall, and can't be walked on. In Map.cpp, in bordersFloor, change at(x + dx, y + dy) == Terrain::Floor to isWalkable(x + dx, y + dy).
The Dijkstra map spreads its distances over floor, and the stairs are part of the way now. Without this change, the stairs would be a wall to every monster, and the distances would go around them. In DijkstraMap.cpp, change map.at(next.x, next.y) != Terrain::Floor to !map.isWalkable(next.x, next.y).
Every Place That Asked
The game's own code asks the same question in five places, each of which should ask about walking. Open Game.cpp. In playerStep, change level_.map.at(to.x, to.y) == Terrain::Floor to level_.map.isWalkable(to.x, to.y), so the player can step onto the stairs.
The scroll of magic mapping marks every floor cell as explored, and every wall beside one. In applyEffect, find this line:
if (level_.map.at(x, y) == Terrain::Floor || level_.map.bordersFloor(x, y))
Change it to these two:
bool ground = level_.map.isWalkable(x, y);
if (ground || level_.map.bordersFloor(x, y))
A wandering monster picks a random cell to head for, as does a scroll of teleportation, and the stairs are as good as any. In randomFloorCell, change level_.map.at(cell.x, cell.y) == Terrain::Floor to level_.map.isWalkable(cell.x, cell.y). A monster can stand on the stairs too, so in isFree, change level_.map.at(cell.x, cell.y) == Terrain::Floor to level_.map.isWalkable(cell.x, cell.y).
Last, the map is drawn where there's floor, or a wall beside it. In drawMap, find this line:
if (terrain == Terrain::Floor || level_.map.bordersFloor(x, y))
Change it to these two:
bool ground = level_.map.isWalkable(x, y);
if (ground || level_.map.bordersFloor(x, y))
The changes to magic mapping and to drawMap show nothing new. The stairs always have floor beside them. And bordersFloor looks at the cell itself as well as its neighbors, so it already said yes for the stairs. But each test should say what it means, and these mean ground you can walk on.
The field of view needs no change. It asks whether a cell blocks sight, and only walls do. So you can see across the stairs as across floor.
Drawing the Stairs
The stairs should stand out, since finding them is the point of a floor. Open Common.h, and add their color to Palette, below FLOOR:
constexpr SDL_Color STAIRS = { 240, 240, 250, 255 };
The glyph view draws them as >, the symbol most roguelikes use for stairs down. Open GlyphView.cpp, and replace the comment above drawTerrain with this:
// Floor is a dot, the stairs down a >, and wall a # on dark stone.
// Remembered cells are drawn in the dimmer, cooler colors
Then add the stairs below the floor's if:
else if (terrain == Terrain::StairsDown)
{
SDL_Color color = lit ? Palette::STAIRS : Palette::MEMORY_WALL;
drawCell(cell, '>', color);
}
Remembered stairs are drawn in the remembered walls' color, which is brighter than remembered floor. So a > you've seen is easy to find again on the map.
The tile view has a picture of the stairs. Its edges are see-through, so drawn on their own, they'd show the black behind them. They need a floor tile underneath. Open TileView.cpp, and replace the whole of drawTerrain with this:
// Remembered cells are tinted, which darkens and cools them, as the glyph
// view's memory colors do. The stairs' tile is see-through around its
// edges, so a floor is drawn under it first
void TileView::drawTerrain(Point cell, Terrain terrain, bool lit) const
{
SDL_Color tint = lit ? NO_TINT : Palette::MEMORY_TINT;
const char* name = "wall";
if (terrain == Terrain::Floor)
{
name = "floor";
}
else if (terrain == Terrain::StairsDown)
{
drawTile(cell, "floor", tint);
name = "stairs";
}
drawTile(cell, name, tint);
}
From the book's repository, copy tiles.png and tiles.txt from the Chapter 15 folder's assets folder into your project's assets folder, in place of the old ones. They hold the stairs, and the pictures of every monster this chapter adds, and of gold, ready for later in the chapter.
Placing the Stairs
Where should the stairs go? Anywhere at random would put them now and then in the room you arrive in, and a floor you can leave at once isn't much of a floor. The game puts them where the walk is longest instead: on the cell farthest from the arrival point, by the shortest way there on foot. Every floor then has to be crossed to find the way down.
The Dijkstra map from Chapter 10 already measures walks. Made from the player's cell, it gives every cell its distance by the shortest way, and the farthest cell is the one with the largest distance. In Game.cpp, in newLevel, add this below placeItems(rooms);, to place the stairs once the floor is made:
placeStairs();
Replace the comment above newLevel with this:
// Makes the floor at a depth, in place of the one before: its rooms, the
// player in the middle of the first, its monsters and items, and its
// stairs down
Then add placeStairs below newLevel:
// The stairs down go on the cell farthest from the player by walking, so
// every floor has to be crossed to find the way down
void Game::placeStairs()
{
DijkstraMap fromArrival;
fromArrival.compute(level_.map, { player_.pos });
Point farthest = player_.pos;
int farthestDistance = 0;
for (int y = 0; y < MAP_H; ++y)
{
for (int x = 0; x < MAP_W; ++x)
{
int distance = fromArrival.at({ x, y });
if (distance != DijkstraMap::UNREACHED &&
distance > farthestDistance)
{
farthest = { x, y };
farthestDistance = distance;
}
}
}
level_.stairs = farthest;
level_.map.set(farthest.x, farthest.y, Terrain::StairsDown);
}
In the preceding code, fromArrival is a Dijkstra map with the player's cell as its one goal, so each cell's distance is its walk from where the player starts. A cell the walk can't reach has the distance UNREACHED, and is skipped. The loop keeps the farthest cell found so far, and the stairs go there, both in the map's terrain and in level_.stairs. The next image shows the result for seed 42.

The stairs are seventy-two steps from the player, by the shortest way, in a room at the top right of the map. Other rooms are farther away in a straight line, but none is farther on foot.
Add the declaration to Game.h, below placeItems's:
void placeStairs();
Nothing calls placeStairs but newLevel, so it's private, with the other functions that set up a floor.
Going Down
Going down is a key, >, and that raises a question SDL answers in an unexpected way. On a US keyboard, > is Shift and the period. The key event's key, which most of the game's keys are checked against, is the character on the key itself. So it says period whether Shift is held or not. The game needs the character the keys type together, which SDL_GetKeyFromScancode works out from the key and the modifiers held, such as Shift.
In Game.cpp, add this helper to the unnamed namespace, below stepFor:
// The character a key types with the modifiers held, such as > for
// Shift and the period on a US keyboard. A key event's keycode is
// the key's own character, the period, whether Shift is held or not
SDL_Keycode typedKey(const SDL_KeyboardEvent& key)
{
return SDL_GetKeyFromScancode(key.scancode, key.mod, false);
}
Its last argument, false, asks for the typed character, as text would get it, rather than a key event's keycode.
Replace the comment above handlePlayingKey with this:
// Esc quits, > goes down the stairs, g picks up, i shows the inventory, d
// drops, x starts looking around, and the moving keys move the player
Then add the stairs' key below the if for Esc:
// > goes down the stairs. It's Shift and the period on a US keyboard,
// so it's checked before the moving keys, where the period alone waits
if (typedKey(key) == SDLK_GREATER)
{
descend();
redraw_ = true;
return;
}
The order matters. The period alone is the key that waits a turn, and stepFor checks only the key itself. If the moving keys came first, Shift and the period would be taken as a wait, and the stairs' check would never see it.
The trip itself is descend. Add it below placeStairs:
// > on the stairs goes down to a new floor. The player keeps everything
// they have, and the floor they leave is gone
void Game::descend()
{
log_.markRead();
if (level_.map.at(player_.pos.x, player_.pos.y) != Terrain::StairsDown)
{
log_.add("There are no stairs down here.");
return;
}
newLevel(level_.depth + 1);
++turn_;
log_.add("You go down the stairs to floor " +
std::to_string(level_.depth) + ".");
}
Pressing > anywhere but on the stairs says so. It takes no turn, as a bump into a wall takes none. On the stairs, newLevel makes the next floor, one deeper, and puts the player in its first room. The player's health, pack and statuses come along unchanged, since none of them is on the level.
The turn counter goes up, since taking the stairs is an action. But finishTurn isn't called, so no monster acts on the turn you arrive: the new floor's monsters haven't noticed you yet. Add the declaration to Game.h, below pickUp's:
void descend();
It goes with pickUp, among the things the player can do with a key.
What the Look Panel Says
Pointing at the stairs should say what they are. The look panel needs room for one more line, since "Stairs down, as you remember it." can take two. In Game.cpp, change LOOK_PANEL_LINES = 8 to LOOK_PANEL_LINES = 9. Then, in drawLookPanel, find these lines:
bool floor = level_.map.at(cell->x, cell->y) == Terrain::Floor;
std::string ground = floor ? "Floor" : "A wall";
Change them to these:
std::string ground = "A wall";
Terrain terrain = level_.map.at(cell->x, cell->y);
if (terrain == Terrain::Floor)
{
ground = "Floor";
}
else if (terrain == Terrain::StairsDown)
{
ground = "Stairs down";
}
With three kinds of ground, one ?: can't choose between them, so the name starts as "A wall" and changes if the cell is anything else.
Design note: Rogue's one staircase. Rogue had one staircase on each level, drawn as
%, in a room chosen at random. So now and then it was in the room you arrived in. Pressing>on it made a new level, and<took you up, but only while you carried the Amulet of Yendor, which lay twenty-six levels down. Without it, "your way is magically blocked". Going up didn't take you back to a level you'd seen, either: Rogue kept no levels at all, made a new one for every trip, and threw away whatever you left behind.
Our dungeon is like Rogue's in what it keeps, and different in where it puts the way down. A long walk to the stairs means every floor is explored, fought through, and picked over, which is what makes it worth having.
Checkpoint: Press F5. The first floor looks as it always has, but somewhere on it are the stairs. Press F1 to see the whole floor: the > is in the room at the top right, as Figure 15.4 showed. Then press F1 again, and walk there. It's a long way, and you may have to fight on the way. Press > before you reach the stairs, and the log says "There are no stairs down here." On the stairs, press >: "You go down the stairs to floor 2." The next image shows a second floor reached that way.

Your floor 2 will look different, unless you took exactly the same steps, fought exactly the same fights, and arrived on turn 78. Every monster's every move on the way rolls the dice, and the next floor is made from whatever rolls come next. The way down goes on forever, for now: there's no bottom yet.
How Common Each Kind Is
The third step changes how the game chooses items. Every kind has been equally likely, so plate armor turned up as often as a potion of healing. Of eighteen kinds, eight are weapons and armor, so nearly half of everything found was something to wield or wear. This step gives each kind a number that says how common it is.
Frequencies
Open Item.h, and add the new field to ItemDef, below tile:
int frequency; // how common it is, against the others' frequencies
A kind's frequency only means something against the others'. One with a frequency of 6 is twice as common as one with 3, and what the numbers add up to doesn't matter. The field has no default written in place, since every kind should say how common it is.
Every row of the table gains its frequency, after its tile, so the rows are easiest typed again. In Item.h, delete everything from // name, class, effect, tile, then down to 0, 0, 5, true },, which is the comment that lists the fields, and every row. Then type the potions and scrolls in their place:
// name, class, effect, tile, frequency, then a weapon's damage from and
// to, and armor's defense and noise
{ "healing", ItemClass::Potion, ItemEffect::Heal, "potion", 7 },
{ "greater healing", ItemClass::Potion, ItemEffect::HealFully, "potion",
4 },
{ "strength", ItemClass::Potion, ItemEffect::Strengthen, "potion", 3 },
{ "poison", ItemClass::Potion, ItemEffect::Poison, "potion", 5 },
{ "regeneration", ItemClass::Potion, ItemEffect::Regenerate, "potion",
4 },
{ "confusion", ItemClass::Potion, ItemEffect::Confuse, "potion", 5 },
{ "teleportation", ItemClass::Scroll, ItemEffect::Teleport, "scroll",
5 },
{ "magic mapping", ItemClass::Scroll, ItemEffect::MapFloor, "scroll",
4 },
{ "identify", ItemClass::Scroll, ItemEffect::Identify, "scroll", 6 },
{ "fear", ItemClass::Scroll, ItemEffect::Frighten, "scroll", 4 },
And the weapons and armor below them:
{ "dagger", ItemClass::Weapon, ItemEffect::None, "dagger", 3, 1, 4 },
{ "short sword", ItemClass::Weapon, ItemEffect::None, "sword", 2, 2, 6 },
{ "spear", ItemClass::Weapon, ItemEffect::None, "spear", 2, 2, 8 },
{ "mace", ItemClass::Weapon, ItemEffect::None, "mace", 2, 3, 7 },
{ "long sword", ItemClass::Weapon, ItemEffect::None, "sword", 1, 3, 9 },
{ "leather armor", ItemClass::Armor, ItemEffect::None, "leather-armor",
3, 0, 0, 1 },
{ "chain mail", ItemClass::Armor, ItemEffect::None, "metal-armor",
2, 0, 0, 3 },
{ "plate armor", ItemClass::Armor, ItemEffect::None, "metal-armor",
1, 0, 0, 5, true },
Across the whole table, healing is the most common kind, at 7, and identify next, at 6. Those are what a player most needs. Plate armor and the long sword are the rarest, at 1, and the best of their kinds. Together the potions come to 28, the scrolls to 19, the weapons to 10 and the armor to 6, out of 63. So about three items in four are something to drink or read.
Choosing by Frequency
To choose a kind by frequency, picture the kinds' frequencies laid end to end, each kind's share as long as its number. A roll lands somewhere along the whole length, and the kind whose share it lands in is the one chosen. The next image shows a roll of 30.

The roll of 30 goes past healing's 7, greater healing's 4, strength's 3, poison's 5, regeneration's 4 and confusion's 5. Those take it down to 2. Teleportation's share is 5, more than what's left, so the roll lands there.
In Game.cpp, add the function below placeItems:
// A kind of item, chosen by frequency, so a kind with a frequency of 10
// turns up twice as often as one with 5. The roll lands somewhere in the
// total, and the kinds take their shares of it in turn
const ItemDef* Game::chooseItemKind()
{
int total = 0;
for (const ItemDef& def : ITEM_DEFS)
{
total += def.frequency;
}
int roll = rng_.roll(1, total);
for (const ItemDef& def : ITEM_DEFS)
{
roll -= def.frequency;
if (roll <= 0)
{
return &def;
}
}
return &ITEM_DEFS[0];
}
In the preceding code, the first loop adds up the frequencies, to 63. The roll is from 1 to the total. Then the second loop takes each kind's frequency off it in turn, until it reaches zero or less, in the kind it landed in. The last line can never be reached, since the roll is never more than the total. But the compiler can't know that, and the function must return something.
Add its declaration to Game.h, above placeItems's:
const ItemDef* chooseItemKind();
Deeper floors should hold a little more, too. In Game.cpp, find these lines, near the top:
// How many items a new dungeon starts with
constexpr int ITEM_COUNT = 6;
Change them to these:
// How many items a floor starts with: ITEMS_BASE, and one more for
// every two floors down
constexpr int ITEMS_BASE = 6;
Replace the comment above placeItems with this:
// Puts the floor's items in its rooms, any of them: ITEMS_BASE, and one
// more for every two floors down. One to a cell, never on the player
In placeItems, find these lines:
int lastKind = static_cast<int>(std::size(ITEM_DEFS)) - 1;
for (int i = 0; i < ITEM_COUNT; ++i)
Change them to these:
int wanted = ITEMS_BASE + level_.depth / 2;
for (int i = 0; i < wanted; ++i)
The division is between whole numbers, so it rounds down: floor 1 has six items, floors 2 and 3 have seven, and floor 9 has ten. Last, in placeItems, change &ITEM_DEFS[rng_.roll(0, lastKind)] to chooseItemKind().
Checkpoint: Press F5. The game plays as before, and six items are too few to show the shares. So make them easy to see: for a moment, set ITEMS_BASE to 60, press F5, and press F1.
Sixty items lie about the first floor, and with seed 42 there are thirty potions, sixteen scrolls, ten weapons and four suits of armor. That's close to the shares, which would give twenty-seven, eighteen, ten and six. Set ITEMS_BASE back to 6 before you go on.
Monsters by Floor
The fourth step is the biggest. Each kind of monster gets the floors it's found on, so the deeper you go, the tougher the monsters. Five new kinds arrive for the middle floors, some in packs, one slow, and two that never flee. It ends with floors whose monsters suit their depth.
Bands
Open Monster.h. The new kinds bring two new traits. Add them to Trait, below SHRIEKS:
// It acts every other turn; it fights to the death
constexpr unsigned int SLOW = 1 << 4;
constexpr unsigned int NEVER_FLEES = 1 << 5;
A kind's floors are two numbers: the first floor it's found on, and the last. Add them to MonsterDef, below tile:
int minFloor; // the floors it's found on, from minFloor to
int maxFloor; // maxFloor
And how many of a kind are found together is one more. Add it below venom:
int pack = 1; // how many of it are found together
A pack of one is a monster on its own, which most kinds are. So that's the default, written in place, and the table's rows can leave it out. 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.
// What a kind doesn't need from the end of a row, it can leave out
Every row changes, since each gains its floors. And the kinds are in a new order, from shallow to deep. In Monster.h, delete everything from // name, glyph, color, tile, down to 5, 5, 1, 3, 0, 0, 4 },, which is the comment that lists the fields, and every row. Type the shallow band in their place:
// name, glyph, color, tile, floors from and to,
// sight, hp, damage from and to, defense, traits, venom, pack
{ "giant white rat", 'r', { 215, 215, 205, 255 }, "giant-white-rat",
1, 3, 6, 4, 1, 3, 0 },
{ "fruit bat", 'b', { 205, 120, 90, 255 }, "fruit-bat",
1, 3, 6, 3, 1, 2, 0, Trait::NEVER_SLEEPS | Trait::ERRATIC },
{ "jackal", 'j', { 205, 150, 80, 255 }, "jackal",
1, 3, 7, 5, 1, 3, 0, 0, 0, 3 },
{ "kobold", 'k', { 110, 185, 95, 255 }, "kobold",
1, 4, 6, 7, 2, 4, 1 },
{ "shrieker", ',', { 210, 130, 220, 255 }, "shrieker",
1, 5, 5, 4, 0, 0, 0, Trait::STATIONARY | Trait::SHRIEKS },
{ "cave spider", 'S', { 150, 130, 175, 255 }, "cave-spider",
2, 4, 5, 5, 1, 3, 0, 0, 4 },
{ "goblin", 'g', { 160, 205, 60, 255 }, "goblin",
2, 5, 7, 9, 2, 5, 1 },
Then the middle band, below the goblin:
{ "cave orc", 'o', { 205, 125, 60, 255 }, "cave-orc",
4, 6, 7, 16, 4, 8, 3, 0, 0, 2 },
{ "wolf", 'C', { 175, 175, 185, 255 }, "wolf",
4, 6, 8, 14, 3, 7, 1, 0, 0, 3 },
{ "zombified kobold", 'z', { 125, 155, 110, 255 }, "zombified-kobold",
4, 6, 5, 20, 3, 6, 2, Trait::SLOW | Trait::NEVER_FLEES },
{ "giant spider", 'S', { 185, 70, 70, 255 }, "giant-spider",
5, 7, 6, 16, 3, 7, 2, 0, 6 },
{ "skeleton orc", 's', { 230, 230, 215, 255 }, "skeleton-orc",
5, 6, 6, 18, 4, 8, 4, Trait::NEVER_FLEES | Trait::NEVER_SLEEPS },
Of the two bands, the shallow one is the seven kinds from before, now with their floors, and the jackal with its pack of three. The middle band is new. The cave orc is a tough fighter in pairs, and wolves hunt in packs of three. The zombified kobold is slow, and too mindless to flee. The giant spider's bite poisons for six turns, and the skeleton orc never flees and never sleeps.
The bands overlap, so the change from floor to floor is gradual, as the next image shows. The deep band, on floors 7 to 9, comes in the last step.

Kobolds are still about on floor 4, where the first wolves and cave orcs appear, and shriekers last until floor 5. A floor in the middle has old enemies and new ones.
Choosing a Kind
A floor chooses each monster from the kinds found on it, with one twist: one time in ten, it chooses from the kinds found three floors further down. Most of a floor's monsters suit it, but now and then something worse is waiting. That keeps the player from ever feeling quite safe.
In Game.cpp, the old count is about to go. Find these lines, near the top:
// How many monsters a new dungeon starts with
constexpr int MONSTER_COUNT = 8;
Change them to these:
// How many monsters a floor starts with: MONSTERS_BASE, and
// MONSTERS_PER_FLOOR more for each floor down. One in DEEPER_ONE_IN is
// of a kind from three floors deeper (all *tune*)
constexpr int MONSTERS_BASE = 16;
constexpr int MONSTERS_PER_FLOOR = 1;
constexpr int DEEPER_ONE_IN = 10;
The first floor wants seventeen monsters, sixteen and one more, and floor 9 twenty-five. That's more than twice what the first floor had before, but they're spread over the whole floor, and most of them are asleep.
Now the choosing. Add the first part of chooseMonsterKind below placeMonsters:
// A kind of monster for this floor: one that's found on it, or, one time
// in DEEPER_ONE_IN, one that's found three floors further down, if any
// kind is found that deep
const MonsterDef* Game::chooseMonsterKind()
{
// Every kind found on a floor
auto kindsOn = [](int floor)
{
std::vector<const MonsterDef*> kinds;
for (const MonsterDef& def : MONSTER_DEFS)
{
if (def.minFloor <= floor && floor <= def.maxFloor)
{
kinds.push_back(&def);
}
}
return kinds;
};
The helper kindsOn is a lambda with a name, like Chapter 10's mix, so it can be used more than once. It collects every kind found on the floor it's given, as pointers into the table, since the table's rows are what the monsters will point at. Its brackets are empty, since it needs nothing from the code around it: MONSTER_DEFS is a table of the whole program, not a variable of the function. Add the rest of the function below it, with a blank line in between:
std::vector<const MonsterDef*> kinds;
if (rng_.roll(1, DEEPER_ONE_IN) == 1)
{
kinds = kindsOn(level_.depth + 3);
}
if (kinds.empty())
{
kinds = kindsOn(level_.depth);
}
if (kinds.empty())
{
// Below every band, for now: any kind will have to do
for (const MonsterDef& def : MONSTER_DEFS)
{
kinds.push_back(&def);
}
}
return kinds[rng_.roll(0, static_cast<int>(kinds.size()) - 1)];
}
The vector starts empty. One time in ten, it's filled with the kinds three floors deeper. If it's still empty, because the roll didn't come up or because nothing lives that deep, it's filled with the floor's own. The last if is for a floor below every band, where even that finds nothing. Until the last step gives the dungeon a bottom, the stairs go on past floor 7, the deepest any kind is found so far, and any kind will have to do down there.
Packs
A pack is a group of one kind that's placed together, in one room, and that sleeps and wakes together. Each pack gets a number of its own, so its members can find each other. In Monster.h, 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, how
// long it's been searching for them there, and the pack it belongs to, if
// any (packs are numbered from 1; 0 is none)
Then add the pack's number below searchTurns:
int packId = 0;
Placing monsters changes all through, so it's easiest written again. In Game.cpp, delete everything from // Puts MONSTER_COUNT monsters of random kinds down to the closing brace of placeMonsters. Type the first part of the new one in its place:
// Puts the floor's monsters in its rooms: MONSTERS_BASE, and
// MONSTERS_PER_FLOOR more for each floor down, while there's room. A pack
// comes all together, in one room, asleep or awake together. The first
// room is the player's, so it's left empty
void Game::placeMonsters(const std::vector<SDL_Rect>& rooms)
{
int lastRoom = static_cast<int>(rooms.size()) - 1;
if (lastRoom < 1)
{
return;
}
int wanted = MONSTERS_BASE + MONSTERS_PER_FLOOR * level_.depth;
int packsSoFar = 0;
for (int tries = 0; tries < 100; ++tries)
{
if (static_cast<int>(level_.monsters.size()) >= wanted)
{
return;
}
Then the rest of it below, with a blank line in between:
const MonsterDef* kind = chooseMonsterKind();
const SDL_Rect& room = rooms[rng_.roll(1, lastRoom)];
int packId = 0;
if (kind->pack > 1)
{
++packsSoFar;
packId = packsSoFar;
}
// Most start asleep, and the rest wandering
bool awake = kind->has(Trait::NEVER_SLEEPS) ||
rng_.roll(1, AWAKE_ONE_IN) == 1;
for (int i = 0; i < kind->pack; ++i)
{
std::optional<Point> cell = freeCellIn(room);
if (cell)
{
addMonster(*kind, *cell, packId, awake);
}
}
}
}
In the preceding code, each try chooses a kind and a room, and places the kind's whole pack there, one monster to a free cell. A kind that comes in packs gets the next pack number, and everyone else keeps 0, for none. Whether the newcomers start awake is rolled once, for the pack, so they all start the same.
The loop stops when the floor has its monsters, or after a hundred tries, if the rooms are too full to take any more. A pack is placed whole, even when it takes the count past what's wanted. So a floor can end up with a monster or two over. Seed 42's first floor, for one, has eighteen.
Finding a free cell, and making a monster, are now functions of their own, since each is used once per monster in a pack. Add them below chooseMonsterKind:
// A cell in the room with no monster on it, chosen at random, or nothing
// if a few tries find none
std::optional<Point> Game::freeCellIn(const SDL_Rect& room)
{
for (int tries = 0; tries < 20; ++tries)
{
Point cell = { room.x + rng_.roll(0, room.w - 1),
room.y + rng_.roll(0, room.h - 1) };
if (monsterAt(cell) == nullptr)
{
return cell;
}
}
return std::nullopt;
}
The first tries twenty cells at random, and gives up with std::nullopt if every one it tries is taken. Then add the second below it:
// A new monster of a kind, on a cell, with its kind's health and defense
void Game::addMonster(const MonsterDef& kind, Point cell, int packId,
bool awake)
{
Monster monster;
monster.def = &kind;
monster.pos = cell;
monster.hp = kind.hp;
monster.maxHp = kind.hp;
monster.defense = kind.defense;
monster.packId = packId;
monster.state = awake ? MonsterState::Wandering : MonsterState::Asleep;
level_.monsters.push_back(monster);
}
Now addMonster copies its kind's health and defense, as the old loop did, and sets the monster's pack and state. The #include <iterator> at the top of Game.cpp was there for std::size, which counted the kinds. Nothing counts them now. Find this line, and delete it:
#include <iterator> // std::size, to count the kinds of monster
Add the three declarations to Game.h, below placeMonsters's:
const MonsterDef* chooseMonsterKind();
std::optional<Point> freeCellIn(const SDL_Rect& room);
void addMonster(const MonsterDef& kind, Point cell, int packId,
bool awake);
All three are private, like the rest of the setting up, since only placeMonsters uses them.
Waking Together
A pack that's placed together should wake together. When one member wakes, the members still asleep should wake too, and do as it does. A blow, the player close by, and a noise each wake a monster. Each will call a new function, wakePack, to wake the rest of its pack.
A blow wakes the monster struck. In Game.cpp, in playerAttacks, add this below monster.lastSeen = player_.pos;:
wakePack(monster);
A monster asleep beside the player, or one that stirs while it sees them, wakes in actAsleep. Add the same line there, below announce(monster, "wakes up!");:
wakePack(monster);
And a noise wakes the monsters that hear it, in makeNoise. Add it there too, below that function's announce(monster, "wakes up!");:
wakePack(monster);
Now the function itself. Add it below actAsleep:
// A pack wakes together: when one of its members wakes, the others still
// asleep wake too, and do as it does. The player is told of each one they
// can see. A dead member, not yet taken off the floor, is left alone
void Game::wakePack(const Monster& woken)
{
if (woken.packId == 0)
{
return;
}
for (Monster& other : level_.monsters)
{
bool sameAsleep = other.packId == woken.packId &&
other.state == MonsterState::Asleep &&
other.isAlive();
if (sameAsleep)
{
other.state = woken.state;
other.lastSeen = woken.lastSeen;
other.target = woken.target;
announce(other, "wakes up!");
}
}
}
In the preceding code, a monster in no pack has nothing to do. A pack's sleeping members take the woken one's state, and where it's heading, and where it last saw the player. If the player woke it, the whole pack hunts; if a noise woke it, the whole pack goes to see what the noise was. Members already awake are left as they are, since they have plans of their own.
Each member that wakes is announced, if you can see it, as the first one was. Three jackals waking together say "The jackal wakes up! x3", since the log counts a message repeated. The test of isAlive leaves a dead member alone. Nothing in this chapter kills a monster in its sleep, but in Chapter 21 a bolt of lightning can kill two of a pack and wake the third, and the two dead ones shouldn't wake.
Add the declaration to Game.h, below announce's:
void wakePack(const Monster& woken);
The parameter is a const reference, since wakePack only reads the monster that woke. It changes the others, which it finds in level_.monsters.
Slow, and Fearless
A slow monster acts every other turn. In monstersAct, add this below the check for the player's death:
// A slow monster acts only every other turn
if (monster.def->has(Trait::SLOW) && turn_ % 2 == 1)
{
continue;
}
The % gives what's left over when one number is divided by another. So turn_ % 2 is 1 on odd turns, which a slow monster skips. The zombified kobold is slow, and you can walk away from one at twice its speed. Fight it, though, and it takes a long time to put down. It has twenty health, the most of its band.
A monster that never flees ignores its wounds. In actHunting, find this line:
if (isBadlyWounded(monster) || monster.afraid > 0)
Change it to these two:
bool fearless = monster.def->has(Trait::NEVER_FLEES);
if (!fearless && (isBadlyWounded(monster) || monster.afraid > 0))
It ignores the scroll of fear too. Replace the comment above frightenMonsters with this:
// The scroll of fear: every monster in sight is afraid for FEAR_TURNS,
// waking if it was asleep, and flees, whatever its health. A monster that
// can't move can't flee, and one that never flees doesn't, so they take
// no notice
Then, in frightenMonsters, find this line:
if (inSight && !monster.def->has(Trait::STATIONARY))
Change it to these three:
bool unmoved = monster.def->has(Trait::STATIONARY) ||
monster.def->has(Trait::NEVER_FLEES);
if (inSight && !unmoved)
Read a scroll of fear at a skeleton orc, and it keeps coming.
A Longer List
With seventeen or more monsters on a floor, the list that F4 shows in the side panel runs past the panel's bottom, over the log. It should stop above the look panel, while that's showing, or else above the panel's bottom. And it should say how many more there are. In drawSidePanel, find these lines:
// Then every monster: its letter, its cell, and its state
y += row;
for (const Monster& monster : level_.monsters)
Change them to these:
// Then every monster that fits above the look panel, while it's
// showing, or above the side panel's bottom: its letter, its cell,
// and its state
y += row;
float bottom = MAP_VIEW_H;
if (lookTarget())
{
bottom -= LOOK_PANEL_LINES * row + 2 * MARGIN;
}
float lastY = bottom - 2 * row;
int shown = 0;
for (const Monster& monster : level_.monsters)
At the start of the loop, above y += row;, add the check that stops it:
if (y + row > lastY)
{
break;
}
Count each monster shown, below the line that draws it:
++shown;
And below the loop, say how many didn't fit:
int hidden = static_cast<int>(level_.monsters.size()) - shown;
if (hidden > 0)
{
y += row;
glyphs_.drawText("and " + std::to_string(hidden) + " more", x, y,
Palette::DEBUG);
}
Above the loop, lastY keeps two rows free at the bottom, one for the "and 2 more" line, and one as a margin. The look panel is only drawn while there's something to look at, so the list uses the whole panel the rest of the time.
Design note: Rogue's monster ladder. Rogue had a kind of monster for each letter of the alphabet. It kept them in a line from shallow to deep: the kobold first, then the floating eye, the bat and the snake, and so on down to the dragon last. To make a monster, it counted along the line by the level's number, give or take a roll from six back to three on. So most of a level's monsters came from a few levels shallower, and three in ten from just below. Our bands do the same job in the other direction: each kind says which floors it lives on, and one monster in ten comes from three floors down.
A ladder is a single list, easy to keep, but every new kind moves the ones after it. Bands let each kind be placed on its own, which matters when the kinds move into a text file in the next chapter.
Checkpoint: Press F5, and press F4. The list has grown: eighteen monsters, more than fit, so it ends with "and 2 more". Three packs of jackals, three each, are listed together, two of them wandering. Press F1 as well, to see where everyone is, as the next image shows.

Three wolves sleep in one room, below and to the right of yours, beside a pack of jackals. Wolves belong to floors 4 to 6. So they're the one-in-ten roll, a pack from three floors down, on the first floor. They're asleep for now. Fight anything nearby, and the noise may wake one, and with it, the whole pack.
The Bottom of the Dungeon
The last step adds the deep band of monsters, ends the dungeon at floor 9, and scatters gold. It ends with the whole descent, from the first floor to the bottom.
The Deep Band
The deep band brings two more traits. In Monster.h, add them to Trait, below NEVER_FLEES:
// It heals every turn; its blows heal it by half the damage they do
constexpr unsigned int REGENERATES = 1 << 6;
constexpr unsigned int DRAINS = 1 << 7;
Then add the deep band at the end of the table, below the skeleton orc:
{ "ogre", 'O', { 205, 165, 110, 255 }, "ogre",
7, 9, 7, 26, 5, 10, 2 },
{ "forest troll", 'T', { 105, 165, 95, 255 }, "forest-troll",
7, 9, 7, 22, 4, 8, 2, Trait::REGENERATES },
{ "shadow hound", 'Z', { 130, 95, 180, 255 }, "shadow-hound",
7, 9, 9, 14, 3, 7, 2, 0, 0, 3 },
{ "vampire", 'V', { 205, 45, 65, 255 }, "vampire",
8, 9, 8, 24, 4, 8, 3, Trait::DRAINS },
Here, the ogre has the most health of any monster yet, 26, and the hardest blows, 5 to 10. The forest troll regenerates, and the shadow hounds hunt in packs of three. The vampire, found only on the last two floors, heals itself by draining you.
A troll heals a little every turn, like a player drinking a potion of regeneration that never runs out. Its health goes up at the end of each turn, after the statuses have ticked. In Game.cpp, add how much, below HEAL_EVERY:
// A monster that regenerates heals REGENERATION health a turn (*tune*)
constexpr int REGENERATION = 1;
Then, in finishTurn, in the monsters' loop, add this below the if that announces a death:
else if (monster.def->has(Trait::REGENERATES))
{
monster.hp = SDL_min(monster.maxHp, monster.hp + REGENERATION);
}
A troll at half its health, left alone for eleven turns, is back to full. Any fight with one has to be finished, since breaking off to drink a potion gives it time to heal too.
A vampire heals by half the damage it does. That's part of its blow, so monsterAttacks changes the monster now, as well as the player. Replace the comment above monsterAttacks with this:
// A monster strikes the player. If the blow kills, the player remembers
// what dealt it, for the death screen. A monster that drains heals by half
// the damage it does
In monsterAttacks, change const Monster& monster to Monster& monster, since the function now changes the monster's health. Then add the drain below player_.hp -= damage;:
if (monster.def->has(Trait::DRAINS))
{
monster.hp = SDL_min(monster.maxHp, monster.hp + damage / 2);
}
In Game.h, change void monsterAttacks(const Monster& monster); to void monsterAttacks(Monster& monster);, so the declaration matches.
Nine Floors
The dungeon needs a bottom. Add these to the unnamed namespace in Game.cpp, below ITEMS_BASE:
// The floor at the bottom of the dungeon, which has no stairs down
constexpr int LAST_FLOOR = 9;
// Each floor has GOLD_PILES_MIN to GOLD_PILES_MAX piles of gold, of
// GOLD_MIN to GOLD_MAX pieces for each floor down (*tune*)
constexpr int GOLD_PILES_MIN = 3;
constexpr int GOLD_PILES_MAX = 6;
constexpr int GOLD_MIN = 5;
constexpr int GOLD_MAX = 20;
The gold's numbers are for later in this step. In newLevel, find the line that places the stairs:
placeStairs();
Replace it with these lines:
if (depth < LAST_FLOOR)
{
placeStairs();
}
The last floor gets no stairs, so > there finds none. Arriving at the bottom deserves a word. In descend, add this below the message for the new floor:
if (level_.depth == LAST_FLOOR)
{
log_.add("This is the bottom of the dungeon.");
}
Floor 9 has no way down. It has the deep band's monsters, and items and gold, like any floor, but no prize yet. Chapter 20 puts the three Amulets of SDL on floors 3, 6 and 9, and finding all three is what the descent is for.
Every floor from 1 to 9 has kinds of monster of its own now, and there's no floor 10. So the last fallback in chooseMonsterKind is never needed: on floors 7 to 9, the roll for three floors deeper finds nothing, and the floor's own kinds are chosen. In chooseMonsterKind, find the last fallback, and delete it:
if (kinds.empty())
{
// Below every band, for now: any kind will have to do
for (const MonsterDef& def : MONSTER_DEFS)
{
kinds.push_back(&def);
}
}
On floor 9, every monster is now one of the deep band's four kinds.
Gold
Gold is an item, of a class of its own, so it can lie on the floor as items do. But it never goes in the pack. Picking it up adds it to the player's purse. Open Item.h, and add the class to ItemClass, below Armor:
Gold,
Then add gold to the end of the table, below plate armor:
// Gold is placed in piles of its own, never chosen as an item, so its
// frequency is 0. A pile's count is its number of gold pieces
{ "gold", ItemClass::Gold, ItemEffect::None, "gold", 0 },
Its frequency is 0, so chooseItemKind never chooses it: a share of nothing can't be landed in. Gold is placed by a function of its own, in piles. A pile's count, which says how many potions are in a stack, says how many gold pieces are in the pile.
Open Player.h, and add the purse below strength:
// The gold the player has picked up. It isn't carried in the pack
int gold = 0;
Gold has a color of its own in the glyph view. Open Common.h, and find this line:
// Weapons and armor, in the glyph view
Change it to these two:
// Weapons, armor and gold, in the glyph view
constexpr SDL_Color GOLD = { 245, 205, 60, 255 };
Each floor gets three to six piles. A pile is worth more the deeper it lies: 5 to 20 pieces on the first floor, and nine times that on the ninth. Those are the numbers you added with LAST_FLOOR. In Game.cpp, add the function below descend:
// Puts GOLD_PILES_MIN to GOLD_PILES_MAX piles of gold in the rooms, the
// deeper the bigger, one to a cell, never on the player or another item
void Game::placeGold(const std::vector<SDL_Rect>& rooms)
{
int lastRoom = static_cast<int>(rooms.size()) - 1;
int piles = rng_.roll(GOLD_PILES_MIN, GOLD_PILES_MAX);
for (int i = 0; i < piles; ++i)
{
const SDL_Rect& room = rooms[rng_.roll(0, lastRoom)];
Point pos = { room.x + rng_.roll(0, room.w - 1),
room.y + rng_.roll(0, room.h - 1) };
if (pos != player_.pos && floorItemAt(pos) == nullptr)
{
FloorItem pile;
pile.pos = pos;
pile.item.def = findItemDef("gold");
pile.item.count = rng_.roll(GOLD_MIN, GOLD_MAX) * level_.depth;
level_.items.push_back(pile);
}
}
}
In the preceding code, a pile goes in any room, the first included, but never under the player, or on another item. A pile that would land on one is simply not placed, so now and then a floor has one fewer. Add the declaration to Game.h, below placeStairs's:
void placeGold(const std::vector<SDL_Rect>& rooms);
Each floor makes its gold once its items are down. In Game.cpp, in newLevel, add this below placeItems(rooms);:
placeGold(rooms);
Replace the comment above newLevel with this:
// Makes the floor at a depth, in place of the one before: its rooms, the
// player in the middle of the first, its monsters, items and gold, and
// its stairs down, unless it's the last floor
A pile needs a name, for the log. In itemName, add this below the armor's if:
if (def.itemClass == ItemClass::Gold)
{
if (item.count == 1)
{
return "a gold piece";
}
return std::to_string(item.count) + " gold pieces";
}
And a look, for the map: a $, in gold. In itemLook, add this below the armor's if:
if (def.itemClass == ItemClass::Gold)
{
return { '$', Palette::GOLD, def.tile, false };
}
Picking gold up goes in the purse. Replace the comment above pickUp with this:
// g picks up the item the player stands on, if the pack has room for it,
// or the gold, which goes into the player's purse. That takes a turn;
// finding nothing there, or no room, doesn't
Then, in pickUp, find this line:
if (!player_.inventory.add(here->item))
Change it to these:
if (here->item.def->itemClass == ItemClass::Gold)
{
player_.gold += here->item.count;
}
else if (!player_.inventory.add(here->item))
Gold never fills the pack, so it can always be picked up. The message is the same as any item's, from itemName: "You pick up 19 gold pieces."
In the Panel
The side panel should say which floor you're on, and how much gold you have. In drawSidePanel, add the floor below the title, below its y += 2 * row;:
glyphs_.drawText("Floor " + std::to_string(level_.depth), x, y,
Palette::TEXT);
y += row;
And add the gold above the turn's line, in its own color:
glyphs_.drawText("Gold " + std::to_string(player_.gold), x, y,
Palette::GOLD);
y += row;
Checkpoint: Change the seed to 31, and press F5. The panel says "Floor 1", and "Gold 0". A pile of gold lies right beside you, to your left: step onto it with h, and "You see 19 gold pieces here." Pick it up with g: "You pick up 19 gold pieces." The panel says "Gold 19", as the next image shows.

Figure 15.1 showed a run on this seed, three floors down. Play on, and see how deep you can get. Floors 7 to 9 are the deep band's, and floor 9 is the bottom: "This is the bottom of the dungeon."
Playing the Game
The dungeon is a far more dangerous place now. The first floor has seventeen monsters or more, not eight, and packs that come at you three at a time. And you still have thirty health and a short sword. The one-in-ten roll means a pack of wolves can be waiting on the first floor.
Corridors are your friend against packs. In a corridor, only one or two can reach you at a time, while in a room they surround you. Sleeping monsters are best left asleep: walk around them, rather than past them. Passing right beside one wakes it, and its pack with it.
Every floor's stairs are as far as they can be from where you arrive, so there's no hurrying through. Gold doesn't help you fight, yet, but it's the score. Later chapters give the player more to fight with: bows and arrows in Chapter 21, and in Chapter 22, a class with its own kit.
Understanding the Code
A floor is a Level: its depth, its map, its monsters, its items, and where its stairs are. The game keeps only the floor the player is on, in level_. And newLevel replaces it with a new one: rooms, the player, monsters, items, gold, and the stairs. The stairs go at the end of the longest walk from the player, found with a Dijkstra map.
The player belongs to Game, so they keep everything on the way down. The key > is the character Shift and the period type together, found with SDL_GetKeyFromScancode, and checked before the moving keys.
Items are chosen by frequency: a roll in the total of every kind's frequency, landing in one kind's share. Monsters are chosen from the kinds whose range of floors includes the floor. One time in ten, they're chosen from three floors deeper, falling back to the floor's own kinds. A pack is placed whole, in one room, with a pack number, and wakePack wakes its sleeping members when one of them wakes. The deep band brings regeneration, at the end of each turn, and draining, in the monster's blow.
Experimenting
Try each of these, and put each back afterward:
- Start deeper. In the constructor, change
newLevel(1)tonewLevel(9). Press F5, with seed 42, and press F1 and F4. The panel says "Floor 9", there are no stairs, and every monster is one of the deep band's four kinds. It's the quickest way to see a deep floor, and a good way to test one. - Always deeper. Set
DEEPER_ONE_INto 1, so every monster is chosen from three floors down. On the first floor, with seed 42, F4's list holds only the kinds of floor 4, three packs of wolves among them. - Plate for everyone. Set plate armor's frequency to 63, about as much as every other kind together. Half the items you find are plate armor now, since its share of the total is 63 in 125.
The first experiment is worth keeping in mind for every chapter from here on. A change to a deep floor can be tested on a deep floor, in seconds, rather than after a long walk down.
Common Errors and Fixes
If the game didn't build, or doesn't behave as described, the problem is probably one of these.
C2039: 'mapVIEW_W': is not a member of 'Level', and the same for mapVIEW_H, mapW and mapH, with C2065: 'reveallevel_': undeclared identifier, and many more. Replace All ran without Match case, and changed MAP_VIEW_W, MAP_W, revealMap_ and the rest as well as map_. Press Ctrl+Z until MAP_W and revealMap_ are back, turn on Match case, and run the Replace All again.
C3646: 'level_': unknown override specifier, and C4430: missing type specifier - int assumed. Note: C++ does not support default-int, in Game.h, followed by errors about level_ all through Game.cpp. The #include "Level.h" is missing from Game.h, so it doesn't know what a Level is.
C2065: 'level_': undeclared identifier, in newLevel, followed by C3861: 'placeMonsters': identifier not found, and more about rng_ and player_. The definition is missing its Game::, so it's a function of its own, outside the class. And the class's members mean nothing to it. Chapter 14's tickStatuses gave a linker error for the same mistake, since its body used nothing but its parameter.
Walking onto the stairs says "You bump into the wall.", and takes no turn. The test in playerStep still asks for Terrain::Floor, so the stairs count as a wall.
C2511: 'void Game::monsterAttacks(Monster &)': overloaded member function not found in 'Game', followed by C2597: illegal reference to non-static member 'Game::rng_'. The definition in Game.cpp takes a Monster&, but the declaration in Game.h still takes a const Monster&. Change the declaration to match.
C2679: binary '=': no operator found which takes a right-hand operand of type 'void' (or there is no acceptable conversion), twice, in chooseMonsterKind. The lambda kindsOn is missing its return kinds;, so it returns nothing, and nothing can't be stored in a vector.
Potions of healing never turn up. A row in ITEM_DEFS is missing its frequency, and a missing number in a row is 0. With a frequency of 0, a kind is never chosen. Check that every row has a number after its tile.
No giant white rats anywhere. The rat's row is missing its floors, as 6, 4, 1, 3, 0. The numbers fill the fields in order, so its first floor is 6 and its last is 4. No floor is in between. Put 1, 3, in front of the rat's sight.
FAQ
Q) Why are there no stairs up?
A) Going back up would mean keeping every floor the player has left, its map, monsters and items. Chapter 17's save file would have to save them all, too. A one-way descent keeps one floor, and it suits a game about pressing on. Rogue had stairs up, but only for the way home with the Amulet, and it made a new level for every trip.
Q) Why are the frequencies such small numbers, and not percentages?
A) Percentages have to add up to 100, so making one kind more common means making another less common. And every new kind means redoing the sums. Rogue's tables were percentages, and a build of Rogue made for its authors' own testing checked that every table added up to 100. Frequencies only need to make sense against each other, so a new kind just takes its place.
Q) Why does going down count as a turn, but not let the monsters act?
A) The turn counter counts the player's actions, and taking the stairs is one. The rest of a turn, the monsters' moves, the statuses' ticks and the healing, belongs to the floor where the turn was taken. And that floor is gone. The new floor's first turn starts with your first move on it.
AI Exercise (Optional)
Stairs up are a natural next step, and a good test of what a floor would have to remember. If you'd like to try them 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. A struct Level holds one floor: int depth, a Map map, std::vector<Monster> monsters, std::vector<FloorItem> items, and Point stairs. The class Game holds one Level, level_, and void Game::newLevel(int depth) replaces it with a new one, made from the game's random number generator, rng_; going down the stairs calls newLevel(level_.depth + 1). I'd like stairs up, so the player can go back to a floor they've left and find it as they left it. Show me how Game could keep the floors the player has visited, with each curly brace on its own line, and explain what happens to the floors' monsters while the player is away."
Notice what the preceding prompt does. It names the one function that makes floors, and the one member that holds the floor in use. So the answer can build on them rather than start again. And it asks the question every design with stairs up must answer: what the monsters on a floor do while nobody's there.
When the answer comes back, look at what it keeps. A vector of Levels is the obvious answer, but a reference or pointer into a vector can be left dangling when the vector grows. So check how the answer finds the current floor. Then think about the save file in Chapter 17, which would have to hold every floor.
Summary
The dungeon is nine floors deep now, kept one floor at a time in a Level, with stairs at the end of each floor's longest walk, and > to take them. Items turn up by frequency, and monsters by the floors their kinds are found on, some of them in packs that sleep and wake together. The deep band brings trolls that regenerate and vampires that drain, with gold to collect on the way down. In Chapter 16, the monsters and the items move out of the code and into text files, so a new kind of monster needs no compiling at all.
