Every potion in Rogue SDL is the same potion. You pick one up, a number on the HUD goes up by one, and pressing H drinks one. That works perfectly well for a single kind of thing, and it falls apart at two. A scroll would need a key of its own, and so would the next kind of thing, and the one after that, until the keyboard runs out of letters, and you run out of room in your head for what they all do.
This chapter gives you a pack. Everything you pick up, apart from gold, goes into it, up to twelve things, and pressing I opens an inventory over the dimmed dungeon, where each thing has a letter, and pressing the letter uses it. And there's something new to find in the dungeon: a scroll of magic mapping, which shows you the whole level at once. It does properly what Chapter 31's "See it all" experiment did by cheating.
Underneath, the game learns something bigger. While the inventory is open, the keys mean different things: A is a step to the left while you're playing, and the first thing in your pack while you're looking in it. So the game gets states, a class for each mode it can be in, all built on one base class with virtual functions, and kept on a stack, with the inventory on top of the game it came from. It's the family of classes Chapter 30 promised, where virtual finally earns its keep. By the end of the chapter, your pack looks like Figure 35.1.

SDL3 Projects/Rogue SDL Part 6 — the complete source for this chapter lives here, with its thirty-six files and the assets folder, which now holds seven sounds beside the font. The chapter carries on in your own project from Chapter 34. If you'd rather start from the book's copy of Part 5, make a copy of the Rogue SDL Part 5 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:
- See why a game with more than one mode outgrows a single
switch, and meet the State pattern - Build a family of game states on a base class with virtual functions, and keep them on a stack
- Hand every key to the state on top, draw every state from the bottom up, and let a state ask for the stack to change, rather than change it itself
- Give the player a pack of twelve slots, one for each letter from a to l, in place of the potion count
- Open an inventory over the dimmed dungeon, and use what's in it
- Save the pack, in a new version of the save file
- Add a scroll of magic mapping, and read it
- Play, experiment, fix the most common mistakes, and try an optional AI exercise
Let's open the pack.
Planning Part 6
Part 5 finished with thirty files. Part 6 adds six, and changes thirteen. Here's what each one is for:
| File | What's new |
|---|---|
GameState.h, GameState.cpp |
New: the base class of every state, and the change a state can ask for |
PlayingState.h, PlayingState.cpp |
New: the state you play in, with the keys that Game::handleKey used to handle |
InventoryState.h, InventoryState.cpp |
New: the inventory, drawn over the dungeon |
Game.h, Game.cpp |
A stack of states, the functions they call, and items used from the pack |
Player.h, Player.cpp |
The pack, in place of the potion count |
Item.h, Item.cpp |
The scroll of magic mapping, and the potion's full name |
Map.h, Map.cpp |
A way to explore the whole level at once |
MapGenerator.cpp |
A scroll on about half of the levels |
Common.h |
The scroll's color, and the shade behind the inventory |
HUD.cpp |
I for the inventory, in place of H, and no potion count |
SaveLoad.cpp |
Version 2 of the save file, which carries the pack |
Sound.h |
The scroll's sound |
There's one more sound, too: scroll.wav, for reading a scroll, which goes in the assets folder with the other six.
We'll add Part 6 in three stages, and play the game at the end of each. First, the states. The keys move out of Game and into a playing state, and the game plays exactly as it did before, which is how you'll know the move worked. Then the pack, with its inventory, a second state on top of the first, and a save file that remembers what you carry. And last, the scroll.
States
One Game, Two Sets of Keys
Right now, every key has exactly one meaning, and Game::handleKey knows them all. It's a single function: a check for game over, and a switch that turns each key into a step, a potion, the stairs, a save, a load, or quitting.
Now picture an inventory. With the pack on the screen, each item has a letter, and pressing the letter uses the item. But A is already a step to the left, and D a step to the right, so while the inventory is open, those keys have to mean something else. Even Esc changes its meaning: while you're playing, it quits the game, but in the inventory, it should only close the inventory. Figure 35.2 shows the clash.

The quickest fix is a bool, say inventoryOpen_, with an if at the top of handleKey that sends the letters one way while it's true, and another if in draw, to show the inventory. That works, for one extra mode.
But Chapter 36 adds another, for aiming a fireball, where the arrows move a target rather than you, and that's another bool, and more ifs in both functions. Worse, the modes start to tangle. Can the inventory open while you're aiming? What does Esc do when both bools are true? Every new mode has to know about all the others, and handleKey becomes a knot that nobody wants to touch.
You've met the neater answer already, in a small way. Chapter 11's moles, and Chapter 17's runner, were each in exactly one named state at a time, and a switch on the state chose what happened. That's a state machine, and it's the right idea.
What's different here is how much each mode needs. A mole's states differ by a few lines each, but the game's modes each have their own keys, their own drawing, and their own things to show. Put all of that into one switch, and it's the knot again.
The State Pattern
So each mode becomes a class of its own. A playing state knows what the keys mean while you're playing, an inventory state knows what they mean in the inventory, and each one draws whatever it adds to the picture. They all share a base class, GameState, whose handleKey and draw are virtual. The game hands every key to whichever state it's in, through a pointer to the base class, without knowing which state it is, just as Chapter 22's list of enemies called update on each enemy without knowing its kind. Figure 35.3 shows the family.

This is the State pattern, one of the twenty-three patterns in Design Patterns, the 1994 book by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, who are often called the Gang of Four. Its idea is that an object whose behavior depends on its state can hand that behavior to a separate object for each state, and swap them over, rather than asking which state it's in everywhere it does anything. Adding a mode then means adding a class, and none of the others have to change.
Robert Nystrom's Game Programming Patterns, which Chapters 28 and 29 grew from, has a chapter on State that's well worth reading next. It starts from a platformer's heroine, whose jumps, ducks, and dives soon need so many flags that every change breaks something, and it follows the same road as this chapter: an enum and a switch, then a class for each state, and then a stack of them, which it calls a pushdown automaton. It's free to read online, at gameprogrammingpatterns.com.
The states in this chapter are small. The playing state is the old switch, moved, and the inventory state is a box on the screen and a handful of keys. Neither of them has a single member variable. Everything they work on still belongs to Game, and they reach it through a reference, as you'll see.
A Stack of States
Here's the catch with swapping one state for another. When the inventory opens, the game you were playing hasn't gone anywhere. It's still there, dimmed, behind the box, and when the inventory closes, you should be exactly where you were. So the inventory doesn't replace the playing state. It goes on top of it.
The game keeps its states on a stack, like Chapter 15's std::stack of things to undo, and like the screens that chapter said a game can keep the same way. The playing state is at the bottom, from the moment the game starts. Pressing I pushes an inventory state on top, and closing the inventory pops it off again, which leaves the playing state on top, exactly as it was.
Two rules make the stack work, and Figure 35.4 shows both. Every key goes to the state on top, and only to it, so while the inventory is open, the playing state never hears a thing. And every state draws, from the bottom up, so the inventory draws over the dungeon, and whatever is on top is drawn last, over everything else.

In this game, the stack is never more than two states tall, though nothing in the code limits it. Chapter 36's aiming closes the inventory and opens the aiming state in one go, so even then, the stack stays at two.
Asking for a Change
Who changes the stack? The obvious answer is the state itself: the inventory knows when it's done, so it could take itself off. But the stack holds each state in a std::unique_ptr, and taking one off destroys the state. The inventory would destroy itself in the middle of its own handleKey, and whatever the function did next would be using memory that had been handed back. It's the trap Chapter 28 warned about, for a component that swaps itself.
So a state never touches the stack. Once it has dealt with a key, it returns a state change: a small struct that says whether it wants to be closed, and whether another state should be opened on top of the stack. The game makes the change afterward, once the state's function has returned, when it's safe.
There are four possible answers, as Figure 35.5 shows: nothing, which is what almost every key asks for; close, as the inventory asks when you press Esc; open, as the playing state asks when you press I; and both, which replaces the state on top with a new one, as Chapter 36's aiming will.

Never let a state change the stack while one of its own functions is running, even if you give it a way to. A state that takes itself off is destroyed mid-function, and the rest of the function works on memory that has been handed back. That's undefined behavior: it might crash at once, or much later, somewhere else, or seem to work until the day it doesn't. Return a StateChange, and let the game make it.
The game's side of it is only a few lines, which you'll write in a moment: close if asked, then open if asked. Since the game does it only after the state has finished with the key, it's always safe, whatever the state asked for.
GameState.h
Add a header called GameState.h, and below its #pragma once, type the includes, two forward declarations, and the change a state can ask for:
#pragma once
#include <SDL3/SDL.h>
#include <memory> // std::unique_ptr, for the state to open
#include "Common.h"
class Game;
class GameState;
// What a state asks to have done to the stack, once it has dealt with a
// key: to be closed, taken off the top, or to have another state opened
// on top, or both, or neither
struct StateChange
{
bool close = false;
std::unique_ptr<GameState> open;
};
In the preceding code, StateChange holds the two answers. The bool called close starts out false, and open is a std::unique_ptr to the state to open, if there is one, which starts out empty, holding nullptr, as Chapter 10's owners do. So a StateChange that nobody fills in asks for nothing, and a state only has to fill in what it wants.
The two forward declarations say that Game and GameState are classes, which is all this header needs to know about them, as Chapter 28 showed. The struct needs the name GameState before that class is written, below it, since it holds a pointer to one. And the header mustn't include Game.h, since Game.h is going to include this header: two headers that include each other are Chapter 28's circular includes, and the forward declaration of Game keeps them apart. The <memory> header is for std::unique_ptr, and Common.h is for Point, which the class uses.
Then add the class below the struct, with a blank line in between:
// A mode the game can be in, such as playing, or looking through the
// inventory. The game keeps a stack of them. Every key goes to the state
// on top, and every state draws, from the bottom up, over the dungeon
class GameState
{
public:
virtual ~GameState() = default;
virtual StateChange handleKey(Game& game, SDL_Keycode key) = 0;
// Draws whatever the state adds to the picture: nothing, unless a
// state says otherwise
virtual void draw(SDL_Renderer*, const Game&) const {}
protected:
static Point directionOf(SDL_Keycode key);
};
In the preceding code, GameState is the base class of every state, and its virtual functions are all Chapter 22's. The destructor is virtual, since the states will be owned, and deleted, through pointers to GameState, and it's = default, since it has nothing to do apart from making sure each state is destroyed whole.
The function handleKey is pure virtual, with = 0, so every state must write its own, and GameState is abstract: there can't be a plain GameState, only a playing state, an inventory state, and so on. It gets the game, as a Game&, so that it can look at the game and ask it to do things, along with the key that was pressed, and it returns the StateChange the state wants.
The function draw is virtual too, but not pure. The body is the empty pair of braces at the end of the line, so a state with nothing to add to the picture doesn't have to write one. Its parameters have no names, since the empty body doesn't use them, which is Chapter 28's way of saying so. It's const, because drawing never changes a state, and the game it gets is const, because drawing never changes the game either.
Last, the protected static function directionOf turns a key into a step, for any state that moves something around the map. It's static because it doesn't need a state to work on, only a key, like Chapter 18's getCount, and it's protected, as Chapter 20 showed, so that every state can call it, and nothing else needs to.
GameState.cpp
Add a C++ file called GameState.cpp, and type this:
#include "GameState.h"
// The step an arrow key, or W, A, S, or D, stands for, and no step at all,
// { 0, 0 }, for any other key
Point GameState::directionOf(SDL_Keycode key)
{
switch (key)
{
case SDLK_UP:
case SDLK_W:
return { 0, -1 };
case SDLK_DOWN:
case SDLK_S:
return { 0, 1 };
case SDLK_LEFT:
case SDLK_A:
return { -1, 0 };
case SDLK_RIGHT:
case SDLK_D:
return { 1, 0 };
default:
return { 0, 0 };
}
}
In the preceding code, directionOf is the part of the old handleKey that turned the arrows, and W, A, S, and D, into steps, moved into a function of its own, which returns the step as a Point. Any other key returns { 0, 0 }, no step at all, so that whoever calls it can tell a step from everything else. The word static goes only on the declaration, inside the class, and not on the definition here.
Why is it in the base class, when only the playing state uses it in this chapter? Because moving something around the map is a job for more than one state. The playing state moves the @, and Chapter 36's aiming state will move a target, with the same keys. With the keys in one place, the two can never disagree about which key goes which way.
Checkpoint: Click in GameState.cpp, and press Ctrl+F7 to compile it on its own. The Error List should stay empty.
What the States Can Reach
A state gets the game as a Game&, but a reference doesn't open any doors. The playing state isn't a Game, so everything private in Game is as closed to it as it is to any other class, as Chapter 18 showed. And nearly everything the playing state needs to do, such as moveOrAttack and takeStairs, is private, since until now, only Game has needed it.
The fix is to decide exactly what a state may do, and make just that public. The game still owns everything: the map, the player, the monsters, the treasure, the HUD, the sounds, the flashes, and now the states as well. A state owns nothing of the game's, and borrows the game only for as long as one of its functions runs, as Figure 35.6 shows.

