Chapter 6 left you a real game, one you can fight in and die in. It also left a main.cpp nearly 800 lines long. This chapter doesn't add a single feature. At its end, the game looks and plays exactly as it does now, down to the last pixel. What changes is the shape of the code, and that matters, since twenty more chapters will be built on it.
The trouble is easy to see. Everything a run needs lives in runGame: the dungeon, the monsters, the player, the log and the turn count are among its thirteen local variables, in a function 147 lines long. Every function that works on the run has to be handed the parts it needs. Sixteen functions take four or more parameters, and drawFrame takes nine. Each new feature would add parameters to chains of calls, and more places to forget one.
The fix is a class. An object of the class holds one run, with its dungeon, its monsters and the rest as its members. The functions that work on the run become the class's member functions, and reach those members directly. The next image shows the end of the chapter: the same code, reshaped.

On the left is main.cpp as it is now. The right shows the three files it becomes. And runGame, the biggest piece, splits three ways: setting up a run, playing it, and handling the keys.
Chapter 07 in the book's repository holds this chapter's finished project. The chapter carries on in your own project from Chapter 6, or from the book's Chapter 06 folder.In this chapter, we will:
- Design a class,
Game, that holds one run of the game - Give it a small public face, and keep everything else private
- Move the game's functions into it, where they lose their long parameter lists
- Start each run with a new
Game, so that nothing ever needs resetting - Give the game a state, so that each key does what the moment calls for
There are five checkpoints along the way, and nothing on the screen will change at any of them. That's the point: a change to the code's shape that changes what the game does has gone wrong.
Planning the Chapter
This chapter adds two files, and changes one:
| File | Status | What it's for |
|---|---|---|
src/Game.h |
New | The Game class: one run of the game |
src/Game.cpp |
New | The run: setting it up, its keys, its turn, and its drawing |
src/main.cpp |
Changed | SDL, the window and the font, and a new Game for each run |
| The other files | Unchanged |
The next image shows how the files fit together at the end of the chapter. The class sits between main.cpp and everything else.

What goes in the class is everything about one run: its seed and its dice, the map, the monsters, the player, the log, the turn count, and the flags that the loop keeps. Everything that isn't about one run stays out. Setting up SDL, the window and the font stays in main.cpp, which makes a new Game for each run. And the small helpers that need nothing from the run, such as the key table and the damage rule, stay as plain functions in Game.cpp.
We'll write the class beside the old code, which goes on running the game the whole time. Each part compiles as it's written, and the whole class builds before anything uses it. Only then does main.cpp switch over, and the old code goes. That's how to make a big change safely: never be far from a program that builds and runs.
Most of the class's functions are ones you've already written, with a new first line and their long parameter lists gone. You can copy each one from main.cpp and change it to match, rather than type it again. Some move across without any change at all, and for those, the chapter just says what to copy.
We'll build it in six steps: the class's header; its helpers and how a run starts; its keys and its turn; its drawing; the switch in main.cpp; and last, a state for the game.
The Game Class
A class's header says what an object of it holds, and what it can do. The functions themselves come afterward, in Game.cpp.
One Run as an Object
In Solution Explorer, right-click src, choose Add > New Item, and add a Header File (.h) called Game.h. It starts with the includes for everything a run holds:
#pragma once
#include <SDL3/SDL.h>
#include <string> // std::string, for text on the screen
#include <vector> // std::vector, for the monsters and the rooms
#include "Common.h"
#include "GlyphCache.h"
#include "Map.h"
#include "MessageLog.h"
#include "Monster.h"
#include "Player.h"
#include "Rng.h"
The class's members will be a Map, a MessageLog, a Player, and so on, so the header includes theirs. A member must be a complete type, not just a name. The compiler needs to know how big a Game is, and it can only know that if it knows how big each of its members is.
Then the class itself, with its private part empty for now:
// One run of the game, from a new dungeon to the player's death: the
// dungeon and everyone in it, the rules that play out each turn, and the
// screen that shows it all
class Game
{
public:
// A seed of 0 means a random one
Game(SDL_Renderer* renderer, GlyphCache& glyphs, unsigned int seed);
// Plays the run. Returns true if the player asked to play again
bool run();
private:
};
A Game is one run of the game. From outside, there are only two things to do with one. Making it sets up a new dungeon, with a seed of 0 meaning a random one. Calling run plays it, and says, when it returns, whether the player asked to play again.
Everything else will be private, the class's own business, which no code outside it can see or call. With class, members are private unless they're listed under public:, which is the opposite of struct.
Directly below private:, add the functions that set up a run, handle the keys, and play the turn:
// Setting up
void placeMonsters(const std::vector<SDL_Rect>& rooms);
// Input
void handleEvent(const SDL_Event& event);
void handleKey(const SDL_KeyboardEvent& key);
void playerStep(Point step);
// The turn
void playerAttacks(Monster& monster);
void removeDeadMonsters();
void finishTurn();
void monstersAct();
void monsterAttacks(const Monster& monster);
Point chaseStep(const Monster& monster);
Point wanderStep(const Monster& monster);
bool seesPlayer(const Monster& monster) const;
bool isFree(Point cell);
Monster* monsterAt(Point pos);
These are Chapter 6's functions, with one thing in common: their long parameter lists are gone. Each takes only what's different from one call to the next, such as the monster that's attacking. Everything else will be the class's own. The function removeDead is now removeDeadMonsters, since there's no monsters parameter any more to say what it removes.
Below them, add the functions that draw a frame, one part each:
// Drawing
void draw() const;
void setColor(SDL_Color color) const;
void drawCell(int col, int row, char c, SDL_Color color,
SDL_Color background = Palette::BACKGROUND) const;
void drawMap() const;
void drawMonsters() const;
void drawPanels() const;
void drawHealthBar(float x, float y, float width,
const Entity& entity) const;
void drawSidePanel() const;
void drawLog() const;
void drawCentered(const std::string& text, float centerX, float y,
SDL_Color color) const;
void drawDeathScreen() const;
Every drawing function ends in const. That's a promise that it doesn't change the Game, since drawing only looks. And the compiler holds each one to it.
The function drawFrame is now just draw. One drawing function keeps a default argument, the background in drawCell. It goes here, in the declaration, and not again where the function is written.
Last, the data members, below the drawing functions, with a blank line in between:
SDL_Renderer* renderer_;
GlyphCache& glyphs_;
// The run: its seed and dice, the dungeon, who's in it, and what's
// been said
unsigned int seed_;
Rng rng_;
Map map_;
std::vector<Monster> monsters_;
Player player_;
MessageLog log_;
int turn_ = 0; // a turn passes each time the player acts
bool revealMap_ = false; // the debugging aid on F1
bool running_ = true;
bool playAgain_ = false;
bool redraw_ = true;
The members are runGame's local variables, nearly all of them, and two of its parameters. Each name ends in an underscore. That's a common convention, which tells a member from a local variable or a parameter at a glance: turn_ is the run's turn count, wherever you see it.
The first two members are borrowed. The renderer belongs to main, which made it, so a Game keeps a pointer to it, and never destroys it. The glyph cache belongs to main too, and outlives every Game. So glyphs_ is a reference: another name for main's cache, rather than a copy. A reference member must be given its object when the Game is made, and can never be changed to refer to another.
The rest are the run's own: its seed and dice, the dungeon and who's in it, the log, and the turn count. Then the flags that the loop keeps, such as running_. Members with a value after them, such as turn_ = 0, start with that value. The constructor will give the first four theirs. The rest start as their own constructors make them: the map as solid rock, the monsters and the log empty, and the player as Player's defaults say.
So drawFrame's nine parameters are gone: draw reaches the same nine things as members. The next image shows the difference.

