Back to blog
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
Share:𝕏

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.cpp

Entity 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
./dungeon

What This Project Demonstrates

  • InheritanceHero and Monster extend Entity
  • Polymorphism — virtual rollAttack() overridden in each subclass
  • smart pointersstd::unique_ptr<Monster> for heap-allocated monsters
  • STLstd::vector, std::find, std::move
  • Random numbersstd::mt19937 with std::uniform_int_distribution
  • EnumsHeroClass with switch dispatch
  • Modern C++ — range-for, auto, std::move, ostringstream

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.