Chapter 32 · Project · ~46 min read

Rogue SDL, Part 3: Monsters and Treasure

The dungeons of Chapter 31 are big, dark, and completely empty. You can explore them from top to bottom, and down level after level, without meeting a soul, which makes Rogue SDL a very peaceful game, and not much of a roguelike. This chapter fills the dungeon.

Every level gets its monsters, rats at first, then goblins, and then orcs, more of them and stronger the deeper you go. Every level gets its treasure too: gold to collect, and potions that heal you. You fight by walking into a monster, and it fights back, on its own turn, after yours. And for the first time, you can die, and start again from the top.

Along the way, we'll meet an idea that holds most games together: a table of numbers that says what every kind of thing is like, so that a rat and an orc can share the same code, and differ only in their numbers. By the end of the chapter, the dungeon will be a dangerous place, as in Figure 32.1.

The end of Part 3, three levels down. The @ is fighting a goblin, the green g. The top row of the HUD shows health, gold, potions, and the depth, and the messages below tell the story so far: a pile of gold picked up, and then a fight.
Figure 32.1 — The end of Part 3, three levels down. The @ is fighting a goblin, the green g. The top row of the HUD shows health, gold, potions, and the depth, and the messages below tell the story so far: a pile of gold picked up, and then a fight.
Project folder: SDL3 Projects/Rogue SDL Part 3 — the complete source for this chapter lives here, with its twenty-two files and the assets folder. The chapter carries on in your own project from Chapter 31. If you'd rather start from the book's copy of Part 2, make a copy of the Rogue SDL Part 2 folder beside it in SDL3 Projects, where it still finds the SDL3 and SDL3_ttf folders, and open the copy's .slnx file.

In this chapter, we will:

  • Give every kind of monster and treasure a row in a table of stats, and share the code between them
  • Fill each level with monsters and treasure, more and tougher the deeper it is
  • Fight by bumping into a monster, and pick treasure up by walking onto it
  • Remove the dead from a vector with std::erase_if and a lambda
  • Let the monsters take their turns after yours: noticing you, closing in, and hitting back
  • Drink potions to heal, and show your health, gold, and potions on the HUD
  • Die, and start again with a fresh game
  • Play, experiment, fix the most common mistakes, and try an optional AI exercise

Let's populate the place.

Planning Part 3

Part 2 finished with eighteen files. Part 3 adds four more, and changes nine. Here's what each one is for:

File What's new
Enemy.h, Enemy.cpp New: the kinds of monster, their stats, and the Enemy class
Item.h, Item.cpp New: the kinds of treasure, their stats, and the Item class
Common.h Points that can be compared, and colors for the monsters and treasure
MapGenerator.h, MapGenerator.cpp Monsters and treasure, put in the rooms as each level is built
Player.h, Player.cpp Health, an attack, gold, and potions
HUD.h, HUD.cpp The player's numbers on the top row, and a message for when you die
Game.h, Game.cpp Fighting, picking up, the monsters' turns, and starting again

The monsters and the treasure are entities, as Chapter 30 promised: each is drawn as one character in one color, at a cell, so Enemy and Item are built on Entity, just as Player is. Figure 32.2 shows the family, and the tables beside it.

The entity family, and its tables. Player, Enemy, and Item are all built on Entity. Every enemy looks up what it's like, its name, character, color, health, attack, and sight, in a table with a row for each kind of monster, and every item in a table with a row for each kind of treasure.
Figure 32.2 — The entity family, and its tables. Player, Enemy, and Item are all built on Entity. Every enemy looks up what it's like, its name, character, color, health, attack, and sight, in a table with a row for each kind of monster, and every item in a table with a row for each kind of treasure.

What makes a rat a rat, and not an orc? Only its numbers: how it looks, how much health it has, how hard it hits, and how far it can see. So there's no class for each kind of monster. There's one Enemy class, and a table of stats with a row for each kind, which is the idea Chapter 31 used for the map's looks. Adding a kind of monster means adding a row, and the code doesn't change at all.

Note

Why isn't each kind of monster a class of its own, built on Enemy, with virtual functions, as in Chapter 22? Because nothing about them behaves differently yet: they all move, fight, and draw themselves in the same way. When the only difference is numbers, a table says so more simply than a family of classes, and the monsters can live in a plain std::vector<Enemy>, as values, rather than behind pointers. Chapter 35 is where virtual earns its keep, as Chapter 30 said.

We'll add Part 3 in four stages, and play the game at the end of each. First, the monsters and treasure appear, although they don't do anything yet. Then you can fight them and pick treasure up. Then the monsters fight back, and you'll be glad of the potions. And last, you can die, and start again.

Monsters and Treasure

Comparing Points

Before anything else, the game needs to compare two cells, to find out, for example, whether a monster is standing where the player wants to step. So far, a Point can't be compared with == at all, since C++ doesn't know what it would mean for a struct. Chapter 24 showed the answer: ask the compiler to write == for you, comparing every member in turn. In Common.h, add this to Point, below int y = 0;, with a blank line in between:

bool operator==(const Point& other) const = default;

In the preceding code, the = default asks for the ordinary meaning of ==: two points are equal when their xs are equal and their ys are equal. Since C++20, the compiler writes != from it too. It's const, since comparing two points doesn't change either of them.

The monsters and treasure need their colors. Add these at the end of the palette, below TEXT_DIM, with a blank line in between:

// Monsters and treasure
constexpr SDL_Color RAT = { 180, 180, 100, 255 };
constexpr SDL_Color GOBLIN = { 100, 220, 100, 255 };
constexpr SDL_Color ORC = { 220, 100, 100, 255 };
constexpr SDL_Color POTION = { 220, 80, 220, 255 };
constexpr SDL_Color GOLD = { 240, 220, 80, 255 };

In the preceding code, the rats are a dirty yellow, the goblins green, and the orcs red, so you can tell at a glance what's coming. The potions are a bright magenta, and the gold is the same yellow as the stairs.

Enemy.h and Enemy.cpp

The monsters come first. Add a header called Enemy.h, and below its #pragma once, type the include, and the three things that describe a kind of monster:

#pragma once
#include "Entity.h"

// The kinds of monster, from the weakest to the strongest
enum class MonsterKind
{
    Rat,
    Goblin,
    Orc
};

// What every monster of one kind is like
struct MonsterStats
{
    const char* name;
    char glyph;
    SDL_Color color;
    int maxHp;
    int attack;   // the damage it does with each hit
    int sight;    // how far away it can notice the player, in cells
};

const MonsterStats& statsOf(MonsterKind kind);

In the preceding code, MonsterKind is an enum class of the three kinds, from the weakest to the strongest. A MonsterStats is everything about one kind: its name, for messages such as "The rat hits you for 2", its character and color, its health when it's full, the damage it does with each hit, and how many cells away it can notice you. The name is a const char*, Chapter 11's type for text in double quotation marks, since every name is fixed when the program's written. Last, statsOf gives the stats for a kind, as a const reference, so they can be read without being copied, and never changed.

Note

Rogue itself drew every monster as a capital letter, from A for aquator to Z for zombie, so its players learned to fear the letters of the alphabet. This game uses small letters instead, r for rat, g for goblin, and o for orc, so that each one is easy to name at a glance.

Now the class itself. Add it below statsOf’s declaration, with a blank line in between:

// A monster, of one of the kinds above
class Enemy : public Entity
{
public:
    Enemy(MonsterKind kind, Point position);

    const MonsterStats& getStats() const;
    int getHp() const;
    bool isAlive() const;
    void takeDamage(int amount);

private:
    MonsterKind kind_;
    int hp_;
};

In the preceding code, an Enemy is built publicly on Entity, so it has a position, a character, and a color, and draws itself, like the player. It adds two things of its own: its kind, and its health, hp_, which goes down as you hit it. There are no setters for either. A monster's kind never changes, and its health only changes through takeDamage, and isAlive says whether it has any left.

Add a C++ file called Enemy.cpp, and type the table and statsOf:

#include "Enemy.h"

namespace
{
    // One for each kind of monster, in the same order as the enum
    constexpr MonsterStats MONSTER_STATS[] = {
        { "rat", 'r', Palette::RAT, 4, 2, 6 },
        { "goblin", 'g', Palette::GOBLIN, 8, 3, 7 },
        { "orc", 'o', Palette::ORC, 14, 5, 8 }
    };
}

const MonsterStats& statsOf(MonsterKind kind)
{
    return MONSTER_STATS[static_cast<int>(kind)];
}

In the preceding code, MONSTER_STATS has one row for each kind of monster, in the same order as the enum: the rat, the goblin, and the orc. A rat has 4 health, hits for 2, and notices you from 6 cells away; an orc has 14 health, hits for 5, and notices you from 8. The table is in an unnamed namespace, as the map's looks were, since only this file uses it directly, and everything else asks statsOf. That turns the kind into its place in the table, with the same static_cast that Chapter 31's draw used, and returns the row.

The table follows Chapter 31's warning: its rows must stay in the same order as the enum, and a new kind goes at the end of both. Next, the constructor. Add it below statsOf, with a blank line in between:

Enemy::Enemy(MonsterKind kind, Point position)
    : Entity(position, statsOf(kind).glyph, statsOf(kind).color),
      kind_(kind),
      hp_(statsOf(kind).maxHp)
{
}

In the preceding code, the constructor passes the position, and its kind's character and color, to Entity’s constructor, as Player’s does, and keeps the kind. The monster's health starts full, at its kind's maxHp.

Last, the four member functions. Add them below the constructor, with a blank line in between:

const MonsterStats& Enemy::getStats() const
{
    return statsOf(kind_);
}

int Enemy::getHp() const
{
    return hp_;
}

bool Enemy::isAlive() const
{
    return hp_ > 0;
}

void Enemy::takeDamage(int amount)
{
    hp_ -= amount;
}

In the preceding code, getStats returns the stats for the monster's own kind, so the game can ask a monster for its name or its attack. Then getHp returns its health, isAlive says whether it has any left, and takeDamage takes some away. A monster's health can go below zero, and it doesn't matter: a dead monster is cleared away at the end of the turn, as we'll see, and nobody ever sees its health.

Checkpoint: Click in Enemy.cpp, and press Ctrl+F7 to compile it on its own. The Error List should stay empty.

Item.h and Item.cpp

Treasure works in exactly the same way, with a table of its own. Add a header called Item.h, and below its #pragma once, type the include, the kinds of treasure, and their stats:

#pragma once
#include "Entity.h"

// The kinds of treasure
enum class ItemKind
{
    Potion,
    Gold
};

// What every item of one kind is like
struct ItemStats
{
    const char* name;
    char glyph;
    SDL_Color color;
};

const ItemStats& statsOf(ItemKind kind);

In the preceding code, there are two kinds of treasure, potions and gold, and an ItemStats is how a kind looks, and what it's called. There's a statsOf for items too. It has the same name as the one for monsters, but it takes an ItemKind rather than a MonsterKind, which is Chapter 8's overloading: the compiler picks the right one by the type of the argument.

Now the class. Add it below statsOf’s declaration, with a blank line in between:

// Something lying on the floor, waiting to be picked up
class Item : public Entity
{
public:
    Item(ItemKind kind, Point position, int amount = 1);

    ItemKind getKind() const;
    int getAmount() const;

private:
    ItemKind kind_;
    int amount_;   // how many coins, for gold
};