Every member function reaches every member, so the parameter lists shrink to what's different from call to call. And anything a later chapter adds to a run, it adds once, as a member, rather than to every function that passes the run along.
That reach has a price, though. A parameter list said what a function worked with: drawFrame's nine parameters listed everything it touched. But a member function's first line says none of that, since any member function can read and change any member, and by the end of the book the class has some 180 of them. The one signal left is const. So every function that only looks is marked const, and the compiler holds it to that.
Design note: Rogue's globals. Rogue, written in C in 1980, kept the whole state of a game in global variables: the player,
player; the monsters on the level,mlist; the objects lying on it,lvl_obj; and the level you were on,level. All were declared in its header for every file to use. That made every call short, just as members do. But it also meant that any function, anywhere in the program, could change any of it. A class gives you the short calls without that free-for-all: the run's state can only be reached by the run's own functions, and the compiler sees to it.
The class is declared. Now for its functions.
Starting a Run
The file Game.cpp starts with the helpers that don't belong to a run, then the functions that set a run up and play it.
Helpers That Stay Free
Add a C++ File (.cpp) called Game.cpp, and type its includes, with an unnamed namespace that's empty for now:
#include "Game.h"
#include <iterator> // std::size, to count the kinds of monster
#include <random> // std::random_device, for a random seed
#include "Fov.h"
#include "RoomGen.h"
namespace
{
}
Its first include is its own header, which brings in everything the class needs. The rest are what only Game.cpp needs: <iterator> for std::size, <random> for a random seed, and the field of view and the dungeon generator, which the class uses but doesn't hold.
The game's constants and the key table don't belong to one run, so they stay out of the class, in the namespace. They don't change at all, so there's no need to type them again. From main.cpp, copy everything from // How far the player can see, in cells down to the closing brace of toggleFullscreen, and paste it into Game.cpp, inside the namespace.
That's the game's five constants, the key table, stepFor, presentationScale, and toggleFullscreen, 77 lines in all. The old copies stay in main.cpp for now, since the old code still uses them. They're in an unnamed namespace there too, so the two copies don't clash: each file's unnamed namespace is its own.
Then add a new helper below toggleFullscreen:
// The seed a run plays: the one asked for, or a random one for 0
unsigned int chooseSeed(unsigned int seed)
{
if (seed != 0)
{
return seed;
}
std::random_device entropy;
return entropy();
}
The function chooseSeed gives a run its seed: the one it's asked for, or a random one, if it's asked for 0. It's the code from the top of runGame, made into a function, for a reason the constructor will show in a moment.
Four more of the old functions move across unchanged. From main.cpp, copy rollDamage, distanceSquared, isNextTo and withArticle, each with the comment above it, into Game.cpp, below chooseSeed.
Copy them one at a time, with a blank line between each, as they were. They work out damage, distance, and words, from nothing but their parameters. So they don't need the run either. A function that needs nothing from the class is simpler as a plain function: anyone reading it can see that it can't change the game.
The Constructor
Now the class's own functions, starting with the constructor, which sets up a run. Add it below the namespace:
// A new run: the dungeon, its monsters, the player in the middle of the
// first room, and what the player can see from there
Game::Game(SDL_Renderer* renderer, GlyphCache& glyphs, unsigned int seed)
: renderer_(renderer), glyphs_(glyphs), seed_(chooseSeed(seed)),
rng_(seed_)
{
std::vector<SDL_Rect> rooms = RoomGen::generate(map_, rng_);
placeMonsters(rooms);
player_.pos = RoomGen::centerOf(rooms.front());
player_.hp = PLAYER_HP;
player_.maxHp = PLAYER_HP;
Fov::compute(map_, player_.pos, SIGHT_RADIUS);
log_.add("Welcome to the dungeon.");
log_.add("Move with the arrow keys, the numpad, or h j k l y u b n.");
}
Every member function's name starts with Game::, which says it belongs to the class. Inside it, the members are in reach by name, as map_ and player_.
The line that starts with a colon is the member initializer list. It gives members their first values as they're made, before the constructor's body runs. The reference, glyphs_, must be given its object here, since a reference must refer to something from the moment it's made. The renderer comes along with it. The seed is made from chooseSeed, which is why that's a function, and then the dice are made from the seed.
That last step is safe because of an order the compiler keeps, whatever order the list is written in. Members are made in the order they're declared in the class. And seed_ is declared just above rng_, so the seed has its value by the time the dice read it. The next image shows the order.

