diff --git a/hsm/limbo_hsm.cpp b/hsm/limbo_hsm.cpp index 09b2bcd..6ecc049 100644 --- a/hsm/limbo_hsm.cpp +++ b/hsm/limbo_hsm.cpp @@ -179,7 +179,7 @@ bool LimboHSM::dispatch(const String &p_event, const Variant &p_cargo) { } } - if (!event_consumed && p_event == EVENT_FINISHED && !(get_parent()->is_class("LimboState"))) { + if (!event_consumed && p_event == EVENT_FINISHED && !(get_parent() && get_parent()->is_class("LimboState"))) { _exit(); } diff --git a/tests/limbo_test.h b/tests/limbo_test.h index cbca952..f47c35f 100644 --- a/tests/limbo_test.h +++ b/tests/limbo_test.h @@ -24,10 +24,12 @@ public: int num_callbacks = 0; void callback() { num_callbacks += 1; } + void callback_delta(double delta) { num_callbacks += 1; } protected: static void _bind_methods() { ClassDB::bind_method(D_METHOD("callback"), &CallbackCounter::callback); + ClassDB::bind_method(D_METHOD("callback_delta", "delta"), &CallbackCounter::callback_delta); } }; diff --git a/tests/test_hsm.h b/tests/test_hsm.h new file mode 100644 index 0000000..bc981f5 --- /dev/null +++ b/tests/test_hsm.h @@ -0,0 +1,182 @@ +/** + * test_hsm.h + * ============================================================================= + * Copyright 2021-2023 Serhii Snitsaruk + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + * ============================================================================= + */ + +#ifndef TEST_HSM_H +#define TEST_HSM_H + +#include "limbo_test.h" + +#include "modules/limboai/hsm/limbo_hsm.h" +#include "modules/limboai/hsm/limbo_state.h" + +#include "core/object/object.h" +#include "core/object/ref_counted.h" +#include "core/os/memory.h" +#include "core/variant/variant.h" + +namespace TestHSM { + +class TestGuard : public RefCounted { + GDCLASS(TestGuard, RefCounted); + +public: + bool permitted_to_enter = false; + bool can_enter() { return permitted_to_enter; } +}; + +TEST_CASE("[Modules][LimboAI] HSM") { + Node *agent = memnew(Node); + LimboHSM *hsm = memnew(LimboHSM); + + Ref alpha_entries = memnew(CallbackCounter); + Ref alpha_exits = memnew(CallbackCounter); + Ref alpha_updates = memnew(CallbackCounter); + Ref beta_entries = memnew(CallbackCounter); + Ref beta_exits = memnew(CallbackCounter); + Ref beta_updates = memnew(CallbackCounter); + + LimboState *state_alpha = memnew(LimboState); + state_alpha->call_on_enter(callable_mp(alpha_entries.ptr(), &CallbackCounter::callback)); + state_alpha->call_on_update(callable_mp(alpha_updates.ptr(), &CallbackCounter::callback_delta)); + state_alpha->call_on_exit(callable_mp(alpha_exits.ptr(), &CallbackCounter::callback)); + + LimboState *state_beta = memnew(LimboState); + state_beta->call_on_enter(callable_mp(beta_entries.ptr(), &CallbackCounter::callback)); + state_beta->call_on_update(callable_mp(beta_updates.ptr(), &CallbackCounter::callback_delta)); + state_beta->call_on_exit(callable_mp(beta_exits.ptr(), &CallbackCounter::callback)); + + hsm->add_child(state_alpha); + hsm->add_child(state_beta); + + hsm->add_transition(state_alpha, state_beta, "event_one"); + hsm->add_transition(state_beta, state_alpha, "event_two"); + + hsm->set_initial_state(state_alpha); + hsm->initialize(agent); + hsm->set_active(true); + + SUBCASE("Test get_root()") { + CHECK(state_alpha->get_root() == hsm); + CHECK(state_beta->get_root() == hsm); + CHECK(hsm->get_root() == hsm); + } + SUBCASE("Test with basic workflow and transitions") { + REQUIRE(hsm->is_active()); + REQUIRE(hsm->get_active_state() == state_alpha); + CHECK(alpha_entries->num_callbacks == 1); // * entered + CHECK(alpha_updates->num_callbacks == 0); + CHECK(alpha_exits->num_callbacks == 0); + CHECK(beta_entries->num_callbacks == 0); + CHECK(beta_updates->num_callbacks == 0); + CHECK(beta_exits->num_callbacks == 0); + + hsm->update(0.01666); + CHECK(alpha_entries->num_callbacks == 1); + CHECK(alpha_updates->num_callbacks == 1); // * updated + CHECK(alpha_exits->num_callbacks == 0); + CHECK(beta_entries->num_callbacks == 0); + CHECK(beta_updates->num_callbacks == 0); + CHECK(beta_exits->num_callbacks == 0); + + hsm->update(0.01666); + CHECK(alpha_entries->num_callbacks == 1); + CHECK(alpha_updates->num_callbacks == 2); // * updated x2 + CHECK(alpha_exits->num_callbacks == 0); + CHECK(beta_entries->num_callbacks == 0); + CHECK(beta_updates->num_callbacks == 0); + CHECK(beta_exits->num_callbacks == 0); + + hsm->dispatch("event_one"); + REQUIRE(hsm->get_active_state() == state_beta); + CHECK(alpha_entries->num_callbacks == 1); + CHECK(alpha_updates->num_callbacks == 2); + CHECK(alpha_exits->num_callbacks == 1); // * (1) exited + CHECK(beta_entries->num_callbacks == 1); // * (2) entered + CHECK(beta_updates->num_callbacks == 0); + CHECK(beta_exits->num_callbacks == 0); + + hsm->update(0.01666); + CHECK(alpha_entries->num_callbacks == 1); + CHECK(alpha_updates->num_callbacks == 2); + CHECK(alpha_exits->num_callbacks == 1); + CHECK(beta_entries->num_callbacks == 1); + CHECK(beta_updates->num_callbacks == 1); // * updated + CHECK(beta_exits->num_callbacks == 0); + + hsm->update(0.01666); + CHECK(alpha_entries->num_callbacks == 1); + CHECK(alpha_updates->num_callbacks == 2); + CHECK(alpha_exits->num_callbacks == 1); + CHECK(beta_entries->num_callbacks == 1); + CHECK(beta_updates->num_callbacks == 2); // * updated x2 + CHECK(beta_exits->num_callbacks == 0); + + hsm->dispatch("event_two"); + REQUIRE(hsm->get_active_state() == state_alpha); + CHECK(alpha_entries->num_callbacks == 2); // * (2) entered + CHECK(alpha_updates->num_callbacks == 2); + CHECK(alpha_exits->num_callbacks == 1); + CHECK(beta_entries->num_callbacks == 1); + CHECK(beta_updates->num_callbacks == 2); + CHECK(beta_exits->num_callbacks == 1); // * (1) exited + + hsm->update(0.01666); + CHECK(alpha_entries->num_callbacks == 2); + CHECK(alpha_updates->num_callbacks == 3); // * updated + CHECK(alpha_exits->num_callbacks == 1); + CHECK(beta_entries->num_callbacks == 1); + CHECK(beta_updates->num_callbacks == 2); + CHECK(beta_exits->num_callbacks == 1); + + hsm->dispatch(LimboState::EVENT_FINISHED); + CHECK(alpha_entries->num_callbacks == 2); + CHECK(alpha_updates->num_callbacks == 3); + CHECK(alpha_exits->num_callbacks == 2); // * exited + CHECK(beta_entries->num_callbacks == 1); + CHECK(beta_updates->num_callbacks == 2); + CHECK(beta_exits->num_callbacks == 1); + CHECK_FALSE(hsm->is_active()); // * not active + CHECK(hsm->get_active_state() == nullptr); + } + SUBCASE("Test transition with guard") { + Ref guard = memnew(TestGuard); + state_beta->set_guard(callable_mp(guard.ptr(), &TestGuard::can_enter)); + + SUBCASE("When entry is permitted") { + guard->permitted_to_enter = true; + hsm->dispatch("event_one"); + CHECK(hsm->get_active_state() == state_beta); + CHECK(alpha_exits->num_callbacks == 1); + CHECK(beta_entries->num_callbacks == 1); + } + SUBCASE("When entry is not permitted") { + guard->permitted_to_enter = false; + hsm->dispatch("event_one"); + CHECK(hsm->get_active_state() == state_alpha); + CHECK(alpha_exits->num_callbacks == 0); + CHECK(beta_entries->num_callbacks == 0); + } + } + SUBCASE("When there is no transition for given event") { + hsm->dispatch("not_found"); + CHECK(alpha_exits->num_callbacks == 0); + CHECK(beta_entries->num_callbacks == 0); + CHECK(hsm->is_active()); + CHECK(hsm->get_active_state() == state_alpha); + } + + memdelete(agent); + memdelete(hsm); +} + +} //namespace TestHSM + +#endif // TEST_HSM_H