Backend Systemsbeginner
C++ Project: Text Dungeon Game
Build a turn-based text dungeon game in C++ using OOP, polymorphism, smart pointers, STL, and random number generation.
Asma HafeezApril 17, 20267 min read
cppprojectgameooppolymorphism
C++ Project: Text Dungeon Game
This project applies the full C++ beginner curriculum: classes, inheritance, smart pointers, STL, templates, and modern C++ idioms.
What We're Building
=== Dungeon Explorer ===
Enter your name: Alice
Choose class: [1] Warrior [2] Mage [3] Rogue
> 1
Alice the Warrior enters the dungeon!
HP: 120 | ATK: 15 | DEF: 8
--- Floor 1 ---
You encounter a Goblin (HP: 20)!
[1] Attack [2] Use Item [3] Run
> 1
You deal 12 damage! Goblin has 8 HP.
Goblin hits you for 4 damage! You have 116 HP.
> 1
You deal 15 damage! Goblin defeated!
Gained 10 XP and 5 gold.
--- Floor 2 ---
You find a Health Potion!
...Project Structure
dungeon/
├── Entity.h / Entity.cpp
├── Hero.h / Hero.cpp
├── Monster.h / Monster.cpp
├── Item.h / Item.cpp
├── Dungeon.h / Dungeon.cpp
└── main.cppEntity Base Class
CPP
// Entity.h
#pragma once
#include <string>
class Entity {
protected:
std::string name_;
int hp_, maxHp_;
int attack_, defense_;
public:
Entity(std::string name, int hp, int atk, int def)
: name_(std::move(name)), hp_(hp), maxHp_(hp), attack_(atk), defense_(def) {}
virtual ~Entity() = default;
const std::string& name() const { return name_; }
int hp() const { return hp_; }
int maxHp() const { return maxHp_; }
bool isAlive() const { return hp_ > 0; }
virtual int takeDamage(int amount) {
int actual = std::max(0, amount - defense_ / 2);
hp_ = std::max(0, hp_ - actual);
return actual;
}
void heal(int amount) {
hp_ = std::min(maxHp_, hp_ + amount);
}
virtual int rollAttack() const;
virtual std::string statusLine() const;
};CPP
// Entity.cpp
#include "Entity.h"
#include <random>
#include <sstream>
#include <algorithm>
static std::mt19937 rng(std::random_device{}());
int Entity::rollAttack() const {
std::uniform_int_distribution<int> dist(attack_ - 3, attack_ + 3);
return std::max(1, dist(rng));
}
std::string Entity::statusLine() const {
return name_ + " [HP: " + std::to_string(hp_) + "/" + std::to_string(maxHp_) + "]";
}Hero Classes
CPP
// Hero.h
#pragma once
#include "Entity.h"
#include <vector>
#include <string>
enum class HeroClass { Warrior, Mage, Rogue };
class Hero : public Entity {
HeroClass class_;
int xp_, level_, gold_;
int xpToNext_;
std::vector<std::string> inventory_;
public:
Hero(const std::string& name, HeroClass heroClass);
void gainXP(int amount);
void gainGold(int amount);
void addItem(const std::string& item);
bool useItem(const std::string& item);
int rollAttack() const override;
std::string classname() const;
std::string fullStatus() const;
int level() const { return level_; }
int gold() const { return gold_; }
const std::vector<std::string>& inventory() const { return inventory_; }
};CPP
// Hero.cpp
#include "Hero.h"
#include <algorithm>
#include <iostream>
#include <random>
#include <sstream>
static std::mt19937 rng2(std::random_device{}());
Hero::Hero(const std::string& name, HeroClass heroClass)
: Entity(name, 0, 0, 0), class_(heroClass), xp_(0), level_(1), gold_(0), xpToNext_(50)
{
switch (heroClass) {
case HeroClass::Warrior: hp_ = maxHp_ = 120; attack_ = 15; defense_ = 8; break;
case HeroClass::Mage: hp_ = maxHp_ = 80; attack_ = 22; defense_ = 3; break;
case HeroClass::Rogue: hp_ = maxHp_ = 100; attack_ = 18; defense_ = 5; break;
}
}
std::string Hero::classname() const {
switch (class_) {
case HeroClass::Warrior: return "Warrior";
case HeroClass::Mage: return "Mage";
case HeroClass::Rogue: return "Rogue";
}
return "";
}
int Hero::rollAttack() const {
std::uniform_int_distribution<int> dist(attack_ - 4, attack_ + 6);
int roll = std::max(1, dist(rng2));
// Rogues have a 25% crit chance
if (class_ == HeroClass::Rogue) {
std::uniform_int_distribution<int> crit(1, 4);
if (crit(rng2) == 1) { roll *= 2; std::cout << "Critical hit! "; }
}
return roll;
}
void Hero::gainXP(int amount) {
xp_ += amount;
while (xp_ >= xpToNext_) {
xp_ -= xpToNext_;
level_++;
xpToNext_ = static_cast<int>(xpToNext_ * 1.5);
attack_ += 2; defense_ += 1; maxHp_ += 10;
hp_ = maxHp_;
std::cout << "Level up! Now level " << level_ << "!\n";
}
}
void Hero::gainGold(int amount) { gold_ += amount; }
void Hero::addItem(const std::string& item) { inventory_.push_back(item); }
bool Hero::useItem(const std::string& item) {
auto it = std::find(inventory_.begin(), inventory_.end(), item);
if (it == inventory_.end()) return false;
inventory_.erase(it);
if (item == "Health Potion") {
heal(40);
std::cout << "Restored 40 HP. Now at " << hp_ << " HP.\n";
return true;
}
return false;
}
std::string Hero::fullStatus() const {
std::ostringstream ss;
ss << name_ << " the " << classname()
<< " | Level " << level_
<< " | HP: " << hp_ << "/" << maxHp_
<< " | ATK: " << attack_
<< " | DEF: " << defense_
<< " | Gold: " << gold_;
return ss.str();
}Monster Classes
CPP
// Monster.h
#pragma once
#include "Entity.h"
#include <string>
class Monster : public Entity {
int xpReward_, goldReward_;
std::string description_;
public:
Monster(std::string name, int hp, int atk, int def, int xp, int gold, std::string desc)
: Entity(std::move(name), hp, atk, def)
, xpReward_(xp), goldReward_(gold)
, description_(std::move(desc)) {}
int xpReward() const { return xpReward_; }
int goldReward() const { return goldReward_; }
const std::string& description() const { return description_; }
static std::unique_ptr<Monster> random(int floor);
};CPP
// Monster.cpp
#include "Monster.h"
#include <random>
#include <memory>
static std::mt19937 rng3(std::random_device{}());
std::unique_ptr<Monster> Monster::random(int floor) {
int scale = floor;
std::uniform_int_distribution<int> roll(1, 4);
switch (roll(rng3)) {
case 1: return std::make_unique<Monster>("Goblin",
10+scale*5, 5+scale*2, 2, 8+scale*3, 3+scale, "A small but vicious creature.");
case 2: return std::make_unique<Monster>("Skeleton",
15+scale*5, 8+scale*2, 4, 12+scale*3, 5+scale, "An undead warrior.");
case 3: return std::make_unique<Monster>("Troll",
25+scale*8, 10+scale*3, 6, 18+scale*4, 8+scale, "A big ugly brute.");
default: return std::make_unique<Monster>("Dragon",
50+scale*10, 18+scale*4, 8, 40+scale*8, 20+scale, "A fearsome dragon!");
}
}Dungeon & Main
CPP
// main.cpp
#include "Hero.h"
#include "Monster.h"
#include <iostream>
#include <limits>
static void clearInput() {
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
static int getChoice(int min, int max) {
int choice;
while (!(std::cin >> choice) || choice < min || choice > max) {
std::cin.clear();
clearInput();
std::cout << "Enter " << min << "-" << max << ": ";
}
clearInput();
return choice;
}
static bool combat(Hero& hero, Monster& monster) {
std::cout << "\nYou encounter a " << monster.name()
<< " — " << monster.description() << "\n";
while (hero.isAlive() && monster.isAlive()) {
std::cout << "\n" << hero.fullStatus() << "\n";
std::cout << monster.statusLine() << "\n";
std::cout << "[1] Attack [2] Use Item [3] Flee\n> ";
int choice = getChoice(1, 3);
if (choice == 1) {
int dmg = hero.rollAttack();
int actual = monster.takeDamage(dmg);
std::cout << "You deal " << actual << " damage!\n";
} else if (choice == 2) {
if (hero.inventory().empty()) {
std::cout << "No items!\n"; continue;
}
std::cout << "Items: ";
for (const auto& item : hero.inventory()) std::cout << "[" << item << "] ";
std::cout << "\nUse: ";
std::string item; std::getline(std::cin, item);
hero.useItem(item);
} else {
std::cout << "You flee!\n";
return false;
}
if (monster.isAlive()) {
int dmg = monster.rollAttack();
int actual = hero.takeDamage(dmg);
std::cout << monster.name() << " hits you for " << actual << " damage!\n";
}
}
if (!hero.isAlive()) return false;
std::cout << monster.name() << " defeated! +"
<< monster.xpReward() << " XP, +"
<< monster.goldReward() << " gold.\n";
hero.gainXP(monster.xpReward());
hero.gainGold(monster.goldReward());
return true;
}
int main() {
std::cout << "=== Dungeon Explorer ===\nEnter your name: ";
std::string name; std::getline(std::cin, name);
std::cout << "Choose class: [1] Warrior [2] Mage [3] Rogue\n> ";
int classChoice = getChoice(1, 3);
HeroClass heroClass = static_cast<HeroClass>(classChoice - 1);
Hero hero(name, heroClass);
std::cout << "\n" << name << " the " << hero.classname() << " enters the dungeon!\n";
std::cout << hero.fullStatus() << "\n\n";
hero.addItem("Health Potion");
hero.addItem("Health Potion");
for (int floor = 1; floor <= 5 && hero.isAlive(); floor++) {
std::cout << "\n=== Floor " << floor << " ===\n";
auto monster = Monster::random(floor);
if (!combat(hero, *monster)) {
if (!hero.isAlive()) {
std::cout << "\nGame Over! " << name << " was defeated.\n";
return 0;
}
}
if (floor % 2 == 0) {
std::cout << "You find a Health Potion!\n";
hero.addItem("Health Potion");
}
}
if (hero.isAlive()) {
std::cout << "\n=== YOU WIN! ===\n";
std::cout << hero.fullStatus() << "\n";
}
return 0;
}Building
Bash
g++ -std=c++20 -Wall -o dungeon Entity.cpp Hero.cpp Monster.cpp main.cpp
./dungeonWhat This Project Demonstrates
- Inheritance —
HeroandMonsterextendEntity - Polymorphism — virtual
rollAttack()overridden in each subclass - smart pointers —
std::unique_ptr<Monster>for heap-allocated monsters - STL —
std::vector,std::find,std::move - Random numbers —
std::mt19937withstd::uniform_int_distribution - Enums —
HeroClasswithswitchdispatch - Modern C++ — range-for,
auto,std::move,ostringstream
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.