// Smoke test de la FSM de sala (RoomFsm) extreta de GameScene. Cobreix: // T1 loadConfig — fitxer real → tots els camps llegits. // T2 loadConfig — fitxer absent → fallbacks compilats; corbes default // resoltes per UiConfig::curveByName. // T3 loadConfig — claus parcials → camps absents conserven fallback. // T4 advance — ROOM_INTRO consumeix les 5 sub-fases en ordre. // T5 advance — ROOM_INTRO desemboca a PLAYING quan acaba l'última fase. // T6 advance — PLAYING és sumidor: no avança res ni canvia d'estat. // T7 advance — ROOM_OUTRO consumeix les 4 sub-fases i recau a ROOM_INTRO. // T8 advance — un delta gran salta més d'una sub-fase en un sol pas. // T9 formatRoomTitle — substitueix `{n}` quan el Locale té la clau. // T10 formatRoomTitle — sense Locale, retorna la pròpia clau (fallback). // T11 stateName — strings esperats per a cada State. // // El render no es testeja: la FSM és pura i no pinta res. Les regles de // gating d'input dels jugadors viuen a `GameScene::update` i estan // cobertes pel smoke run del binari (xvfb-run) — la part testable és // que `advance` retorna l'estat correcte, que és el senyal que activa // el gating al callsite. // // Execució: // make build && cd build && ctest --output-on-failure // O bé: // ./build/test_game_fsm #include #include #include #include #include #include "core/locale/locale.hpp" #include "core/ui/ui_config.hpp" #include "game/scenes/room_fsm.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 CHECK(cond) check((cond), #cond, __FILE__, __LINE__) auto writeTemp(const std::string& path, const std::string& content) -> std::string { std::ofstream file(path); file << content; return path; } void testLoadConfigFull() { std::cout << "T1 loadConfig — fitxer real complet\n"; const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_full.yaml").string(); writeTemp(PATH, "room_intro:\n" " fade_in_ms: 100\n" " text_delay_ms: 200\n" " text_enter_ms: 300\n" " text_hold_ms: 400\n" " text_exit_ms: 500\n" "room_outro:\n" " text_enter_ms: 600\n" " text_hold_ms: 700\n" " text_exit_ms: 800\n" " fade_out_ms: 900\n" "text:\n" " enter_curve: outQuad\n" " exit_curve: inQuad\n"); const auto CFG = RoomFsm::loadConfig(PATH); CHECK(CFG.intro_fade_in_ms == 100); CHECK(CFG.intro_text_delay_ms == 200); CHECK(CFG.intro_text_enter_ms == 300); CHECK(CFG.intro_text_hold_ms == 400); CHECK(CFG.intro_text_exit_ms == 500); CHECK(CFG.outro_text_enter_ms == 600); CHECK(CFG.outro_text_hold_ms == 700); CHECK(CFG.outro_text_exit_ms == 800); CHECK(CFG.outro_fade_out_ms == 900); CHECK(CFG.text_enter_curve == UiConfig::curveByName("outQuad")); CHECK(CFG.text_exit_curve == UiConfig::curveByName("inQuad")); std::filesystem::remove(PATH); } void testLoadConfigMissing() { std::cout << "T2 loadConfig — fitxer absent (fallbacks)\n"; const auto CFG = RoomFsm::loadConfig("/tmp/aee_2026_does_not_exist.yaml"); CHECK(CFG.intro_fade_in_ms == 500); CHECK(CFG.intro_text_delay_ms == 500); CHECK(CFG.intro_text_enter_ms == 500); CHECK(CFG.intro_text_hold_ms == 1000); CHECK(CFG.intro_text_exit_ms == 500); CHECK(CFG.outro_text_enter_ms == 500); CHECK(CFG.outro_text_hold_ms == 1000); CHECK(CFG.outro_text_exit_ms == 500); CHECK(CFG.outro_fade_out_ms == 500); CHECK(CFG.text_enter_curve != nullptr); CHECK(CFG.text_exit_curve != nullptr); } void testLoadConfigPartial() { std::cout << "T3 loadConfig — claus parcials (mantenir fallback)\n"; const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_partial.yaml").string(); writeTemp(PATH, "room_intro:\n" " fade_in_ms: 42\n"); const auto CFG = RoomFsm::loadConfig(PATH); CHECK(CFG.intro_fade_in_ms == 42); CHECK(CFG.intro_text_delay_ms == 500); // fallback CHECK(CFG.outro_fade_out_ms == 500); // fallback std::filesystem::remove(PATH); } auto makeShortConfig() -> RoomFsm::Config { // Durades curtes per fer els tests d'`advance` deterministes i // independents dels valors per defecte. RoomFsm::Config cfg{}; cfg.intro_fade_in_ms = 10; cfg.intro_text_delay_ms = 20; cfg.intro_text_enter_ms = 30; cfg.intro_text_hold_ms = 40; cfg.intro_text_exit_ms = 50; cfg.outro_text_enter_ms = 11; cfg.outro_text_hold_ms = 22; cfg.outro_text_exit_ms = 33; cfg.outro_fade_out_ms = 44; return cfg; } void testAdvanceIntroPhases() { std::cout << "T4 advance — ROOM_INTRO consumeix sub-fases en ordre\n"; const auto CFG = makeShortConfig(); RoomFsm::Transition t{}; auto s = RoomFsm::State::ROOM_INTRO; s = RoomFsm::advance(s, t, CFG, 5); // dins fase 0 CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 0 && t.elapsed_ms == 5); s = RoomFsm::advance(s, t, CFG, 10); // surt de fase 0, entra a fase 1 (5 ms dins) CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 1 && t.elapsed_ms == 5); s = RoomFsm::advance(s, t, CFG, 20); // surt de fase 1 (15 dins), entra a fase 2 amb 5 ms (5+20-20=5) CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 2 && t.elapsed_ms == 5); } void testAdvanceIntroToPlaying() { std::cout << "T5 advance — ROOM_INTRO acaba a PLAYING\n"; const auto CFG = makeShortConfig(); RoomFsm::Transition t{}; const int TOTAL = CFG.intro_fade_in_ms + CFG.intro_text_delay_ms + CFG.intro_text_enter_ms + CFG.intro_text_hold_ms + CFG.intro_text_exit_ms; const auto S = RoomFsm::advance(RoomFsm::State::ROOM_INTRO, t, CFG, TOTAL); CHECK(S == RoomFsm::State::PLAYING); } void testAdvancePlayingNoOp() { std::cout << "T6 advance — PLAYING és sumidor\n"; const auto CFG = makeShortConfig(); RoomFsm::Transition t{42, 999}; const auto S = RoomFsm::advance(RoomFsm::State::PLAYING, t, CFG, 1000); CHECK(S == RoomFsm::State::PLAYING); CHECK(t.phase == 42); // intacte CHECK(t.elapsed_ms == 999); } void testAdvanceOutroToIntro() { std::cout << "T7 advance — ROOM_OUTRO recau a ROOM_INTRO\n"; const auto CFG = makeShortConfig(); RoomFsm::Transition t{}; const int TOTAL = CFG.outro_text_enter_ms + CFG.outro_text_hold_ms + CFG.outro_text_exit_ms + CFG.outro_fade_out_ms; const auto S = RoomFsm::advance(RoomFsm::State::ROOM_OUTRO, t, CFG, TOTAL); CHECK(S == RoomFsm::State::ROOM_INTRO); } void testAdvanceLargeDeltaJumps() { std::cout << "T8 advance — un delta gran salta múltiples sub-fases\n"; const auto CFG = makeShortConfig(); RoomFsm::Transition t{}; // 10+20+5 = 35 ms → consumeix fase 0 i 1, queda a fase 2 amb 5 ms. const auto S = RoomFsm::advance(RoomFsm::State::ROOM_INTRO, t, CFG, 35); CHECK(S == RoomFsm::State::ROOM_INTRO); CHECK(t.phase == 2); CHECK(t.elapsed_ms == 5); } void testFormatRoomTitleWithLocale() { std::cout << "T9 formatRoomTitle — substitució amb Locale\n"; const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale.yaml").string(); writeTemp(PATH, "game:\n room_intro: \"Sala {n}\"\n"); Locale::get().load(PATH); CHECK(RoomFsm::formatRoomTitle(1) == "Sala 1"); CHECK(RoomFsm::formatRoomTitle(7) == "Sala 7"); std::filesystem::remove(PATH); } void testFormatRoomTitleNoLocale() { std::cout << "T10 formatRoomTitle — fallback sense traducció\n"; const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale_empty.yaml").string(); writeTemp(PATH, "other: foo\n"); Locale::get().load(PATH); // Sense la clau "game.room_intro", Locale retorna la pròpia clau, // i com que no porta `{n}`, la substitució no canvia res. CHECK(RoomFsm::formatRoomTitle(3) == "game.room_intro"); std::filesystem::remove(PATH); } void testStateName() { std::cout << "T11 stateName — strings esperats\n"; CHECK(std::string(RoomFsm::stateName(RoomFsm::State::ROOM_INTRO)) == "ROOM_INTRO"); CHECK(std::string(RoomFsm::stateName(RoomFsm::State::PLAYING)) == "PLAYING"); CHECK(std::string(RoomFsm::stateName(RoomFsm::State::ROOM_OUTRO)) == "ROOM_OUTRO"); } } // namespace auto main() -> int { testLoadConfigFull(); testLoadConfigMissing(); testLoadConfigPartial(); testAdvanceIntroPhases(); testAdvanceIntroToPlaying(); testAdvancePlayingNoOp(); testAdvanceOutroToIntro(); testAdvanceLargeDeltaJumps(); testFormatRoomTitleWithLocale(); testFormatRoomTitleNoLocale(); testStateName(); if (g_failures == 0) { std::cout << "OK: tots els tests de RoomFsm passen\n"; return 0; } std::cerr << g_failures << " tests han fallat\n"; return 1; }