In Game.h, find these lines, at the start of the private part of the class:
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 saveGame();
void loadGame();
And change them to this:
// What the states can look at
bool isGameOver() const;
// What the states can ask the game to do
void newGame();
void quit();
void moveOrAttack(int dx, int dy);
void drinkPotion();
void takeStairs();
void saveGame();
void loadGame();
private:
void newLevel();
void handleKey(SDL_Keycode key);
void attack(Enemy& enemy);
void pickUp();
In the preceding code, six functions move from the private part of the class to the public part, under a comment that says what they're for. They're the ones the old handleKey called: newGame, moveOrAttack, drinkPotion, takeStairs, saveGame, and loadGame.
Two new ones join them. The first, quit, ends the game, since a state can't set running_ itself, and the second, isGameOver, says whether the player has died. It's const, since looking doesn't change anything. The rest stay private: newLevel, attack, pickUp, and the others below them are the game's own business, and so is handleKey, which only the game's loop calls.
Why not make the states friends of Game, allowed to see its private members, and let them reach everything? Because a friend can reach everything, and a state needs only a handful of things. A short public list says exactly what a state may do, and it's a promise the compiler keeps: a state that reaches for anything else won't compile.
In Game.cpp, add the two new functions below isLoaded, with a blank line in between:
bool Game::isGameOver() const
{
return gameOver_;
}
void Game::quit()
{
running_ = false;
}
In the preceding code, isGameOver hands back gameOver_, and quit sets running_ to false. The loop in run checks running_ as soon as it has dealt with the key, and ends, exactly as it always has when you press Esc.
PlayingState.h and PlayingState.cpp
The first state is the one you play in. Add a header called PlayingState.h, and below its #pragma once, type this:
#pragma once
#include "GameState.h"
// Playing: moving, fighting, going downstairs, and saving and loading
class PlayingState : public GameState
{
public:
StateChange handleKey(Game& game, SDL_Keycode key) override;
};
In the preceding code, PlayingState is a GameState, and it overrides handleKey, with override on the end, as Chapter 22 insisted, so that any mistake in its parameters is caught at once. It doesn't override draw, since the playing state has nothing to add to the picture: the game draws the dungeon, the HUD, and everything else, whichever state it's in. There are no member variables, since everything the playing state works on belongs to the game.
Add a C++ file called PlayingState.cpp, and type the includes, and the start of handleKey:
#include "PlayingState.h"
#include "Game.h"
// Every key press is one action, or none
StateChange PlayingState::handleKey(Game& game, SDL_Keycode key)
{
StateChange change;
// Once the player has died, only two keys do anything
if (game.isGameOver())
{
if (key == SDLK_R)
game.newGame();
else if (key == SDLK_ESCAPE)
game.quit();
return change;
}
In the preceding code, PlayingState.cpp includes Game.h, since it calls the game's functions, and a forward declaration says nothing about those. The header didn't need it: its handleKey only mentions a Game&, and GameState.h has already said that Game is a class. The word override isn't repeated here, outside the class, any more than static was in GameState.cpp.
The function starts with a StateChange that asks for nothing, and every way out of the function returns it. Once the player has died, only R and Esc do anything, as before, but now they ask the game: game.newGame() starts a new game, and game.quit() ends this one.
Then add the steps below the if, with a blank line in between:
// A key that stands for a step moves the player, or attacks
Point step = directionOf(key);
if (step != Point{ 0, 0 })
{
game.moveOrAttack(step.x, step.y);
return change;
}
In the preceding code, directionOf turns the key into a step. If it's a real step, anything but { 0, 0 }, the game moves the player that way, or attacks whatever is there, and that's the end of the key. The comparison needs the name Point in front of the braces, since C++ won't compare with a bare { 0, 0 }, and it uses the != that the compiler writes from Chapter 32's ==.
Last, the other keys. Add the rest of the function below the steps, with a blank line in between:
switch (key)
{
case SDLK_H:
game.drinkPotion();
break;
case SDLK_PERIOD:
game.takeStairs();
break;
case SDLK_F5:
game.saveGame();
break;
case SDLK_F9:
game.loadGame();
break;
case SDLK_ESCAPE:
game.quit();
break;
}
return change;
}
In the preceding code, H drinks a potion, the period key takes the stairs, F5 and F9 save and load, and Esc quits, each by asking the game, where the old switch called the functions itself. Any other key does nothing. Whatever happened, the function returns change, which still asks for nothing: in this first stage, the playing state never changes the stack.
Notice what isn't here. There's no dx or dy, and no long list of arrow keys, which now live in directionOf, and nothing touches the game's variables, as running_ = false did. The playing state can only ask.
Checkpoint: Click in PlayingState.cpp, and press Ctrl+F7. It compiles on its own, now that the functions it calls are public.
The Stack in the Game
Now the game can keep a stack of states. In Game.h, find this line at the top:
#include <vector> // std::vector, for the monsters and the treasure
And change it to this:
#include <memory> // std::unique_ptr, for the states
#include <vector> // std::vector, for the monsters, treasure, and states
In the preceding code, <memory> brings in std::unique_ptr, for the states, and the vector's comment says what else it will hold. Then add this below #include "FlashEffects.h":
#include "GameState.h"
In the preceding code, GameState.h brings in the base class, and StateChange. Then find the comment above the class:
// The whole game. It owns the glyphs, the map, the player, the monsters,
// the treasure, the HUD, the sounds, and the flashes, 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, the HUD, the sounds, and the flashes, and runs the loop
// that waits for a key, and hands it to the state on top of the stack
In the preceding code, the comment says what the loop does with a key now. Then add this below FlashEffects flashes_;:
std::vector<std::unique_ptr<GameState>> states_; // the top is last
In the preceding code, states_ is the stack: a vector of std::unique_ptr<GameState>, the owned list of a family that Chapter 22 built, so each element owns one state, of any kind. The bottom of the stack is the front of the vector, and the top is its back, where push_back adds a state and pop_back removes one, which is what the comment means by "the top is last".
Why not Chapter 15's std::stack, which that chapter suggested for a game's screens? Because a std::stack only lets you see its top. It has push, pop, and top, and no way to look at anything below, since it has no iterators, as Chapter 13 said. That's all handleKey needs, but draw has to visit every state, from the bottom up. A vector does everything a stack does, with push_back, pop_back, and back, and lets you walk through it too.
Over in Game.cpp, add this below #include <string>:
#include <utility> // std::move
In the preceding code, <utility> brings in std::move, which the new handleKey needs. Then add this below #include "MapGenerator.h":
#include "PlayingState.h"
In the preceding code, the game includes PlayingState.h, since it makes the first state. Then find the constructor:
Game::Game(SDL_Renderer* renderer)
: renderer_(renderer), glyphs_(renderer, FONT_PATH, FONT_SIZE)
{
newGame();
}
And change it to this:
// The game starts with one state on its stack: playing
Game::Game(SDL_Renderer* renderer)
: renderer_(renderer), glyphs_(renderer, FONT_PATH, FONT_SIZE)
{
states_.push_back(std::make_unique<PlayingState>());
newGame();
}
In the preceding code, the constructor puts a playing state on the stack before the game begins, made with std::make_unique, as Chapter 22 made its enemies. The std::unique_ptr<PlayingState> it makes turns into the std::unique_ptr<GameState> that the vector holds, just as a PlayingState* would turn into a GameState*. From then on, the stack is never empty. Nothing in this game ever closes the playing state, so there's always a state on top to take the keys.
Next comes the heart of it. Replace the whole of handleKey, from its comment to its closing brace, with this:
// Hands a key to the state on top of the stack, and then changes the stack
// as it asks. A state is only ever closed here, after it has finished with
// the key, and never while it's still dealing with it
void Game::handleKey(SDL_Keycode key)
{
StateChange change = states_.back()->handleKey(*this, key);
if (change.close)
states_.pop_back();
if (change.open)
states_.push_back(std::move(change.open));
}
In the preceding code, the first line does three things. The call to states_.back() gives the unique_ptr on top of the stack, the arrow reaches the state it owns, and since handleKey is virtual, the call goes to whichever kind of state is on top, a playing state or an inventory state, as Chapter 22's virtual calls did. The game passes itself as *this. The pointer this points at the game, as Chapter 18 showed, and the star makes it the game itself, which is what the state's Game& wants.
Then the change is made, in this order: close first, and then open, so that a change that asks for both replaces the state on top with the new one. Popping the top destroys the state it owns, through its virtual destructor, and it's safe, since that state's handleKey has already returned. Opening needs std::move, from Chapter 10, since a unique_ptr can't be copied: it hands the new state over to the vector, and leaves change.open empty. The if tests the unique_ptr itself, which counts as true when it owns something.
Notice where std::move isn't needed. What std::make_unique hands back is a temporary, which nothing else could ever use, so C++ moves it without being asked, which is how Chapter 28's recipes returned their unique_ptrs. And a local variable, such as a state's change, is moved out automatically when a function returns it. Only something with a name, which will still be there afterward, such as change.open, has to be moved on purpose.
Last, every state gets to draw. In draw, add this below hud_.draw(renderer_, glyphs_, player_, depth_, gameOver_);, with a blank line in between:
// Anything the states add, over the top, starting at the bottom
for (const std::unique_ptr<GameState>& state : states_)
state->draw(renderer_, *this);
In the preceding code, the loop goes through the vector from its front, the bottom of the stack, to its back, the top, so each state draws over the ones below it, and the top one is drawn last. The loop variable is a reference, since a unique_ptr can't be copied, as Chapter 22's loops found, and a const one, since draw is a const function, which can't change the vector. For the same reason, *this is a const Game& here, which is exactly what a state's draw asks for.
The states draw after the HUD, so anything a state draws goes over everything, the HUD included. Right now, the only state is the playing state, whose draw is GameState’s empty one, so nothing on the screen changes at all.
That's the first stage. The keys have moved out of Game into a state of their own, and the game has a stack with that one state on it.
Checkpoint: Press F5, and play. Everything works exactly as it did at the end of Part 5: the arrows, and W, A, S, and D, move and fight, H drinks a potion, the period key takes the stairs, F5 and F9 save and load, and Esc quits. That's the point. The game's insides have changed completely, and nothing you can see has, which is how Chapter 21 checked a refactoring.
A Pack
The Player's Pack
The pack belongs to the player, like their gold. Until now, the player has had a count of potions, and nothing more. The count makes way for a vector of item kinds, one for each thing carried, in the order they were picked up. In Player.h, find this line at the top:
#include "Entity.h"
And change it to this:
#include <vector> // std::vector, for the inventory
#include "Entity.h"
#include "Item.h"
In the preceding code, <vector> is for the pack, and Item.h is for ItemKind, which is what the pack holds. Then add this below POTION_HEAL:
constexpr int INVENTORY_SIZE = 12; // slots, one for each letter a to l
In the preceding code, INVENTORY_SIZE is how many things the pack holds: twelve, one for each letter from a to l. That's plenty for a game of this size, and it fits in the inventory's box on the screen with room to spare. Next, find these lines in the class:
int getPotions() const;
void setPotions(int potions);
bool isAlive() const;
void takeDamage(int amount);
void addGold(int amount);
void addPotion();
bool drinkPotion();
And change them to this:
const std::vector<ItemKind>& getInventory() const;
bool isAlive() const;
void takeDamage(int amount);
void heal(int amount);
void addGold(int amount);
bool addToInventory(ItemKind kind);
void removeFromInventory(int slot);
In the preceding code, getInventory returns the pack as a const reference, so whoever asks can look at every item without a copy being made, and without being able to change anything, like Chapter 28's const getters. The player adds to the pack with addToInventory, which returns false when the pack is full, and takes things out with removeFromInventory, by slot. And heal is the half of drinkPotion that did the healing, on its own, since drinking a potion is about to become the game's business. Last, find the potion count at the bottom of the class:
int potions_ = 0;
And change it to this:
std::vector<ItemKind> inventory_; // everything carried, but gold
In the preceding code, inventory_ starts out empty, as vectors do. Gold doesn't go in it, as the comment says: gold is a number, and it stays one.
In Player.cpp, find the potion count's getter and setter:
int Player::getPotions() const
{
return potions_;
}
void Player::setPotions(int potions)
{
potions_ = potions;
}
And change them to this:
const std::vector<ItemKind>& Player::getInventory() const
{
return inventory_;
}
In the preceding code, getInventory hands back a reference to inventory_ itself, which is safe, since the player lasts as long as the game does. There's no setter. The save file will fill a pack one item at a time, with addToInventory, as you'll see. Next, replace the whole of addPotion with this:
// Heals, though never past full health
void Player::heal(int amount)
{
hp_ = std::min(hp_ + amount, PLAYER_MAX_HP);
}
In the preceding code, heal adds health, and std::min stops it at PLAYER_MAX_HP, exactly as drinking a potion did. It takes the amount as a parameter, rather than using POTION_HEAL itself, since healing is the player's job, but how much a potion heals is the potion's business. Then replace the whole of drinkPotion with this:
// Puts an item in the inventory, if there's room. Returns true if there was
bool Player::addToInventory(ItemKind kind)
{
if (static_cast<int>(inventory_.size()) == INVENTORY_SIZE)
return false;
inventory_.push_back(kind);
return true;
}
// Takes the item in a slot out of the inventory. The items after it move
// up a slot each
void Player::removeFromInventory(int slot)
{
inventory_.erase(inventory_.begin() + slot);
}
In the preceding code, addToInventory puts an item at the end of the pack, unless it already holds INVENTORY_SIZE items, and returns whether it did. The pack's size is a size_t, as Chapter 13 explained, and it's cast to an int to match INVENTORY_SIZE, so that the comparison is between two numbers of the same kind.
The function removeFromInventory erases the item in a slot, with begin() + slot marking it, as Chapter 13 showed. The items after it move up a slot each, so the pack never has a gap in it. Last, find this line in reset:
potions_ = 0;
And change it to this:
inventory_.clear();
In the preceding code, a new game starts with an empty pack, as it started with no potions.
What the HUD Says
The status line has shown the potion count since Chapter 32, and the potions are in the pack now, so the count goes. In HUD.cpp, find these lines in draw:
" Gold " + std::to_string(player.getGold()) +
" Potions " + std::to_string(player.getPotions()) +
And change them to this:
" Gold " + std::to_string(player.getGold()) +
In the preceding code, the status line keeps the health, the gold, and the depth. Then find the first line of the keys:
std::string help = "Arrows or WASD: move and attack H: drink a potion"
And change it to this:
std::string help = "Arrows or WASD: move and attack I: inventory"
In the preceding code, the row of keys offers I, for the inventory, in place of H, which won't drink anything anymore. The rest of the row is unchanged, with F5 and F9 still on it, as Chapter 33 left them.
Saving the Pack
The save file's line for the player ends with the potion count, and there's no potion count anymore. So the format has to change, and when a format changes, its version goes up, as Chapter 33 promised, so that a file written the old way is turned away, rather than read wrongly. In SaveLoad.cpp, find the version:
constexpr int SAVE_VERSION = 1;
And change it to this:
constexpr int SAVE_VERSION = 2;
In the preceding code, the version is 2. A game saved by Part 5 starts with ROGUE_SDL_SAVE 1, so load turns it away at its very first check, and the game says there's no saved game it can read.
Any game you saved with Part 5 won't load in Part 6. That's on purpose: a version 1 file has a potion count where version 2 expects the next line, and reading one as the other would give you a wrong game, or none at all. If there's a game you'd like to finish, play it out before you build this stage. Each later change to the format turns away the files before it in the same way.
Then find the lines in save that write the player:
file << "PLAYER " << at.x << " " << at.y << " " << player.getHp()
<< " " << player.getGold() << " " << player.getPotions() << "\n";
And change them to this:
file << "PLAYER " << at.x << " " << at.y << " " << player.getHp()
<< " " << player.getGold() << "\n";
for (ItemKind kind : player.getInventory())
file << "CARRY " << static_cast<int>(kind) << "\n";
In the preceding code, the PLAYER line ends with the gold now, and a line follows for every item in the pack: the word CARRY, and the item's kind, as a number, just as the ITEM lines write theirs. A player carrying two potions gets two lines, both CARRY 0, and a player with an empty pack gets none. The lines are written in slot order, and read back in the same order, so the pack comes back exactly as it was, letters and all.
In load, find the part that reads the player:
else if (word == "PLAYER")
{
int hp = 0;
int gold = 0;
int potions = 0;
file >> at.x >> at.y >> hp >> gold >> potions;
newPlayer.setPosition(at);
newPlayer.setHp(hp);
newPlayer.setGold(gold);
newPlayer.setPotions(potions);
}
And change it to this:
else if (word == "PLAYER")
{
int hp = 0;
int gold = 0;
file >> at.x >> at.y >> hp >> gold;
newPlayer.setPosition(at);
newPlayer.setHp(hp);
newPlayer.setGold(gold);
}
else if (word == "CARRY")
{
int kind = 0;
file >> kind;
if (kind < 0 || kind >= ITEM_KINDS)
return false;
newPlayer.addToInventory(static_cast<ItemKind>(kind));
}
In the preceding code, the PLAYER line no longer reads a potion count. A CARRY line reads a kind, and checks that it's one of the ITEM_KINDS kinds, as the ITEM lines do, since a number that isn't a kind would make a pack item that isn't anything. If it isn't, load returns false, and the game is left as it was. Otherwise, the item goes into the new player's pack.
A CARRY line has no place to read, so its at stays { 0, 0 }, as a Point starts, and the check below the branches, that every place is inside the map, passes, as it does for the DEPTH line.
Checkpoint: Click in Player.cpp, and press Ctrl+F7. Do the same in HUD.cpp, and in SaveLoad.cpp. All three compile on their own. The game as a whole won't build yet, since Game.cpp still counts potions, and that's next.
Picking Things Up
Everything the player picks up, apart from gold, goes into the pack now, if there's room. In Game.cpp, find these lines in pickUp:
sounds_.pickup.play();
if (item->getKind() == ItemKind::Gold)
And change them to this:
std::string name = statsOf(item->getKind()).name;
if (item->getKind() == ItemKind::Gold)
In the preceding code, name is the item's name, from its row in the stats table, turned into a std::string ready for the messages below. The sound has gone from here, since picking something up can fail now, and a sound should only play when it doesn't. Next, find the else that picks up a potion:
else
{
player_.addPotion();
hud_.addMessage("You pick up a potion. Press H to drink it.");
}
And change it to this:
else if (player_.addToInventory(item->getKind()))
{
hud_.addMessage("You pick up a " + name + ".");
}
else
{
hud_.addMessage("Your inventory is full, so you leave the " + name +
".");
return;
}
sounds_.pickup.play();
In the preceding code, anything that isn't gold goes into the pack, and the message names it: "You pick up a potion." If the pack is full, addToInventory returns false, the message says so, and return leaves the item where it lies, since the erase_if below would otherwise sweep it away. The pickup sound plays only once something has really been picked up.
Notice that the step still happened, and the turn still ends: moveOrAttack calls endTurn after pickUp, whatever pickUp did. Walking onto something you can't carry is still a step, and the monsters still get their turn.
Using Things
The inventory will need two more things from the game, to show the pack: the glyphs, to draw with, and the player, to look at. In Game.h, add these above bool isGameOver() const;:
const GlyphCache& getGlyphs() const;
const Player& getPlayer() const;
In the preceding code, both functions return const references, so the inventory can use the game's glyph cache, and look at the player, without copying either, and without being able to change them. A GlyphCache can't be copied anyway, since Chapter 30 deleted its copies. Then find the declaration of drinkPotion among the public functions:
void drinkPotion();
And change it to this:
void useItem(int slot);
In the preceding code, useItem uses whatever is in a slot of the pack. Over in Game.cpp, add the two getters above isGameOver, with a blank line in between:
const GlyphCache& Game::getGlyphs() const
{
return glyphs_;
}
const Player& Game::getPlayer() const
{
return player_;
}
In the preceding code, each getter hands back a reference to one of the game's own members. Then replace the whole of drinkPotion with this:
// Uses the item in a slot of the inventory, which takes a turn
void Game::useItem(int slot)
{
ItemKind kind = player_.getInventory()[slot];
player_.removeFromInventory(slot);
switch (kind)
{
case ItemKind::Potion:
player_.heal(POTION_HEAL);
sounds_.drink.play();
flashes_.add(player_.getPosition(), Palette::FLASH_HEAL);
hud_.addMessage("You drink the potion, and feel better.");
break;
default:
break;
}
endTurn();
}
In the preceding code, useItem looks up the kind of item in the slot, and takes it out of the pack first, since whatever it does, it's used up. Then a switch on the kind does whatever that kind of item does. A potion heals the player by POTION_HEAL, with Chapter 34's drink sound and green flash, and a message that says so. Every other kind does nothing, for now, through default: gold never goes in the pack, and the scroll, later in the chapter, gets a case of its own. Whatever was used, it takes a turn, so endTurn gives the monsters theirs.
There's no check for an empty slot here. The inventory checks the slot before it calls useItem, so the game is never asked for one that isn't there.
The Shade
The inventory is drawn over the dungeon, which is dimmed behind it, so that the box stands out, and the dungeon is still there to see. The dimming is a see-through black, laid over the whole window, like Chapter 34's flashes over a single cell. In Common.h, add these at the end of the palette, below FLASH_HEAL, with a blank line in between:
// A see-through black, to dim the dungeon behind the inventory
constexpr SDL_Color SHADE = { 0, 0, 0, 170 };
In the preceding code, SHADE is black, with an alpha of 170, which is two thirds of the way to solid. With SDL_BLENDMODE_BLEND, the black counts for 170 parts out of 255, and whatever is underneath for the other 85, so everything behind the shade keeps a third of its brightness. A capture of the game shows exactly that: the window's background, 10, 10, 16, comes out as 3, 3, 5, and the brightest pixels of the HUD's text, 200, 200, 210, as 67, 67, 70.
InventoryState.h
Add a header called InventoryState.h, and below its #pragma once, type this:
#pragma once
#include "GameState.h"
// Looking through the inventory. Each item has a letter, and pressing it
// uses the item. Escape closes the inventory
class InventoryState : public GameState
{
public:
StateChange handleKey(Game& game, SDL_Keycode key) override;
void draw(SDL_Renderer* renderer, const Game& game) const override;
};
In the preceding code, InventoryState overrides both of GameState’s functions: handleKey, for the letters and Esc, and draw, for the box and what's in it. Like the playing state, it has no member variables. It doesn't need to remember what's in the pack, since the player already does, and the inventory asks the game every time.
InventoryState.cpp
Add a C++ file called InventoryState.cpp, and type the includes and the size of the box:
#include "InventoryState.h"
#include <string> // std::string, for each line
#include <vector> // std::vector, for the inventory
#include "Game.h"
// The box the inventory is shown in, in cells
constexpr int BOX_X = 22;
constexpr int BOX_Y = 6;
constexpr int BOX_W = 36;
constexpr int BOX_H = INVENTORY_SIZE + 6;
In the preceding code, the box is measured in cells, like everything else on the map. It starts 22 cells in from the left, and 6 rows down, and it's 36 cells wide, and INVENTORY_SIZE + 6 rows tall, which is 18: a row for every slot, and six more, for the title, the instructions, and the space around them. With 22 cells on each side of a box 36 cells wide, it sits in the middle of the map's 80, across.
Then add handleKey below the constants, with a blank line in between:
// A letter uses the item in the slot it stands for, and closes the
// inventory. Escape closes it without using anything
StateChange InventoryState::handleKey(Game& game, SDL_Keycode key)
{
StateChange change;
if (key == SDLK_ESCAPE)
{
change.close = true;
return change;
}
// SDL's keycodes for the letters are in order, like the letters, so a
// is slot 0, b is slot 1, and so on
if (key < SDLK_A || key > SDLK_Z)
return change;
int slot = static_cast<int>(key - SDLK_A);
if (slot >= static_cast<int>(game.getPlayer().getInventory().size()))
return change;
game.useItem(slot);
change.close = true;
return change;
}
In the preceding code, Esc asks for the inventory to be closed, and does nothing else. A key that isn't a letter does nothing at all, and the inventory stays open.
A letter stands for a slot, and here the keycodes help. SDL's keycode for each letter is the letter's own character code, so SDLK_A is 97, which is 'a', SDLK_B is 98, and so on, up to SDLK_Z, which is 122, all in order, as Figure 35.7 shows. Taking SDLK_A away from a letter's keycode gives its place in the alphabet, which is exactly its slot: 0 for a, 1 for b, and 11 for l. It's the same trick as Chapter 30's c - FIRST_GLYPH, which found a character's texture. An SDL_Keycode is an unsigned 32-bit number, so the difference is cast to an int.