In the preceding code, an Item is an entity too, with its kind, and an amount, which is how many coins a pile of gold holds. The constructor's amount has a default value of 1, as Chapter 8 showed, so a potion can be made without one.

Add a C++ file called Item.cpp, and type the table and statsOf:

#include "Item.h"

namespace
{
    // One for each kind of item, in the same order as the enum
    constexpr ItemStats ITEM_STATS[] = {
        { "potion", '!', Palette::POTION },
        { "gold", '$', Palette::GOLD }
    };
}

const ItemStats& statsOf(ItemKind kind)
{
    return ITEM_STATS[static_cast<int>(kind)];
}

In the preceding code, ITEM_STATS has a row for each kind of treasure, in the enum's order: a potion is a magenta !, and gold is a yellow $. Then statsOf returns a kind's row, as the monsters' version does.

Then add the constructor and the two getters below statsOf, with a blank line in between:

Item::Item(ItemKind kind, Point position, int amount)
    : Entity(position, statsOf(kind).glyph, statsOf(kind).color),
      kind_(kind),
      amount_(amount)
{
}

ItemKind Item::getKind() const
{
    return kind_;
}

int Item::getAmount() const
{
    return amount_;
}

In the preceding code, the constructor gives Entity the position, and the kind's character and color, and keeps the kind and the amount. The getters return them.

Checkpoint: Click in Item.cpp, and press Ctrl+F7. It should compile, just as Enemy.cpp did.

Filling the Rooms

The generator puts the monsters and treasure in the rooms, while it builds each level. It needs to know how deep the level is, so that deeper levels can be more dangerous, and it needs somewhere to put what it makes: the game's vectors of monsters and items, which it will fill. In MapGenerator.h, add these two lines above class Map;:

class Enemy;
class Item;

In the preceding code, the header forward-declares Enemy and Item, as it does Map, since it only refers to them, in the parameters of its functions. Then find the comment above the class, and the class's first few lines:

// Builds a new level on a map: it splits the map into areas, puts a room
// in each one, joins the rooms with corridors, and puts stairs down in one
// of them
class MapGenerator
{
public:
    MapGenerator(Map& map);

    Point generate();

And change them to this:

// Builds a new level on a map: it splits the map into areas, puts a room
// in each one, joins the rooms with corridors, puts stairs down in one of
// them, and fills the rooms with monsters and treasure
class MapGenerator
{
public:
    MapGenerator(Map& map);

    Point generate(int depth, std::vector<Enemy>& enemies,
                   std::vector<Item>& items);

In the preceding code, the comment says what else the generator does now, and generate takes the depth of the level it's building, and two vectors, by reference, since it adds the level's monsters to one and its treasure to the other.

The generator needs three new private functions to do that. Add these below carveCorridor’s declaration:

Point takeFreeSpot(bool awayFromStart);
void addMonsters(int depth, std::vector<Enemy>& enemies);
void addTreasure(std::vector<Item>& items);

In the preceding code, takeFreeSpot finds a cell for something to go in, and addMonsters and addTreasure make the monsters and the treasure. Then add these two members below rooms_:

int startRoom_ = 0;             // the room the player starts in
std::vector<Point> taken_;      // cells that already have something

In the preceding code, startRoom_ remembers which room the player starts in, so that no monster is put right beside them, and taken_ is a list of the cells that already have something in them, so that no two things are ever put in the same place.

Now MapGenerator.cpp. Finding a free cell will use std::find, which is in <algorithm> too. At the top of the file, find this line:

#include <algorithm>   // std::min and std::max

And change it to this:

#include <algorithm>   // std::min, std::max, and std::find

In the preceding code, the comment adds std::find to what <algorithm> is there for. Then add these two below #include <cstdlib>, above #include "Map.h":

#include "Enemy.h"
#include "Item.h"

In the preceding code, MapGenerator.cpp includes the headers of the classes it makes, since making an Enemy needs the whole class, not only its name. Next, find the comment and the first line of generate:

// Builds the level, and returns the cell where the player starts
Point MapGenerator::generate()

And change them to this:

// Builds the level for a depth, adds its monsters and treasure to the two
// vectors, and returns the cell where the player starts
Point MapGenerator::generate(int depth, std::vector<Enemy>& enemies,
                             std::vector<Item>& items)

In the preceding code, the comment and the parameters are changed to match the new declaration. Every new level starts with no cells taken, so add this below rooms_.clear();:

taken_.clear();

In the preceding code, taken_ is emptied, as rooms_ is, since a generator could be asked for more than one level. Then find this line, a little further down:

Point start = centerOf(rooms_[SDL_rand(roomCount)]);

And change it to this:

startRoom_ = SDL_rand(roomCount);
Point start = centerOf(rooms_[startRoom_]);

In the preceding code, the room that's picked for the start is kept in startRoom_, rather than forgotten as soon as its middle is found. Then add these lines at the end of generate, above return start;:

// Nothing else can go where the player starts, or on the stairs
taken_.push_back(start);
taken_.push_back(stairs);
addMonsters(depth, enemies);
addTreasure(items);

In the preceding code, the player's start and the stairs are marked as taken, so that nothing is ever put on either. Then addMonsters fills the level with monsters, as deep as it is, and addTreasure scatters the treasure, before generate returns the start as it always has.

Now the three new functions, at the end of the file. First, add takeFreeSpot below carveCorridor, with a blank line in between:

// A random cell in a random room, where nothing has been put yet, and, if
// asked, not in the room where the player starts. It's taken from then on
Point MapGenerator::takeFreeSpot(bool awayFromStart)
{
    int roomCount = static_cast<int>(rooms_.size());
    while (true)
    {
        int index = SDL_rand(roomCount);
        if (awayFromStart && index == startRoom_)
            continue;

        const SDL_Rect& room = rooms_[index];
        Point spot = { randomBetween(room.x, room.x + room.w - 1),
                       randomBetween(room.y, room.y + room.h - 1) };
        if (std::find(taken_.begin(), taken_.end(), spot) == taken_.end())
        {
            taken_.push_back(spot);
            return spot;
        }
    }
}

In the preceding code, takeFreeSpot keeps trying until it finds a free cell. Each try picks a room at random, and skips it if it's the player's and awayFromStart is true, with continue, which goes straight on to the next try. Otherwise, it picks a random cell in the room, anywhere from its left column to its right, and its top row to its bottom.

Then std::find looks for that cell in taken_. It walks the vector from begin() to end(), comparing each element with the cell, with the == that Point has now, and returns an iterator to the first one that matches, or end() if none does, as Chapter 15's find did for maps. So if it returns end(), the cell is free: it's added to taken_, and returned. The while (true) looks alarming, but it always ends, since a level has hundreds of floor cells, and only a few dozen things to put on them.

Next, the monsters. Add addMonsters below takeFreeSpot, with a blank line in between:

// Puts monsters in the rooms, away from the player: more of them the
// deeper the level, and stronger ones too
void MapGenerator::addMonsters(int depth, std::vector<Enemy>& enemies)
{
    int count = 3 + depth + SDL_rand(3);
    for (int i = 0; i < count; ++i)
    {
        // Rats at every depth, goblins from depth 2, and orcs from depth 4
        int roll = SDL_rand(100);
        MonsterKind kind = MonsterKind::Rat;
        if (depth >= 4 && roll >= 85)
            kind = MonsterKind::Orc;
        else if (depth >= 2 && roll >= 60)
            kind = MonsterKind::Goblin;

        enemies.push_back(Enemy(kind, takeFreeSpot(true)));
    }
}

In the preceding code, the number of monsters is 3, plus the depth, plus a random 0, 1, or 2, so there are 4 to 6 on the first level, and 9 to 11 on the sixth.

For each one, roll is a random number from 0 to 99, a percentage, which picks the kind. On the first level, every monster is a rat. From depth 2, a roll of 60 or more makes a goblin, which is 40 in a hundred. From depth 4, a roll of 85 or more makes an orc instead, 15 in a hundred, leaving 25 goblins and 60 rats. Figure 32.3 shows what you can expect at each depth.

Who you'll meet, and how many. Every level has 3 monsters, plus its depth, plus 0 to 2 more. The first level has only rats, goblins join them from depth 2, and orcs from depth 4, with the chances for each shown in the bars.
Figure 32.3 — Who you'll meet, and how many. Every level has 3 monsters, plus its depth, plus 0 to 2 more. The first level has only rats, goblins join them from depth 2, and orcs from depth 4, with the chances for each shown in the bars.

Each monster is made from its kind and a free cell, away from the player's start room, and pushed onto the end of the vector with push_back. The Enemy in the call is made right there, as a temporary, and push_back puts it into the vector.

Tip

The numbers in addMonsters, and in the two stats tables, are the whole of the game's difficulty. If the game feels too easy, or too hard, those are the numbers to change, one at a time, playing a few levels after each change to feel the difference. Game designers call this balancing, and they spend far longer on it than on writing the code that uses the numbers.

Last, the treasure. Add addTreasure below addMonsters, with a blank line in between:

// Scatters potions and piles of gold through the rooms
void MapGenerator::addTreasure(std::vector<Item>& items)
{
    int potions = randomBetween(1, 3);
    for (int i = 0; i < potions; ++i)
        items.push_back(Item(ItemKind::Potion, takeFreeSpot(false)));

    int piles = randomBetween(2, 5);
    for (int i = 0; i < piles; ++i)
    {
        int coins = randomBetween(5, 15);
        items.push_back(Item(ItemKind::Gold, takeFreeSpot(false), coins));
    }
}

In the preceding code, every level gets 1 to 3 potions, and 2 to 5 piles of gold, with 5 to 15 coins in each pile. Treasure can go anywhere, even in the room where the player starts, so takeFreeSpot is given false. A potion is made without an amount, so it gets the default of 1.

Checkpoint: Click in MapGenerator.cpp, and press Ctrl+F7. It compiles on its own. Don't press F5 yet, since Game.cpp still calls generate with no arguments, which the build would stop at.

Showing Them

The game owns the monsters and the treasure, in two vectors. In Game.h, add these below #include <SDL3/SDL.h>:

#include <vector>   // std::vector, for the monsters and the treasure
#include "Enemy.h"

In the preceding code, Game.h includes <vector>, and Enemy.h, since the game will hold a vector of Enemy values, which needs the whole class. For the same reason, add this below #include "HUD.h":

#include "Item.h"

In the preceding code, Item.h joins the includes, in alphabetical order. Then find the comment above the class:

// The whole game. It owns the glyphs, the map, the player, and the HUD,
// and runs the loop that waits for a key, acts on it, and draws what
// happened

And change it to this:

// The whole game. It owns the glyphs, the map, the player, the monsters,
// the treasure, and the HUD, and runs the loop that waits for a key, acts
// on it, and draws what happened

In the preceding code, the comment adds the monsters and the treasure to what the game owns. Last, add the two vectors below Player player_;:

std::vector<Enemy> enemies_;
std::vector<Item> items_;

In the preceding code, enemies_ holds every monster on the level, and items_ every item lying on the floor, as values, so the vectors own them, and they're destroyed with the vectors, or when they're taken out.

In Game.cpp, each new level empties the vectors, and hands them to the generator. Replace the whole of newLevel with this:

// Builds a new level, with its monsters and treasure, puts the player at
// its start, and looks around
void Game::newLevel()
{
    enemies_.clear();
    items_.clear();
    MapGenerator generator(map_);
    player_.setPosition(generator.generate(depth_, enemies_, items_));
    FOV::compute(map_, player_.getPosition(), SIGHT_RADIUS);
}

In the preceding code, the monsters and the treasure of the last level are cleared away with clear, and the generator is given the depth and the two vectors, and fills them as it builds the level. Everything else is as before.

The monsters and the treasure have to be drawn, but only where the player can see them. In draw, add these lines below map_.draw(renderer_, glyphs_);, with a blank line on each side:

// Treasure, then monsters, wherever the player can see them
for (const Item& item : items_)
{
    if (map_.at(item.getPosition()).visible)
        item.draw(renderer_, glyphs_);
}
for (const Enemy& enemy : enemies_)
{
    if (map_.at(enemy.getPosition()).visible)
        enemy.draw(renderer_, glyphs_);
}

In the preceding code, each item and each monster is drawn only if the tile it's on is visible. A monster you saw a moment ago isn't remembered, as walls are, since it might have moved since. The treasure is drawn first, and the monsters after it, and the player after both. A character has no background, though, so a monster standing on a pile of gold doesn't hide it: the two are drawn over each other, in the same cell.

Checkpoint: Press F5. Explore, and before long, you'll find a rat, an r, sitting in a room, as in Figure 32.4, and potions and gold lying about. They don't do anything yet. Walk into a rat, and you walk straight through it, since tryMove only knows about walls, and walk onto the gold, and it stays where it is.

Monsters appear. The rat, the r, is in sight, so it's drawn, but for now it just sits there, and the @ could walk right through it.
Figure 32.4 — Monsters appear. The rat, the r, is in sight, so it's drawn, but for now it just sits there, and the @ could walk right through it.

Fighting and Collecting

The Player's Numbers

Before the player can fight, they need health, and an attack, and something to carry the treasure in. In Player.h, add these below class Map;, with a blank line in between:

constexpr int PLAYER_MAX_HP = 20;   // the player's health, when it's full
constexpr int PLAYER_ATTACK = 4;    // the damage the player does with a hit

In the preceding code, the player starts with 20 health, and does 4 damage with every hit, which kills a rat in one blow, a goblin in two, and an orc in four. They're constants in Player.h, since the HUD will need the first one too. Then find the class:

// You: the @
class Player : public Entity
{
public:
    Player();

