feat(entities): andamiatge del sistema d'entitats (classes, factory, bus, YAML, placeholders, smoke tests)
This commit is contained in:
1 parent
984cc820c0
commit
96b71b0f65
48 files changed
+1957
No files matched your search
@@ -0,0 +1,282 @@
|
||||
// Smoke tests del sistema d'entitats. Stand-alone, sense frameworks externs.
|
||||
//
|
||||
// Tests coberts:
|
||||
// T1 factory — tots els tipus es registren i es creen.
|
||||
// T2 loader — llegeix `data/maps/test_entities.yaml` i retorna els spawns.
|
||||
// T3 event bus — subscribe + publish + payload (entity*, int).
|
||||
// T4 sarcòfag — perímetre computat correctament; notifyTileEntered
|
||||
// passa a OPENING quan es visiten tots els tiles.
|
||||
// T5 manager — 2 sarcs + 1 door: forçant l'obertura dels sarcs, la porta
|
||||
// s'obri sola; contactar la porta OPEN emet `level_complete`.
|
||||
// T6 animation — 3 frames @ 10 fps, `tick(0.15)` → frame índex == 1.
|
||||
// T7 plantilles — carrega els 7 YAML d'entitat sense excepcions.
|
||||
// T8 decoration — Torch no col·lisiona ni és interactable.
|
||||
//
|
||||
// Execució (des de la ruta del repo):
|
||||
// make build && cd build && ctest --output-on-failure
|
||||
// O bé:
|
||||
// ./build/test_entities_smoke
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "game/entities/animation_component.hpp"
|
||||
#include "game/entities/concrete/door.hpp"
|
||||
#include "game/entities/concrete/floor_trap.hpp"
|
||||
#include "game/entities/concrete/mummy.hpp"
|
||||
#include "game/entities/concrete/rolling_rock.hpp"
|
||||
#include "game/entities/concrete/sarcophagus.hpp"
|
||||
#include "game/entities/concrete/torch.hpp"
|
||||
#include "game/entities/concrete/wall_trap.hpp"
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/entity_loader.hpp"
|
||||
#include "game/entities/entity_manager.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void check(bool cond, const char* msg, const char* file, int line) {
|
||||
if (!cond) {
|
||||
std::cerr << " FAIL: " << msg << " (" << file << ":" << line << ")\n";
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
|
||||
#define REQUIRE(cond) check((cond), #cond, __FILE__, __LINE__)
|
||||
|
||||
void testFactory() {
|
||||
std::cout << "[T1] factory\n";
|
||||
auto& factory = EntityFactory::instance();
|
||||
|
||||
const std::vector<std::string> TYPES = {
|
||||
"torch", "sarcophagus", "door", "mummy",
|
||||
"rolling_rock", "wall_trap", "floor_trap",
|
||||
};
|
||||
for (const auto& type : TYPES) {
|
||||
fkyaml::node tmpl = EntityLoader::loadTemplate(type);
|
||||
auto ent = factory.create(type, tmpl);
|
||||
REQUIRE(ent != nullptr);
|
||||
if (ent != nullptr) { REQUIRE(ent->getName() == type); }
|
||||
}
|
||||
}
|
||||
|
||||
void testLoader() {
|
||||
std::cout << "[T2] loader\n";
|
||||
auto spawns = EntityLoader::readSpawns("data/maps/test_entities.yaml");
|
||||
// Al test_entities.yaml hem declarat 5 spawns.
|
||||
REQUIRE(spawns.size() == 5);
|
||||
|
||||
// Primer spawn és torch.
|
||||
if (!spawns.empty()) {
|
||||
REQUIRE(spawns[0]["type"].get_value<std::string>() == "torch");
|
||||
}
|
||||
}
|
||||
|
||||
void testEventBus() {
|
||||
std::cout << "[T3] event bus\n";
|
||||
EventBus bus;
|
||||
|
||||
int int_received = -1;
|
||||
int entity_ptr_flag = 0;
|
||||
bus.subscribe("a_int", [&](const Event& ev) {
|
||||
const int* v = std::get_if<int>(&ev.payload);
|
||||
if (v != nullptr) { int_received = *v; }
|
||||
});
|
||||
bus.subscribe("a_ent", [&](const Event& ev) {
|
||||
if (std::get_if<Entity*>(&ev.payload) != nullptr) { entity_ptr_flag = 1; }
|
||||
});
|
||||
|
||||
bus.publish("a_int", 42);
|
||||
REQUIRE(int_received == 42);
|
||||
|
||||
Entity fake;
|
||||
bus.publish("a_ent", &fake);
|
||||
REQUIRE(entity_ptr_flag == 1);
|
||||
|
||||
// Publish d'un nom sense subscriptors no ha de crashejar.
|
||||
bus.publish("desconegut");
|
||||
|
||||
// Unsubscribe elimina els handlers.
|
||||
auto id = bus.subscribe("a_int", [&](const Event&) { int_received = 999; });
|
||||
bus.unsubscribe(id);
|
||||
bus.publish("a_int", 7);
|
||||
REQUIRE(int_received == 7);
|
||||
}
|
||||
|
||||
void testSarcophagusPerimeter() {
|
||||
std::cout << "[T4] sarcofag perimetre\n";
|
||||
fkyaml::node tmpl = EntityLoader::loadTemplate("sarcophagus");
|
||||
auto sarc = std::make_unique<Sarcophagus>(tmpl);
|
||||
// Ancorat a (col=1, row=1) amb tile_size=[3,1] → rectangle de tiles
|
||||
// cobreix (1,1), (2,1), (3,1). Perímetre ortogonal esperat (sense
|
||||
// BlockedPredicate):
|
||||
// Nord: (1,0), (2,0), (3,0)
|
||||
// Sud: (1,2), (2,2), (3,2)
|
||||
// Oest: (0,1)
|
||||
// Est: (4,1)
|
||||
// Total: 8 tiles.
|
||||
sarc->place(1, 1, {});
|
||||
REQUIRE(sarc->getPerimeterSize() == 8);
|
||||
REQUIRE(sarc->getState() == Sarcophagus::State::CLOSED);
|
||||
|
||||
// Visitem un tile que NO és al perímetre: res canvia.
|
||||
sarc->notifyTileEntered(5, 5);
|
||||
REQUIRE(sarc->getVisitedCount() == 0);
|
||||
|
||||
// Visitem tots els del perímetre.
|
||||
const std::vector<std::pair<int, int>> PERIM = {
|
||||
{1, 0}, {2, 0}, {3, 0}, {1, 2}, {2, 2}, {3, 2}, {0, 1}, {4, 1},
|
||||
};
|
||||
for (const auto& [c, r] : PERIM) { sarc->notifyTileEntered(c, r); }
|
||||
REQUIRE(sarc->getVisitedCount() == 8);
|
||||
REQUIRE(sarc->getState() == Sarcophagus::State::OPENING);
|
||||
|
||||
// Un update amb l'animació `opening` d'1 frame loop=false completa
|
||||
// la transició a OPEN.
|
||||
sarc->update(0.0F);
|
||||
REQUIRE(sarc->getState() == Sarcophagus::State::OPEN);
|
||||
}
|
||||
|
||||
void testEntityManagerEndToEnd() {
|
||||
std::cout << "[T5] manager end-to-end\n";
|
||||
EntityManager manager;
|
||||
|
||||
// 2 sarcòfags, 1 porta.
|
||||
auto sarc1 = std::make_unique<Sarcophagus>(EntityLoader::loadTemplate("sarcophagus"));
|
||||
sarc1->place(0, 0, {});
|
||||
auto sarc2 = std::make_unique<Sarcophagus>(EntityLoader::loadTemplate("sarcophagus"));
|
||||
sarc2->place(4, 4, {});
|
||||
Sarcophagus* s1 = manager.addSarcophagus(std::move(sarc1));
|
||||
Sarcophagus* s2 = manager.addSarcophagus(std::move(sarc2));
|
||||
REQUIRE(manager.sarcophagusCount() == 2);
|
||||
|
||||
auto door = std::make_unique<Door>(EntityLoader::loadTemplate("door"));
|
||||
Door* door_ptr = door.get();
|
||||
manager.add(std::move(door));
|
||||
manager.setDoor(door_ptr);
|
||||
|
||||
// Subscriptor local per capturar `level_complete`.
|
||||
bool level_complete_fired = false;
|
||||
manager.getBus().subscribe("level_complete", [&](const Event&) {
|
||||
level_complete_fired = true;
|
||||
});
|
||||
|
||||
// Forcem l'obertura dels dos sarcs visitant els seus perímetres.
|
||||
auto forceOpen = [](Sarcophagus* s) {
|
||||
const int COL0 = s->getTilePos().col;
|
||||
const int ROW0 = s->getTilePos().row;
|
||||
const int W = s->getTileWidth();
|
||||
const int H = s->getTileHeight();
|
||||
for (int dc = 0; dc < W; ++dc) {
|
||||
s->notifyTileEntered(COL0 + dc, ROW0 - 1);
|
||||
s->notifyTileEntered(COL0 + dc, ROW0 + H);
|
||||
}
|
||||
for (int dr = 0; dr < H; ++dr) {
|
||||
s->notifyTileEntered(COL0 - 1, ROW0 + dr);
|
||||
s->notifyTileEntered(COL0 + W, ROW0 + dr);
|
||||
}
|
||||
};
|
||||
forceOpen(s1);
|
||||
forceOpen(s2);
|
||||
|
||||
// Un primer tick: cada sarc transiciona OPENING → OPEN i publica;
|
||||
// l'handler del manager compta i, en arribar al total, crida
|
||||
// door->open() — que en aquest mateix tick (la iteració sobre
|
||||
// entities continua) farà transitar la porta OPENING → OPEN perquè
|
||||
// l'animació és d'1 frame loop=false.
|
||||
manager.update(0.0F);
|
||||
REQUIRE(manager.openedSarcophagusCount() == 2);
|
||||
REQUIRE(door_ptr->getState() == Door::State::OPEN);
|
||||
|
||||
// Contacte amb la porta oberta → level_complete.
|
||||
door_ptr->onPlayerContact(nullptr);
|
||||
REQUIRE(level_complete_fired);
|
||||
|
||||
// Un segon contacte NO torna a disparar (idempotent).
|
||||
level_complete_fired = false;
|
||||
door_ptr->onPlayerContact(nullptr);
|
||||
REQUIRE(!level_complete_fired);
|
||||
}
|
||||
|
||||
void testAnimationComponent() {
|
||||
std::cout << "[T6] animation component\n";
|
||||
AnimationComponent anim({10, 20, 30}, 10.0F, true);
|
||||
REQUIRE(anim.frameCount() == 3);
|
||||
REQUIRE(anim.currentFrame() == 10);
|
||||
|
||||
anim.tick(0.15F); // 1.5 frames de 10 fps → hauria d'avançar a l'índex 1
|
||||
REQUIRE(anim.currentFrame() == 20);
|
||||
|
||||
anim.tick(0.10F);
|
||||
REQUIRE(anim.currentFrame() == 30);
|
||||
|
||||
// Loop: un tick més i torna al principi.
|
||||
anim.tick(0.10F);
|
||||
REQUIRE(anim.currentFrame() == 10);
|
||||
|
||||
// Variant no-loop: es para al darrer i queda completed.
|
||||
AnimationComponent once({1, 2}, 10.0F, false);
|
||||
once.tick(1.0F); // molts frames; ha d'arribar al final i quedar-se.
|
||||
REQUIRE(once.currentFrame() == 2);
|
||||
REQUIRE(once.completed());
|
||||
|
||||
// Variant d'1 frame no-loop: completed des del principi.
|
||||
AnimationComponent single({5}, 1.0F, false);
|
||||
REQUIRE(single.completed());
|
||||
}
|
||||
|
||||
void testAllTemplatesLoad() {
|
||||
std::cout << "[T7] plantilles YAML\n";
|
||||
const std::vector<std::string> TYPES = {
|
||||
"torch", "sarcophagus", "door", "mummy",
|
||||
"rolling_rock", "wall_trap", "floor_trap",
|
||||
};
|
||||
for (const auto& type : TYPES) {
|
||||
try {
|
||||
fkyaml::node tmpl = EntityLoader::loadTemplate(type);
|
||||
REQUIRE(tmpl.contains("name"));
|
||||
REQUIRE(tmpl["name"].get_value<std::string>() == type);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << " excepció carregant '" << type << "': " << e.what() << '\n';
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void testDecorationFlags() {
|
||||
std::cout << "[T8] decoration flags\n";
|
||||
fkyaml::node tmpl = EntityLoader::loadTemplate("torch");
|
||||
Torch torch(tmpl);
|
||||
REQUIRE(!torch.collides());
|
||||
REQUIRE(!torch.interactable());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto main() -> int {
|
||||
std::cout << "== smoke tests: entity system ==\n";
|
||||
|
||||
testFactory();
|
||||
testLoader();
|
||||
testEventBus();
|
||||
testSarcophagusPerimeter();
|
||||
testEntityManagerEndToEnd();
|
||||
testAnimationComponent();
|
||||
testAllTemplatesLoad();
|
||||
testDecorationFlags();
|
||||
|
||||
if (g_failures == 0) {
|
||||
std::cout << "ALL OK\n";
|
||||
return 0;
|
||||
}
|
||||
std::cerr << g_failures << " smoke check(s) failed\n";
|
||||
return 1;
|
||||
}
|
||||
Reference in new issue
Block a user