// 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 #include #include #include #include #include #include #include #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 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() == "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(&ev.payload); if (v != nullptr) { int_received = *v; } }); bus.subscribe("a_ent", [&](const Event& ev) { if (std::get_if(&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(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> 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(EntityLoader::loadTemplate("sarcophagus")); sarc1->place(0, 0, {}); auto sarc2 = std::make_unique(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(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 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() == 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; }