    bool tryMove(int dx, int dy, const Map& map);
};

And change it to this:

// You: the @
class Player : public Entity
{
public:
    Player();

    bool tryMove(int dx, int dy, const Map& map);

    int getHp() const;
    int getAttack() const;
    int getGold() const;
    int getPotions() const;

    void addGold(int amount);
    void addPotion();

private:
    int hp_ = PLAYER_MAX_HP;
    int gold_ = 0;
    int potions_ = 0;
};

In the preceding code, the player has getters for its health, its attack, its gold, and its potions, and two functions that add to what it carries. Its three numbers are private members, with default member values: full health, no gold, and no potions. The attack isn't a member, since it never changes, and getAttack returns the constant.

In Player.cpp, add the four getters below tryMove, with a blank line in between:

int Player::getHp() const
{
    return hp_;
}

int Player::getAttack() const
{
    return PLAYER_ATTACK;
}

int Player::getGold() const
{
    return gold_;
}

int Player::getPotions() const
{
    return potions_;
}

In the preceding code, each getter returns its number, and getAttack returns PLAYER_ATTACK. Then add the two functions that add to what the player carries below them, with a blank line in between:

void Player::addGold(int amount)
{
    gold_ += amount;
}

void Player::addPotion()
{
    ++potions_;
}

In the preceding code, addGold adds a pile's coins to the player's gold, and addPotion adds one potion.

The Status Line

The HUD's top row has only shown the depth so far. Now it shows the player's numbers too, so the HUD needs to see the player. In HUD.h, add this below class GlyphCache;:

class Player;

In the preceding code, the HUD forward-declares Player, since it only refers to one. Then find the declaration of draw:

void draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
          int depth) const;

And change it to this:

void draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
          const Player& player, int depth) const;

In the preceding code, draw takes the player, as a const reference, since the HUD only reads the player's numbers.

In HUD.cpp, add this below #include "GlyphCache.h":

#include "Player.h"

In the preceding code, HUD.cpp includes the whole of Player.h, since it calls the player's getters, and uses PLAYER_MAX_HP. Then find the start of draw:

void HUD::draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
               int depth) const
{
    int row = MAP_H;   // the first row below the map
    std::string status = "Depth " + std::to_string(depth);
    glyphs.drawText(renderer, status, { 1, row }, Palette::TEXT);

And change it to this:

void HUD::draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
               const Player& player, int depth) const
{
    int row = MAP_H;   // the first row below the map
    std::string status =
        "HP " + std::to_string(player.getHp()) + "/" +
        std::to_string(PLAYER_MAX_HP) +
        "   Gold " + std::to_string(player.getGold()) +
        "   Potions " + std::to_string(player.getPotions()) +
        "   Depth " + std::to_string(depth);
    glyphs.drawText(renderer, status, { 1, row }, Palette::TEXT);

In the preceding code, the status line is built from pieces joined with +, so it reads, for example, HP 14/20 Gold 28 Potions 1 Depth 3. The first piece, "HP " + std::to_string(...), is a std::string, since std::to_string makes one, and adding a string to a std::string gives another std::string, so every + after it works too.

The keys have changed as well, since walking into a monster now attacks it. Find the lines that make the keys' row:

std::string help = "Arrows or WASD: move   .: take the stairs down"
                   "   Esc: quit";

And change them to this:

std::string help = "Arrows or WASD: move and attack"
                   "   .: take the stairs down   Esc: quit";

In the preceding code, the arrow keys move and attack, and the line breaks in a new place, so each line of code stays short.

Checkpoint: Click in Player.cpp, and press Ctrl+F7, and then do the same in HUD.cpp. Both compile. The game won't build yet, since Game.cpp still calls the HUD's draw without the player.

Bumping

In a roguelike, you fight by walking into things. Step toward a monster that's in the way, and you attack it, instead of stepping. Step anywhere else, and you move, and pick up anything that's lying there. Either way, you've taken a turn. Figure 32.5 shows the rule.

The bump rule. A step into a monster attacks it, and a step into a wall does nothing, and takes no turn. A step onto open floor moves you, and a step onto treasure moves you and picks it up. Attacking and moving each take a turn.
Figure 32.5 — The bump rule. A step into a monster attacks it, and a step into a wall does nothing, and takes no turn. A step onto open floor moves you, and a step onto treasure moves you and picks it up. Attacking and moving each take a turn.

The game needs new private functions for all that. In Game.h, add these below handleKey’s declaration:

void moveOrAttack(int dx, int dy);
void attack(Enemy& enemy);
void pickUp();

In the preceding code, moveOrAttack decides what a step does, attack hits a monster, and pickUp picks up whatever's underfoot. Then add these below takeStairs’s declaration:

void endTurn();
Enemy* enemyAt(Point cell);
Item* itemAt(Point cell);

In the preceding code, endTurn finishes a turn, and enemyAt and itemAt find what's at a cell, if anything is.

In Game.cpp, handleKey hands the step to moveOrAttack, instead of taking it itself. Find the lines at the end of handleKey that take the step:

if (player_.tryMove(dx, dy, map_))
{
    FOV::compute(map_, player_.getPosition(), SIGHT_RADIUS);
    if (map_.at(player_.getPosition()).terrain == Terrain::StairsDown)
        hud_.addMessage("There are stairs down here. Press . to go down.");
}

And change them to this:

moveOrAttack(dx, dy);

In the preceding code, handleKey still turns a key into dx and dy, and moveOrAttack does the rest. Add it below handleKey, with a blank line in between:

// Attacks the monster in the way, if there is one. Otherwise, steps, looks
// around, and picks up anything lying there. Either way, it's a turn
void Game::moveOrAttack(int dx, int dy)
{
    Point next = { player_.getPosition().x + dx,
                   player_.getPosition().y + dy };
    if (Enemy* enemy = enemyAt(next))
    {
        attack(*enemy);
        endTurn();
        return;
    }

    if (player_.tryMove(dx, dy, map_))
    {
        FOV::compute(map_, player_.getPosition(), SIGHT_RADIUS);
        if (map_.at(player_.getPosition()).terrain == Terrain::StairsDown)
            hud_.addMessage("There are stairs down here. Press . to go down.");
        pickUp();
        endTurn();
    }
}

In the preceding code, next is the cell the player is stepping into. The if does two things at once: it declares enemy, a pointer to the monster at that cell, and tests it. If there's a monster there, enemy isn't nullptr, so the if is true, and the player attacks it, and the turn ends. A variable declared in an if’s condition exists only inside that if, so the rest of the function can't use enemy by mistake when it's nullptr. The * turns the pointer into the monster itself, for attack, which takes a reference.

Otherwise, the player tries to step, as before. If the step works, the game looks around, mentions the stairs if the player is on them, picks up anything lying there, and ends the turn. If it doesn't, because there's a wall in the way, nothing happens at all, and no turn is taken, so the monsters won't get a free move out of a bump into a wall.

The function enemyAt finds the monster at a cell. Add it above draw, with a blank line in between:

// The monster at a cell, or nullptr if there isn't one. The pointer is
// only good until the vector of monsters next changes
Enemy* Game::enemyAt(Point cell)
{
    for (Enemy& enemy : enemies_)
    {
        if (enemy.getPosition() == cell)
            return &enemy;
    }
    return nullptr;
}

In the preceding code, the loop goes through the monsters by reference, and if one is standing at the cell, returns its address, with Chapter 10's &. If none is, it returns nullptr. The == is Point’s, comparing the monster's position with the cell.

Warning

A pointer into a vector is only good until the vector changes, as Chapters 13 and 21 warned. When a monster is removed from enemies_, the monsters after it move down a place, and a pointer to one of them then points at a different monster, or past the end. So use what enemyAt returns right away, and never keep it. In moveOrAttack, the pointer is used by attack, and then forgotten, before endTurn removes the dead.

Used at once, and never kept, the pointer is perfectly safe, and moveOrAttack is ready for its first fight.

Attacking

Now the fight itself. Add attack below moveOrAttack, with a blank line in between:

// The player hits a monster, which may die
void Game::attack(Enemy& enemy)
{
    int damage = player_.getAttack();
    enemy.takeDamage(damage);

    std::string name = enemy.getStats().name;
    if (enemy.isAlive())
    {
        hud_.addMessage("You hit the " + name + " for " +
                        std::to_string(damage) + ".");
    }
    else
    {
        hud_.addMessage("You kill the " + name + "!");
    }
}

In the preceding code, the player's attack is taken off the monster's health. Then the HUD says what happened, with the monster's name. The name is a const char*, so it's put into a std::string, name, first, and the messages are built by adding pieces to it, as the HUD's status line was. If the monster is still alive, the message says how hard the player hit it, and if it isn't, that it's dead. The dead monster is still in the vector, for now, since endTurn clears the dead away.

Picking Up

The next function, pickUp, picks up whatever is lying where the player stands. Add the first part of it below attack, with a blank line in between:

// Picks up anything lying where the player stands
void Game::pickUp()
{
    Point here = player_.getPosition();
    Item* item = itemAt(here);
    if (!item)
        return;

    if (item->getKind() == ItemKind::Gold)
    {
        player_.addGold(item->getAmount());
        hud_.addMessage("You pick up " + std::to_string(item->getAmount()) +
                        " gold.");
    }
    else
    {
        player_.addPotion();
        hud_.addMessage("You pick up a potion.");
    }

In the preceding code, here is the player's cell, and itemAt finds the item there, if there is one. If there isn't, there's nothing to do. Otherwise, gold adds its coins to the player's, and anything else, which is a potion, adds a potion, with a message either way. The -> reaches the item's functions through the pointer, as Chapter 10 showed.

The item belongs to the player now, so it has to come off the floor. Add these lines below the else’s closing brace, with a blank line in between, to finish pickUp:

    // It's the player's now, so it isn't lying on the floor anymore
    std::erase_if(items_, [here](const Item& lying)
    {
        return lying.getPosition() == here;
    });
}

In the preceding code, std::erase_if removes every item in items_ for which the lambda returns true: the one lying at here. It's Chapter 24's std::erase_if, which Chapter 27's Asteroids used to clear away its bullets. The lambda needs here to compare with, so it captures it, in the square brackets, as Chapter 24 showed. There's only ever one item on a cell, so only one is removed.

The item's pointer, item, is used for the last time just before this, which matters: after std::erase_if, the item it pointed to has gone.

The function itemAt works like enemyAt. Add it below enemyAt, with a blank line in between:

// The item at a cell, or nullptr if there isn't one, with the same warning
Item* Game::itemAt(Point cell)
{
    for (Item& item : items_)
    {
        if (item.getPosition() == cell)
            return &item;
    }
    return nullptr;
}

In the preceding code, the loop returns the address of the item at the cell, or nullptr if there isn't one, and the comment points back to enemyAt’s warning, which applies here too.

The End of a Turn

Last, endTurn, which will grow into the monsters' turn later in the chapter. For now, it clears away the monsters that have died. Add it below takeStairs, with a blank line in between:

// The player has taken a turn, and any monster killed in it is cleared away
void Game::endTurn()
{
    std::erase_if(enemies_, [](const Enemy& enemy)
    {
        return !enemy.isAlive();
    });
}

In the preceding code, std::erase_if removes every monster whose isAlive is false. This lambda doesn't need anything from outside, so its square brackets are empty. Figure 32.6 shows what happens to the vector.

Clearing away the dead. std::erase_if calls the lambda for each monster, and removes every one for which it returns true. The survivors close up, in the same order, and the vector is shorter.
Figure 32.6 — Clearing away the dead. std::erase_if calls the lambda for each monster, and removes every one for which it returns true. The survivors close up, in the same order, and the vector is shorter.

The HUD's draw needs the player now. Last, find the line in draw that draws the HUD:

hud_.draw(renderer_, glyphs_, depth_);

And change it to this:

hud_.draw(renderer_, glyphs_, player_, depth_);

In the preceding code, the game passes its player to the HUD, along with the depth.

Checkpoint: Press F5. The top row of the HUD shows HP 20/20 Gold 0 Potions 0 Depth 1. Find a rat, and walk into it: "You kill the rat!" Walk onto a $, and the gold goes onto the HUD, and off the floor, and potions do the same. The monsters still don't fight back, so enjoy it while it lasts.

The Monsters' Turn

Hitting Back

A monster that hits back needs something to hit. The player's health has to be able to go down, and a potion has to be able to bring it back up. In Player.h, add this below PLAYER_ATTACK:

constexpr int POTION_HEAL = 8;      // the health a potion gives back

In the preceding code, a potion gives back 8 health. Then add this above addGold’s declaration:

void takeDamage(int amount);

In the preceding code, takeDamage takes health away. Then add this below addPotion’s declaration:

bool drinkPotion();

In the preceding code, drinkPotion drinks a potion, if the player has one, and says whether they did.

In Player.cpp, both will need std::min and std::max. Add this below #include "Player.h":

#include <algorithm>   // std::min and std::max

In the preceding code, <algorithm> brings in the two functions that Chapter 31 met. Then add takeDamage above addGold, with a blank line in between:

// Never below zero, so that the HUD never shows a negative number
void Player::takeDamage(int amount)
{
    hp_ = std::max(hp_ - amount, 0);
}

In the preceding code, the damage is taken off the player's health, but std::max never lets the result go below 0. Without it, a big hit on a weak player would leave them on -3, which the HUD would show, and nobody wants to read HP -3/20.

Last, add drinkPotion at the end of the file, below addPotion, with a blank line in between:

// Drinks a potion, if there's one to drink, and heals, though never past
// full health. Returns true if a potion was drunk
bool Player::drinkPotion()
{
    if (potions_ == 0)
        return false;

    --potions_;
    hp_ = std::min(hp_ + POTION_HEAL, PLAYER_MAX_HP);
    return true;
}

In the preceding code, if the player has no potions, drinkPotion returns false, and nothing changes. Otherwise, one potion is used up, and the player gets POTION_HEAL health back, but std::min never lets it go past PLAYER_MAX_HP, so a potion can't take a player who's nearly at full health past full.

Noticing, and Closing In

Now the monsters' side of the turn. A monster doesn't know where you are until it notices you, and then it does one of two things: if it's right next to you, it attacks, and otherwise, it takes a step toward you. Figure 32.7 shows each monster's decision.

What a monster decides, on its turn. If it hasn't noticed the player, it does nothing. If it has, and the player is next to it, it attacks. Otherwise, it steps along whichever way the player is farther, and if a wall is in the way, it stays put, as the goblin on the right does.
Figure 32.7 — What a monster decides, on its turn. If it hasn't noticed the player, it does nothing. If it has, and the player is next to it, it attacks. Otherwise, it steps along whichever way the player is farther, and if a wall is in the way, it stays put, as the goblin on the right does.

In Game.h, add this below pickUp’s declaration:

void drinkPotion();

In the preceding code, drinkPotion is the game's side of drinking a potion, which takes a turn. Then add these below endTurn’s declaration:

void monsterTurn(Enemy& enemy);
bool notices(const Enemy& enemy) const;

In the preceding code, monsterTurn takes one monster's turn, and notices says whether a monster has noticed the player. It's const, since noticing doesn't change anything.

In Game.cpp, the monsters will measure distances with std::abs. Add this below #include "Game.h":

#include <cstdlib>   // std::abs

In the preceding code, <cstdlib> brings in std::abs for whole numbers, as it did for the generator.

Now endTurn gives every monster its turn, once the dead have been cleared away. Replace the whole of endTurn with this:

// The player has taken a turn, so now the monsters take theirs. The dead
// are cleared away first
void Game::endTurn()
{
    std::erase_if(enemies_, [](const Enemy& enemy)
    {
        return !enemy.isAlive();
    });

    for (Enemy& enemy : enemies_)
    {
        if (notices(enemy))
            monsterTurn(enemy);
    }
}

In the preceding code, after the dead are removed, the loop goes through the living monsters, by reference, and every one that notices the player takes its turn. The dead have to go first, or a monster killed this turn would still hit back.

Next, the monster's turn. Add the first part of monsterTurn below endTurn, with a blank line in between:

// A monster that has noticed the player attacks if it's next to them, and
// otherwise steps toward them
void Game::monsterTurn(Enemy& enemy)
{
    Point from = enemy.getPosition();
    Point to = player_.getPosition();
    int dx = to.x - from.x;
    int dy = to.y - from.y;

    if (std::abs(dx) + std::abs(dy) == 1)
    {
        int damage = enemy.getStats().attack;
        player_.takeDamage(damage);
        hud_.addMessage("The " + std::string(enemy.getStats().name) +
                        " hits you for " + std::to_string(damage) + ".");
        return;
    }

In the preceding code, dx and dy are how far the player is from the monster, across and down. If the Manhattan distance between them, from Chapter 31, is exactly 1, the player is right beside the monster, above, below, or to one side, and the monster attacks. The player takes its kind's attack in damage, and the HUD says so. The name goes into a std::string before anything is added to it, since "The " and the name are both const char*s, and C++ can't add two of those. Then return ends the monster's turn.

Otherwise, the monster steps closer. Add these lines below the if’s closing brace, with a blank line in between, to finish monsterTurn:

    // One step along whichever way is farther. A wall in the way stops it
    Point step = from;
    if (std::abs(dx) > std::abs(dy))
        step.x += dx > 0 ? 1 : -1;
    else
        step.y += dy > 0 ? 1 : -1;

    if (!map_.isBlocked(step) && !enemyAt(step))
        enemy.setPosition(step);
}

In the preceding code, step starts at the monster's cell, and moves one cell along whichever way the player is farther: across if dx is the bigger distance, and down or up if dy is. Chapter 4's ternary operator picks 1 or -1, depending on which side the player is on. If that cell isn't blocked, by a wall, or by another monster, the monster moves there. Otherwise, it stays where it is, and its turn is over.

Note

This is the simplest chase there is, and it isn't clever. A monster steps straight toward you, and if a wall is in the way, it stops there, even if a doorway is one step to the side, so it's easy to leave one pressing its nose against a wall. Chapter 33 gives the monsters a real pathfinder, A*, which finds the shortest way around anything, and makes them far more dangerous.

A monster notices the player under two conditions. Add notices below monsterTurn, with a blank line in between:

// A monster notices the player when it stands where the player can see it,
// and the player is within its own sight
bool Game::notices(const Enemy& enemy) const
{
    Point from = enemy.getPosition();
    Point to = player_.getPosition();
    int dx = to.x - from.x;
    int dy = to.y - from.y;
    int sight = enemy.getStats().sight;
    return map_.at(from).visible && dx * dx + dy * dy <= sight * sight;
}

In the preceding code, a monster notices the player if it's standing somewhere the player can see, which the field of view has already worked out, and if the player is within the monster's own sight, measured as a circle with the test from Chapter 31. Using the player's field of view means that walls hide you from monsters just as they hide monsters from you, without any more rays. And a rat, with a sight of 6, can be seen a little before it sees you, which gives you a moment to decide what to do.

Potions

The monsters bite, so the potions matter now. In handleKey, add this case above case SDLK_PERIOD::

case SDLK_H:
    drinkPotion();
    return;

In the preceding code, the H key drinks a potion, and returns, since drinking isn't a step. Then add drinkPotion below pickUp, with a blank line in between:

// Drinking a potion takes a turn, but trying to drink one you haven't got
// doesn't
void Game::drinkPotion()
{
    if (!player_.drinkPotion())
    {
        hud_.addMessage("You have no potions.");
        return;
    }

    hud_.addMessage("You drink a potion, and feel better.");
    endTurn();
}

In the preceding code, if the player has no potion, the HUD says so, and nothing else happens, and no turn is taken. Otherwise, the potion is drunk, the HUD says so, and the turn ends, so the monsters get their turn. Drinking a potion in the middle of a fight costs you a hit, which is how it should be.

The message about picking up a potion can mention H now. Last, in pickUp, find the message about a potion:

hud_.addMessage("You pick up a potion.");

And change it to this:

hud_.addMessage("You pick up a potion. Press H to drink it.");

In the preceding code, the message tells the player how to use what they've found. The keys' row needs the same news. In HUD.cpp, find the first line of the keys:

std::string help = "Arrows or WASD: move and attack"

And change it to this:

std::string help = "Arrows or WASD: move and attack   H: drink a potion"

In the preceding code, the keys' row includes H, between moving and the stairs.

Checkpoint: Press F5, and walk up to a rat. When it notices you, it comes to meet you, and if you're the one who steps up beside it, it bites: "The rat hits you for 2." If it steps up beside you instead, you get the first blow, and one is enough for a rat. Kill it, collect a potion, and press H when you're hurt, and your health comes back. Now try slipping around a corner from a monster that's chasing you: once it can't see you, it stops where it is, and doesn't come after you. But don't let your health reach 0: the game doesn't know what dying is yet, and you'd carry on at HP 0/20.

Death, and Starting Again

When your health reaches 0, the game should end, say so, and offer another go. The HUD needs a color for bad news. In Common.h, add this below TEXT_DIM:

constexpr SDL_Color TEXT_BAD = { 240, 80, 80, 255 };

In the preceding code, TEXT_BAD is a red, for the one line of the HUD that you never want to see.

The player needs to say whether they're alive, and to be put back as they were at the start of a game. In Player.h, add this below getPotions’s declaration:

bool isAlive() const;

In the preceding code, isAlive says whether the player has any health left. Then add this below drinkPotion’s declaration:

void reset();

In the preceding code, reset puts the player back as they were at the start. Now, in Player.cpp, add isAlive below getPotions, with a blank line in between:

bool Player::isAlive() const
{
    return hp_ > 0;
}

In the preceding code, isAlive is true while the player has any health at all. Then add reset at the end of the file, below drinkPotion, with a blank line in between:

// Everything back to how it was at the start of the game
void Player::reset()
{
    hp_ = PLAYER_MAX_HP;
    gold_ = 0;
    potions_ = 0;
}

In the preceding code, reset sets the three numbers back to their starting values. The player's position doesn't need resetting, since the new game's first level puts the player at its start anyway.

The HUD needs to forget the last game's messages, and show a different second row when the player has died. In HUD.h, add this below addMessage’s declaration:

void clear();

In the preceding code, clear will empty the messages. Then find the declaration of draw:

void draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
          const Player& player, int depth) const;