A letter past the end of the pack does nothing, and the inventory stays open, so pressing m, or pressing c with two things in the pack, is harmless. But a letter that stands for an item uses it, through the game, and then the inventory asks to be closed, since using something ends your turn, and you'll want to see what happened.
Why is Esc the key that closes it, and not I again? Because i is a letter, and a slot: the ninth. If the I key closed the inventory, the ninth thing in your pack could never be used.
Now the drawing. Add the start of draw below handleKey, with a blank line in between:
// Dims the dungeon, and lists the inventory in a box over it
void InventoryState::draw(SDL_Renderer* renderer, const Game& game) const
{
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, Palette::SHADE.r, Palette::SHADE.g,
Palette::SHADE.b, Palette::SHADE.a);
SDL_RenderFillRect(renderer, nullptr);
In the preceding code, the blend mode comes first, so that the shade's alpha counts, and then the shade fills the whole window, since a null pointer in place of a rectangle means everything. The game draws the states last, after the HUD, so the HUD is dimmed too.
By the time the inventory draws, FlashEffects::draw has already set the same blend mode, earlier in the frame, and the renderer remembers it. But the inventory shouldn't depend on what another class happened to do first, so it sets the blend mode itself.
Then the box. Add this below the shade, with a blank line in between:
SDL_FRect box = { BOX_X * CELL_PX, BOX_Y * CELL_PX, BOX_W * CELL_PX,
BOX_H * CELL_PX };
SDL_SetRenderDrawColor(renderer, Palette::BACKGROUND.r,
Palette::BACKGROUND.g, Palette::BACKGROUND.b, 255);
SDL_RenderFillRect(renderer, &box);
SDL_SetRenderDrawColor(renderer, Palette::TEXT_DIM.r, Palette::TEXT_DIM.g,
Palette::TEXT_DIM.b, 255);
SDL_RenderRect(renderer, &box);
In the preceding code, box is the box in pixels, which is its cells times CELL_PX: 352 across, 96 down, 576 wide, and 288 tall. It's filled with the window's own background color, which covers the dimmed dungeon behind it, and then outlined in the dim text color, with SDL_RenderRect, which draws a rectangle's edges, and not its inside.
The four numbers go into the SDL_FRect’s floats without a static_cast, unlike Chapter 34's flashes, since each is made from constants, and the compiler can see that 352 fits in a float exactly. A variable's value can't be known until the game runs, so with a variable in the braces, such as left * CELL_PX, where left is made with int left = BOX_X;, Visual Studio warns C4838: conversion from 'int' to 'float' requires a narrowing conversion. That's why Chapter 34's flashes cast theirs.
Then the title. Add this below the box, with a blank line in between:
const GlyphCache& glyphs = game.getGlyphs();
glyphs.drawText(renderer, "Inventory", { BOX_X + 2, BOX_Y + 1 },
Palette::TEXT);
In the preceding code, the inventory borrows the game's glyph cache, through getGlyphs, and writes "Inventory" two cells in from the box's left edge, on its second row, in the bright text color.
Last, the items. Add the rest of draw below the title, with a blank line in between:
// Every item on a line of its own, after its letter
const std::vector<ItemKind>& items = game.getPlayer().getInventory();
if (items.empty())
{
glyphs.drawText(renderer, "Nothing yet.", { BOX_X + 2, BOX_Y + 3 },
Palette::TEXT_DIM);
}
for (size_t i = 0; i < items.size(); ++i)
{
char letter = static_cast<char>('a' + i);
std::string line = std::string(1, letter) + ") " +
statsOf(items[i]).name;
glyphs.drawText(renderer, line,
{ BOX_X + 2, BOX_Y + 3 + static_cast<int>(i) },
Palette::TEXT);
}
glyphs.drawText(renderer, "Press a letter to use an item, or Esc.",
{ BOX_X + 2, BOX_Y + BOX_H - 2 }, Palette::TEXT_DIM);
}
In the preceding code, an empty pack says "Nothing yet." in the dim color, where the first item would be. Otherwise, each item gets a line of its own, from the box's fourth row down: its letter, a parenthesis, two spaces, and its name, from the stats table, so the first line of a pack with a potion in it reads a) potion. The letter is 'a' + i, the other way around from the subtraction in handleKey, cast back to a char. The last line, near the bottom of the box, says what to do. Figure 35.8 shows where everything goes.

