Files
aee-arcade/tests/test_entities_smoke.cpp
T

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;
}