The members that aren't in the list are made by their own constructors, before the body runs. The map starts as solid rock, and the monsters and the log start empty. Then the body makes the dungeon, places the monsters, puts the player in the middle of the first room, and works out what they can see. The rooms are a local variable, rooms, since they're only needed here.
Playing the Run
Then run, which plays the run once it's set up. Add it below the constructor:
bool Game::run()
{
// The window may already be smaller than the picture
glyphs_.setScale(presentationScale(renderer_));
while (running_)
{
// Sleep until something happens. A turn-based game has nothing to
// do while the player is thinking
SDL_Event event;
if (!SDL_WaitEvent(&event))
{
SDL_Log("SDL_WaitEvent failed: %s", SDL_GetError());
break;
}
handleEvent(event);
if (redraw_)
{
draw();
redraw_ = false;
}
}
return playAgain_;
}
The loop is runGame's, without its long chain of branches. Each event goes to handleEvent, and then, if anything changed, draw draws the frame. When running_ turns false, the loop ends, and run returns playAgain_.
Next is placeMonsters. It's long, so it comes in two parts. Add the first below run:
// Puts MONSTER_COUNT monsters of random kinds on the floor of the rooms,
// one to a cell. The first room is the player's, so it's left empty
void Game::placeMonsters(const std::vector<SDL_Rect>& rooms)
{
int lastRoom = static_cast<int>(rooms.size()) - 1;
int lastKind = static_cast<int>(std::size(MONSTER_DEFS)) - 1;
if (lastRoom < 1)
{
return;
}
Add the rest below the if, with a blank line in between:
for (int i = 0; i < MONSTER_COUNT; ++i)
{
// A few tries each: a cell that's taken just means another roll
for (int tries = 0; tries < 20; ++tries)
{
const SDL_Rect& room = rooms[rng_.roll(1, lastRoom)];
Point pos = { room.x + rng_.roll(0, room.w - 1),
room.y + rng_.roll(0, room.h - 1) };
if (monsterAt(pos) == nullptr)
{
Monster monster;
monster.pos = pos;
monster.def = &MONSTER_DEFS[rng_.roll(0, lastKind)];
monster.hp = monster.def->hp;
monster.maxHp = monster.def->hp;
monster.defense = monster.def->defense;
monsters_.push_back(monster);
break;
}
}
}
}
It's the function from Chapters 5 and 6, with two changes. It rolls the run's own dice, rng_, and it puts each monster straight into monsters_, rather than filling a vector of its own to hand back. The function also asks monsterAt, a member now too, whether a cell is taken.
Checkpoint: Click in Game.cpp, and press Ctrl+F7 to compile it. It compiles, since every function it calls is declared in the class, even the ones you haven't written yet. Don't press F5 yet, though.
Building the whole program also links it, which matches every call with the function it calls. The linker would find three that don't exist yet: handleEvent, monsterAt, and draw. It would say LNK2019: unresolved external symbol for each, and LNK1120: 3 unresolved externals.
The Keys and the Turn
The long chain of ifs and else ifs that sat in runGame's loop splits in two: one function for events of every kind, and one for keys.
Events and Keys
Add the one for events, handleEvent, below placeMonsters:
void Game::handleEvent(const SDL_Event& event)
{
if (event.type == SDL_EVENT_QUIT)
{
running_ = false;
}
else if (event.type == SDL_EVENT_KEY_DOWN)
{
handleKey(event.key);
}
else if (event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED)
{
// The picture is shown at a new size, so the characters are drawn
// again to match it
glyphs_.setScale(presentationScale(renderer_));
redraw_ = true;
}
else if (event.type == SDL_EVENT_WINDOW_EXPOSED ||
event.type == SDL_EVENT_WINDOW_RESIZED)
{
redraw_ = true;
}
}
Quitting, a new size, and an uncovered window are dealt with here, as before. A key goes on to handleKey, with event.key, the part of the event that describes a key.
Then handleKey, in three parts. Add the first below handleEvent:
void Game::handleKey(const SDL_KeyboardEvent& key)
{
if (key.key == SDLK_ESCAPE)
{
running_ = false;
return;
}
if (key.key == SDLK_RETURN && (key.mod & SDL_KMOD_ALT) != 0)
{
// Alt+Enter. Holding the keys down doesn't flicker between the
// window and the screen, because repeats are ignored
if (!key.repeat)
{
toggleFullscreen(renderer_);
}
return;
}
The keys that work whatever is happening, Escape and Alt+Enter so far, are checked first. Once one is dealt with, return ends the function. There's no else chain to follow: a key that gets past a check simply wasn't that key.
Add the F1 key's check below them:
if (key.key == SDLK_F1)
{
// A debugging aid: F1 shows the whole map, and F1 again hides it
if (!key.repeat)
{
revealMap_ = !revealMap_;
redraw_ = true;
}
return;
}
Its comment moves here from runGame, where it sat beside the flag. Add the rest of handleKey below it, with a blank line in between:
if (!player_.isAlive())
{
// The death screen is up. R ends this run, and asks for another.
// The other keys do nothing
if (key.key == SDLK_R)
{
playAgain_ = true;
running_ = false;
}
return;
}
Point step;
if (stepFor(key, step))
{
playerStep(step);
redraw_ = true;
}
}
While the player is dead, only R does anything, as before. Otherwise, a moving key becomes a step, and playerStep takes it.
A Step
A moving key ends up in playerStep, which attacks or steps. Add it below handleKey:
// Stepping onto a monster attacks it. An attack or a step takes a turn, and
// then the rest of the turn plays out. Bumping into a wall takes no turn
void Game::playerStep(Point step)
{
log_.markRead();
Point to = { player_.pos.x + step.x, player_.pos.y + step.y };
Monster* monster = monsterAt(to);
if (monster != nullptr)
{
playerAttacks(*monster);
removeDeadMonsters();
finishTurn();
}
else if (map_.at(to.x, to.y) == Terrain::Floor)
{
player_.pos = to;
Fov::compute(map_, player_.pos, SIGHT_RADIUS);
finishTurn();
}
else
{
log_.add("You bump into the wall.");
}
}
In the preceding code, the walking code from runGame has become a function, with one difference: the tookTurn flag is gone. An attack and a step each call finishTurn themselves, and a bump into a wall doesn't. So there's nothing left to remember. The next image follows one key through the class.

