Combat encounter¶
Nous allons refactoriser le code suivant afin d’appliquer les principes de responsabilité unique et de Stepdown Rule.
Créez un répertoire
TP12/ainsi que les répertoires et fichiers suivants.
TP12/
├── 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 "character.h"
#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)Vous pouvez ajouter ce TP aux inputs des tâches dans tasks.json et créer une configuration dans launch.json afin de pouvoir le compiler, l’exécuter et le déboguer via l’interface de VSCodium, si vous le souhaitez.
Exécutez le code et essayez de comprendre le comportement du programme.
Indices
Nous avons un combat simple (CombatEncounter) entre un héros et un ennemi.
Character :
Les deux personnages sont des instances de Character, avec les attributs name, health, attack, defense et healAmount. Un personnage est considéré comme vivant si health > 0. Il peut subir des dégâts en perdant des points de health et se soigner en gagnant healAmount points de health.
CombatEncounter::run() :
Pour le moment, la méthode run() de CombatEncounter est responsable de nombreuses tâches, notamment :
l’affichage de l’introduction,
la boucle de combat,
l’affichage des points de vie de chaque personnage,
le tour du héros avec le choix de son action,
le calcul des dégâts et leur affichage,
le tour de l’ennemi avec les mêmes responsabilités,
l’affichage de la fin du combat.
Notre objectif est de refactoriser CombatEncounter::run(), ce qui nous amènera probablement à modifier Character, CombatEncounter et à introduire de nouvelles classes.
Compilez et exécutez régulièrement
Comme le code est actuellement mal factorisé, il est difficile d’écrire des tests unitaires pour CombatEncounter::run(). Dans un code bien factorisé, il est normalement possible de tester les sous-fonctions de run().
Il est donc important de compiler et d’exécuter régulièrement le code afin de vérifier que le comportement du programme reste correct.
Par exemple, avec les données définies dans main :
KnightetGoblincommencent respectivement avec24 HPet30 HP.Knightinflige 8 points de dégâts par attaque.Goblininflige 7 points de dégâts par attaque.Si
Knightsuit la séquenceAttack,Attack,Attack,Heal,Attack,Attack, alors il gagne avec1 HP.
Dans un premier temps, nous allons extraire les affichages sous forme de méthodes statiques dans une classe
CombatPrinter.
Indices
Chaque std::cout correspond à un type d’affichage qui devrait se trouver dans CombatPrinter.
Dans cette classe, vous devez identifier les méthodes suivantes (leurs signatures sont à déterminer) :
printIntroductionprintHealthprintAttackprintHealprintInvalidActionprintResult
Dans l’implémentation des méthodes d’affichage de CombatPrinter, certains affichages concernent uniquement un personnage, comme ses attributs ou ses points de vie.
Dans ce cas, la responsabilité revient naturellement à la classe Character.
Vous pouvez extraire l’affichage des attributs des personnages dans
Character::displayInformationet l’affichage des points de vie dansCharacter::displayHealth.
Une fois les fonctions liées à la sortie (std::cout) correctement factorisées, nous allons passer à la lecture des actions de l’utilisateur (std::cin).
Pour le moment, cette responsabilité est directement gérée par CombatEncounter à l’aide de enum Action.
Nous allons extraire
enum Actionet le placer dans une classeInputAction, avec une méthodereadActionqui récupère l’entrée de l’utilisateur.
Indices
#ifndef INPUT_ACTION_H
#define INPUT_ACTION_H
class InputAction {
public:
static int readAction();
enum Action {
ATTACK = 1,
HEAL = 2
};
};
#endifMaintenant, nous voulons que la boucle de combat soit facile à lire. Un pseudo-code de CombatEncounter::run() devrait ressembler à ceci :
afficher l’introduction ;
tant que le héros et l’ennemi sont vivants :
afficher les points de vie des personnages,
le héros joue son tour,
si l’ennemi est encore vivant, il joue son tour ;
afficher le résultat.
Nous allons extraire les tours du héros et de l’ennemi dans
CombatEncounter::playHeroTurn()etCombatEncounter::playEnemyTurn()afin queCombatEncounter::run()respecte correctement The Stepdown Rule.
Finalement, le calcul des dégâts dans le tour du héros et celui de l’ennemi est identique. Extraire ce calcul dans une fonction permet de rendre le code moins redondant et plus facile à modifier si la formule de calcul évolue par la suite.
Nous allons donc extraire le calcul de
int damagedans une méthodeCombatEncounter::calculateDamage.
Avez-vous accompli votre tâche ?
La lecture de chaque fonction doit être simple et fluide.
Si une méthode n’a pas besoin d’être utilisée en dehors de la classe, elle doit être
private.Si une méthode ne modifie pas l’objet de sa classe, elle doit être
const.Si une méthode ne modifie pas un argument, cet argument doit être
const.Pour travailler sur un même objet passé en argument sans le copier, n’oubliez pas d’utiliser
&(sauf pour les types légers commeintoubool).