Refactoring a Console C++ RPG: From Raw Pointers to Robust Design

This article walks through modernizing a simple C++ console RPG game. The original implementation, commonly found in beginner tutorials, had several issues: manual memory management, weak input validation, a rogue class with a non-functional dodge ability, and verbose conditional logic for character classes. We'll address each flaw step by step, ending with a cleaner, more maintainable design.

Original Code (Summary)

The original code defined a Character base class with pure virtual attack and specialAbility. A Player class derived from Character and used a className string to decide which ability to execute. The Monster class only performed basic attacks. The battle loop allocated Player via new and manually delete at the end. Input was read with std::cin >> choice without handling bad input. The dodge ability of the rogue printed text but never actually prevented damage.

Known Defects

  • Memory leak risk: raw new/delete instead of smart pointers.
  • Non‑functional rogue dodge: no state machine to skip damage.
  • No input validation: entering letters crashes the program.
  • Monster AI too simple: never calls specialAbility.
  • Defense formula can stall: zero damage if damage <= def, risking infinite loops.
  • String‑based class discrimination: brittle and violates Open/Closed principle.
  • Unbalanced numbers: warrior’s power strike can kill the monster in two turns.

Refactoring Goals and Decisions

We target six improvements:

  • Replace raw pointers with std::unique_ptr — automatic cleanup.
  • Introduce a dodging boolean in Character; takeDamage checks it before applying damage.
  • Add a clearInputBuffer routine after each std::cin read; loop until a valid choice is entered.
  • Give the monster a 30% chance to use Frenzy every turn.
  • Change damage to std::max(1, damage - def) — at least 1 damage.
  • Create discrete subclasses (Warrior, Mage, Rogue) each overriding specialAbility without string comparisons.
  • Adjust hit points, attack, and defense to make battles last 3–4 turns on average.

The revised class hierarchy:

class Character {
protected:
    std::string name;
    int hp, maxHp, atk, def;
    bool dodging = false;
public:
    Character(std::string n, int h, int a, int d);
    virtual void attack(Character& target) = 0;
    virtual void specialAbility(Character& target) = 0;
    void takeDamage(int damage);
    void setDodge(bool v);
    bool isAlive() const;
    virtual void displayStatus() const;
    std::string getName() const;
};

class Warrior : public Character {
public:
    Warrior(std::string name) : Character(name, 100, 15, 10) {}
    void attack(Character& target) override;
    void specialAbility(Character& target) override;
};

class Mage : public Character { /* similar constructor with 80 HP, 20 ATK, 5 DEF */ };
class Rogue : public Character { /* 90 HP, 12 ATK, 8 DEF – dodge sets dodging flag */ };
class Monster : public Character { /* 150 HP, 18 ATK, 8 DEF – 30% chance of Frenzy */ };

Key Code Sections (Rewritten)

Smart player creation (no manual delete):

std::unique_ptr<Character> createPlayer() {
    std::string name;
    std::cout << "Enter your name: ";
    std::getline(std::cin, name);

    while (true) {
        std::cout << "Choose class:\n1. Warrior\n2. Mage\n3. Rogue\n";
        int choice;
        std::cin >> choice;
        clearInputBuffer();  // ignore rest of line

        switch (choice) {
            case 1: return std::make_unique<Warrior>(name);
            case 2: return std::make_unique<Mage>(name);
            case 3: return std::make_unique<Rogue>(name);
            default: std::cout << "Invalid input. Please enter 1-3.\n";
        }
    }
}

Damage with dodge and minimum damage floor:

void Character::takeDamage(int damage) {
    if (dodging) {
        std::cout << name << " dodged the attack!\n";
        dodging = false;
        return;
    }
    int actual = std::max(1, damage - def);
    hp -= actual;
    if (hp < 0) hp = 0;
    std::cout << name << " takes " << actual << " damage!\n;
}

Monster AI with random special attack:

void Monster::attack(Character& target) {
    if (rand() % 100 < 30) {
        specialAbility(target);   // Frenzy
    } else {
        std::cout << name << " claws " << target.getName() << "!\n";
        target.takeDamage(atk);
    }
}

Battle loop with robust input handling:

void battleLoop(Character& player, Character& monster) {
    while (player.isAlive() && monster.isAlive()) {
        std::cout << "\n===== BATTLE =====\n";
        player.displayStatus();
        monster.displayStatus();

        int choice;
        while (true) {
            std::cout << "Choose action:\n1. Attack\n2. Skill\n";
            if (std::cin >> choice && (choice == 1 || choice == 2))
                break;
            clearInputBuffer();
            std::cout << "Invalid choice! Enter 1 or 2.\n";
        }

        if (choice == 1) player.attack(monster);
        else player.specialAbility(monster);

        if (monster.isAlive())
            monster.attack(player);
    }
}

The complete rewritten program compiles with C++14/17 and runs as a single‑file console application. Memory is managed automatically, input mistakes don't crash the game, the rogue's dodge works, and the monster fights back with one‑third probability. The battles are balanced sothat a typical encounter lasts 3–4 rounds, making the experience more engaging.

Tags: C++ RPG game development Code Refactoring smart pointers

Posted on Sat, 06 Jun 2026 17:31:45 +0000 by visualed