And change it to this:

void draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
          const Player& player, int depth, bool gameOver) const;

In the preceding code, draw is told whether the game is over. Then, in HUD.cpp, add clear below addMessage, with a blank line in between:

void HUD::clear()
{
    messages_.clear();
}

In the preceding code, clear empties the deque of messages. Then find the first two lines of draw:

void HUD::draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
               const Player& player, int depth) const

And change them to this:

void HUD::draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
               const Player& player, int depth, bool gameOver) const

In the preceding code, the definition is changed to match the new declaration. Then find the lines that draw the keys:

// The keys
std::string help = "Arrows or WASD: move and attack   H: drink a potion"
                   "   .: take the stairs down   Esc: quit";
glyphs.drawText(renderer, help, { 1, row + 1 }, Palette::TEXT_DIM);

And change them to this:

// The keys, or once the player has died, what to do about it
std::string help = "Arrows or WASD: move and attack   H: drink a potion"
                   "   .: take the stairs down   Esc: quit";
SDL_Color helpColor = Palette::TEXT_DIM;
if (gameOver)
{
    help = "You have died. Press R to play again, or Esc to quit.";
    helpColor = Palette::TEXT_BAD;
}
glyphs.drawText(renderer, help, { 1, row + 1 }, helpColor);

In the preceding code, the row's text and its color are kept in two variables, which start as the keys, in the dim color. If the game is over, both change: the text says what happened and what to do about it, and the color is TEXT_BAD. Then the one call to drawText draws whichever it is.

Now the game. In Game.h, add this above newLevel’s declaration:

void newGame();

In the preceding code, newGame will start a whole new game. Then add this below depth_:

bool gameOver_ = false;  // true once the player has died

In the preceding code, gameOver_ becomes true when the player dies, and stays true until a new game starts.

In Game.cpp, the game starts with a new game, rather than a new level and a welcome. Find these two lines in the constructor:

newLevel();
hud_.addMessage("Welcome to Rogue SDL. Find the stairs down: >");

And change them to this:

newGame();

In the preceding code, the constructor calls newGame, which does both. Add newGame below run, with a blank line in between:

// Starts again from the top, with a fresh player at depth 1
void Game::newGame()
{
    depth_ = 1;
    gameOver_ = false;
    player_.reset();
    hud_.clear();
    newLevel();
    hud_.addMessage("Welcome to Rogue SDL. Find the stairs down: >");
}

In the preceding code, newGame puts everything back to the start: depth 1, a living player with full health and empty pockets, and a HUD with no messages. Then it builds the first level, and welcomes the player. It's the same whether it's the first game or the fiftieth, and it does nothing else: in particular, it doesn't end a turn, so no monster gets a free hit at the start of a new game.

Once the player has died, only two keys should do anything. Add these lines at the top of handleKey, above int dx = 0;, with a blank line in between:

// Once the player has died, only two keys do anything
if (gameOver_)
{
    if (key == SDLK_R)
        newGame();
    else if (key == SDLK_ESCAPE)
        running_ = false;
    return;
}

In the preceding code, while gameOver_ is true, R starts a new game, and Escape quits, and every other key is ignored, since the return ends handleKey before the switch is reached.

The player dies during the monsters' turn, so that's where the game has to notice. Next, find the loop at the end of endTurn:

for (Enemy& enemy : enemies_)
{
    if (notices(enemy))
        monsterTurn(enemy);
}

And change it to this:

for (Enemy& enemy : enemies_)
{
    if (!notices(enemy))
        continue;

    monsterTurn(enemy);
    if (!player_.isAlive())
    {
        gameOver_ = true;
        hud_.addMessage("You die.");
        return;
    }
}

In the preceding code, a monster that hasn't noticed the player is skipped with continue. After each monster's turn, if the player is dead, the game is over: the HUD says so, and return ends the turn at once, so the rest of the monsters don't carry on hitting a player who's already dead.

Last, find the line in draw that draws the HUD:

hud_.draw(renderer_, glyphs_, player_, depth_);

And change it to this:

hud_.draw(renderer_, glyphs_, player_, depth_, gameOver_);

In the preceding code, the HUD is told whether the game is over.

That's the whole of Part 3: four new files, and every change to the old ones typed.

Checkpoint: Press F5, and play until something kills you, which an orc, deeper down, will be happy to do. When your health reaches 0, the HUD says "You die.", and its second row turns red, as in Figure 32.8. Press R, and you're back at depth 1, with full health and empty pockets, in a brand-new dungeon. Press Escape, and the game ends.

Death, at depth 6. An orc, the red o, has done its job. The health on the HUD is 0, never less, the messages say how it happened, and the red row says what you can do about it.
Figure 32.8 — Death, at depth 6. An orc, the red o, has done its job. The health on the HUD is 0, never less, the messages say how it happened, and the red row says what you can do about it.

The Complete Files

Here are the thirteen files that are new or changed, in full, exactly as they are in the repository's SDL3 Projects/Rogue SDL Part 3. The other nine, main.cpp, Entity.h, Entity.cpp, GlyphCache.h, GlyphCache.cpp, Map.h, Map.cpp, FOV.h, and FOV.cpp, are just as they were at the end of Chapter 31. First, Common.h:

#pragma once
#include <SDL3/SDL.h>

// A place on the map, counted in cells, not pixels
struct Point
{
    int x = 0;
    int y = 0;

    bool operator==(const Point& other) const = default;
};

// The map is a grid of square cells, CELL_PX pixels across, MAP_W cells
// wide and MAP_H cells tall. Below it are HUD_ROWS rows of text, and the
// window is exactly the size of both: 1280 by 720 pixels
constexpr int CELL_PX = 16;
constexpr int MAP_W = 80;
constexpr int MAP_H = 40;
constexpr int HUD_ROWS = 5;
constexpr int WINDOW_W = MAP_W * CELL_PX;
constexpr int WINDOW_H = (MAP_H + HUD_ROWS) * CELL_PX;

// How far the player can see, in cells
constexpr int SIGHT_RADIUS = 8;

// Every color in the game, in one place
namespace Palette
{
    constexpr SDL_Color BACKGROUND = { 10, 10, 16, 255 };
    constexpr SDL_Color WALL = { 180, 160, 110, 255 };
    constexpr SDL_Color FLOOR = { 110, 110, 130, 255 };
    constexpr SDL_Color PLAYER = { 255, 255, 255, 255 };

    // The stairs down
    constexpr SDL_Color STAIRS = { 240, 220, 80, 255 };

    // The colors of things remembered, but out of sight
    constexpr SDL_Color WALL_REMEMBERED = { 60, 55, 40, 255 };
    constexpr SDL_Color FLOOR_REMEMBERED = { 40, 40, 55, 255 };
    constexpr SDL_Color STAIRS_REMEMBERED = { 110, 100, 40, 255 };

    // The HUD's text
    constexpr SDL_Color TEXT = { 200, 200, 210, 255 };
    constexpr SDL_Color TEXT_DIM = { 100, 100, 110, 255 };
    constexpr SDL_Color TEXT_BAD = { 240, 80, 80, 255 };

    // Monsters and treasure
    constexpr SDL_Color RAT = { 180, 180, 100, 255 };
    constexpr SDL_Color GOBLIN = { 100, 220, 100, 255 };
    constexpr SDL_Color ORC = { 220, 100, 100, 255 };
    constexpr SDL_Color POTION = { 220, 80, 220, 255 };
    constexpr SDL_Color GOLD = { 240, 220, 80, 255 };
}

In the preceding code, a Point can be compared with ==, and the palette has the monsters, the treasure, and the red for bad news.

Next, Enemy.h:

#pragma once
#include "Entity.h"

// The kinds of monster, from the weakest to the strongest
enum class MonsterKind
{
    Rat,
    Goblin,
    Orc
};

// What every monster of one kind is like
struct MonsterStats
{
    const char* name;
    char glyph;
    SDL_Color color;
    int maxHp;
    int attack;   // the damage it does with each hit
    int sight;    // how far away it can notice the player, in cells
};

const MonsterStats& statsOf(MonsterKind kind);

// A monster, of one of the kinds above
class Enemy : public Entity
{
public:
    Enemy(MonsterKind kind, Point position);

    const MonsterStats& getStats() const;
    int getHp() const;
    bool isAlive() const;
    void takeDamage(int amount);

private:
    MonsterKind kind_;
    int hp_;
};

In the preceding code, every kind of monster is described by a MonsterStats, and an Enemy is an entity with a kind and some health.

Then Enemy.cpp:

#include "Enemy.h"

namespace
{
    // One for each kind of monster, in the same order as the enum
    constexpr MonsterStats MONSTER_STATS[] = {
        { "rat", 'r', Palette::RAT, 4, 2, 6 },
        { "goblin", 'g', Palette::GOBLIN, 8, 3, 7 },
        { "orc", 'o', Palette::ORC, 14, 5, 8 }
    };
}

const MonsterStats& statsOf(MonsterKind kind)
{
    return MONSTER_STATS[static_cast<int>(kind)];
}

