283 lines
10 KiB
C++
283 lines
10 KiB
C++
// 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;
|
|
}
|