The line starts with std::string(1, letter), which makes a string of one character, the letter, and that matters. A char is a number, so letter + ") " wouldn't join anything: it would add 97 or so to the address of the text, as Chapter 10's pointer arithmetic does, and point somewhere past its end. Here, the compiler catches it, though only because of what comes next. With letter + ") " + at the start of the line, the build stops with C2110: '+': cannot add two pointers, as in Chapter 32, since the letter and the parenthesis have already made one pointer, and the name is another. Starting with a std::string means every + after it joins text, as Chapter 32's messages did.
Checkpoint: Click in InventoryState.cpp, and press Ctrl+F7, and then do the same in Game.cpp. Both compile on their own. The game as a whole still won't build, since the playing state's H key asks for drinkPotion, which has gone, and that's the last thing to fix.
Opening It
The inventory is ready, and all that's left is the key that opens it. In PlayingState.cpp, add this below #include "PlayingState.h":
#include <memory> // std::make_unique
In the preceding code, <memory> is for std::make_unique. The file would compile without it, since GameState.h includes it already, but a file should include what it uses itself, as Chapter 23 advised. Then add this below #include "Game.h":
#include "InventoryState.h"
In the preceding code, the playing state includes the inventory state, since it's going to make one. Then find the key that drinks a potion:
case SDLK_H:
game.drinkPotion();
And change it to this:
case SDLK_I:
change.open = std::make_unique<InventoryState>();
In the preceding code, the I key asks for an inventory state to be opened on top of the playing state, by putting a new one in change.open. The playing state doesn't touch the stack. It returns the change, and the game pushes the new state once handleKey has returned. From then on, every key goes to the inventory, until it asks to be closed.
Opening the inventory takes no turn, and neither does closing it with Esc, so you can look in your pack as often as you like. Using something takes a turn, as drinking a potion always did.
Checkpoint: Press F5, and pick up a potion or two. The messages say "You pick up a potion." Press I, and the dungeon dims, and the inventory lists them, each with its letter. Press a, and you drink the first: the inventory closes, and you see the green flash, and your health go up, if you'd lost any. Then open it again with I, and press Esc, and it closes without using anything.
A Scroll of Magic Mapping
A New Kind of Treasure
The pack can hold anything now, so it's time for something that isn't a potion. A scroll of magic mapping shows you the whole level at once: every room, every corridor, and the stairs, as if you'd explored it all. Rogue itself had one, and it's the proper version of Chapter 31's "See it all" experiment, which showed the whole level by skipping the check for explored tiles.
In Item.h, find the end of the list of kinds, and the count below it:
Gold
};
constexpr int ITEM_KINDS = 2; // how many kinds there are
And change them to this:
Gold,
MagicMapping
};
constexpr int ITEM_KINDS = 3; // how many kinds there are
In the preceding code, MagicMapping goes at the end of the enum, and ITEM_KINDS is now 3. New kinds always go at the end, as Chapter 33 warned: the save file stores kinds as numbers, and a new kind anywhere else would change the numbers of all the kinds after it. A potion is still 0, and gold is still 1, so a version 2 save made before the scroll existed still loads.
In Item.cpp, find the rows of the stats table:
{ "potion", '!', Palette::POTION },
{ "gold", '$', Palette::GOLD }
And change them to this:
{ "potion of healing", '!', Palette::POTION },
{ "gold", '$', Palette::GOLD },
{ "scroll of magic mapping", '?', Palette::SCROLL }
In the preceding code, the scroll's row gives its name, its character, a question mark, and its color. And the potion gets its full name, potion of healing, now that there's more than one kind of magic to carry. The static_assert below the table checks the count, as Chapter 33 set it up to do, so a row missing here would stop the build.
The scroll's color goes in the palette. In Common.h, add this below GOLD:
constexpr SDL_Color SCROLL = { 180, 220, 255, 255 };
In the preceding code, SCROLL is a pale blue, which stands out from the potion's purple and the gold's yellow.
Reading It
Magic mapping is a job for the map. In Map.h, add this below void clearVisible();:
void exploreAll();
In the preceding code, exploreAll joins the map's other functions that change its tiles. Then, in Map.cpp, add it below clearVisible, with a blank line in between:
// Marks every tile explored, as if the player had seen the whole level.
// The rock still isn't drawn, since there's no open ground beside it
void Map::exploreAll()
{
for (Tile& tile : tiles_)
tile.explored = true;
}
In the preceding code, exploreAll marks every tile explored, as if the player had seen it. It doesn't mark anything visible, so the level is drawn in its remembered colors, apart from what's in sight, and monsters and treasure still appear only where you can see them: the scroll shows you the map, but not what's on it. The solid rock stays hidden too, since draw skips any wall with no open ground beside it, explored or not.
The scroll needs a sound. Copy scroll.wav from SDL3 Projects/Rogue SDL Part 6/assets, in the book's repository, into your own project's assets folder, beside the other six. Then, in Sound.h, add this below the stairs sound:
Sound scroll{ "assets/scroll.wav" }; // reading a scroll
In the preceding code, scroll loads the new file, along with the others, when the game is made. Now useItem can read a scroll. In Game.cpp, add this above default: in useItem:
case ItemKind::MagicMapping:
map_.exploreAll();
sounds_.scroll.play();
hud_.addMessage("The scroll shows you the whole level.");
break;
In the preceding code, reading a scroll of magic mapping explores the whole level, plays the scroll's sound, and says so. It's used up like a potion, since useItem has already taken it out of the pack, and it takes a turn.
The mapping lasts for the rest of the level. Every tile stays explored, and the save file writes explored tiles as capital letters, so a mapped level is still mapped after F5 and F9. The next level down is new, and unexplored, as always.
Finding One
Last, the scrolls have to be somewhere. In MapGenerator.cpp, find the comment above addTreasure:
// Scatters potions and piles of gold through the rooms
And change it to this:
// Scatters potions, scrolls, and piles of gold through the rooms
In the preceding code, the comment names the scrolls. Then add this below items.push_back(Item(ItemKind::Potion, takeFreeSpot(false)));, with a blank line in between:
// A scroll of magic mapping on about half of the levels
if (SDL_rand(2) == 0)
items.push_back(Item(ItemKind::MagicMapping, takeFreeSpot(false)));
In the preceding code, SDL_rand(2) is 0 or 1, each as likely as the other, so about half of the levels get a scroll, in a room, where potions go. Like a potion, a scroll can even turn up in the room you start in.
That's the whole of Part 6: six new files, a new sound, and every change to the old files typed.
Checkpoint: Press F5, and explore until you find a pale blue ?. There's one on about half of the levels, so if this level has none, take the stairs down, and try the next. Pick it up, press I, and it's in your pack as a scroll of magic mapping. Press its letter, and the whole level appears, in the dim colors of places remembered, as in Figure 35.9. Then go and find the stairs.