Enemy::Enemy(MonsterKind kind, Point position)
    : Entity(position, statsOf(kind).glyph, statsOf(kind).color),
      kind_(kind),
      hp_(statsOf(kind).maxHp)
{
}

const MonsterStats& Enemy::getStats() const
{
    return statsOf(kind_);
}

int Enemy::getHp() const
{
    return hp_;
}

bool Enemy::isAlive() const
{
    return hp_ > 0;
}

void Enemy::takeDamage(int amount)
{
    hp_ -= amount;
}

In the preceding code, the table has a row for each kind of monster, and every Enemy takes its looks and its health from its row.

Then Item.h:

#pragma once
#include "Entity.h"

// The kinds of treasure
enum class ItemKind
{
    Potion,
    Gold
};

// What every item of one kind is like
struct ItemStats
{
    const char* name;
    char glyph;
    SDL_Color color;
};

const ItemStats& statsOf(ItemKind kind);

// Something lying on the floor, waiting to be picked up
class Item : public Entity
{
public:
    Item(ItemKind kind, Point position, int amount = 1);

    ItemKind getKind() const;
    int getAmount() const;

private:
    ItemKind kind_;
    int amount_;   // how many coins, for gold
};

In the preceding code, every kind of treasure is described by an ItemStats, and an Item is an entity with a kind and an amount.

Then Item.cpp:

#include "Item.h"

namespace
{
    // One for each kind of item, in the same order as the enum
    constexpr ItemStats ITEM_STATS[] = {
        { "potion", '!', Palette::POTION },
        { "gold", '$', Palette::GOLD }
    };
}

const ItemStats& statsOf(ItemKind kind)
{
    return ITEM_STATS[static_cast<int>(kind)];
}

Item::Item(ItemKind kind, Point position, int amount)
    : Entity(position, statsOf(kind).glyph, statsOf(kind).color),
      kind_(kind),
      amount_(amount)
{
}

ItemKind Item::getKind() const
{
    return kind_;
}

int Item::getAmount() const
{
    return amount_;
}

In the preceding code, the table has a row for potions and a row for gold.

Then Player.h:

#pragma once
#include "Entity.h"

class Map;

constexpr int PLAYER_MAX_HP = 20;   // the player's health, when it's full
constexpr int PLAYER_ATTACK = 4;    // the damage the player does with a hit
constexpr int POTION_HEAL = 8;      // the health a potion gives back

// You: the @
class Player : public Entity
{
public:
    Player();

    bool tryMove(int dx, int dy, const Map& map);

    int getHp() const;
    int getAttack() const;
    int getGold() const;
    int getPotions() const;
    bool isAlive() const;

    void takeDamage(int amount);
    void addGold(int amount);
    void addPotion();
    bool drinkPotion();
    void reset();

private:
    int hp_ = PLAYER_MAX_HP;
    int gold_ = 0;
    int potions_ = 0;
};

In the preceding code, the player has health, an attack, gold, and potions, and can take damage, drink a potion, and start again.

Then Player.cpp:

#include "Player.h"
#include <algorithm>   // std::min and std::max
#include "Map.h"

Player::Player()
    : Entity({ 0, 0 }, '@', Palette::PLAYER)
{
}

// Steps one cell, unless the way is blocked. Returns true if the player
// moved
bool Player::tryMove(int dx, int dy, const Map& map)
{
    Point next = { getPosition().x + dx, getPosition().y + dy };
    if (map.isBlocked(next))
        return false;

    setPosition(next);
    return true;
}

int Player::getHp() const
{
    return hp_;
}

int Player::getAttack() const
{
    return PLAYER_ATTACK;
}

int Player::getGold() const
{
    return gold_;
}

int Player::getPotions() const
{
    return potions_;
}

bool Player::isAlive() const
{
    return hp_ > 0;
}

// Never below zero, so that the HUD never shows a negative number
void Player::takeDamage(int amount)
{
    hp_ = std::max(hp_ - amount, 0);
}

void Player::addGold(int amount)
{
    gold_ += amount;
}

void Player::addPotion()
{
    ++potions_;
}

// Drinks a potion, if there's one to drink, and heals, though never past
// full health. Returns true if a potion was drunk
bool Player::drinkPotion()
{
    if (potions_ == 0)
        return false;

    --potions_;
    hp_ = std::min(hp_ + POTION_HEAL, PLAYER_MAX_HP);
    return true;
}

// Everything back to how it was at the start of the game
void Player::reset()
{
    hp_ = PLAYER_MAX_HP;
    gold_ = 0;
    potions_ = 0;
}

In the preceding code, health never goes below 0, or above full.

Then HUD.h:

#pragma once
#include <SDL3/SDL.h>
#include <deque>   // std::deque, for the messages
#include <string>   // std::string, for the messages

class GlyphCache;
class Player;

// The rows of text below the map: how things stand, which keys do what,
// and the last few things that happened, with the newest at the bottom
class HUD
{
public:
    void addMessage(const std::string& message);
    void clear();
    void draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
              const Player& player, int depth, bool gameOver) const;

private:
    std::deque<std::string> messages_;
};

In the preceding code, the HUD can forget its messages, and draws the player's numbers.

Then HUD.cpp:

#include "HUD.h"
#include "Common.h"
#include "GlyphCache.h"
#include "Player.h"

// The first row is how things stand, and the second is the keys, so the
// messages get the rows left over
constexpr int MESSAGE_ROWS = HUD_ROWS - 2;

// Adds a message at the bottom, and drops the oldest from the top when
// there are too many to show
void HUD::addMessage(const std::string& message)
{
    messages_.push_back(message);
    if (messages_.size() > MESSAGE_ROWS)
        messages_.pop_front();
}

void HUD::clear()
{
    messages_.clear();
}

void HUD::draw(SDL_Renderer* renderer, const GlyphCache& glyphs,
               const Player& player, int depth, bool gameOver) const
{
    int row = MAP_H;   // the first row below the map
    std::string status =
        "HP " + std::to_string(player.getHp()) + "/" +
        std::to_string(PLAYER_MAX_HP) +
        "   Gold " + std::to_string(player.getGold()) +
        "   Potions " + std::to_string(player.getPotions()) +
        "   Depth " + std::to_string(depth);
    glyphs.drawText(renderer, status, { 1, row }, Palette::TEXT);

    // The keys, or once the player has died, what to do about it
    std::string help = "Arrows or WASD: move and attack   H: drink a potion"
                       "   .: take the stairs down   Esc: quit";
    SDL_Color helpColor = Palette::TEXT_DIM;
    if (gameOver)
    {
        help = "You have died. Press R to play again, or Esc to quit.";
        helpColor = Palette::TEXT_BAD;
    }
    glyphs.drawText(renderer, help, { 1, row + 1 }, helpColor);

    // The newest message is bright, and the older ones are dim
    for (size_t i = 0; i < messages_.size(); ++i)
    {
        bool newest = i + 1 == messages_.size();
        glyphs.drawText(renderer, messages_[i],
                        { 1, row + 2 + static_cast<int>(i) },
                        newest ? Palette::TEXT : Palette::TEXT_DIM);
    }
}

In the preceding code, the top row shows the player's numbers, and the second row shows the keys, or, once the player has died, what to do next.

Next, the generator. First MapGenerator.h:

#pragma once
#include <SDL3/SDL.h>
#include <vector>   // std::vector, for the rooms
#include "Common.h"

class Enemy;
class Item;
class Map;

// Builds a new level on a map: it splits the map into areas, puts a room
// in each one, joins the rooms with corridors, puts stairs down in one of
// them, and fills the rooms with monsters and treasure
class MapGenerator
{
public:
    MapGenerator(Map& map);

    Point generate(int depth, std::vector<Enemy>& enemies,
                   std::vector<Item>& items);

private:
    void split(SDL_Rect area, int cuts);
    void addRoom(SDL_Rect area);
    void carve(Point a, Point b);
    void carveCorridor(Point from, Point to);
    Point takeFreeSpot(bool awayFromStart);
    void addMonsters(int depth, std::vector<Enemy>& enemies);
    void addTreasure(std::vector<Item>& items);

    Map& map_;                      // the map being built, not owned
    std::vector<SDL_Rect> rooms_;   // every room, in the order it was made
    int startRoom_ = 0;             // the room the player starts in
    std::vector<Point> taken_;      // cells that already have something
};

In the preceding code, generate takes the depth and the two vectors it fills, and the generator remembers the start room and the cells it has used.

Then MapGenerator.cpp:

#include "MapGenerator.h"
#include <algorithm>   // std::min, std::max, and std::find
#include <cstdlib>   // std::abs
#include "Enemy.h"
#include "Item.h"
#include "Map.h"

// An area is cut in two only if its longer side is at least twice this, so
// both pieces are at least this long
constexpr int MIN_AREA = 10;
// and after this many cuts, it isn't cut again, however big it is
constexpr int MAX_CUTS = 6;
// The smallest room, in cells
constexpr int MIN_ROOM_W = 4;
constexpr int MIN_ROOM_H = 3;

namespace
{
    // A random whole number from low to high, including both
    int randomBetween(int low, int high)
    {
        if (high <= low)
            return low;
        return low + SDL_rand(high - low + 1);
    }

    Point centerOf(SDL_Rect room)
    {
        return { room.x + room.w / 2, room.y + room.h / 2 };
    }
}

MapGenerator::MapGenerator(Map& map)
    : map_(map)
{
}

// Builds the level for a depth, adds its monsters and treasure to the two
// vectors, and returns the cell where the player starts
Point MapGenerator::generate(int depth, std::vector<Enemy>& enemies,
                             std::vector<Item>& items)
{
    map_.fill(Terrain::Wall);
    rooms_.clear();
    taken_.clear();
    split({ 0, 0, MAP_W, MAP_H }, 0);

    // Join each room to the one made after it, so that every room can be
    // reached from every other
    for (size_t i = 1; i < rooms_.size(); ++i)
        carveCorridor(centerOf(rooms_[i - 1]), centerOf(rooms_[i]));

    // Start in the middle of a room chosen at random
    int roomCount = static_cast<int>(rooms_.size());
    startRoom_ = SDL_rand(roomCount);
    Point start = centerOf(rooms_[startRoom_]);

    // and put the stairs in the middle of the room farthest away from it
    Point stairs = start;
    int farthest = 0;
    for (const SDL_Rect& room : rooms_)
    {
        Point center = centerOf(room);
        int distance = std::abs(center.x - start.x) +
                       std::abs(center.y - start.y);
        if (distance > farthest)
        {
            farthest = distance;
            stairs = center;
        }
    }
    map_.at(stairs).terrain = Terrain::StairsDown;

    // Nothing else can go where the player starts, or on the stairs
    taken_.push_back(start);
    taken_.push_back(stairs);
    addMonsters(depth, enemies);
    addTreasure(items);
    return start;
}