Each function has one job, and passes on what isn't its job. Later chapters add keys at the level they belong to: a key that works everywhere goes in handleKey, and one that acts in the dungeon goes further down.
The Turn
The rest of the class's rules are Chapter 6's turn, nearly word for word. Each loses its parameters, and uses the members instead: rng_ for rng, log_ for log, and so on. Add playerAttacks below playerStep:
// The player strikes a monster. A blow in melee always hits
void Game::playerAttacks(Monster& monster)
{
int damage = rollDamage(rng_, player_.minDamage, player_.maxDamage,
monster.defense);
monster.hp -= damage;
std::string name = monster.def->name;
if (monster.isAlive())
{
log_.add("You hit the " + name + " for " + std::to_string(damage) +
".");
}
else
{
log_.add("You kill the " + name + "!");
}
}
The monster is still a parameter, since it's a different one each time. The player is the run's own, player_. Add removeDeadMonsters below it:
// Takes the dead monsters out of the dungeon
void Game::removeDeadMonsters()
{
std::erase_if(monsters_, [](const Monster& monster)
{
return !monster.isAlive();
});
}
Then finishTurn, the rest of the turn once the player has acted, below removeDeadMonsters:
// The rest of a turn, once the player has acted: the monsters act, the
// player heals a little now and then, and last of all comes the one check
// for the player's death
void Game::finishTurn()
{
monstersAct();
++turn_;
bool healing = player_.isAlive() && player_.hp < player_.maxHp;
if (healing && turn_ % HEAL_EVERY == 0)
{
++player_.hp;
}
if (!player_.isAlive())
{
log_.add("You die...");
}
}
The turn count is now the member turn_, so finishTurn changes it directly. Chapter 6's reference parameter isn't needed. Add monstersAct below finishTurn:
// The monsters' turn. A monster that can see the player attacks if it's
// next to them, and otherwise comes closer. The rest wander. Once the
// player is dead, no monster acts
void Game::monstersAct()
{
for (Monster& monster : monsters_)
{
if (!player_.isAlive())
{
return;
}
if (!seesPlayer(monster))
{
monster.pos = wanderStep(monster);
}
else if (isNextTo(monster.pos, player_.pos))
{
monsterAttacks(monster);
}
else
{
monster.pos = chaseStep(monster);
}
}
}
Each monster in monsters_ acts in turn, as before. Add monsterAttacks below monstersAct:
// A monster strikes the player. If the blow kills, the player remembers
// what dealt it, for the death screen
void Game::monsterAttacks(const Monster& monster)
{
int damage = rollDamage(rng_, monster.def->minDamage,
monster.def->maxDamage, player_.defense);
player_.hp -= damage;
std::string name = monster.def->name;
log_.add("The " + name + " hits you for " + std::to_string(damage) +
".");
if (!player_.isAlive())
{
player_.killedBy = withArticle(name);
}
}
The attacker is a parameter, and the player is player_, the other way around from playerAttacks. Add the chase below it:
// Where a monster that sees the player steps: to the free neighbor nearest
// the player, if that's nearer than where it stands
Point Game::chaseStep(const Monster& monster)
{
Point best = monster.pos;
int bestDistance = distanceSquared(monster.pos, player_.pos);
for (Point step : DIRECTIONS)
{
Point to = { monster.pos.x + step.x, monster.pos.y + step.y };
int distance = distanceSquared(to, player_.pos);
if (distance < bestDistance && isFree(to))
{
best = to;
bestDistance = distance;
}
}
return best;
}
The chase takes just the monster, since the map and the player are members. Add the wandering below it:
// Where a monster that can't see the player steps: in a random direction,
// if the cell there is free, or nowhere
Point Game::wanderStep(const Monster& monster)
{
Point step = DIRECTIONS[rng_.roll(0, 7)];
Point to = { monster.pos.x + step.x, monster.pos.y + step.y };
if (isFree(to))
{
return to;
}
return monster.pos;
}
The wandering takes just the monster too, and rolls the run's dice. Add seesPlayer and isFree below wanderStep:
// True if the monster can see the player. It can if the player can see its
// cell, since sight is symmetric, and the player is within its sight,
// measured as the field of view measures the player's
bool Game::seesPlayer(const Monster& monster) const
{
int sight = monster.def->sight;
return map_.isVisible(monster.pos.x, monster.pos.y) &&
distanceSquared(monster.pos, player_.pos) <= sight * (sight + 1);
}
// True if a monster could step onto the cell: floor, with nobody on it
bool Game::isFree(Point cell)
{
return map_.at(cell.x, cell.y) == Terrain::Floor &&
cell != player_.pos && monsterAt(cell) == nullptr;
}
The first of these ends in const, and the second doesn't, for a reason. A const function can only call other const functions, since it promised to change nothing. Calling one that might would break the promise.
The function isFree calls monsterAt, which isn't const, because it hands out a pointer to a monster. The caller is free to change that monster, as playerStep does when it attacks. So isFree can't promise either.
Add monsterAt, the last of the turn's functions, below isFree:
// The monster standing on a cell, or nullptr if there's none
Monster* Game::monsterAt(Point pos)
{
for (Monster& monster : monsters_)
{
if (monster.pos == pos)
{
return &monster;
}
}
return nullptr;
}
It looks through monsters_ for one standing on the cell, and returns a pointer to it, or nullptr if there's none, just as before.
Checkpoint: Click in Game.cpp, and press Ctrl+F7 to compile it again. It compiles. Only the drawing is left to write.
Drawing
The drawing functions come across the same way, and lose more parameters than any. Every one of them took the renderer, and most took the glyph cache too. Each is const, as declared.
The Frame
A frame is drawn by draw, which calls the rest in order. Add it below monsterAt:
void Game::draw() const
{
setColor(Palette::BACKGROUND);
SDL_RenderClear(renderer_);
drawMap();
drawMonsters();
drawCell(player_.pos.x, player_.pos.y, '@', Palette::PLAYER);
drawPanels();
drawSidePanel();
drawLog();
if (!player_.isAlive())
{
drawDeathScreen();
}
SDL_RenderPresent(renderer_);
}
It's drawFrame, renamed, with no parameters at all. Add setColor and drawCell below it:
void Game::setColor(SDL_Color color) const
{
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, color.a);
}
// One map cell: its background first, so nothing drawn there before can
// show through, then its character
void Game::drawCell(int col, int row, char c, SDL_Color color,
SDL_Color background) const
{
float x = static_cast<float>(col * CELL_PX);
float y = static_cast<float>(row * CELL_PX);
float size = static_cast<float>(CELL_PX);
SDL_FRect cell = { x, y, size, size };
setColor(background);
SDL_RenderFillRect(renderer_, &cell);
glyphs_.drawInCell(c, x, y, size, color);
}
Both use renderer_ now, and drawCell uses glyphs_ too. The default background isn't repeated here: a default argument goes in the declaration, and only there.
The Map and Its Monsters
Add drawMap below drawCell, in two parts. Here's the first:
// The map as the player knows it: what they can see, in full color, and
// what they remember, dimmed. With revealMap_, a debugging aid, all the
// rest is drawn as if remembered too. Walls are drawn only where they
// border floor
void Game::drawMap() const
{
for (int y = 0; y < MAP_H; ++y)
{
for (int x = 0; x < MAP_W; ++x)
{
bool visible = map_.isVisible(x, y);
if (!visible && !map_.isExplored(x, y) && !revealMap_)
{
continue;
}
Add the rest below the check, inside the loop over the columns:
if (map_.at(x, y) == Terrain::Floor)
{
SDL_Color color = visible ? Palette::FLOOR
: Palette::MEMORY_FLOOR;
drawCell(x, y, '.', color);
}
else if (map_.bordersFloor(x, y))
{
SDL_Color color = visible ? Palette::WALL
: Palette::MEMORY_WALL;
SDL_Color back = visible ? Palette::WALL_BACK
: Palette::MEMORY_WALL_BACK;
drawCell(x, y, '#', color, back);
}
}
}
}
The map is map_, and the debugging aid is revealMap_, rather than a parameter. Add drawMonsters below drawMap:
// The monsters the player can see. With revealMap_, all of them
void Game::drawMonsters() const
{
for (const Monster& monster : monsters_)
{
if (map_.isVisible(monster.pos.x, monster.pos.y) || revealMap_)
{
drawCell(monster.pos.x, monster.pos.y, monster.def->glyph,
monster.def->color);
}
}
}
It draws from monsters_, with map_ deciding which can be seen.
The Panels
Add drawPanels, which fills the side panel and the log's strip, below drawMonsters:
// The side panel and the message log, each with a line along the edge it
// shares with the map
void Game::drawPanels() const
{
setColor(Palette::PANEL);
SDL_FRect panel = { static_cast<float>(PANEL_X), 0.0f,
static_cast<float>(PANEL_W),
static_cast<float>(MAP_VIEW_H) };
SDL_RenderFillRect(renderer_, &panel);
SDL_FRect log = { 0.0f, static_cast<float>(LOG_Y),
static_cast<float>(SCREEN_W),
static_cast<float>(LOG_H) };
SDL_RenderFillRect(renderer_, &log);
setColor(Palette::FRAME);
SDL_RenderLine(renderer_, static_cast<float>(PANEL_X), 0.0f,
static_cast<float>(PANEL_X),
static_cast<float>(MAP_VIEW_H));
SDL_RenderLine(renderer_, 0.0f, static_cast<float>(LOG_Y),
static_cast<float>(SCREEN_W), static_cast<float>(LOG_Y));
}
Then the health bar, which can show any entity's health, below drawPanels:
// A bar that shows how much health is left: green while there's plenty,
// orange below half, and red below a quarter
void Game::drawHealthBar(float x, float y, float width,
const Entity& entity) const
{
float share = static_cast<float>(entity.hp) / entity.maxHp;
share = SDL_clamp(share, 0.0f, 1.0f);
SDL_Color color = Palette::HEALTH_GOOD;
if (share < 0.25f)
{
color = Palette::DANGER;
}
else if (share < 0.5f)
{
color = Palette::HEALTH_LOW;
}
SDL_FRect whole = { x, y, width, HEALTH_BAR_H };
setColor(Palette::FRAME);
SDL_RenderFillRect(renderer_, &whole);
SDL_FRect left = { x, y, width * share, HEALTH_BAR_H };
setColor(color);
SDL_RenderFillRect(renderer_, &left);
}
The entity is still a parameter, since it can be a different one each time. Add drawSidePanel below it:
// The side panel: the game's name, the player's health, then the run so
// far, one row under another, and a reminder when a debugging aid is on
void Game::drawSidePanel() const
{
float x = PANEL_X + MARGIN;
float y = MARGIN;
float row = glyphs_.lineHeight();
glyphs_.drawText("AMULETS OF SDL", x, y, Palette::TEXT);
y += 2 * row;
glyphs_.drawText("Health " + std::to_string(player_.hp) + "/" +
std::to_string(player_.maxHp), x, y, Palette::TEXT);
y += row;
drawHealthBar(x, y, PANEL_W - 2 * MARGIN, player_);
y += row;
glyphs_.drawText("Turn " + std::to_string(turn_), x, y, Palette::TEXT);
y += row;
glyphs_.drawText("Seed " + std::to_string(seed_), x, y,
Palette::DIM_TEXT);
if (revealMap_)
{
y += 2 * row;
glyphs_.drawText("DEBUG: map revealed (F1)", x, y, Palette::DEBUG);
}
}
The side panel had six parameters, and now has none. Add drawLog below it:
// The last LOG_LINES messages, oldest at the top. The ones that came since
// the player's last command are bright, and the rest are dim
void Game::drawLog() const
{
const std::vector<MessageLog::Entry>& entries = log_.entries();
int count = static_cast<int>(entries.size());
int first = SDL_max(0, count - LOG_LINES);
float y = LOG_Y + (LOG_H - LOG_LINES * LOG_LINE_H) / 2.0f;
for (int i = first; i < count; ++i)
{
const MessageLog::Entry& entry = entries[i];
SDL_Color color = entry.isNew ? Palette::TEXT : Palette::DIM_TEXT;
glyphs_.drawText(entry.withCount(), MARGIN, y, color);
y += LOG_LINE_H;
}
}
That's every panel.
The Death Screen
The death screen centers its lines with drawCentered, as before. Add it below drawLog:
// A line of text, centered on centerX
void Game::drawCentered(const std::string& text, float centerX, float y,
SDL_Color color) const
{
float width = text.size() * glyphs_.charWidth();
glyphs_.drawText(text, centerX - width / 2.0f, y, color);
}
Then the death screen, in two parts. Add the first below drawCentered:
// The death screen. The dungeon fades, and a box in the middle of it says
// what killed the player, and when
void Game::drawDeathScreen() const
{
// The shade is see-through, so it's blended with what's under it
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
setColor(Palette::SHADE);
SDL_FRect mapView = { 0.0f, 0.0f, static_cast<float>(MAP_VIEW_W),
static_cast<float>(MAP_VIEW_H) };
SDL_RenderFillRect(renderer_, &mapView);
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_NONE);
Add the rest below the shade, with a blank line in between:
float centerX = MAP_VIEW_W / 2.0f;
float row = glyphs_.lineHeight();
SDL_FRect box = { centerX - 300.0f, MAP_VIEW_H / 2.0f - 3 * row, 600.0f,
6 * row };
setColor(Palette::PANEL);
SDL_RenderFillRect(renderer_, &box);
setColor(Palette::FRAME);
SDL_RenderRect(renderer_, &box);
float y = box.y + row;
drawCentered("YOU HAVE DIED", centerX, y, Palette::DANGER);
y += 2 * row;
drawCentered("Killed by " + player_.killedBy + ", on turn " +
std::to_string(turn_) + ".", centerX, y, Palette::TEXT);
y += row;
drawCentered("Press R to play again, or Esc to quit.", centerX, y,
Palette::DIM_TEXT);
}
The killer's name comes from player_, and the turn from turn_. That's the whole class.
Checkpoint: Press F5. The game builds, and plays exactly as before, since main.cpp still runs it the old way. The class is complete, and nothing uses it yet.
Switching Over
Now main.cpp hands the game to the class, and the old code goes. Open main.cpp. Delete the includes from #include <iterator> down to #include "RoomGen.h", and add these in their place, below #include <SDL3_ttf/SDL_ttf.h>:
#include "Common.h"
#include "Game.h"
#include "GlyphCache.h"
The file needs Common.h for the screen's size, Game.h for the class, and GlyphCache.h for the cache it makes. Everything else is the class's business now.
Then, still in main.cpp, delete everything from // How far the player can see, in cells down to the closing brace of runGame, along with the blank line above it. That's every constant but the window's and the font's, and every function but main: nearly 700 lines, all of them in the class now.
Last, find the loop in main that plays the runs:
// Each run is a new game, with everything in it made afresh.
// Only the first uses the seed from the command line
while (runGame(renderer, glyphs, seed))
{
seed = 0;
}
Replace it with these lines:
// Each run is a new Game, with everything in it made afresh.
// Only the first uses the seed from the command line
bool playAgain = true;
while (playAgain)
{
Game game(renderer, glyphs, seed);
playAgain = game.run();
seed = 0;
}
Each pass of the loop makes a Game, called game, and runs it. A variable made inside the loop's body lasts until the end of that pass. So each run's Game is destroyed before the next is made. Everything in it goes with it: the dungeon, the monsters, the player and the log. The next image shows three runs.

