Combat encounter¶
We will refactor the following code in order to apply the principles of single responsibility and the Stepdown Rule.
Create a
Lab12/directory as well as the following directories and files.
Lab12/
├── include/
│ ├── character.h
│ └── combat-encounter.h
├── source/
│ ├── character.cpp
│ ├── combat-encounter.cpp
│ └── main.cpp
├── compile_flags.txt
└── makefile#ifndef CHARACTER_H
#define CHARACTER_H
#include <string>
class Character {
public:
Character(const std::string& name, int health, int attack, int defense, int healAmount);
std::string getName() const;
int getHealth() const;
int getAttack() const;
int getDefense() const;
int getHealAmount() const;
bool isAlive() const;
void takeDamage(int damage);
void heal();
private:
std::string mName;
int mHealth;
int mAttack;
int mDefense;
int mHealAmount;
};
#endif#ifndef COMBAT_ENCOUNTER_H
#define COMBAT_ENCOUNTER_H
#include "character.h"
class CombatEncounter {
public:
CombatEncounter(const Character& hero, const Character& enemy);
void run();
private:
Character mHero;
Character mEnemy;
enum Action {
ATTACK = 1,
HEAL = 2
};
};
#endif#include "character.h"
#include <string>
Character::Character(const std::string& name, int health, int attack, int defense, int healAmount)
: mName(name), mHealth(health), mAttack(attack), mDefense(defense), mHealAmount(healAmount) {}
std::string Character::getName() const {
return mName;
}
int Character::getHealth() const {
return mHealth;
}
int Character::getAttack() const {
return mAttack;
}
int Character::getDefense() const {
return mDefense;
}
int Character::getHealAmount() const {
return mHealAmount;
}
bool Character::isAlive() const {
return mHealth > 0;
}
void Character::takeDamage(int damage) {
if (damage < 0) {
damage = 0;
}
mHealth -= damage;
if (mHealth < 0) {
mHealth = 0;
}
}
void Character::heal() {
mHealth += mHealAmount;
}#include "combat-encounter.h"
#include <iostream>
CombatEncounter::CombatEncounter(const Character& hero, const Character& enemy)
: mHero(hero), mEnemy(enemy) {}
void CombatEncounter::run() {
std::cout << "A fight begins!" << std::endl;
std::cout << std::endl;
std::cout << mHero.getName() << std::endl;
std::cout << "Health: " << mHero.getHealth() << std::endl;
std::cout << "Attack: " << mHero.getAttack() << std::endl;
std::cout << "Defense: " << mHero.getDefense() << std::endl;
std::cout << "Heal Amount: " << mHero.getHealAmount() << std::endl;
std::cout << std::endl;
std::cout << mEnemy.getName() << std::endl;
std::cout << "Health: " << mEnemy.getHealth() << std::endl;
std::cout << "Attack: " << mEnemy.getAttack() << std::endl;
std::cout << "Defense: " << mEnemy.getDefense() << std::endl;
std::cout << "Heal Amount: " << mEnemy.getHealAmount() << std::endl;
std::cout << std::endl;
while (mHero.isAlive() && mEnemy.isAlive()) {
std::cout << mHero.getName() << ": " << mHero.getHealth() << " HP" << std::endl;
std::cout << mEnemy.getName() << ": " << mEnemy.getHealth() << " HP" << std::endl;
std::cout << "1. Attack" << std::endl;
std::cout << "2. Heal" << std::endl;
int choice = 0;
std::cin >> choice;
if (choice == ATTACK) {
int damage = mHero.getAttack() - mEnemy.getDefense();
mEnemy.takeDamage(damage);
std::cout << mHero.getName() << " attacks " << mEnemy.getName() << " for " << damage << " damage." << std::endl;
} else if (choice == HEAL) {
mHero.heal();
std::cout << mHero.getName() << " heals " << mHero.getHealAmount() << " HP." << std::endl;
} else {
std::cout << "Invalid action." << std::endl;
}
if (mEnemy.isAlive()) {
int damage = mEnemy.getAttack() - mHero.getDefense();
mHero.takeDamage(damage);
mEnemy.heal();
std::cout << mEnemy.getName() << " attacks " << mHero.getName() << " for " << damage << " damage." << std::endl;
}
}
if (mHero.isAlive()) {
std::cout << "Victory!" << std::endl;
} else {
std::cout << "Defeat!" << std::endl;
}
}#include "character.h"
#include "combat-encounter.h"
int main() {
Character Knight("Knight", 24, 10, 3,12);
Character Goblin("Goblin", 30, 10, 2, 2);
CombatEncounter combatEncounter(Knight,Goblin);
combatEncounter.run();
return 0;
}-xc++
-std=c++17
-IincludeEXECUTABLE = combat-encounter
CXX = g++
CXXFLAGS = -std=c++17 -Iinclude
DEBUG_FLAGS = -g -O0
SOURCES = $(wildcard source/*.cpp)
OBJECTS = $(patsubst source/%.cpp,build/objects/%.o,$(SOURCES))
DEPENDENCIES = $(patsubst source/%.cpp,build/dependencies/%.d,$(SOURCES))
all: build/binaries/$(EXECUTABLE)
debug: clean
$(MAKE) all CXXFLAGS="$(CXXFLAGS) $(DEBUG_FLAGS)"
build:
mkdir -p build/objects build/dependencies build/binaries
build/binaries/$(EXECUTABLE): $(OBJECTS) | build
$(CXX) $(OBJECTS) -o build/binaries/$(EXECUTABLE)
build/objects/%.o: source/%.cpp | build
$(CXX) $(CXXFLAGS) -MMD -MF build/dependencies/$*.d -c $< -o $@
clean:
rm -rf build
run:
./build/binaries/$(EXECUTABLE)
.PHONY: all debug build clean run
-include $(DEPENDENCIES)You can add this lab to the task inputs in tasks.json and create a configuration in launch.json in order to compile, run, and debug it through the VSCodium interface, if you wish to do so.
Run the code and try to understand the program’s behavior.
Hints
We have a simple combat (CombatEncounter) between a hero and an enemy.
Character:
Both characters are instances of Character, with the attributes name, health, attack, defense, and healAmount. A character is considered alive if health > 0. It can take damage by losing health points and can heal by gaining healAmount health points.
CombatEncounter::run():
For now, the run() method of CombatEncounter is responsible for many tasks, including:
displaying the introduction,
the combat loop,
displaying each character’s health,
the hero’s turn with action selection,
damage calculation and display,
the enemy’s turn with the same responsibilities,
displaying the end of the fight.
Our goal is to refactor CombatEncounter::run(), which will likely lead us to modify Character, CombatEncounter, and introduce new classes.
Compile and run regularly
Since the code is currently poorly structured, it is difficult to write unit tests for CombatEncounter::run(). In well-structured code, it is usually possible to test the sub-functions of run().
It is therefore important to compile and run the code regularly to ensure that the program’s behavior remains correct.
For example, with the data defined in main:
KnightandGoblinstart with24 HPand30 HP, respectively.Knightdeals 8 damage per attack.Goblindeals 7 damage per attack.If
Knightfollows the sequenceAttack,Attack,Attack,Heal,Attack,Attack, then he wins with1 HP.
First, we will extract the output operations into static methods in a
CombatPrinterclass.
Hints
Each std::cout corresponds to a type of output that should be placed in CombatPrinter.
In this class, you should identify the following methods (their signatures are for you to determine):
printIntroductionprintHealthprintAttackprintHealprintInvalidActionprintResult
In the implementation of the output methods in CombatPrinter, some outputs are related to a single character, such as displaying its attributes or its health.
In this case, the responsibility naturally belongs to the Character class.
You can extract the display of character attributes into
Character::displayInformationand the display of health intoCharacter::displayHealth.
Once the functions related to output (std::cout) are properly refactored, we will move on to handling user input (std::cin).
For now, this responsibility is directly handled by CombatEncounter using enum Action.
We will extract
enum Actionand move it into a classInputAction, with a methodreadActionthat retrieves user input.
Hints
#ifndef INPUT_ACTION_H
#define INPUT_ACTION_H
class InputAction {
public:
static int readAction();
enum Action {
ATTACK = 1,
HEAL = 2
};
};
#endifNow, we want the combat loop to be easy to read. A pseudo-code of CombatEncounter::run() should look like this:
display the introduction;
while the hero and the enemy are alive:
display the characters’ health,
the hero plays their turn,
if the enemy is still alive, the enemy plays their turn;
display the result.
We will extract the hero’s and the enemy’s turns into
CombatEncounter::playHeroTurn()andCombatEncounter::playEnemyTurn()so thatCombatEncounter::run()properly follows The Stepdown Rule.
Finally, the damage calculation in both the hero’s and the enemy’s turns is the same. Extracting this calculation into a function makes the code less redundant and easier to modify if the damage formula changes in the future.
We will therefore extract the calculation of
int damageinto a methodCombatEncounter::calculateDamage.
Have you completed your task?
Each function should be easy and clear to read.
If a method does not need to be used outside the class, it should be
private.If a method does not modify the class instance, it should be
const.If a method does not modify an argument, that argument should be
const.To work on the same object passed as an argument without copying it, do not forget to use
&(except for lightweight types such asintorbool).