// Cuts an area in two, then cuts each piece in two, and so on. An area
// that's too small to cut, or has been cut out by MAX_CUTS cuts, gets a
// room instead
void MapGenerator::split(SDL_Rect area, int cuts)
{
    // Cut across the longer side, so that the pieces don't get too thin
    bool cutAcross = area.h > area.w;
    int length = cutAcross ? area.h : area.w;
    if (cuts == MAX_CUTS || length < MIN_AREA * 2)
    {
        addRoom(area);
        return;
    }

    int cut = randomBetween(MIN_AREA, length - MIN_AREA);
    if (cutAcross)
    {
        split({ area.x, area.y, area.w, cut }, cuts + 1);
        split({ area.x, area.y + cut, area.w, area.h - cut }, cuts + 1);
    }
    else
    {
        split({ area.x, area.y, cut, area.h }, cuts + 1);
        split({ area.x + cut, area.y, area.w - cut, area.h }, cuts + 1);
    }
}

// Carves a room of a random size somewhere inside an area, with at least
// one cell of wall between it and the area's edges
void MapGenerator::addRoom(SDL_Rect area)
{
    SDL_Rect room;
    room.w = randomBetween(MIN_ROOM_W, area.w - 2);
    room.h = randomBetween(MIN_ROOM_H, area.h - 2);
    room.x = randomBetween(area.x + 1, area.x + area.w - room.w - 1);
    room.y = randomBetween(area.y + 1, area.y + area.h - room.h - 1);

    carve({ room.x, room.y }, { room.x + room.w - 1, room.y + room.h - 1 });
    rooms_.push_back(room);
}

// Turns every cell in the box with corners a and b into floor. When a and
// b are in the same row, or the same column, the box is a straight line
void MapGenerator::carve(Point a, Point b)
{
    for (int y = std::min(a.y, b.y); y <= std::max(a.y, b.y); ++y)
    {
        for (int x = std::min(a.x, b.x); x <= std::max(a.x, b.x); ++x)
            map_.at({ x, y }).terrain = Terrain::Floor;
    }
}

// Carves an L-shaped corridor between two cells, turning its corner at
// one end or the other, chosen at random
void MapGenerator::carveCorridor(Point from, Point to)
{
    Point corner = { to.x, from.y };
    if (SDL_rand(2) == 0)
        corner = { from.x, to.y };

    carve(from, corner);
    carve(corner, to);
}

// A random cell in a random room, where nothing has been put yet, and, if
// asked, not in the room where the player starts. It's taken from then on
Point MapGenerator::takeFreeSpot(bool awayFromStart)
{
    int roomCount = static_cast<int>(rooms_.size());
    while (true)
    {
        int index = SDL_rand(roomCount);
        if (awayFromStart && index == startRoom_)
            continue;

        const SDL_Rect& room = rooms_[index];
        Point spot = { randomBetween(room.x, room.x + room.w - 1),
                       randomBetween(room.y, room.y + room.h - 1) };
        if (std::find(taken_.begin(), taken_.end(), spot) == taken_.end())
        {
            taken_.push_back(spot);
            return spot;
        }
    }
}

// Puts monsters in the rooms, away from the player: more of them the
// deeper the level, and stronger ones too
void MapGenerator::addMonsters(int depth, std::vector<Enemy>& enemies)
{
    int count = 3 + depth + SDL_rand(3);
    for (int i = 0; i < count; ++i)
    {
        // Rats at every depth, goblins from depth 2, and orcs from depth 4
        int roll = SDL_rand(100);
        MonsterKind kind = MonsterKind::Rat;
        if (depth >= 4 && roll >= 85)
            kind = MonsterKind::Orc;
        else if (depth >= 2 && roll >= 60)
            kind = MonsterKind::Goblin;

        enemies.push_back(Enemy(kind, takeFreeSpot(true)));
    }
}

// Scatters potions and piles of gold through the rooms
void MapGenerator::addTreasure(std::vector<Item>& items)
{
    int potions = randomBetween(1, 3);
    for (int i = 0; i < potions; ++i)
        items.push_back(Item(ItemKind::Potion, takeFreeSpot(false)));

    int piles = randomBetween(2, 5);
    for (int i = 0; i < piles; ++i)
    {
        int coins = randomBetween(5, 15);
        items.push_back(Item(ItemKind::Gold, takeFreeSpot(false), coins));
    }
}

In the preceding code, takeFreeSpot finds a cell nothing else is in, addMonsters puts more and tougher monsters on deeper levels, and addTreasure scatters potions and gold.

Then Game.h:

#pragma once
#include <SDL3/SDL.h>
#include <vector>   // std::vector, for the monsters and the treasure
#include "Enemy.h"
#include "GlyphCache.h"
#include "HUD.h"
#include "Item.h"
#include "Map.h"
#include "Player.h"

// The whole game. It owns the glyphs, the map, the player, the monsters,
// the treasure, and the HUD, and runs the loop that waits for a key, acts
// on it, and draws what happened
class Game
{
public:
    Game(SDL_Renderer* renderer);

    bool isLoaded() const;
    void run();

private:
    void newGame();
    void newLevel();
    void handleKey(SDL_Keycode key);
    void moveOrAttack(int dx, int dy);
    void attack(Enemy& enemy);
    void pickUp();
    void drinkPotion();
    void takeStairs();
    void endTurn();
    void monsterTurn(Enemy& enemy);
    bool notices(const Enemy& enemy) const;
    Enemy* enemyAt(Point cell);
    Item* itemAt(Point cell);
    void draw() const;

    SDL_Renderer* renderer_;
    GlyphCache glyphs_;
    Map map_;
    Player player_;
    std::vector<Enemy> enemies_;
    std::vector<Item> items_;
    HUD hud_;
    int depth_ = 1;          // how many levels down the player is
    bool gameOver_ = false;  // true once the player has died
    bool running_ = true;
    bool dirty_ = true;      // true when the window needs drawing again
};

In the preceding code, the game owns the monsters and the treasure, in two vectors, and knows whether the game is over.

And last, Game.cpp:

#include "Game.h"
#include <cstdlib>   // std::abs
#include <string>   // std::string and std::to_string
#include "FOV.h"
#include "MapGenerator.h"

// The font every character is drawn in, and its size
const std::string FONT_PATH = "assets/RobotoMono-Light.ttf";
constexpr float FONT_SIZE = 18.0f;

Game::Game(SDL_Renderer* renderer)
    : renderer_(renderer), glyphs_(renderer, FONT_PATH, FONT_SIZE)
{
    newGame();
}

bool Game::isLoaded() const
{
    return glyphs_.isLoaded();
}

// Sleeps until something happens, deals with it, and draws the window again
// if anything changed
void Game::run()
{
    while (running_)
    {
        if (dirty_)
        {
            draw();
            dirty_ = false;
        }

        SDL_Event event;
        if (!SDL_WaitEvent(&event))
            break;

        switch (event.type)
        {
        case SDL_EVENT_QUIT:
            running_ = false;
            break;
        case SDL_EVENT_WINDOW_EXPOSED:
            dirty_ = true;
            break;
        case SDL_EVENT_KEY_DOWN:
            handleKey(event.key.key);
            dirty_ = true;
            break;
        }
    }
}

// Starts again from the top, with a fresh player at depth 1
void Game::newGame()
{
    depth_ = 1;
    gameOver_ = false;
    player_.reset();
    hud_.clear();
    newLevel();
    hud_.addMessage("Welcome to Rogue SDL. Find the stairs down: >");
}

// Builds a new level, with its monsters and treasure, puts the player at
// its start, and looks around
void Game::newLevel()
{
    enemies_.clear();
    items_.clear();
    MapGenerator generator(map_);
    player_.setPosition(generator.generate(depth_, enemies_, items_));
    FOV::compute(map_, player_.getPosition(), SIGHT_RADIUS);
}

// Every key press is one action, or none
void Game::handleKey(SDL_Keycode key)
{
    // Once the player has died, only two keys do anything
    if (gameOver_)
    {
        if (key == SDLK_R)
            newGame();
        else if (key == SDLK_ESCAPE)
            running_ = false;
        return;
    }

    int dx = 0;
    int dy = 0;
    switch (key)
    {
    case SDLK_UP:
    case SDLK_W:
        dy = -1;
        break;
    case SDLK_DOWN:
    case SDLK_S:
        dy = 1;
        break;
    case SDLK_LEFT:
    case SDLK_A:
        dx = -1;
        break;
    case SDLK_RIGHT:
    case SDLK_D:
        dx = 1;
        break;
    case SDLK_H:
        drinkPotion();
        return;
    case SDLK_PERIOD:
        takeStairs();
        return;
    case SDLK_ESCAPE:
        running_ = false;
        return;
    default:
        return;
    }

    moveOrAttack(dx, dy);
}

// Attacks the monster in the way, if there is one. Otherwise, steps, looks
// around, and picks up anything lying there. Either way, it's a turn
void Game::moveOrAttack(int dx, int dy)
{
    Point next = { player_.getPosition().x + dx,
                   player_.getPosition().y + dy };
    if (Enemy* enemy = enemyAt(next))
    {
        attack(*enemy);
        endTurn();
        return;
    }

    if (player_.tryMove(dx, dy, map_))
    {
        FOV::compute(map_, player_.getPosition(), SIGHT_RADIUS);
        if (map_.at(player_.getPosition()).terrain == Terrain::StairsDown)
            hud_.addMessage("There are stairs down here. Press . to go down.");
        pickUp();
        endTurn();
    }
}

// The player hits a monster, which may die
void Game::attack(Enemy& enemy)
{
    int damage = player_.getAttack();
    enemy.takeDamage(damage);

    std::string name = enemy.getStats().name;
    if (enemy.isAlive())
    {
        hud_.addMessage("You hit the " + name + " for " +
                        std::to_string(damage) + ".");
    }
    else
    {
        hud_.addMessage("You kill the " + name + "!");
    }
}

// Picks up anything lying where the player stands
void Game::pickUp()
{
    Point here = player_.getPosition();
    Item* item = itemAt(here);
    if (!item)
        return;

    if (item->getKind() == ItemKind::Gold)
    {
        player_.addGold(item->getAmount());
        hud_.addMessage("You pick up " + std::to_string(item->getAmount()) +
                        " gold.");
    }
    else
    {
        player_.addPotion();
        hud_.addMessage("You pick up a potion. Press H to drink it.");
    }

    // It's the player's now, so it isn't lying on the floor anymore
    std::erase_if(items_, [here](const Item& lying)
    {
        return lying.getPosition() == here;
    });
}

// Drinking a potion takes a turn, but trying to drink one you haven't got
// doesn't
void Game::drinkPotion()
{
    if (!player_.drinkPotion())
    {
        hud_.addMessage("You have no potions.");
        return;
    }

    hud_.addMessage("You drink a potion, and feel better.");
    endTurn();
}

// Goes down to a new level, if the player is standing on the stairs
void Game::takeStairs()
{
    if (map_.at(player_.getPosition()).terrain != Terrain::StairsDown)
    {
        hud_.addMessage("There are no stairs here.");
        return;
    }

    ++depth_;
    newLevel();
    hud_.addMessage("You go down the stairs to depth " +
                    std::to_string(depth_) + ".");
}

// The player has taken a turn, so now the monsters take theirs. The dead
// are cleared away first
void Game::endTurn()
{
    std::erase_if(enemies_, [](const Enemy& enemy)
    {
        return !enemy.isAlive();
    });

    for (Enemy& enemy : enemies_)
    {
        if (!notices(enemy))
            continue;

        monsterTurn(enemy);
        if (!player_.isAlive())
        {
            gameOver_ = true;
            hud_.addMessage("You die.");
            return;
        }
    }
}