The next run starts from nothing, so nothing from the last one can leak into it. And there's no list of things to reset that a later chapter could forget to add to. Only the first run uses the seed from Command Arguments, as before.
Checkpoint: Press F5, with seed 42 in Command Arguments. The game plays exactly as before: the same dungeon, the same jackal, and the same fight. Die, or press Escape, and see that R and Escape still do what they did. Now main.cpp is just under 100 lines, and the rest of the game is the class.
Game States
One more change finishes the job. Two of the game's checks for the player's death decide what the game is doing: one decides what a key does, and the other whether to draw the death screen. The others, in monstersAct and finishTurn, belong to the turn's rules, and stay.
As the game grows, it'll be in many more situations than alive or dead. The player will be choosing from the inventory, or aiming a spell, or reading the help. Each of those needs its own keys and its own drawing.
So the game gets a state: a value that says what it's doing right now. Each key is handled according to the state, and each frame is drawn according to it. This is a state machine, one of the oldest patterns in games.
The States
Open Game.h, and add the states below the includes:
// What the game is doing. It decides what the keys do, and what's drawn
enum class GameState
{
Playing,
Dead,
};
A GameState is an enum class, as Terrain is, with two values so far. Then add two declarations below handleKey's:
void handlePlayingKey(const SDL_KeyboardEvent& key);
void handleDeadKey(const SDL_KeyboardEvent& key);
Each state handles its own keys, in a function of its own. Last, add the state itself above revealMap_:
GameState state_ = GameState::Playing;
Every run starts in Playing. Then open Game.cpp, and find the check for death in handleKey, with the lines below it:
if (!player_.isAlive())
{
// The death screen is up. R ends this run, and asks for another.
// The other keys do nothing
if (key.key == SDLK_R)
{
playAgain_ = true;
running_ = false;
}
return;
}
Point step;
Replace them with these lines:
// The keys above work in every state. What the others do depends on
// the state the game is in
switch (state_)
{
case GameState::Playing:
handlePlayingKey(key);
break;
case GameState::Dead:
handleDeadKey(key);
break;
}
}
// The moving keys move the player
void Game::handlePlayingKey(const SDL_KeyboardEvent& key)
{
Point step;
The keys that work in every state stay at the top of handleKey. Then a switch on the state passes the key to that state's function. The step's code, below it, is now the start of handlePlayingKey, since moving is something that only happens while playing. Add the dead state's keys below handlePlayingKey:
// The death screen is up. R ends this run, and asks for another. The other
// keys do nothing
void Game::handleDeadKey(const SDL_KeyboardEvent& key)
{
if (key.key == SDLK_R)
{
playAgain_ = true;
running_ = false;
}
}
It's the code that was in handleKey, in a function of its own. Now the state needs to change when the player dies. In finishTurn, add this line below log_.add("You die...");:
state_ = GameState::Dead;
The one check for death, at the end of the turn, is now also where the game enters its Dead state. And in draw, change if (!player_.isAlive()) to if (state_ == GameState::Dead), so that the death screen is drawn for the state, rather than by asking the player.
Design note: waiting for a key. When Rogue asked "which object do you want to quaff?", the function asking,
get_item, calledreadcharand waited right there for your answer, in a loop, until you gave a good one. A program in a terminal can do that, since nothing else needs it while it waits. One with a window can't. While you think, it must still answer the window's events, drawing itself again when it's uncovered or resized, so it can't sit inside a function, waiting. Instead, it remembers what it's waiting for, its state, and handles each key by it when the key arrives. That's why this game has states, and why later chapters add so many.
Checkpoint: Press F5. The game plays exactly as before, once again. Die, and the death screen appears, drawn for the state now. Press R, and a new run starts.
The next image shows the two states, and the ones to come.

Each new state adds a value to GameState, a case to the switch in handleKey, a function for its keys, and whatever it draws. By the end of the book, there are twelve.
Understanding the Code
A Game is one run of the game. Its members hold the run's state: its seed and dice, the map, the monsters, the player, the log, the turn count, and the flags the loop keeps. The renderer and the glyph cache are borrowed from main. The constructor sets the run up, with the seed before the dice, since members are made in the order they're declared. Then run plays it, returning whether the player wants another.
Each event goes to handleEvent, and each key to handleKey. That function handles the keys that work in every state, then passes the rest to the state's own function. A moving key becomes playerStep, which attacks or steps, and calls finishTurn for the rest of the turn. The drawing functions are all const, since drawing changes nothing.
What isn't about one run stays outside the class. SDL, the window and the font stay in main.cpp, which makes a new Game for each run. And the small helpers that need only their parameters stay as plain functions in Game.cpp's unnamed namespace.
Experimenting
Nothing in this chapter changed how the game plays, so these experiments are about the class. Try each, and put each back afterward:
- Watch runs come and go. Add a destructor to the class, in
Game.h, belowrun:~Game() { SDL_Log("The run is over."); }. Then die, press R, and look at the console window behind the game: there's a line for each run that ended. - Break a promise. At the start of
draw, add++turn_;and build. The compiler refuses: C3490: 'turn_' cannot be modified because it is being accessed through a const object. - Knock on a private door. In
main, below the line that makes theGame, addgame.draw();and build. The compiler says C2248: 'Game::draw': cannot access private member declared in class 'Game', since only the class itself may call its private functions. - Swap the order. In
Game.h, putRng rng_;aboveunsigned int seed_;, and run seed 42. The side panel still says Seed 42, but the dungeon isn't seed 42's, and seed 7 gives the same one. The dice were started from whatever was inseed_before it had a value. In a Debug build, that's always the same junk, since Debug fills new local variables with the byte 0xCC before they're set. A Release build could give anything.
The last one is the trap from Figure 7.4. It's worth seeing once, since Visual Studio doesn't warn about it at its usual settings.
Common Errors and Fixes
If the game didn't build, or doesn't behave as described, the problem is probably one of these.
C2270: 'drawMonsters': modifiers not allowed on nonmember functions, followed by C2065: 'monsters_': undeclared identifier, and more. A member function's first line is missing Game::, so the compiler thinks it's a plain function. Plain functions have no members to reach, and can't be const. It should read void Game::drawMonsters() const.
C2511: 'void Game::drawMonsters(void)': overloaded member function not found in 'Game'. The function's first line in Game.cpp doesn't match its declaration in Game.h. Here, the const is missing. Check that every parameter, and the const, are the same in both places.
C2662: 'Monster *Game::monsterAt(Point)': cannot convert 'this' pointer from 'const Game' to 'Game &'. A const function calls monsterAt, which isn't const. Here, isFree was made const. Take the const off it, in both Game.h and Game.cpp.
C2530: 'Game::glyphs_': references must be initialized. The constructor's member initializer list is missing glyphs_(glyphs). A reference member must be given its object in the list.
LNK2019: unresolved external symbol "private: void __cdecl Game::drawLog(void)const ", followed by LNK1120: 1 unresolved externals. A function declared in Game.h, here drawLog, has no definition in Game.cpp. Check that each function in the class has been written, with Game:: before its name.
C2065: 'Game': undeclared identifier, in main.cpp. The file doesn't include Game.h.
C2065: 'Dead': undeclared identifier. The value of an enum class needs its type's name before it: GameState::Dead.
The dungeon doesn't match the seed. The panel says Seed 42, but the dungeon isn't seed 42's, and in a Debug build, every seed gives the same one. In Game.h, seed_ must be declared above rng_. The members are made in the order they're declared, so rng_ was made from seed_ before seed_ had a value.
FAQ
Q) Why do the members' names end in an underscore?
A) It tells a member from a local variable or a parameter at a glance. And it lets a parameter and a member share a name without either hiding the other, as seed and seed_ do in the constructor. Some programmers use a prefix instead, such as m_seed. What matters is that a project picks one way and keeps to it.
Q) Why are the key table and the damage rule plain functions, rather than members?
A) They need little or nothing from a run. A plain function can reach only what it's given, so its parameters show everything it can change. The damage rule is given the run's dice, so it can take rolls from their stream, but it can't touch the map, the monsters or the player. Making it a member would let it reach every member, for no reason, and a reader would have to read the whole of it to know what it touches.
Q) Why make a new Game for each run, rather than resetting the old one?
A) A reset has to set every member back, and every member a later chapter adds would need adding to it. Forget one, and the old run leaks into the new one. A new Game is made by the same constructor every time, so the second run starts exactly as the first did.
Q) Why a switch on one state, rather than a stack of them?
A) Some games keep a stack of states: the inventory goes on top of the game, and when it closes, the game underneath carries on. That's useful when screens open over each other, many deep. This game's screens rarely do. The one that's opened from several places, the help, will simply remember the state it came from, in Chapter 22. One state, and a switch, is the simplest thing that works, and a stack can come later if the game ever needs it.
Q) Won't the Game class grow too big?
A) It will grow. By the end of the book, Game.cpp is about 6,000 lines, with some 180 of the class's functions. What keeps that manageable is that anything with a clear seam is split off: Chapter 8's views draw the map, Chapter 16's data files hold the monsters and the items, and Chapter 17's saving lives in a file of its own, even though its functions belong to the class. The rules stay together, since they all work on the same run.
AI Exercise (Optional)
A game state is a good thing to practice with, since adding one touches every part of the pattern. If you'd like to add a pause with an AI's help, open your AI chatbot of choice, and try a prompt like this:
"I'm writing a roguelike in C++ with SDL 3. A class, Game, holds one run. It has a member state_ of type GameState, an enum class with the values Playing and Dead. In Game::handleKey(const SDL_KeyboardEvent& key), the keys that work in every state come first, then a switch on state_ calls handlePlayingKey(key) or handleDeadKey(key). Game::draw() const draws the map, the panels and the log, then the death screen if state_ is GameState::Dead. I'd like a new state, Paused: P pauses while playing, and P again carries on, no other key does anything while paused, and the map is drawn with the word PAUSED in the middle. Show me every change, with each curly brace on its own line, and explain them."
Notice what the preceding prompt does. It names the class, its state, and the functions that handle keys and draw, so the AI can follow the pattern that's there, rather than invent another. The prompt also says what the new state should do, key by key.
When the answer comes back, check it against the pattern. Does the new state get its own value, its own case, and its own function for keys? Escape is above the switch, so does it still quit while paused? And does the game take no turns while paused, so the monsters wait too?
Summary
The game is a class now: one Game for each run, whose members are the run's state, whose functions reach them directly, and whose state decides what each key does. In Chapter 8, the drawing gets the same treatment. The game learns to show its dungeon in tiles as well as letters, and a key switches between the two, without the rules ever knowing which is on the screen.