The Complete Files
Here are the nineteen files that are new or changed, in full, exactly as they are in the repository's SDL3 Projects/Rogue SDL Part 6. The other seventeen are just as they were at the end of Chapter 34. First, Common.h:
#pragma once
#include <SDL3/SDL.h>
#include <functional> // std::hash
// 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 to hash a Point, so that it can be a key in an unordered_map. Each
// cell gets its own number: the same number as its tile's index in the
// Map's vector
template <>
struct std::hash<Point>
{
size_t operator()(const Point& point) const
{
return point.y * MAP_W + point.x;
}
};
// 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 };
constexpr SDL_Color SCROLL = { 180, 220, 255, 255 };
// Flashes, which start see-through, and fade from there
constexpr SDL_Color FLASH_HIT = { 255, 60, 60, 170 };
constexpr SDL_Color FLASH_HEAL = { 80, 255, 120, 140 };
// A see-through black, to dim the dungeon behind the inventory
constexpr SDL_Color SHADE = { 0, 0, 0, 170 };
}
In the preceding code, the palette adds the scroll's pale blue and the shade behind the inventory.
Next, GameState.h:
#pragma once
#include <SDL3/SDL.h>
#include <memory> // std::unique_ptr, for the state to open
#include "Common.h"
class Game;
class GameState;
// What a state asks to have done to the stack, once it has dealt with a
// key: to be closed, taken off the top, or to have another state opened
// on top, or both, or neither
struct StateChange
{
bool close = false;
std::unique_ptr<GameState> open;
};
// A mode the game can be in, such as playing, or looking through the
// inventory. The game keeps a stack of them. Every key goes to the state
// on top, and every state draws, from the bottom up, over the dungeon
class GameState
{
public:
virtual ~GameState() = default;
virtual StateChange handleKey(Game& game, SDL_Keycode key) = 0;
// Draws whatever the state adds to the picture: nothing, unless a
// state says otherwise
virtual void draw(SDL_Renderer*, const Game&) const {}
protected:
static Point directionOf(SDL_Keycode key);
};
In the preceding code, a StateChange asks for the stack to change, and GameState is the abstract base class of every state.
Then GameState.cpp:
#include "GameState.h"
// The step an arrow key, or W, A, S, or D, stands for, and no step at all,
// { 0, 0 }, for any other key
Point GameState::directionOf(SDL_Keycode key)
{
switch (key)
{
case SDLK_UP:
case SDLK_W:
return { 0, -1 };
case SDLK_DOWN:
case SDLK_S:
return { 0, 1 };
case SDLK_LEFT:
case SDLK_A:
return { -1, 0 };
case SDLK_RIGHT:
case SDLK_D:
return { 1, 0 };
default:
return { 0, 0 };
}
}
In the preceding code, directionOf turns the arrows, and W, A, S, and D, into steps.
Then PlayingState.h:
#pragma once
#include "GameState.h"
// Playing: moving, fighting, going downstairs, and saving and loading
class PlayingState : public GameState
{
public:
StateChange handleKey(Game& game, SDL_Keycode key) override;
};
In the preceding code, the playing state overrides handleKey, and uses GameState’s empty draw.
Then PlayingState.cpp:
#include "PlayingState.h"
#include <memory> // std::make_unique
#include "Game.h"
#include "InventoryState.h"
// Every key press is one action, or none
StateChange PlayingState::handleKey(Game& game, SDL_Keycode key)
{
StateChange change;
// Once the player has died, only two keys do anything
if (game.isGameOver())
{
if (key == SDLK_R)
game.newGame();
else if (key == SDLK_ESCAPE)
game.quit();
return change;
}
// A key that stands for a step moves the player, or attacks
Point step = directionOf(key);
if (step != Point{ 0, 0 })
{
game.moveOrAttack(step.x, step.y);
return change;
}
switch (key)
{
case SDLK_I:
change.open = std::make_unique<InventoryState>();
break;
case SDLK_PERIOD:
game.takeStairs();
break;
case SDLK_F5:
game.saveGame();
break;
case SDLK_F9:
game.loadGame();
break;
case SDLK_ESCAPE:
game.quit();
break;
}
return change;
}
In the preceding code, each key is turned into a request to the game, and I opens the inventory.
Then InventoryState.h:
#pragma once
#include "GameState.h"
// Looking through the inventory. Each item has a letter, and pressing it
// uses the item. Escape closes the inventory
class InventoryState : public GameState
{
public:
StateChange handleKey(Game& game, SDL_Keycode key) override;
void draw(SDL_Renderer* renderer, const Game& game) const override;
};
In the preceding code, the inventory state has its own handleKey and draw, which override both of GameState’s.
Then InventoryState.cpp:
#include "InventoryState.h"
#include <string> // std::string, for each line
#include <vector> // std::vector, for the inventory
#include "Game.h"
// The box the inventory is shown in, in cells
constexpr int BOX_X = 22;
constexpr int BOX_Y = 6;
constexpr int BOX_W = 36;
constexpr int BOX_H = INVENTORY_SIZE + 6;
// A letter uses the item in the slot it stands for, and closes the
// inventory. Escape closes it without using anything
StateChange InventoryState::handleKey(Game& game, SDL_Keycode key)
{
StateChange change;
if (key == SDLK_ESCAPE)
{
change.close = true;
return change;
}
// SDL's keycodes for the letters are in order, like the letters, so a
// is slot 0, b is slot 1, and so on
if (key < SDLK_A || key > SDLK_Z)
return change;
int slot = static_cast<int>(key - SDLK_A);
if (slot >= static_cast<int>(game.getPlayer().getInventory().size()))
return change;
game.useItem(slot);
change.close = true;
return change;
}
// Dims the dungeon, and lists the inventory in a box over it
void InventoryState::draw(SDL_Renderer* renderer, const Game& game) const
{
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, Palette::SHADE.r, Palette::SHADE.g,
Palette::SHADE.b, Palette::SHADE.a);
SDL_RenderFillRect(renderer, nullptr);
SDL_FRect box = { BOX_X * CELL_PX, BOX_Y * CELL_PX, BOX_W * CELL_PX,
BOX_H * CELL_PX };
SDL_SetRenderDrawColor(renderer, Palette::BACKGROUND.r,
Palette::BACKGROUND.g, Palette::BACKGROUND.b, 255);
SDL_RenderFillRect(renderer, &box);
SDL_SetRenderDrawColor(renderer, Palette::TEXT_DIM.r, Palette::TEXT_DIM.g,
Palette::TEXT_DIM.b, 255);
SDL_RenderRect(renderer, &box);
const GlyphCache& glyphs = game.getGlyphs();
glyphs.drawText(renderer, "Inventory", { BOX_X + 2, BOX_Y + 1 },
Palette::TEXT);
// Every item on a line of its own, after its letter
const std::vector<ItemKind>& items = game.getPlayer().getInventory();
if (items.empty())
{
glyphs.drawText(renderer, "Nothing yet.", { BOX_X + 2, BOX_Y + 3 },
Palette::TEXT_DIM);
}
for (size_t i = 0; i < items.size(); ++i)
{
char letter = static_cast<char>('a' + i);
std::string line = std::string(1, letter) + ") " +
statsOf(items[i]).name;
glyphs.drawText(renderer, line,
{ BOX_X + 2, BOX_Y + 3 + static_cast<int>(i) },
Palette::TEXT);
}
glyphs.drawText(renderer, "Press a letter to use an item, or Esc.",
{ BOX_X + 2, BOX_Y + BOX_H - 2 }, Palette::TEXT_DIM);
}
In the preceding code, a letter uses the item in its slot, Esc closes the inventory, and draw dims the window and lists the pack in a box.
Then Item.h:
#pragma once
#include "Entity.h"
// The kinds of treasure
enum class ItemKind
{
Potion,
Gold,
MagicMapping
};
constexpr int ITEM_KINDS = 3; // how many kinds there are
// 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, the scroll of magic mapping is the third kind of item.
Then Item.cpp:
#include "Item.h"
#include <iterator> // std::size
namespace
{
// One for each kind of item, in the same order as the enum
constexpr ItemStats ITEM_STATS[] = {
{ "potion of healing", '!', Palette::POTION },
{ "gold", '$', Palette::GOLD },
{ "scroll of magic mapping", '?', Palette::SCROLL }
};
static_assert(std::size(ITEM_STATS) == ITEM_KINDS,
"There must be one ItemStats for each ItemKind");
}
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 stats table has a row for the scroll, and the potion's full name.
Then Player.h:
#pragma once
#include <vector> // std::vector, for the inventory
#include "Entity.h"
#include "Item.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
constexpr int INVENTORY_SIZE = 12; // slots, one for each letter a to l
// You: the @
class Player : public Entity
{
public:
Player();
bool tryMove(int dx, int dy, const Map& map);
int getHp() const;
void setHp(int hp);
int getAttack() const;
int getGold() const;
void setGold(int gold);
const std::vector<ItemKind>& getInventory() const;
bool isAlive() const;
void takeDamage(int amount);
void heal(int amount);
void addGold(int amount);
bool addToInventory(ItemKind kind);
void removeFromInventory(int slot);
void reset();
private:
int hp_ = PLAYER_MAX_HP;
int gold_ = 0;
std::vector<ItemKind> inventory_; // everything carried, but gold
};
In the preceding code, the player carries a pack of up to INVENTORY_SIZE item kinds, in place of a potion count.
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_;
}
void Player::setHp(int hp)
{
hp_ = hp;
}
int Player::getAttack() const
{
return PLAYER_ATTACK;
}
int Player::getGold() const
{
return gold_;
}
void Player::setGold(int gold)
{
gold_ = gold;
}
const std::vector<ItemKind>& Player::getInventory() const
{
return inventory_;
}
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;
}
// Heals, though never past full health
void Player::heal(int amount)
{
hp_ = std::min(hp_ + amount, PLAYER_MAX_HP);
}
// Puts an item in the inventory, if there's room. Returns true if there was
bool Player::addToInventory(ItemKind kind)
{
if (static_cast<int>(inventory_.size()) == INVENTORY_SIZE)
return false;
inventory_.push_back(kind);
return true;
}
// Takes the item in a slot out of the inventory. The items after it move
// up a slot each
void Player::removeFromInventory(int slot)
{
inventory_.erase(inventory_.begin() + slot);
}
// Everything back to how it was at the start of the game
void Player::reset()
{
hp_ = PLAYER_MAX_HP;
gold_ = 0;
inventory_.clear();
}
In the preceding code, items go into the pack and come out of it by slot, and heal does the healing.
Then Map.h:
#pragma once
#include <SDL3/SDL.h>
#include <vector> // std::vector, for the tiles
#include "Common.h"
class GlyphCache;
// What a cell of the map is made of
enum class Terrain
{
Wall,
Floor,
StairsDown
};
// One cell of the map
struct Tile
{
Terrain terrain = Terrain::Wall;
bool explored = false; // the player has seen it, at least once
bool visible = false; // the player can see it right now
};
// The dungeon: a grid of tiles, MAP_W across and MAP_H down, kept in one
// vector, a row at a time
class Map
{
public:
Map();
Tile& at(Point cell);
const Tile& at(Point cell) const;
bool isInside(Point cell) const;
bool isBlocked(Point cell) const;
bool isNextToOpen(Point cell) const;
void fill(Terrain terrain);
void clearVisible();
void exploreAll();
void draw(SDL_Renderer* renderer, const GlyphCache& glyphs) const;
private:
std::vector<Tile> tiles_;
};
In the preceding code, the map can explore itself, all at once.
Then Map.cpp:
#include "Map.h"
#include "GlyphCache.h"
namespace
{
// How each kind of terrain looks: its character, its color when it's
// in sight, and its color when it's only remembered
struct TerrainLook
{
char glyph;
SDL_Color inSight;
SDL_Color remembered;
};
// One for each kind of terrain, in the same order as the enum
constexpr TerrainLook TERRAIN_LOOKS[] = {
{ '#', Palette::WALL, Palette::WALL_REMEMBERED },
{ '.', Palette::FLOOR, Palette::FLOOR_REMEMBERED },
{ '>', Palette::STAIRS, Palette::STAIRS_REMEMBERED }
};
}
Map::Map()
: tiles_(MAP_W * MAP_H)
{
}
// The tile at a cell. Row y starts y whole rows into the vector
Tile& Map::at(Point cell)
{
return tiles_[cell.y * MAP_W + cell.x];
}
const Tile& Map::at(Point cell) const
{
return tiles_[cell.y * MAP_W + cell.x];
}
bool Map::isInside(Point cell) const
{
return cell.x >= 0 && cell.x < MAP_W && cell.y >= 0 && cell.y < MAP_H;
}
// Walls block the way, and so does everything outside the map
bool Map::isBlocked(Point cell) const
{
return !isInside(cell) || at(cell).terrain == Terrain::Wall;
}
// Whether any of the eight cells around this one is open ground, rather
// than wall
bool Map::isNextToOpen(Point cell) const
{
for (int dy = -1; dy <= 1; ++dy)
{
for (int dx = -1; dx <= 1; ++dx)
{
Point next = { cell.x + dx, cell.y + dy };
if (isInside(next) && at(next).terrain != Terrain::Wall)
return true;
}
}
return false;
}
// Replaces every tile with a new one, made of the given terrain
void Map::fill(Terrain terrain)
{
for (Tile& tile : tiles_)
tile = { terrain };
}
void Map::clearVisible()
{
for (Tile& tile : tiles_)
tile.visible = false;
}
// Marks every tile explored, as if the player had seen the whole level.
// The rock still isn't drawn, since there's no open ground beside it
void Map::exploreAll()
{
for (Tile& tile : tiles_)
tile.explored = true;
}
// Draws every tile the player has explored: brightly if it's in sight, and
// dimly if it's only remembered. Solid rock, with no open ground beside
// it, isn't drawn at all
void Map::draw(SDL_Renderer* renderer, const GlyphCache& glyphs) const
{
for (int y = 0; y < MAP_H; ++y)
{
for (int x = 0; x < MAP_W; ++x)
{
Point cell = { x, y };
const Tile& tile = at(cell);
bool isRock = tile.terrain == Terrain::Wall && !isNextToOpen(cell);
if (!tile.explored || isRock)
continue;
int kind = static_cast<int>(tile.terrain);
const TerrainLook& look = TERRAIN_LOOKS[kind];
glyphs.draw(renderer, look.glyph, cell,
tile.visible ? look.inSight : look.remembered);
}
}
}
In the preceding code, exploreAll marks every tile explored.
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, scrolls, 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)));
// A scroll of magic mapping on about half of the levels
if (SDL_rand(2) == 0)
items.push_back(Item(ItemKind::MagicMapping, 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, about half of the levels get a scroll of magic mapping.
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()) +
" 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 I: inventory"
" .: go down F5: save F9: load 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 status line drops the potion count, and the row of keys offers I for the inventory.
Then Sound.h:
#pragma once
#include <SDL3/SDL.h>
#include <string> // std::string, for the file's path
// One sound effect, loaded from a WAV file when it's made, and ready to
// play at any moment
class Sound
{
public:
Sound(const std::string& path);
~Sound();
Sound(const Sound&) = delete;
Sound& operator=(const Sound&) = delete;
bool isLoaded() const;
void play() const;
private:
Uint8* samples_ = nullptr; // the sound itself
Uint32 length_ = 0; // its size, in bytes
SDL_AudioStream* stream_ = nullptr; // the way to the speakers
};
// Every sound in the game, each loaded from its own file
struct Sounds
{
Sound hit{ "assets/hit.wav" }; // the player hits a monster
Sound kill{ "assets/kill.wav" }; // and kills it
Sound hurt{ "assets/hurt.wav" }; // a monster hits the player
Sound pickup{ "assets/pickup.wav" }; // anything picked up
Sound drink{ "assets/drink.wav" }; // a potion
Sound stairs{ "assets/stairs.wav" }; // going down
Sound scroll{ "assets/scroll.wav" }; // reading a scroll
};
In the preceding code, Sounds has a seventh sound, for reading a scroll.
Then SaveLoad.cpp:
#include "SaveLoad.h"
#include <fstream> // std::ofstream and std::ifstream
#include "Enemy.h"
#include "Item.h"
#include "Map.h"
#include "Player.h"
// The first word of every save file, and the version of the format that
// follows it. The version goes up whenever the format changes, so that an
// old file is turned away, rather than read wrongly
const std::string SAVE_HEADER = "ROGUE_SDL_SAVE";
constexpr int SAVE_VERSION = 2;
namespace
{
// A tile as one letter: W for wall, F for floor, and S for stairs, as
// a capital if the player has explored it
char encode(const Tile& tile)
{
switch (tile.terrain)
{
case Terrain::Floor:
return tile.explored ? 'F' : 'f';
case Terrain::StairsDown:
return tile.explored ? 'S' : 's';
default:
return tile.explored ? 'W' : 'w';
}
}
// The tile a letter stands for. Anything but F or S is wall
Tile decode(char letter)
{
Tile tile;
tile.explored = letter >= 'A' && letter <= 'Z';
if (letter == 'F' || letter == 'f')
tile.terrain = Terrain::Floor;
else if (letter == 'S' || letter == 's')
tile.terrain = Terrain::StairsDown;
return tile;
}
}
namespace SaveLoad
{
// Writes everything the game needs to carry on later: the depth, the
// player, the monsters, the treasure, and the map. Returns false if the
// file couldn't be written
bool save(const std::string& path, const Map& map, const Player& player,
const std::vector<Enemy>& enemies,
const std::vector<Item>& items, int depth)
{
std::ofstream file(path);
file << SAVE_HEADER << " " << SAVE_VERSION << "\n";
file << "DEPTH " << depth << "\n";
Point at = player.getPosition();
file << "PLAYER " << at.x << " " << at.y << " " << player.getHp()
<< " " << player.getGold() << "\n";
for (ItemKind kind : player.getInventory())
file << "CARRY " << static_cast<int>(kind) << "\n";
for (const Enemy& enemy : enemies)
{
at = enemy.getPosition();
file << "MONSTER " << static_cast<int>(enemy.getKind()) << " "
<< at.x << " " << at.y << " " << enemy.getHp() << "\n";
}
for (const Item& item : items)
{
at = item.getPosition();
file << "ITEM " << static_cast<int>(item.getKind()) << " "
<< at.x << " " << at.y << " " << item.getAmount() << "\n";
}
// The map last, a row of letters to a line
file << "MAP\n";
for (int y = 0; y < MAP_H; ++y)
{
for (int x = 0; x < MAP_W; ++x)
file << encode(map.at({ x, y }));
file << "\n";
}
// Closing the file finishes the writing, so any problem shows now
file.close();
return !file.fail();
}
// Reads a saved game back into the game's variables. If anything in
// the file is missing or wrong, it returns false, and leaves the game
// exactly as it was
bool load(const std::string& path, Map& map, Player& player,
std::vector<Enemy>& enemies, std::vector<Item>& items,
int& depth)
{
std::ifstream file(path);
std::string word;
int version = 0;
file >> word >> version;
if (word != SAVE_HEADER || version != SAVE_VERSION)
return false;
// Read everything into new variables first
int newDepth = 1;
Player newPlayer;
std::vector<Enemy> newEnemies;
std::vector<Item> newItems;
Map newMap;
// A line at a time, each starting with a word that says what it is,
// until the map
while (file >> word && word != "MAP")
{
Point at;
if (word == "DEPTH")
{
file >> newDepth;
}
else if (word == "PLAYER")
{
int hp = 0;
int gold = 0;
file >> at.x >> at.y >> hp >> gold;
newPlayer.setPosition(at);
newPlayer.setHp(hp);
newPlayer.setGold(gold);
}
else if (word == "CARRY")
{
int kind = 0;
file >> kind;
if (kind < 0 || kind >= ITEM_KINDS)
return false;
newPlayer.addToInventory(static_cast<ItemKind>(kind));
}
else if (word == "MONSTER")
{
int kind = 0;
int hp = 0;
file >> kind >> at.x >> at.y >> hp;
if (kind < 0 || kind >= MONSTER_KINDS)
return false;
Enemy enemy(static_cast<MonsterKind>(kind), at);
enemy.setHp(hp);
newEnemies.push_back(enemy);
}
else if (word == "ITEM")
{
int kind = 0;
int amount = 0;
file >> kind >> at.x >> at.y >> amount;
if (kind < 0 || kind >= ITEM_KINDS)
return false;
newItems.push_back(Item(static_cast<ItemKind>(kind), at,
amount));
}
else
{
return false; // a word that has no business here
}
// A place outside the map would crash the game when it's drawn
if (!newMap.isInside(at))
return false;
}
// The map, which must be a full MAP_W letters by MAP_H lines
for (int y = 0; y < MAP_H; ++y)
{
std::string row;
file >> row;
if (static_cast<int>(row.size()) != MAP_W)
return false;
for (int x = 0; x < MAP_W; ++x)
newMap.at({ x, y }) = decode(row[x]);
}
// The whole file was good, so now the game can have it
map = newMap;
player = newPlayer;
enemies = newEnemies;
items = newItems;
depth = newDepth;
return true;
}
}
In the preceding code, version 2 of the save file writes a CARRY line for everything in the pack, and reads them back.
Then Game.h:
#pragma once
#include <SDL3/SDL.h>
#include <memory> // std::unique_ptr, for the states
#include <vector> // std::vector, for the monsters, treasure, and states
#include "Enemy.h"
#include "FlashEffects.h"
#include "GameState.h"
#include "GlyphCache.h"
#include "HUD.h"
#include "Item.h"
#include "Map.h"
#include "Player.h"
#include "Sound.h"
// The whole game. It owns the glyphs, the map, the player, the monsters,
// the treasure, the HUD, the sounds, and the flashes, and runs the loop
// that waits for a key, and hands it to the state on top of the stack
class Game
{
public:
Game(SDL_Renderer* renderer);
bool isLoaded() const;
void run();
// What the states can look at
const GlyphCache& getGlyphs() const;
const Player& getPlayer() const;
bool isGameOver() const;
// What the states can ask the game to do
void newGame();
void quit();
void moveOrAttack(int dx, int dy);
void useItem(int slot);
void takeStairs();
void saveGame();
void loadGame();
private:
void newLevel();
void handleKey(SDL_Keycode key);
void attack(Enemy& enemy);
void pickUp();
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_;
Sounds sounds_;
FlashEffects flashes_;
std::vector<std::unique_ptr<GameState>> states_; // the top is last
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 keeps a stack of states, and its public part is what the states can look at, and what they can ask it to do.
And last, Game.cpp:
#include "Game.h"
#include <cstdlib> // std::abs
#include <string> // std::string and std::to_string
#include <utility> // std::move
#include "AStar.h"
#include "FOV.h"
#include "MapGenerator.h"
#include "PlayingState.h"
#include "SaveLoad.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;
// A frame, in milliseconds, at 60 frames a second
constexpr Sint32 FRAME_MS = 16;
// The save file. It goes in the working directory, which is the project
// folder when the game runs from Visual Studio
const std::string SAVE_PATH = "rogue_save.txt";
// The game starts with one state on its stack: playing
Game::Game(SDL_Renderer* renderer)
: renderer_(renderer), glyphs_(renderer, FONT_PATH, FONT_SIZE)
{
states_.push_back(std::make_unique<PlayingState>());
newGame();
}
bool Game::isLoaded() const
{
return glyphs_.isLoaded();
}
const GlyphCache& Game::getGlyphs() const
{
return glyphs_;
}
const Player& Game::getPlayer() const
{
return player_;
}
bool Game::isGameOver() const
{
return gameOver_;
}
void Game::quit()
{
running_ = false;
}
// Sleeps until something happens, deals with it, and draws the window again
// if anything changed. While anything is flashing, it wakes up every frame
// as well, to draw the flash fading
void Game::run()
{
while (running_)
{
if (dirty_)
{
draw();
dirty_ = false;
}
SDL_Event event;
if (flashes_.isEmpty())
{
if (!SDL_WaitEvent(&event))
break;
}
else
{
// Wait for an event, but for no longer than one frame
bool gotEvent = SDL_WaitEventTimeout(&event, FRAME_MS);
flashes_.removeFinished();
dirty_ = true;
if (!gotEvent)
continue;
}
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);
}
// Hands a key to the state on top of the stack, and then changes the stack
// as it asks. A state is only ever closed here, after it has finished with
// the key, and never while it's still dealing with it
void Game::handleKey(SDL_Keycode key)
{
StateChange change = states_.back()->handleKey(*this, key);
if (change.close)
states_.pop_back();
if (change.open)
states_.push_back(std::move(change.open));
}
// 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);
flashes_.add(enemy.getPosition(), Palette::FLASH_HIT);
std::string name = enemy.getStats().name;
if (enemy.isAlive())
{
sounds_.hit.play();
hud_.addMessage("You hit the " + name + " for " +
std::to_string(damage) + ".");
}
else
{
sounds_.kill.play();
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;
std::string name = statsOf(item->getKind()).name;
if (item->getKind() == ItemKind::Gold)
{
player_.addGold(item->getAmount());
hud_.addMessage("You pick up " + std::to_string(item->getAmount()) +
" gold.");
}
else if (player_.addToInventory(item->getKind()))
{
hud_.addMessage("You pick up a " + name + ".");
}
else
{
hud_.addMessage("Your inventory is full, so you leave the " + name +
".");
return;
}
sounds_.pickup.play();
// 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;
});
}
// Uses the item in a slot of the inventory, which takes a turn
void Game::useItem(int slot)
{
ItemKind kind = player_.getInventory()[slot];
player_.removeFromInventory(slot);
switch (kind)
{
case ItemKind::Potion:
player_.heal(POTION_HEAL);
sounds_.drink.play();
flashes_.add(player_.getPosition(), Palette::FLASH_HEAL);
hud_.addMessage("You drink the potion, and feel better.");
break;
case ItemKind::MagicMapping:
map_.exploreAll();
sounds_.scroll.play();
hud_.addMessage("The scroll shows you the whole level.");
break;
default:
break;
}
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();
sounds_.stairs.play();
hud_.addMessage("You go down the stairs to depth " +
std::to_string(depth_) + ".");
}
// Saves the game, and says whether it worked. Saving doesn't take a turn
void Game::saveGame()
{
if (SaveLoad::save(SAVE_PATH, map_, player_, enemies_, items_, depth_))
hud_.addMessage("Game saved.");
else
hud_.addMessage("The game couldn't be saved.");
}
// Loads the saved game in place of this one, if there's one to load
void Game::loadGame()
{
if (!SaveLoad::load(SAVE_PATH, map_, player_, enemies_, items_, depth_))
{
hud_.addMessage("There's no saved game, or it couldn't be read.");
return;
}
FOV::compute(map_, player_.getPosition(), SIGHT_RADIUS);
hud_.addMessage("Game loaded.");
}
// 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_)
{
monsterTurn(enemy);
if (!player_.isAlive())
{
gameOver_ = true;
hud_.addMessage("You die.");
return;
}
}
}
// A monster that sees the player hunts them. It attacks if it's next to
// them, and otherwise steps along the shortest path to where it saw them
// last, so that it can follow them around corners
void Game::monsterTurn(Enemy& enemy)
{
if (notices(enemy))
enemy.hunt(player_.getPosition());
if (!enemy.isHunting())
return;
Point from = enemy.getPosition();
Point to = player_.getPosition();
if (std::abs(to.x - from.x) + std::abs(to.y - from.y) == 1)
{
int damage = enemy.getStats().attack;
player_.takeDamage(damage);
sounds_.hurt.play();
flashes_.add(to, Palette::FLASH_HIT);
hud_.addMessage("The " + std::string(enemy.getStats().name) +
" hits you for " + std::to_string(damage) + ".");
return;
}
// An empty path means it's where it saw the player last, and they're
// gone, or that there's no way there. Either way, it loses the trail
std::vector<Point> path = AStar::findPath(map_, from, enemy.getLastSeen());
if (path.empty())
{
enemy.giveUp();
return;
}
// It waits, if another monster is in the way
if (!enemyAt(path[0]))
enemy.setPosition(path[0]);
}
// 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_);
flashes_.draw(renderer_);
hud_.draw(renderer_, glyphs_, player_, depth_, gameOver_);
// Anything the states add, over the top, starting at the bottom
for (const std::unique_ptr<GameState>& state : states_)
state->draw(renderer_, *this);
SDL_RenderPresent(renderer_);
}
In the preceding code, every key goes to the state on top of the stack, every state draws after the HUD, anything but gold goes into the pack, and useItem drinks a potion or reads a scroll.
Playing the Game
Press F5, and play a few levels. Figure 35.10 shows a moment from one of them, four levels down.