// A monster that has noticed the player attacks if it's next to them, and
// otherwise steps toward them
void Game::monsterTurn(Enemy& enemy)
{
    Point from = enemy.getPosition();
    Point to = player_.getPosition();
    int dx = to.x - from.x;
    int dy = to.y - from.y;

    if (std::abs(dx) + std::abs(dy) == 1)
    {
        int damage = enemy.getStats().attack;
        player_.takeDamage(damage);
        hud_.addMessage("The " + std::string(enemy.getStats().name) +
                        " hits you for " + std::to_string(damage) + ".");
        return;
    }

    // One step along whichever way is farther. A wall in the way stops it
    Point step = from;
    if (std::abs(dx) > std::abs(dy))
        step.x += dx > 0 ? 1 : -1;
    else
        step.y += dy > 0 ? 1 : -1;

    if (!map_.isBlocked(step) && !enemyAt(step))
        enemy.setPosition(step);
}

// A monster notices the player when it stands where the player can see it,
// and the player is within its own sight
bool Game::notices(const Enemy& enemy) const
{
    Point from = enemy.getPosition();
    Point to = player_.getPosition();
    int dx = to.x - from.x;
    int dy = to.y - from.y;
    int sight = enemy.getStats().sight;
    return map_.at(from).visible && dx * dx + dy * dy <= sight * sight;
}

// The monster at a cell, or nullptr if there isn't one. The pointer is
// only good until the vector of monsters next changes
Enemy* Game::enemyAt(Point cell)
{
    for (Enemy& enemy : enemies_)
    {
        if (enemy.getPosition() == cell)
            return &enemy;
    }
    return nullptr;
}

// The item at a cell, or nullptr if there isn't one, with the same warning
Item* Game::itemAt(Point cell)
{
    for (Item& item : items_)
    {
        if (item.getPosition() == cell)
            return &item;
    }
    return nullptr;
}

void Game::draw() const
{
    SDL_SetRenderDrawColor(renderer_, Palette::BACKGROUND.r,
                           Palette::BACKGROUND.g, Palette::BACKGROUND.b, 255);
    SDL_RenderClear(renderer_);

    map_.draw(renderer_, glyphs_);

    // Treasure, then monsters, wherever the player can see them
    for (const Item& item : items_)
    {
        if (map_.at(item.getPosition()).visible)
            item.draw(renderer_, glyphs_);
    }
    for (const Enemy& enemy : enemies_)
    {
        if (map_.at(enemy.getPosition()).visible)
            enemy.draw(renderer_, glyphs_);
    }

    player_.draw(renderer_, glyphs_);
    hud_.draw(renderer_, glyphs_, player_, depth_, gameOver_);

    SDL_RenderPresent(renderer_);
}

In the preceding code, a step becomes an attack or a move, the monsters take their turns after the player's, and a death leads to a new game.

Playing the Game

Press F5, and see how deep you can get. The first level is only rats, which die in one hit, and it's a good place to collect a potion or two before going down. From depth 2, the goblins take two hits, and hit harder, and from depth 4, the orcs can kill a careless player in four blows. Figure 32.9 shows a fight at depth 3.

Holding a doorway at depth 3. The @ has just killed a rat, and a goblin, the green g, stands in the doorway, where it's the only monster that can reach the @.
Figure 32.9 — Holding a doorway at depth 3. The @ has just killed a rat, and a goblin, the green g, stands in the doorway, where it's the only monster that can reach the @.

There's real tactics in it already. Fight in a corridor, and only one monster can reach you at a time. Retreat around a corner, and a chasing monster loses you. Drink a potion before a fight, rather than in the middle of one, and you don't give anything a free hit. And watch the monsters' letters as you explore: the moment you see an o, it's time to think.

Understanding the Code

Follow one turn through the game. You press an arrow key, and handleKey turns it into a step, and hands it to moveOrAttack.

If a monster is in the way, you attack it, and if not, you step, look around, and pick up anything underfoot. Either way, endTurn follows: the dead are cleared away, and every monster that notices you takes its turn, attacking if it's beside you, and stepping closer if it isn't. If a monster's hit kills you, the game is over there and then. And last, the window is drawn, and the game sleeps until your next key. Figure 32.10 shows the whole turn.

One turn. The player acts first: a step, an attack, or a potion. Then the dead are cleared away, and each monster that has noticed the player takes its turn, attacking or stepping closer. If the player dies, the turn stops there. Then the window is drawn once.
Figure 32.10 — One turn. The player acts first: a step, an attack, or a potion. Then the dead are cleared away, and each monster that has noticed the player takes its turn, attacking or stepping closer. If the player dies, the turn stops there. Then the window is drawn once.

Notice what doesn't take a turn: walking into a wall, trying to drink a potion you haven't got, pressing the period where there are no stairs, and every key the game doesn't use. In a turn-based game, that's important. A key that does nothing shouldn't give the monsters a free move, or a stray key press could get you killed.

The stats tables are worth a second look, too. The Enemy class has no idea what a rat is. It has a kind, and when it needs anything else, it asks statsOf, which reads the kind's row. So every difference between a rat and an orc, how it looks, how tough it is, how hard it hits, and how far it sees, is a number in a table, and the code that uses the numbers is the same for every monster. Chapters 35 and 36 add scrolls and weapons, which do very different things, and each of those will need code of its own, not only numbers.

And notice how the monsters are stored: as values, in a std::vector<Enemy>. There's no new, no delete, and no pointer that owns anything. The vector owns the monsters, std::erase_if destroys the dead, and clear destroys the rest when a new level starts. The only pointers are the ones enemyAt and itemAt return, which own nothing, and are used at once.

Experimenting

The numbers in the tables and in the generator are the whole of the game's balance, so this is a good chapter to play with them. Put each one back afterward, since the next chapter carries on from this one.

  • Orcs from the start. In addMonsters, change depth >= 4 to depth >= 1, and see how long you last with orcs on the first level.
  • A tougher player. Change PLAYER_ATTACK to 8, and orcs take two hits instead of four. Change PLAYER_MAX_HP to 50, and you can survive almost anything, for a while.
  • Sharp-eyed rats. Change the rat's sight, the last number in its row of MONSTER_STATS, from 6 to 12. Rats now notice you the moment you see them.
  • A crowded dungeon. Change 3 + depth + SDL_rand(3) to 10 + depth + SDL_rand(3), and every level is swarming.
  • Rich pickings. In addTreasure, change the piles of gold to randomBetween(10, 20), and the coins in each to randomBetween(50, 100).
  • Start deeper. In newGame, change depth_ = 1; to depth_ = 4;, to meet the orcs sooner, and see whether the potions are enough.

Common Errors and Fixes

C2676: binary '==': 'Point' does not define this operator or a conversion to a type acceptable to the predefined operator, in Game.cpp, and C2678: binary '==': no operator found which takes a left-hand operand of type 'const _Ty' (or there is no acceptable conversion), in a file called xutility. The operator== line is missing from Point, so points can't be compared. The second message comes from inside std::find, which compares with == too. Add the line to Point, in Common.h.

C3493: 'here' cannot be implicitly captured because no default capture mode has been specified. The lambda in pickUp uses here, but its square brackets are empty, so it can't see it. Capture it: [here].

C2110: '+': cannot add two pointers, in monsterTurn. Something like "The " + enemy.getStats().name adds two const char*s, which C++ can't do. Make one side of the first + a std::string, as in "The " + std::string(enemy.getStats().name).

C2065: 'MonsterKind': undeclared identifier, and more, in MapGenerator.cpp. The file doesn't include Enemy.h. The header's forward declaration of Enemy isn't enough, since the generator makes monsters, and uses MonsterKind. Add #include "Enemy.h", and #include "Item.h" beside it.

C2556: 'MonsterStats &statsOf(MonsterKind)': overloaded function differs only by return type from 'const MonsterStats &statsOf(MonsterKind)'. The definition of statsOf in Enemy.cpp has lost the const that its declaration in Enemy.h has. A declaration and its definition must match exactly, return type and all. Put the const back.

Monsters and treasure show up in the dark, all over the level. The ifs are missing from the two loops in draw, so every monster and every item is drawn, whether the player can see it or not. Put back the test of visible above each draw.

After you die, you can still walk around. The game-over block at the top of handleKey is missing its return, so the keys carry on into the switch. Put the return back, after the two keys that are allowed.

The HUD shows negative health, such as HP -3/20. takeDamage is taking the damage off without std::max. Keep it at 0 or above.

Gold never leaves the floor. Step off a pile and back on, and you pick it up again, and again. The std::erase_if at the end of pickUp is missing, so the item is never removed from items_.

AI Exercise (Optional)

If you'd like to add a monster of your own with an AI's help, here's a challenge that shows how well a table of stats pays off. As always, it's optional.

Open your AI chatbot of choice and try a prompt like this:

"I'm writing a roguelike in C++ with SDL 3. Monsters are Enemy objects, held by value in a std::vector<Enemy>. Each Enemy has a MonsterKind, an enum class with the values Rat, Goblin, and Orc, and gets its name, character, color, maxHp, attack, and sight from a constexpr table of MonsterStats, MONSTER_STATS, in the same order as the enum, through a function const MonsterStats& statsOf(MonsterKind kind). On each monster's turn, my Game::monsterTurn(Enemy& enemy) attacks the player if they're next to it, and otherwise steps one cell toward them, unless map_.isBlocked(step) or enemyAt(step) says the cell is taken. MapGenerator::addMonsters(int depth, std::vector<Enemy>& enemies) picks each monster's kind from a random roll. I'd like a new monster: a bat, drawn as a brown b, weak but fast, which flutters about: on half of its turns, it steps in a random direction instead of toward the player. Show me every change, with each curly brace on its own line, and explain how the bat's turn works, and where it appears."

Notice what the preceding prompt does. It describes the table and the order it has to stay in, so the AI knows a new kind needs a new row in the right place. It spells out the monster's turn exactly, down to the two checks on a step, so that the bat's random steps can obey the same rules. And it asks where bats appear, which means changing the generator, a part of the job that's easy to forget.

When the answer comes back, check it against this chapter. Is Bat added at the end of the enum, and its row at the end of the table, as Chapter 31's warning said? Does the bat's random step still check isBlocked and enemyAt, so that it can't fly through walls or into other monsters? Since Enemy has no function that returns its kind, only getStats, see how the AI tells a bat from other monsters: by adding a getKind, or by comparing something in its stats. And is the bat's color added to the palette?

To try it, make bats common on the first level, by changing the roll in addMonsters, and watch one flutter toward you, and away again. If it steps through a wall, or two monsters end up on one cell, ask the AI why.

Summary

The dungeon isn't empty anymore. Monsters and treasure are entities, like the player, and every kind is a row in a table of stats, so a rat and an orc share all their code, and differ only in their numbers.

The generator fills each level as it builds it, with more and tougher monsters the deeper you go. You fight by walking into a monster, pick treasure up by walking onto it, and drink potions with H. After every turn of yours, std::erase_if clears away the dead, and every monster that has noticed you takes its turn, closing in or hitting back. And when your health runs out, the game says so, and R starts it all again.

Next, in Chapter 33, the monsters get smarter. They'll find their way to you around any corner, with the A* pathfinding algorithm, and remember where they last saw you when you slip out of sight. And you'll be able to save the game to a file, and load it again later, or right after a mistake.