The pack gives you choices that the potion count never did. You can see exactly what you're carrying before you decide anything, and since looking is free, it costs nothing to check, even in the middle of a fight, as in Figure 35.10: the orc waits, as every monster does, until you take a turn. A potion is worth more at 6 health than at 18, so it's usually worth waiting.
A scroll of magic mapping is worth more on some levels than others. Read it on a level that's small, or where the stairs turn up at once, and it's wasted. Keep it for a big, tangled level, and it shows you the stairs, and the quickest way to them, and every dead end you won't have to walk into. Since a scroll turns up on only about half of the levels, it's often worth carrying one down a level or two, until it's needed.
Fill the pack, and the next potion stays on the floor, with a message that says so. Twelve slots are plenty for a game this size, but a game with more kinds of treasure would make you choose what to carry, which is exactly what Rogue did.
Understanding the Code
Follow a scroll from the floor to the map. You step onto the ?, and pickUp puts a MagicMapping into the pack. Then you press I. The loop hands the key to Game::handleKey, which hands it to the state on top of the stack, the playing state, whose switch puts a new inventory state in the change it returns.
The game pushes it, and draws: the dungeon, the HUD, and then each state, from the bottom up. The playing state adds nothing, and the inventory dims everything and draws its box.
Now you press the scroll's letter. This time, the state on top is the inventory, so the key goes there, and the same line of Game::handleKey calls a different function, since handleKey is virtual. The inventory works out the slot, and asks the game to use it. The game takes the scroll out of the pack, explores the whole map, plays the sound, adds the message, and gives the monsters their turn.
Then the inventory returns a change that asks to be closed, and once it has returned, the game pops it off the stack, which destroys it. The next frame draws the whole level, with nothing over it.
Notice how little Game::handleKey knows. It's five lines long, it never asks which state is on top, and it won't change when Chapter 36 adds a third state. Each state knows its own keys, and nothing about the others: the playing state doesn't know what the inventory does with a letter, and the inventory doesn't know that A is also a step. That's the knot from the start of the chapter, untied.
Notice who owns what, too. The game owns everything, including the stack, and the stack owns the states. The states own nothing at all, and have no member variables: each is a set of rules for what the keys mean, and what to draw. Whatever they need, they get from the game, while one of their functions runs, and only through its public functions. That's why a state is so cheap to make, and so easy to throw away when it's done.
The stack is the part that makes the inventory feel right. Opening it doesn't stop the game, or save anything, or start anything: it puts a state on top, and the game underneath stays exactly as it was, because nothing touches it. Closing it takes that state off, and there's the game again. That's the pushdown automaton from Nystrom's chapter: it remembers where you came from, so going back is free.
When should you use a family of state classes, rather than Chapter 11's enum and switch? Classes pay off when each state has a lot of its own, such as its own keys, its own drawing, and its own data, and when more states are coming. A mole's five states share everything, and differ by a few lines, so an enum is simpler and better. The game's modes share almost nothing, and there'll be another one in Chapter 36, so classes win. Neither is always right, and the question to ask is how much would be tangled together if the states shared one function.
Experimenting
A few of these change the pack or the scrolls, so put everything back afterward, since the next chapter carries on from this one.
- A small pack. Change
INVENTORY_SIZEto 3, and the pack fills up fast: the fourth potion or scroll you walk over stays on the floor, and the message says why. The box shrinks to match, since its height is worked out from the size. - A darker shade. Change the alpha in
SHADEfrom 170 to 255, and the dungeon disappears completely behind the inventory. Try 60, and it's barely dimmed at all. - Scrolls everywhere. In
addTreasure, changeSDL_rand(2)toSDL_rand(1), which can only be 0, so every level has a scroll. - Start with a map. In
Player::reset, addinventory_.push_back(ItemKind::MagicMapping);belowinventory_.clear();, and every game starts with a scroll in your pack. - Seeing everything. In
Map::exploreAll, put braces around the loop's body, and addtile.visible = true;inside them, belowtile.explored = true;. Read a scroll, and every monster and every item on the level appears, until your next step works out what you can see again, and they vanish. - Closing with I. In
InventoryState::handleKey, changeif (key == SDLK_ESCAPE)toif (key == SDLK_ESCAPE || key == SDLK_I). The I key closes the inventory now, which feels natural, until you're carrying nine things, and find that the ninth can never be used.
Common Errors and Fixes
C2248: 'Game::moveOrAttack': cannot access private member declared in class 'Game', in PlayingState.cpp. A function the playing state calls is still in the private part of Game. Move its declaration up into the public part, under the comment that says what the states can ask the game to do.
C2039: 'getPotions': is not a member of 'Player', in HUD.cpp, or in SaveLoad.cpp. The player's potion count has gone, but a file that used it hasn't caught up. Change it as this chapter shows: the HUD's status line loses the count, and the save file writes CARRY lines instead.
C3668: 'InventoryState::draw': method with override specifier 'override' did not override any base class methods. The inventory's draw has lost its const, so it isn't the same function as GameState’s, which is const, and it doesn't override anything. Put const back, before override in the header, and at the end of the first line in InventoryState.cpp. The same message for handleKey means its parameters don't match GameState’s, or it has a const that GameState’s hasn't.
The inventory never appears. Press I, and nothing seems to happen, but the arrows stop moving the @, and A uses whatever is first in your pack. The keys are going to an inventory that isn't being drawn, and Esc gets you back to the game. Either the loop at the end of Game::draw is missing, so no state draws at all, or the inventory's draw has lost both its const and its override. Then it doesn't override anything, and nothing says so, and the game calls GameState’s draw, which draws nothing. Test builds with each mistake behaved just like this. The second is why override is worth writing every time, as Chapter 22 said: with it, the same mistake is the C3668 above.
C2672: 'std::construct_at': no matching overloaded function found, in a file called xmemory, which isn't one of yours, followed by notes that say "attempting to reference a deleted function", and "see the first reference to ... push_back in 'Game::handleKey'". The new state is pushed without std::move, as in states_.push_back(change.open);, which asks the vector to copy a unique_ptr. The error is reported deep in the standard library, where the copy would have happened, and the notes lead back to your line. Write states_.push_back(std::move(change.open));.
C2280: 'std::unique_ptr<GameState,std::default_delete<GameState>>::unique_ptr(const std::unique_ptr<GameState,std::default_delete<GameState>> &)': attempting to reference a deleted function, in Game::draw. The loop over the states copies each unique_ptr, as in for (std::unique_ptr<GameState> state : states_). Make its variable a const reference, as Chapter 22's loops did: const std::unique_ptr<GameState>& state.
C2065: 'InventoryState': undeclared identifier, and then C2672: 'std::make_unique': no matching overloaded function found, in PlayingState.cpp. The playing state makes an inventory state, but nothing tells it what one is. Add #include "InventoryState.h" to its includes.
C2061: syntax error: identifier 'Game', in GameState.h, followed by a long list of others, among them C3668 for PlayingState::handleKey, and C2065: 'GameState': undeclared identifier, in Game.h. The header includes Game.h in place of the forward declaration class Game;, so the two headers include each other, as Chapter 28's circular includes did, and whichever is read first can't see the other's class. Put class Game; back, and take the include out.
C2059: syntax error: '{', in PlayingState.cpp, on the line that checks for a step. The comparison has bare braces, as in step != { 0, 0 }, which C++ won't compare with. Put the type's name in front: step != Point{ 0, 0 }.
C2110: '+': cannot add two pointers, in InventoryState.cpp. An item's line starts with the letter, as in letter + ") " + ..., so there's no string to join anything to. Start it with a string of one character: std::string(1, letter) + ") " + ....
Warning C4715: 'PlayingState::handleKey': not all control paths return a value, and the game crashes when you press I. The return change; at the end of handleKey is missing, so a key that reaches the end of the function hands back whatever happens to be in memory, rather than a StateChange. In a test run, F9 seemed to work, and pressing I crashed the game with an access violation. Put return change; back, below the switch, and treat this warning as an error whenever you see it.
F9 says "There's no saved game, or it couldn't be read.", though you saved a game before this chapter. That's the version check doing its job: the save is version 1, and the game reads only version 2 now. Start a new game, and save that.
Reading a scroll works, but it's silent, and the console says "Couldn't load assets/scroll.wav: Couldn't open assets/scroll.wav: The system cannot find the file specified." The scroll's sound isn't in your project's assets folder. Copy scroll.wav there from the book's Rogue SDL Part 6 project.
AI Exercise (Optional)
Esc quits the game at once while you're playing, which is harsh: press it one time too many while you're closing the inventory, and your game is gone. If you'd like to fix that with an AI's help, here's a challenge that puts the stack of states to work: a box that asks whether you really want to quit. As always, it's optional.
Open your AI chatbot of choice and try a prompt like this:
"I'm writing a turn-based roguelike in C++ with SDL 3. The game keeps a stack of states, std::vector<std::unique_ptr<GameState>> states_, whose top is the back. GameState has virtual StateChange handleKey(Game& game, SDL_Keycode key) = 0; and virtual void draw(SDL_Renderer*, const Game&) const {}. A StateChange is a struct with bool close = false; and std::unique_ptr<GameState> open;, and Game::handleKey hands every key to the state on top, then pops it if close is true, and pushes open if it's set, so a state never changes the stack itself. PlayingState quits at once when Esc is pressed, by calling game.quit(). I'd like Esc to open a new state, QuitState, instead: a small box over the dimmed game that asks 'Really quit? Y or N'. Y quits, and N or Esc closes the box and goes back to the game. InventoryState already draws a box like that, with a see-through black, Palette::SHADE, over the whole window, SDL_RenderFillRect and SDL_RenderRect for the box, and game.getGlyphs().drawText for the text, so copy its style. Show me the new class and every change to PlayingState, with each curly brace on its own line, and explain how the box gets closed."
Notice what the preceding prompt does. It describes the stack, and the rule that a state never changes it, so that the AI returns a StateChange rather than reaching into the game to pop something. Naming the existing inventory state as the style to copy makes the new box look as if it belongs, and spelling out every key the box answers to, including Esc, covers the one a quick answer might forget.
When the answer comes back, check it against this chapter. Does QuitState override handleKey, with override, and draw, if it draws? When N is pressed, is the box closed by returning a change with close set, rather than by calling anything? Does Y call game.quit(), and does PlayingState open the new state with std::make_unique, in change.open? And does anything in it take a turn, which asking a question never should?
To try it, press Esc, and then N, and play on, and then press Esc and Y. If the box appears, but the arrows still move the @ behind it, or N quits anyway, ask the AI why.
Summary
The game has modes now, and a clean way to keep them apart. Each mode is a state, a class built on GameState, with a virtual handleKey that says what the keys mean, and a virtual draw that adds to the picture. The game keeps its states on a stack, in a vector of unique_ptrs, hands every key to the state on top, and draws every state from the bottom up. A state never changes the stack itself: it returns a StateChange, and the game makes the change once the state's function has returned, closing first and opening second.
And the player has a pack. Everything but gold goes into it, up to twelve things, one for each letter from a to l, and the inventory lists them over the dimmed dungeon, where a letter uses one, and Esc closes it. The save file carries the pack, in version 2. And there's a new kind of treasure to carry: a scroll of magic mapping, which marks the whole level explored, and shows you the way to the stairs.
Next, in Chapter 36, the last part of Rogue SDL, the game gets weapons to wield, from a dagger to a warhammer, and a second kind of scroll, a fireball, which you aim before you read it. The aiming is a third state, stacked where the inventory was, with the first member variables any state has had: the scroll it's aiming, and where. The save file moves to version 3, and the game is finished, looking just like Figure 30.1.
