Merge branch 'unit-testing'

This commit is contained in:
Serhii Snitsaruk 2023-09-12 10:03:30 +02:00
commit 476c4fd497
48 changed files with 3572 additions and 22 deletions

View File

@ -30,12 +30,12 @@ Variant BBNode::get_value(Object *p_agent, const Ref<Blackboard> &p_blackboard,
ERR_FAIL_COND_V_MSG(agent == nullptr, Variant(), "BBNode: p_agent must be a Node."); ERR_FAIL_COND_V_MSG(agent == nullptr, Variant(), "BBNode: p_agent must be a Node.");
return agent->get_node(val); return agent->get_node(val);
} else { } else {
Node *node = Object::cast_to<Node>(val); Object *obj = val;
if (unlikely(node == nullptr && val.get_type() != Variant::NIL)) { if (unlikely(obj == nullptr && val.get_type() != Variant::NIL)) {
WARN_PRINT("BBNode: Unexpected variant type of a blackboard variable."); WARN_PRINT("BBNode: Unexpected variant type of a blackboard variable.");
return p_default; return p_default;
} else { } else {
return node; return obj;
} }
} }
} }

View File

@ -72,7 +72,7 @@ Variant BBParam::get_value(Object *p_agent, const Ref<Blackboard> &p_blackboard,
if (value_source == SAVED_VALUE) { if (value_source == SAVED_VALUE) {
return saved_value; return saved_value;
} else { } else {
ERR_FAIL_COND_V_MSG(!p_blackboard->has_var(variable), Variant(), vformat("BBParam: Blackboard variable doesn't exist: \"%s\".", p_default)); ERR_FAIL_COND_V_MSG(!p_blackboard->has_var(variable), p_default, vformat("BBParam: Blackboard variable \"%s\" doesn't exist.", variable));
return p_blackboard->get_var(variable, p_default); return p_blackboard->get_var(variable, p_default);
} }
} }

View File

@ -199,7 +199,7 @@ void BTPlayer::_bind_methods() {
ADD_SIGNAL(MethodInfo("updated", PropertyInfo(Variant::INT, "p_status"))); ADD_SIGNAL(MethodInfo("updated", PropertyInfo(Variant::INT, "p_status")));
#ifdef DEBUG_ENABLED #ifdef DEBUG_ENABLED
ClassDB::bind_method(D_METHOD("_set_monitor_performance"), &BTPlayer::_set_monitor_performance); ClassDB::bind_method(D_METHOD("_set_monitor_performance", "p_value"), &BTPlayer::_set_monitor_performance);
ClassDB::bind_method(D_METHOD("_get_monitor_performance"), &BTPlayer::_get_monitor_performance); ClassDB::bind_method(D_METHOD("_get_monitor_performance"), &BTPlayer::_get_monitor_performance);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "monitor_performance"), "_set_monitor_performance", "_get_monitor_performance"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "monitor_performance"), "_set_monitor_performance", "_get_monitor_performance");
ADD_PROPERTY_DEFAULT("monitor_performance", false); ADD_PROPERTY_DEFAULT("monitor_performance", false);

View File

@ -11,6 +11,7 @@
#include "bt_set_var.h" #include "bt_set_var.h"
#include "modules/limboai/blackboard/bb_param/bb_param.h"
#include "modules/limboai/util/limbo_utility.h" #include "modules/limboai/util/limbo_utility.h"
#include "core/variant/callable.h" #include "core/variant/callable.h"
@ -24,9 +25,12 @@ String BTSetVar::_generate_name() const {
} }
int BTSetVar::_tick(double p_delta) { int BTSetVar::_tick(double p_delta) {
ERR_FAIL_COND_V_MSG(variable.is_empty(), FAILURE, "BBSetVar: `variable` is not set."); ERR_FAIL_COND_V_MSG(variable.is_empty(), FAILURE, "BTSetVar: `variable` is not set.");
ERR_FAIL_COND_V_MSG(!value.is_valid(), FAILURE, "BBSetVar: `value` is not set."); ERR_FAIL_COND_V_MSG(!value.is_valid(), FAILURE, "BTSetVar: `value` is not set.");
get_blackboard()->set_var(variable, value->get_value(get_agent(), get_blackboard())); Variant error_result = SNAME("Error: BTSetVar failed to get value!");
Variant result = value->get_value(get_agent(), get_blackboard(), error_result);
ERR_FAIL_COND_V_MSG(result == error_result, FAILURE, "BTSetVar: Failed to get parameter value. Returning FAILURE.");
get_blackboard()->set_var(variable, result);
return SUCCESS; return SUCCESS;
}; };

View File

@ -59,7 +59,7 @@ int BTCallMethod::_tick(double p_delta) {
ERR_FAIL_COND_V_MSG(method == StringName(), FAILURE, "BTCallMethod: Method Name is not set."); ERR_FAIL_COND_V_MSG(method == StringName(), FAILURE, "BTCallMethod: Method Name is not set.");
ERR_FAIL_COND_V_MSG(node_param.is_null(), FAILURE, "BTCallMethod: Node parameter is not set."); ERR_FAIL_COND_V_MSG(node_param.is_null(), FAILURE, "BTCallMethod: Node parameter is not set.");
Object *obj = node_param->get_value(get_agent(), get_blackboard()); Object *obj = node_param->get_value(get_agent(), get_blackboard());
ERR_FAIL_COND_V_MSG(obj == nullptr, FAILURE, "BTCallMethod: Failed to get node: " + node_param->to_string()); ERR_FAIL_COND_V_MSG(obj == nullptr, FAILURE, "BTCallMethod: Failed to get object: " + node_param->to_string());
const Variant **argptrs = nullptr; const Variant **argptrs = nullptr;

View File

@ -48,9 +48,13 @@ int BTSetAgentProperty::_tick(double p_delta) {
ERR_FAIL_COND_V_MSG(property == StringName(), FAILURE, "BTSetAgentProperty: `property` is not set."); ERR_FAIL_COND_V_MSG(property == StringName(), FAILURE, "BTSetAgentProperty: `property` is not set.");
ERR_FAIL_COND_V_MSG(!value.is_valid(), FAILURE, "BTSetAgentProperty: `value` is not set."); ERR_FAIL_COND_V_MSG(!value.is_valid(), FAILURE, "BTSetAgentProperty: `value` is not set.");
StringName error_value = SNAME("ErrorGettingValue");
Variant v = value->get_value(get_agent(), get_blackboard(), error_value);
ERR_FAIL_COND_V_MSG(v == Variant(error_value), FAILURE, "BTSetAgentProperty: Couldn't get value of value-parameter.");
bool r_valid; bool r_valid;
get_agent()->set(property, value->get_value(get_agent(), get_blackboard()), &r_valid); get_agent()->set(property, v, &r_valid);
ERR_FAIL_COND_V_MSG(!r_valid, FAILURE, vformat("BTSetAgentProperty: Agent doesn't have property named \"%s\"", property)); ERR_FAIL_COND_V_MSG(!r_valid, FAILURE, vformat("BTSetAgentProperty: Couldn't set property \"%s\" with value \"%s\"", property, v));
return SUCCESS; return SUCCESS;
} }

View File

@ -48,9 +48,9 @@ void BTRandomWait::set_max_duration(double p_max_duration) {
} }
void BTRandomWait::_bind_methods() { void BTRandomWait::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_min_duration"), &BTRandomWait::set_min_duration); ClassDB::bind_method(D_METHOD("set_min_duration", "p_value"), &BTRandomWait::set_min_duration);
ClassDB::bind_method(D_METHOD("get_min_duration"), &BTRandomWait::get_min_duration); ClassDB::bind_method(D_METHOD("get_min_duration"), &BTRandomWait::get_min_duration);
ClassDB::bind_method(D_METHOD("set_max_duration"), &BTRandomWait::set_max_duration); ClassDB::bind_method(D_METHOD("set_max_duration", "p_value"), &BTRandomWait::set_max_duration);
ClassDB::bind_method(D_METHOD("get_max_duration"), &BTRandomWait::get_max_duration); ClassDB::bind_method(D_METHOD("get_max_duration"), &BTRandomWait::get_max_duration);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "min_duration"), "set_min_duration", "get_min_duration"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "min_duration"), "set_min_duration", "get_min_duration");

View File

@ -23,8 +23,8 @@ void BTWaitTicks::_enter() {
} }
int BTWaitTicks::_tick(double p_delta) { int BTWaitTicks::_tick(double p_delta) {
num_passed += 1;
if (num_passed < num_ticks) { if (num_passed < num_ticks) {
num_passed += 1;
return RUNNING; return RUNNING;
} else { } else {
return SUCCESS; return SUCCESS;

View File

@ -4,6 +4,8 @@
Node-type parameter to be used with BT tasks. See [BBParam]. Node-type parameter to be used with BT tasks. See [BBParam].
</brief_description> </brief_description>
<description> <description>
Node-type parameter to be used with BT tasks. See [BBParam].
If blackboard is chosen as source, it allows any [Object]-extended type.
</description> </description>
<tutorials> <tutorials>
</tutorials> </tutorials>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<class name="BTCallMethod" inherits="BTAction" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <class name="BTCallMethod" inherits="BTAction" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description> <brief_description>
BT action that calls a method on the specified [Node]. BT action that calls a method on a specified [Node] or [Object].
</brief_description> </brief_description>
<description> <description>
BTCallMethod calls a method on the specified [Node] and returns [code]SUCCESS[/code]. BTCallMethod calls a method on a specified [Node] or [Object] and returns [code]SUCCESS[/code].
Returns [code]FAILURE[/code] if the action fails executing the node's method. Returns [code]FAILURE[/code] if the action fails executing the method.
</description> </description>
<tutorials> <tutorials>
</tutorials> </tutorials>
@ -14,10 +14,10 @@
Arguments to pass to the method call. Arguments to pass to the method call.
</member> </member>
<member name="method" type="StringName" setter="set_method" getter="get_method" default="&amp;&quot;&quot;"> <member name="method" type="StringName" setter="set_method" getter="get_method" default="&amp;&quot;&quot;">
Node's method that will be called. Method that will be called.
</member> </member>
<member name="node" type="BBNode" setter="set_node_param" getter="get_node_param"> <member name="node" type="BBNode" setter="set_node_param" getter="get_node_param">
Node parameter that will be used to get the node instance. Parameter that will be used to get the node/object instance.
</member> </member>
</members> </members>
</class> </class>

View File

@ -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(); _exit();
} }

View File

@ -16,6 +16,7 @@
#include "core/object/object.h" #include "core/object/object.h"
#include "core/templates/hash_map.h" #include "core/templates/hash_map.h"
#include "core/variant/variant.h"
class LimboHSM : public LimboState { class LimboHSM : public LimboState {
GDCLASS(LimboHSM, LimboState); GDCLASS(LimboHSM, LimboState);
@ -64,7 +65,7 @@ public:
LimboState *get_initial_state() const { return initial_state; } LimboState *get_initial_state() const { return initial_state; }
virtual void initialize(Node *p_agent, const Ref<Blackboard> &p_parent_scope = nullptr); virtual void initialize(Node *p_agent, const Ref<Blackboard> &p_parent_scope = nullptr);
virtual bool dispatch(const String &p_event, const Variant &p_cargo) override; virtual bool dispatch(const String &p_event, const Variant &p_cargo = Variant()) override;
void update(double p_delta) { _update(p_delta); } void update(double p_delta) { _update(p_delta); }
void add_transition(Node *p_from_state, Node *p_to_state, const String &p_event); void add_transition(Node *p_from_state, Node *p_to_state, const String &p_event);

View File

@ -69,7 +69,7 @@ public:
Node *get_agent() const { return agent; } Node *get_agent() const { return agent; }
void set_agent(Node *p_agent) { agent = p_agent; } void set_agent(Node *p_agent) { agent = p_agent; }
virtual bool dispatch(const String &p_event, const Variant &p_cargo); virtual bool dispatch(const String &p_event, const Variant &p_cargo = Variant());
LimboState *named(String p_name); LimboState *named(String p_name);
LimboState *call_on_enter(const Callable &p_callable); LimboState *call_on_enter(const Callable &p_callable);

77
tests/limbo_test.h Normal file
View File

@ -0,0 +1,77 @@
/**
* limbo_test.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 LIMBO_TEST_H
#define LIMBO_TEST_H
#include "core/object/ref_counted.h"
#include "tests/test_macros.h"
#include "modules/limboai/bt/tasks/bt_action.h"
class CallbackCounter : public RefCounted {
GDCLASS(CallbackCounter, RefCounted);
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);
}
};
class BTTestAction : public BTAction {
GDCLASS(BTTestAction, BTAction);
public:
int ret_status = BTTask::SUCCESS;
int num_entries = 0;
int num_ticks = 0;
int num_exits = 0;
protected:
virtual void _enter() override { num_entries += 1; }
virtual void _exit() override { num_exits += 1; }
virtual int _tick(double p_delta) override {
num_ticks += 1;
return ret_status;
}
public:
bool is_status_either(int p_status1, int p_status2) { return (get_status() == p_status1 || get_status() == p_status2); }
BTTestAction(int p_return_status) { ret_status = p_return_status; }
BTTestAction() {}
};
#define CHECK_ENTRIES_TICKS_EXITS(m_task, m_entries, m_ticks, m_exits) \
CHECK(m_task->num_entries == m_entries); \
CHECK(m_task->num_ticks == m_ticks); \
CHECK(m_task->num_exits == m_exits);
#define CHECK_ENTRIES_TICKS_EXITS_UP_TO(m_task, m_entries, m_ticks, m_exits) \
CHECK(m_task->num_entries <= m_entries); \
CHECK(m_task->num_ticks <= m_ticks); \
CHECK(m_task->num_exits <= m_exits);
#define CHECK_STATUS_ENTRIES_TICKS_EXITS(m_task, m_status, m_entries, m_ticks, m_exits) \
CHECK(m_task->get_status() == m_status); \
CHECK(m_task->num_entries == m_entries); \
CHECK(m_task->num_ticks == m_ticks); \
CHECK(m_task->num_exits == m_exits);
#endif // LIMBO_TEST_H

63
tests/test_always_fail.h Normal file
View File

@ -0,0 +1,63 @@
/**
* test_always_fail.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_ALWAYS_FAIL_H
#define TEST_ALWAYS_FAIL_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_always_fail.h"
namespace TestAlwaysFail {
TEST_CASE("[Modules][LimboAI] BTAlwaysFail") {
Ref<BTAlwaysFail> af = memnew(BTAlwaysFail);
SUBCASE("When empty") {
CHECK(af->execute(0.01666) == BTTask::FAILURE);
}
Ref<BTTestAction> task = memnew(BTTestAction);
af->add_child(task);
SUBCASE("When child returns FAILURE") {
task->ret_status = BTTask::FAILURE;
CHECK(af->execute(0.01666) == BTTask::FAILURE);
CHECK(task->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
}
SUBCASE("When child returns SUCCESS") {
task->ret_status = BTTask::SUCCESS;
CHECK(af->execute(0.01666) == BTTask::FAILURE);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
}
SUBCASE("When child returns RUNNING") {
task->ret_status = BTTask::RUNNING;
CHECK(af->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 0);
}
}
} //namespace TestAlwaysFail
#endif // TEST_ALWAYS_FAIL_H

View File

@ -0,0 +1,63 @@
/**
* test_always_succeed.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_ALWAYS_SUCCEED_H
#define TEST_ALWAYS_SUCCEED_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_always_succeed.h"
namespace TestAlwaysSucceed {
TEST_CASE("[Modules][LimboAI] BTAlwaysSucceed") {
Ref<BTAlwaysSucceed> as = memnew(BTAlwaysSucceed);
SUBCASE("When empty") {
CHECK(as->execute(0.01666) == BTTask::SUCCESS);
}
Ref<BTTestAction> task = memnew(BTTestAction);
as->add_child(task);
SUBCASE("When child returns FAILURE") {
task->ret_status = BTTask::FAILURE;
CHECK(as->execute(0.01666) == BTTask::SUCCESS);
CHECK(task->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
}
SUBCASE("When child returns SUCCESS") {
task->ret_status = BTTask::SUCCESS;
CHECK(as->execute(0.01666) == BTTask::SUCCESS);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
}
SUBCASE("When child returns RUNNING") {
task->ret_status = BTTask::RUNNING;
CHECK(as->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 0);
}
}
} //namespace TestAlwaysSucceed
#endif // TEST_ALWAYS_SUCCEED_H

View File

@ -0,0 +1,94 @@
/**
* test_await_animation.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_AWAIT_ANIMATION_H
#define TEST_AWAIT_ANIMATION_H
#include "core/os/memory.h"
#include "limbo_test.h"
#include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/scene/bt_await_animation.h"
#include "scene/animation/animation_player.h"
#include "scene/main/window.h"
#include "scene/resources/animation.h"
#include "scene/resources/animation_library.h"
namespace TestAwaitAnimation {
TEST_CASE("[SceneTree][LimboAI] BTAwaitAnimation") {
AnimationPlayer *player = memnew(AnimationPlayer);
SceneTree::get_singleton()->get_root()->add_child(player);
player->set_process_callback(AnimationPlayer::AnimationProcessCallback::ANIMATION_PROCESS_IDLE);
Ref<AnimationLibrary> anim_lib = memnew(AnimationLibrary);
Ref<Animation> anim = memnew(Animation);
anim->set_name("test");
anim->set_length(0.1);
anim->set_loop_mode(Animation::LOOP_NONE);
REQUIRE(anim_lib->add_animation("test", anim) == OK);
REQUIRE(player->add_animation_library("", anim_lib) == OK);
REQUIRE(player->has_animation("test"));
Ref<BTAwaitAnimation> awa = memnew(BTAwaitAnimation);
awa->set_animation_name("test");
Ref<BBNode> player_param = memnew(BBNode);
awa->set_animation_player(player_param);
Node *dummy = memnew(Node);
SceneTree::get_singleton()->get_root()->add_child(dummy);
Ref<Blackboard> bb = memnew(Blackboard);
SUBCASE("When AnimationPlayer doesn't exist") {
player_param->set_saved_value(NodePath("./NotFound"));
ERR_PRINT_OFF;
awa->initialize(dummy, bb);
CHECK(awa->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When AnimationPlayer exists") {
player_param->set_saved_value(player->get_path());
awa->initialize(dummy, bb);
SUBCASE("When AnimationPlayer is not playing") {
REQUIRE_FALSE(player->is_playing());
CHECK(awa->execute(0.01666) == BTTask::SUCCESS);
}
SUBCASE("When AnimationPlayer is playing") {
player->play("test");
REQUIRE(player->is_playing());
REQUIRE(player->get_current_animation() == "test");
CHECK(awa->execute(0.01666) == BTTask::RUNNING);
SUBCASE("When exceeding max wait time") {
awa->set_max_time(1.0);
ERR_PRINT_OFF;
CHECK(awa->execute(1.0) == BTTask::SUCCESS);
ERR_PRINT_ON;
}
SUBCASE("When animation finishes playing") {
player->seek(888.0, true);
player->notification(Node::NOTIFICATION_INTERNAL_PROCESS);
CHECK_FALSE(player->is_playing());
CHECK_FALSE(player->get_current_animation() == "test");
CHECK(awa->execute(0.01666) == BTTask::SUCCESS);
}
}
}
memdelete(dummy);
memdelete(player);
}
} //namespace TestAwaitAnimation
#endif // TEST_AWAIT_ANIMATION_H

125
tests/test_bb_param.h Normal file
View File

@ -0,0 +1,125 @@
/**
* test_bb_param.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_BB_PARAM_H
#define TEST_BB_PARAM_H
#include "core/object/ref_counted.h"
#include "core/string/node_path.h"
#include "core/variant/variant.h"
#include "limbo_test.h"
#include "modules/limboai/blackboard/bb_param/bb_node.h"
#include "modules/limboai/blackboard/bb_param/bb_param.h"
#include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "tests/test_macros.h"
namespace TestBBParam {
TEST_CASE("[Modules][LimboAI] BBParam") {
Ref<BBParam> param = memnew(BBParam);
Node *dummy = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
SUBCASE("Test with a value and common data types") {
param->set_value_source(BBParam::SAVED_VALUE);
param->set_saved_value(123);
CHECK(param->get_value(dummy, bb) == Variant(123));
param->set_saved_value("test");
CHECK(param->get_value(dummy, bb) == Variant("test"));
param->set_saved_value(3.14);
CHECK(param->get_value(dummy, bb) == Variant(3.14));
}
SUBCASE("Test with a BB variable") {
param->set_value_source(BBParam::BLACKBOARD_VAR);
param->set_variable("test_var");
SUBCASE("With integer") {
bb->set_var("test_var", 123);
CHECK(param->get_value(dummy, bb) == Variant(123));
}
SUBCASE("With String") {
bb->set_var("test_var", "test");
CHECK(param->get_value(dummy, bb) == Variant("test"));
}
SUBCASE("With float") {
bb->set_var("test_var", 3.14);
CHECK(param->get_value(dummy, bb) == Variant(3.14));
}
SUBCASE("When variable doesn't exist") {
ERR_PRINT_OFF;
CHECK(param->get_value(dummy, bb, "default_value") == Variant("default_value"));
ERR_PRINT_ON;
}
}
memdelete(dummy);
}
TEST_CASE("[Modules][LimboAI] BBNode") {
Ref<BBNode> param = memnew(BBNode);
Node *dummy = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
Node *other = memnew(Node);
other->set_name("Other");
dummy->add_child(other);
SUBCASE("With a valid path") {
param->set_value_source(BBParam::SAVED_VALUE);
param->set_saved_value(NodePath("./Other"));
CHECK(param->get_value(dummy, bb).get_type() == Variant::Type::OBJECT);
CHECK(param->get_value(dummy, bb) == Variant(other));
}
SUBCASE("With an invalid path") {
param->set_value_source(BBParam::SAVED_VALUE);
param->set_saved_value(NodePath("./SomeOther"));
ERR_PRINT_OFF;
CHECK(param->get_value(dummy, bb, Variant()).is_null());
ERR_PRINT_ON;
}
SUBCASE("With an object on the blackboard") {
param->set_value_source(BBParam::BLACKBOARD_VAR);
param->set_variable("test_var");
SUBCASE("When variable exists") {
bb->set_var("test_var", other);
CHECK(param->get_value(dummy, bb).get_type() == Variant::Type::OBJECT);
CHECK(param->get_value(dummy, bb) == Variant(other));
}
SUBCASE("When variable doesn't exist") {
CHECK(param->get_value(dummy, bb, Variant()).is_null());
}
SUBCASE("When variable has wrong type") {
bb->set_var("test_var", 123);
ERR_PRINT_OFF;
CHECK(param->get_value(dummy, bb, Variant()).is_null());
ERR_PRINT_ON;
}
SUBCASE("When variable is an object") {
// * Note: We allow also fetching objects on the blackboard.
Ref<RefCounted> some_other = memnew(RefCounted);
bb->set_var("test_var", some_other);
CHECK(param->get_value(dummy, bb) == some_other);
}
}
memdelete(other);
memdelete(dummy);
}
} //namespace TestBBParam
#endif // TEST_BB_PARAM_H

74
tests/test_call_method.h Normal file
View File

@ -0,0 +1,74 @@
/**
* test_call_method.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_CALL_METHOD_H
#define TEST_CALL_METHOD_H
#include "limbo_test.h"
#include "modules/limboai/blackboard/bb_param/bb_node.h"
#include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/scene/bt_call_method.h"
#include "core/os/memory.h"
namespace TestCallMethod {
TEST_CASE("[Modules][LimboAI] BTCallMethod") {
Ref<BTCallMethod> cm = memnew(BTCallMethod);
SUBCASE("When node parameter is null") {
cm->set_node_param(nullptr);
cm->set_method("test");
ERR_PRINT_OFF;
CHECK(cm->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("With object on the blackboard") {
Node *dummy = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
Ref<BBNode> node_param = memnew(BBNode);
cm->set_node_param(node_param);
Ref<CallbackCounter> callback_counter = memnew(CallbackCounter);
bb->set_var("object", callback_counter);
node_param->set_value_source(BBParam::BLACKBOARD_VAR);
node_param->set_variable("object");
cm->set_method("callback");
cm->initialize(dummy, bb);
SUBCASE("When method is empty") {
cm->set_method("");
ERR_PRINT_OFF;
CHECK(cm->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When method doesn't exist") {
cm->set_method("not_found");
ERR_PRINT_OFF;
CHECK(cm->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When method exists") {
CHECK(cm->execute(0.01666) == BTTask::SUCCESS);
CHECK(callback_counter->num_callbacks == 1);
}
memdelete(dummy);
}
}
} //namespace TestCallMethod
#endif // TEST_CALL_METHOD_H

View File

@ -0,0 +1,93 @@
/**
* test_check_agent_property.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_CHECK_AGENT_PROPERTY_H
#define TEST_CHECK_AGENT_PROPERTY_H
#include "limbo_test.h"
#include "modules/limboai/blackboard/bb_param/bb_variant.h"
#include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/scene/bt_check_agent_property.h"
#include "modules/limboai/util/limbo_utility.h"
#include "core/os/memory.h"
#include "core/variant/variant.h"
namespace TestCheckAgentProperty {
// Check with m_correct, m_incorrect and m_invalid values using m_check_type.
#define TC_CHECK_AGENT_PROP(m_task, m_check_type, m_correct, m_incorrect, m_invalid) \
m_task->set_check_type(m_check_type); \
m_task->get_value()->set_saved_value(m_correct); \
CHECK(m_task->execute(0.01666) == BTTask::SUCCESS); \
m_task->get_value()->set_saved_value(m_incorrect); \
CHECK(m_task->execute(0.01666) == BTTask::FAILURE); \
m_task->get_value()->set_saved_value(m_invalid); \
CHECK(m_task->execute(0.01666) == BTTask::FAILURE);
TEST_CASE("[Modules][LimboAI] BTCheckAgentProperty") {
Ref<BTCheckAgentProperty> cap = memnew(BTCheckAgentProperty);
Node *agent = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
cap->initialize(agent, bb);
StringName agent_name = "SimpleNode";
agent->set_name(agent_name);
// * Defaults that should produce successful check:
cap->set_property("name");
cap->set_check_type(LimboUtility::CHECK_EQUAL);
Ref<BBVariant> value = memnew(BBVariant);
cap->set_value(value);
value->set_saved_value(agent_name);
REQUIRE(cap->execute(0.01666) == BTTask::SUCCESS);
SUBCASE("When property is not set") {
cap->set_property("");
ERR_PRINT_OFF;
CHECK(cap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When property is not found") {
cap->set_property("not_found");
ERR_PRINT_OFF;
CHECK(cap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When value is not set") {
cap->set_value(nullptr);
ERR_PRINT_OFF;
CHECK(cap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("With StringName") {
StringName other_name = "OtherName";
TC_CHECK_AGENT_PROP(cap, LimboUtility::CHECK_EQUAL, agent_name, other_name, 123);
TC_CHECK_AGENT_PROP(cap, LimboUtility::CHECK_NOT_EQUAL, other_name, agent_name, 123);
}
SUBCASE("With integer") {
cap->set_property("process_priority");
TC_CHECK_AGENT_PROP(cap, LimboUtility::CHECK_EQUAL, 0, -1, "invalid");
TC_CHECK_AGENT_PROP(cap, LimboUtility::CHECK_GREATER_THAN_OR_EQUAL, 0, 1, "invalid");
TC_CHECK_AGENT_PROP(cap, LimboUtility::CHECK_GREATER_THAN, -1, 1, "invalid");
TC_CHECK_AGENT_PROP(cap, LimboUtility::CHECK_LESS_THAN_OR_EQUAL, 0, -1, "invalid");
TC_CHECK_AGENT_PROP(cap, LimboUtility::CHECK_LESS_THAN, 1, 0, "invalid");
TC_CHECK_AGENT_PROP(cap, LimboUtility::CHECK_NOT_EQUAL, 1, 0, "invalid");
}
memdelete(agent);
}
} //namespace TestCheckAgentProperty
#endif // TEST_CHECK_AGENT_PROPERTY_H

View File

@ -0,0 +1,63 @@
/**
* test_check_trigger.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_CHECK_TRIGGER_H
#define TEST_CHECK_TRIGGER_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/blackboard/bt_check_trigger.h"
#include "modules/limboai/bt/tasks/bt_task.h"
namespace TestCheckTrigger {
TEST_CASE("[Modules][LimboAI] BTCheckTrigger") {
Ref<BTCheckTrigger> ct = memnew(BTCheckTrigger);
Node *dummy = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
ct->initialize(dummy, bb);
SUBCASE("Empty") {
ERR_PRINT_OFF;
ct->set_variable("");
CHECK(ct->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
ct->set_variable("trigger");
SUBCASE("When variable is not set") {
CHECK(ct->execute(0.01666) == BTTask::FAILURE);
}
SUBCASE("When variable set to false") {
bb->set_var("trigger", false);
CHECK(ct->execute(0.01666) == BTTask::FAILURE);
CHECK(bb->get_var("trigger", false) == Variant(false));
}
SUBCASE("When variable set to true") {
bb->set_var("trigger", true);
CHECK(bb->get_var("trigger", false) == Variant(true));
CHECK(ct->execute(0.01666) == BTTask::SUCCESS);
CHECK(bb->get_var("trigger", false) == Variant(false));
}
SUBCASE("When variable set to non-bool") {
bb->set_var("trigger", "Some text");
CHECK(ct->execute(0.01666) == BTTask::FAILURE);
CHECK(bb->get_var("trigger", Variant()) == "Some text");
}
memdelete(dummy);
}
} //namespace TestCheckTrigger
#endif // TEST_CHECK_TRIGGER_H

109
tests/test_check_var.h Normal file
View File

@ -0,0 +1,109 @@
/**
* test_check_var.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_CHECK_VAR_H
#define TEST_CHECK_VAR_H
#include "limbo_test.h"
#include "modules/limboai/blackboard/bb_param/bb_param.h"
#include "modules/limboai/bt/tasks/blackboard/bt_check_var.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/util/limbo_utility.h"
#include "tests/test_macros.h"
namespace TestCheckVar {
// Compare m_correct, m_incorrect and m_invalid to m_value based using m_check_type.
#define TC_CHECK_VALUES(m_task, m_correct, m_incorrect, m_invalid, m_check_type, m_value) \
m_task->get_value()->set_saved_value(m_value); \
m_task->set_check_type(m_check_type); \
m_task->get_blackboard()->set_var("var", m_correct); \
CHECK(m_task->execute(0.01666) == BTTask::SUCCESS); \
m_task->get_blackboard()->set_var("var", m_incorrect); \
CHECK(m_task->execute(0.01666) == BTTask::FAILURE); \
m_task->get_blackboard()->set_var("var", m_invalid); \
CHECK(m_task->execute(0.01666) == BTTask::FAILURE);
TEST_CASE("[Modules][LimboAI] BTCheckVar") {
Ref<BTCheckVar> cv = memnew(BTCheckVar);
Ref<Blackboard> bb = memnew(Blackboard);
Node *dummy = memnew(Node);
cv->initialize(dummy, bb);
SUBCASE("Check with empty variable and value") {
cv->set_variable("");
cv->set_value(nullptr);
ERR_PRINT_OFF;
CHECK(cv->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("With variable and value set") {
cv->set_variable("var");
Ref<BBVariant> value = memnew(BBVariant);
cv->set_value(value);
SUBCASE("When checking against another variable") {
cv->set_check_type(LimboUtility::CHECK_EQUAL);
value->set_value_source(BBParam::BLACKBOARD_VAR);
bb->set_var("var", 123);
SUBCASE("When variable exists") {
value->set_variable("compare_var");
bb->set_var("compare_var", 123);
CHECK(cv->execute(0.01666) == BTTask::SUCCESS);
bb->set_var("compare_var", 567);
CHECK(cv->execute(0.01666) == BTTask::FAILURE);
}
SUBCASE("When variable doesn't exist") {
value->set_variable("not_found");
ERR_PRINT_OFF;
CHECK(cv->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
}
value->set_value_source(BBParam::SAVED_VALUE);
SUBCASE("With integer") {
TC_CHECK_VALUES(cv, 5, 4, "5", LimboUtility::CHECK_EQUAL, 5);
TC_CHECK_VALUES(cv, 5, 4, "5", LimboUtility::CHECK_GREATER_THAN_OR_EQUAL, 5);
TC_CHECK_VALUES(cv, 6, 4, "6", LimboUtility::CHECK_GREATER_THAN, 5);
TC_CHECK_VALUES(cv, 5, 6, "5", LimboUtility::CHECK_LESS_THAN_OR_EQUAL, 5);
TC_CHECK_VALUES(cv, 4, 6, "4", LimboUtility::CHECK_LESS_THAN, 5);
TC_CHECK_VALUES(cv, 4, 5, "4", LimboUtility::CHECK_NOT_EQUAL, 5);
}
SUBCASE("With bool") {
TC_CHECK_VALUES(cv, true, false, "true", LimboUtility::CHECK_EQUAL, true);
TC_CHECK_VALUES(cv, true, false, "true", LimboUtility::CHECK_NOT_EQUAL, false);
}
SUBCASE("With float") {
TC_CHECK_VALUES(cv, 3.14, 3.0, "3.14", LimboUtility::CHECK_EQUAL, 3.14);
TC_CHECK_VALUES(cv, 3.14, 3.0, "3.14", LimboUtility::CHECK_GREATER_THAN_OR_EQUAL, 3.14);
TC_CHECK_VALUES(cv, 4.0, 3.0, "4.0", LimboUtility::CHECK_GREATER_THAN, 3.14);
TC_CHECK_VALUES(cv, 3.14, 4.0, "3.14", LimboUtility::CHECK_LESS_THAN_OR_EQUAL, 3.14);
TC_CHECK_VALUES(cv, 3.0, 4.0, "3.0", LimboUtility::CHECK_LESS_THAN, 3.14);
TC_CHECK_VALUES(cv, 3.0, 3.14, "3.0", LimboUtility::CHECK_NOT_EQUAL, 3.14);
}
SUBCASE("With string") {
TC_CHECK_VALUES(cv, "AAA", "AAC", 123, LimboUtility::CHECK_EQUAL, "AAA");
TC_CHECK_VALUES(cv, "AAC", "AAA", 123, LimboUtility::CHECK_GREATER_THAN_OR_EQUAL, "AAB");
TC_CHECK_VALUES(cv, "AAC", "AAA", 123, LimboUtility::CHECK_GREATER_THAN, "AAB");
TC_CHECK_VALUES(cv, "AAA", "AAC", 123, LimboUtility::CHECK_LESS_THAN_OR_EQUAL, "AAB");
TC_CHECK_VALUES(cv, "AAA", "AAC", 123, LimboUtility::CHECK_LESS_THAN, "AAB");
TC_CHECK_VALUES(cv, "AAA", "AAB", 123, LimboUtility::CHECK_NOT_EQUAL, "AAB");
}
}
memdelete(dummy);
}
} //namespace TestCheckVar
#endif // TEST_CHECK_VAR_H

67
tests/test_delay.h Normal file
View File

@ -0,0 +1,67 @@
/**
* test_delay.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_DELAY_H
#define TEST_DELAY_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_delay.h"
namespace TestDelay {
TEST_CASE("[Modules][LimboAI] BTDelay") {
Ref<BTDelay> del = memnew(BTDelay);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(del->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction(BTTask::SUCCESS));
del->add_child(task);
SUBCASE("Check if delay is observed correctly") {
del->set_seconds(1.0);
CHECK(del->execute(0.35) == BTTask::RUNNING); // * first execution: elapsed 0.0
CHECK_ENTRIES_TICKS_EXITS(task, 0, 0, 0);
CHECK(del->execute(0.35) == BTTask::RUNNING); // * second execution: elapsed 0.35
CHECK_ENTRIES_TICKS_EXITS(task, 0, 0, 0);
CHECK(del->execute(0.35) == BTTask::RUNNING); // * third execution: elapsed 0.7
CHECK_ENTRIES_TICKS_EXITS(task, 0, 0, 0);
SUBCASE("When child task returns SUCCESS") {
task->ret_status = BTTask::SUCCESS;
CHECK(del->execute(0.35) == BTTask::SUCCESS); // * fourth execution: elapsed 1.05
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
}
SUBCASE("When child task returns FAILURE") {
task->ret_status = BTTask::FAILURE;
CHECK(del->execute(0.35) == BTTask::FAILURE); // * fourth execution: elapsed 1.05
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
}
SUBCASE("When child task returns RUNNING") {
task->ret_status = BTTask::RUNNING;
CHECK(del->execute(0.35) == BTTask::RUNNING); // * fourth execution: elapsed 1.05
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 0);
task->ret_status = BTTask::SUCCESS;
CHECK(del->execute(0.35) == BTTask::SUCCESS); // * fifth execution: elapsed 1.4
CHECK_ENTRIES_TICKS_EXITS(task, 1, 2, 1);
}
}
}
} //namespace TestDelay
#endif // TEST_DELAY_H

View File

@ -0,0 +1,121 @@
/**
* test_dynamic_selector.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_DYNAMIC_SELECTOR_H
#define TEST_DYNAMIC_SELECTOR_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_dynamic_selector.h"
namespace TestDynamicSelector {
TEST_CASE("[Modules][LimboAI] BTDynamicSelector") {
Ref<BTDynamicSelector> sel = memnew(BTDynamicSelector);
Ref<BTTestAction> task1 = memnew(BTTestAction());
Ref<BTTestAction> task2 = memnew(BTTestAction());
Ref<BTTestAction> task3 = memnew(BTTestAction());
sel->add_child(task1);
sel->add_child(task2);
sel->add_child(task3);
REQUIRE(sel->get_child_count() == 3);
SUBCASE("Subcase #1: Dynamic selector processes tasks sequentially until first SUCCESS, while re-evaluating its child tasks in every execution tick.") {
task1->ret_status = BTTask::FAILURE;
task2->ret_status = BTTask::RUNNING;
task3->ret_status = BTTask::SUCCESS;
CHECK(sel->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1); // * finished with FAILURE
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 0); // * enetered and running
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0); // * still fresh
SUBCASE("Subcase 1A: With no changes, first task is re-evaluated.") {
CHECK(sel->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * re-evaluated with FAILURE
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 0); // * continued
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0); // * still fresh
}
SUBCASE("Subcase 1B: When second task succeeds, we finish with SUCCESS.") {
task2->ret_status = BTTask::SUCCESS;
CHECK(sel->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * re-evaluated with FAILURE
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 1); // * ticked and exited with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0); // * still fresh
}
SUBCASE("Subcase 1C: When first task re-evaluates to SUCCESS, second task should be cancelled and exited.") {
task1->ret_status = BTTask::SUCCESS;
CHECK(sel->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::FRESH); // * cancelled - status changed to FRESH
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * re-evaluated with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1); // * cancelled - not ticked and exited
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0); // * still fresh
}
SUBCASE("Subcase 1D: When first two fail, third one is executed.") {
task1->ret_status = BTTask::FAILURE;
task2->ret_status = BTTask::FAILURE;
task3->ret_status = BTTask::RUNNING;
CHECK(sel->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * re-evaluated with FAILURE
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 1); // * ticked and exited with FAILURE
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 0); // * entered and running
SUBCASE("Subcase 1D1: First two are re-evaluated, and when all finish with FAILURE, we expect FAILURE.") {
task1->ret_status = BTTask::FAILURE;
task2->ret_status = BTTask::FAILURE;
task3->ret_status = BTTask::FAILURE;
CHECK(sel->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 3, 3, 3); // * re-evaluated with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task2, 2, 3, 2); // * re-evaluated with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 2, 1); // * ticked and exited with SUCCESS
}
}
}
}
} //namespace TestDynamicSelector
#endif // TEST_DYNAMIC_SELECTOR_H

View File

@ -0,0 +1,108 @@
/**
* test_dynamic_sequence.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_DYNAMIC_SEQUENCE_H
#define TEST_DYNAMIC_SEQUENCE_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_dynamic_sequence.h"
namespace TestDynamicSequence {
TEST_CASE("[Modules][LimboAI] BTDynamicSequence") {
Ref<BTDynamicSequence> seq = memnew(BTDynamicSequence);
Ref<BTTestAction> task1 = memnew(BTTestAction());
Ref<BTTestAction> task2 = memnew(BTTestAction());
Ref<BTTestAction> task3 = memnew(BTTestAction());
seq->add_child(task1);
seq->add_child(task2);
seq->add_child(task3);
REQUIRE(seq->get_child_count() == 3);
SUBCASE("Subcase #1: Dynamic sequence processes tasks sequentially, while re-evaluating its child tasks in every execution tick.") {
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::RUNNING;
task3->ret_status = BTTask::SUCCESS;
CHECK(seq->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1); // * finished
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 0); // * running
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0); // * still fresh
SUBCASE("Subcase 1A: With no changes, first task is re-evaluated.") {
CHECK(seq->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * re-evaluated with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 0); // * continued
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0); // * still fresh
}
SUBCASE("Subcase 1B: When first task re-evaluates to FAILURE, second task should be cancelled and exited.") {
task1->ret_status = BTTask::FAILURE;
CHECK(seq->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::FRESH); // * cancelled - status changed to FRESH
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * re-evaluated with FAILURE
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1); // * cancelled - not ticked and exited
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0); // * still fresh
}
SUBCASE("Subcase 1C: When second task finished, third one is executed.") {
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::SUCCESS;
task3->ret_status = BTTask::RUNNING;
CHECK(seq->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * re-evaluated with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 1); // * ticked and exited with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 0); // * entered and running
SUBCASE("Subcase 1C1: First two are re-evaluated, and when all finish with SUCCESS, we expect SUCCESS.") {
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::SUCCESS;
task3->ret_status = BTTask::SUCCESS;
CHECK(seq->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task1, 3, 3, 3); // * re-evaluated with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task2, 2, 3, 2); // * re-evaluated with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 2, 1); // * ticked and exited with SUCCESS
}
}
}
}
} //namespace TestDynamicSequence
#endif // TEST_DYNAMIC_SEQUENCE_H

103
tests/test_for_each.h Normal file
View File

@ -0,0 +1,103 @@
/**
* test_for_each.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_FOR_EACH_H
#define TEST_FOR_EACH_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_for_each.h"
namespace TestForEach {
TEST_CASE("[Modules][LimboAI] BTForEach") {
Ref<BTForEach> fe = memnew(BTForEach);
Node *dummy = memnew(Node);
Ref<Blackboard> blackboard = memnew(Blackboard);
fe->initialize(dummy, blackboard);
Array arr;
arr.append("apple");
arr.append("raspberry");
arr.append("mushroom");
blackboard->set_var("array", arr);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(fe->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction(BTTask::SUCCESS));
fe->add_child(task);
fe->set_array_var("array");
fe->set_save_var("element");
SUBCASE("When child returns SUCCESS") {
CHECK(fe->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
CHECK(blackboard->get_var("element", "wetgoop") == "apple");
CHECK(fe->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 2, 2, 2);
CHECK(blackboard->get_var("element", "wetgoop") == "raspberry");
CHECK(fe->execute(0.01666) == BTTask::SUCCESS); // * finished iterating - returning SUCCESS
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 3, 3, 3);
CHECK(blackboard->get_var("element", "wetgoop") == "mushroom");
}
SUBCASE("When child task takes more than one tick to finish") {
task->ret_status = BTTask::RUNNING;
CHECK(fe->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 0);
CHECK(blackboard->get_var("element", "wetgoop") == "apple");
task->ret_status = BTTask::SUCCESS;
CHECK(fe->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 2, 1);
CHECK(blackboard->get_var("element", "wetgoop") == "apple");
task->ret_status = BTTask::RUNNING;
CHECK(fe->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task, 2, 3, 1);
CHECK(blackboard->get_var("element", "wetgoop") == "raspberry");
task->ret_status = BTTask::SUCCESS;
CHECK(fe->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 2, 4, 2);
CHECK(blackboard->get_var("element", "wetgoop") == "raspberry");
task->ret_status = BTTask::RUNNING;
CHECK(fe->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task, 3, 5, 2);
CHECK(blackboard->get_var("element", "wetgoop") == "mushroom");
task->ret_status = BTTask::SUCCESS;
CHECK(fe->execute(0.01666) == BTTask::SUCCESS);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 3, 6, 3);
CHECK(blackboard->get_var("element", "wetgoop") == "mushroom");
}
}
} //namespace TestForEach
#endif // TEST_FOR_EACH_H

182
tests/test_hsm.h Normal file
View File

@ -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<CallbackCounter> alpha_entries = memnew(CallbackCounter);
Ref<CallbackCounter> alpha_exits = memnew(CallbackCounter);
Ref<CallbackCounter> alpha_updates = memnew(CallbackCounter);
Ref<CallbackCounter> beta_entries = memnew(CallbackCounter);
Ref<CallbackCounter> beta_exits = memnew(CallbackCounter);
Ref<CallbackCounter> 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<TestGuard> 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

63
tests/test_invert.h Normal file
View File

@ -0,0 +1,63 @@
/**
* test_invert.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_INVERT_H
#define TEST_INVERT_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_invert.h"
namespace TestInvert {
TEST_CASE("[Modules][LimboAI] BTInvert") {
Ref<BTInvert> inv = memnew(BTInvert);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(inv->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction);
inv->add_child(task);
SUBCASE("With SUCCESS") {
task->ret_status = BTTask::SUCCESS;
CHECK(inv->execute(0.01666) == BTTask::FAILURE);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
}
SUBCASE("With FAILURE") {
task->ret_status = BTTask::FAILURE;
CHECK(inv->execute(0.01666) == BTTask::SUCCESS);
CHECK(task->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
}
SUBCASE("With RUNNING followed by SUCCESS") {
task->ret_status = BTTask::RUNNING;
CHECK(inv->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 0);
task->ret_status = BTTask::SUCCESS;
CHECK(inv->execute(0.01666) == BTTask::FAILURE);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 2, 1);
}
}
} //namespace TestInvert
#endif // TEST_INVERT_H

100
tests/test_new_scope.h Normal file
View File

@ -0,0 +1,100 @@
/**
* test_new_scope.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_NEW_SCOPE_H
#define TEST_NEW_SCOPE_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_new_scope.h"
namespace TestNewScope {
TEST_CASE("[Modules][LimboAI] BTNewScope") {
Ref<BTNewScope> ns = memnew(BTNewScope);
Node *dummy = memnew(Node);
Ref<Blackboard> parent_bb = memnew(Blackboard);
SUBCASE("When empty") {
ERR_PRINT_OFF;
ns->initialize(dummy, parent_bb);
CHECK(ns->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When not empty") {
Ref<BTTask> parent = memnew(BTTask);
parent->add_child(ns);
Ref<BTTestAction> child = memnew(BTTestAction);
ns->add_child(child);
parent_bb->set_var("fruit", "apple");
parent_bb->set_var("vegetable", "carrot");
REQUIRE(parent_bb->has_var("fruit"));
REQUIRE(parent_bb->get_var("fruit", "wetgoop") == "apple");
REQUIRE(parent_bb->has_var("vegetable"));
REQUIRE(parent_bb->get_var("vegetable", "wetgoop") == "carrot");
parent->initialize(dummy, parent_bb);
CHECK(ns->get_blackboard() != parent->get_blackboard());
CHECK(ns->get_blackboard() == child->get_blackboard());
CHECK(parent->get_blackboard() == parent_bb);
CHECK(ns->get_blackboard()->get_parent_scope() == parent_bb);
ns->get_blackboard()->set_var("fruit", "pear"); // * override "fruit"
CHECK(ns->get_blackboard()->get_var("fruit", "wetgoop") == "pear");
CHECK(child->get_blackboard()->get_var("fruit", "wetgoop") == "pear");
CHECK(parent->get_blackboard()->get_var("fruit", "wetgoop") == "apple");
// * Check if new scope inherits "vegetable"
CHECK(ns->get_blackboard()->has_var("vegetable"));
CHECK(ns->get_blackboard()->get_var("vegetable", "wetgoop") == "carrot");
CHECK(child->get_blackboard()->get_var("vegetable", "wetgoop") == "carrot");
// * Check if "vegetable" from the parent scope is accessible
CHECK(ns->get_blackboard()->has_var("vegetable"));
CHECK(child->get_blackboard()->has_var("vegetable"));
CHECK(ns->get_blackboard()->get_var("vegetable", "wetgoop") == "carrot");
CHECK(child->get_blackboard()->get_var("vegetable", "wetgoop") == "carrot");
// * Check if setting a variable doesn't propagate it up the scope
ns->get_blackboard()->set_var("berry", "raspberry");
CHECK(ns->get_blackboard()->get_var("berry", "wetgoop") == "raspberry");
CHECK(child->get_blackboard()->get_var("berry", "wetgoop") == "raspberry");
CHECK(parent->get_blackboard()->get_var("berry", "wetgoop") == "wetgoop");
CHECK_FALSE(parent->get_blackboard()->has_var("berry"));
// * Check if setting a variable doesn't propagate it up the scope (now with the child task)
child->get_blackboard()->set_var("seed", "sunflower");
CHECK(child->get_blackboard()->get_var("seed", "wetgoop") == "sunflower");
CHECK(ns->get_blackboard()->get_var("seed", "wetgoop") == "sunflower");
CHECK(parent->get_blackboard()->get_var("seed", "wetgoop") == "wetgoop");
CHECK_FALSE(parent->get_blackboard()->has_var("seed"));
// * Check return status
child->ret_status = BTTask::SUCCESS;
CHECK(ns->execute(0.01666) == BTTask::SUCCESS);
child->ret_status = BTTask::FAILURE;
CHECK(ns->execute(0.01666) == BTTask::FAILURE);
child->ret_status = BTTask::RUNNING;
CHECK(ns->execute(0.01666) == BTTask::RUNNING);
}
memdelete(dummy);
}
} //namespace TestNewScope
#endif // TEST_NEW_SCOPE_H

212
tests/test_parallel.h Normal file
View File

@ -0,0 +1,212 @@
/**
* test_parallel.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_PARALLEL_H
#define TEST_PARALLEL_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_parallel.h"
namespace TestParallel {
TEST_CASE("[Modules][LimboAI] BTParallel with num_required_successes: 1 and num_required_failures: 1") {
Ref<BTParallel> par = memnew(BTParallel);
Ref<BTTestAction> task1 = memnew(BTTestAction());
Ref<BTTestAction> task2 = memnew(BTTestAction());
Ref<BTTestAction> task3 = memnew(BTTestAction());
par->add_child(task1);
par->add_child(task2);
par->add_child(task3);
REQUIRE(par->get_child_count() == 3);
SUBCASE("BTParallel composition {RUNNING, SUCCESS, FAILURE} and successes/failures required 1/1") {
// * Case #1: When reached both success and failure required, we expect one that triggered sooner (SUCCESS in this case).
task1->ret_status = BTTask::RUNNING;
task2->ret_status = BTTask::SUCCESS;
task3->ret_status = BTTask::FAILURE;
par->set_num_successes_required(1);
par->set_num_failures_required(1);
par->set_repeat(false);
CHECK(par->execute(0.01666) == BTTask::SUCCESS); // When reached both conditions.
CHECK(task1->get_status() == BTTask::RUNNING);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 0); // * running
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1); // * finished
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1); // * finished
}
SUBCASE("BTParallel composition {RUNNING, SUCCESS, RUNNING} and successes/failures required 1/1") {
// * Case #1b: When reached required number of successes, we expect SUCCESS.
task1->ret_status = BTTask::RUNNING;
task2->ret_status = BTTask::SUCCESS;
task3->ret_status = BTTask::RUNNING;
par->set_num_successes_required(1);
par->set_num_failures_required(1);
par->set_repeat(false);
CHECK(par->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::RUNNING);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 0); // * running
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1); // * finished
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 0); // * running
}
SUBCASE("BTParallel composition {RUNNING, FAILURE, RUNNING} and successes/failures required 1/1") {
// * Case #1c: When reached required number of failures, we expect FAILURE.
task1->ret_status = BTTask::RUNNING;
task2->ret_status = BTTask::FAILURE;
task3->ret_status = BTTask::RUNNING;
par->set_num_successes_required(1);
par->set_num_failures_required(1);
par->set_repeat(false);
CHECK(par->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::RUNNING);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 0); // * running
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1); // * finished
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 0); // * running
}
SUBCASE("BTParallel composition {SUCCESS, RUNNING, FAILURE} with successes/failures required 3/3 (not repeating)") {
// * Case #2: When failed to reach required number of successes or failures,
// * and not all children finished executing while not repeating, we expect RUNNING.
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::RUNNING;
task3->ret_status = BTTask::FAILURE;
par->set_num_successes_required(3);
par->set_num_failures_required(3);
par->set_repeat(false);
CHECK(par->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1); // * finished
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 0); // * running
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1); // * finished
}
SUBCASE("BTParallel composition {SUCCESS, FAILURE, SUCCESS} with successes/failures required 3/3 (not repeating)") {
// * Case #3: When failed to reach required number of successes or failures,
// * and all children finished executing while not repeating, we expect FAILURE.
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::FAILURE;
task3->ret_status = BTTask::SUCCESS;
par->set_num_successes_required(3);
par->set_num_failures_required(3);
par->set_repeat(false);
CHECK(par->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1);
}
SUBCASE("BTParallel composition {SUCCESS, FAILURE, SUCCESS} with successes/failures required 3/3 (repeating)") {
// * Case #4: When failed to reach required number of successes or failures,
// * and all children finished executing while repeating, we expect RUNNING.
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::FAILURE;
task3->ret_status = BTTask::SUCCESS;
par->set_num_successes_required(3);
par->set_num_failures_required(3);
par->set_repeat(true);
CHECK(par->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1);
// * Execution #2: Check if tasks are repeated, when set so (there is no RUNNING task).
CHECK(par->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * repeated
CHECK_ENTRIES_TICKS_EXITS(task2, 2, 2, 2); // * repeated
CHECK_ENTRIES_TICKS_EXITS(task3, 2, 2, 2); // * repeated
}
SUBCASE("BTParallel composition {SUCCESS, RUNNING, FAILURE} with successes/failures required 2/2 (not repeating)") {
// * Case #5: When failed to reach required number of successes or failures,
// * but not all children finished executing (not repeating), we expect RUNNING.
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::RUNNING;
task3->ret_status = BTTask::FAILURE;
par->set_num_successes_required(2);
par->set_num_failures_required(2);
par->set_repeat(false);
CHECK(par->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 0);
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1);
// * Execution #2: Check if tasks are not repeated, when set so.
CHECK(par->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1); // * not repeated
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 0); // * continued
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1); // * not repeated
// * Execution #3: Check if tasks are repeated, when set so.
par->set_repeat(true);
CHECK(par->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2); // * repeated
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 3, 0); // * continued
CHECK_ENTRIES_TICKS_EXITS(task3, 2, 2, 2); // * repeated
}
}
} //namespace TestParallel
#endif // TEST_PARALLEL_H

View File

@ -0,0 +1,77 @@
/**
* test_pause_animation.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_PAUSE_ANIMATION_H
#define TEST_PAUSE_ANIMATION_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/scene/bt_pause_animation.h"
#include "scene/animation/animation_player.h"
#include "scene/main/window.h"
#include "scene/resources/animation.h"
#include "scene/resources/animation_library.h"
namespace TestPauseAnimation {
TEST_CASE("[SceneTree][LimboAI] BTPauseAnimation") {
AnimationPlayer *player = memnew(AnimationPlayer);
SceneTree::get_singleton()->get_root()->add_child(player);
player->set_process_callback(AnimationPlayer::AnimationProcessCallback::ANIMATION_PROCESS_IDLE);
Ref<AnimationLibrary> anim_lib = memnew(AnimationLibrary);
Ref<Animation> anim = memnew(Animation);
anim->set_name("test");
anim->set_length(0.1);
anim->set_loop_mode(Animation::LOOP_NONE);
REQUIRE(anim_lib->add_animation("test", anim) == OK);
REQUIRE(player->add_animation_library("", anim_lib) == OK);
REQUIRE(player->has_animation("test"));
Ref<BTPauseAnimation> pa = memnew(BTPauseAnimation);
Ref<BBNode> player_param = memnew(BBNode);
pa->set_animation_player(player_param);
Node *dummy = memnew(Node);
SceneTree::get_singleton()->get_root()->add_child(dummy);
Ref<Blackboard> bb = memnew(Blackboard);
SUBCASE("When AnimationPlayer doesn't exist") {
player_param->set_saved_value(NodePath("./NotFound"));
ERR_PRINT_OFF;
pa->initialize(dummy, bb);
CHECK(pa->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When AnimationPlayer exists") {
player_param->set_saved_value(player->get_path());
pa->initialize(dummy, bb);
SUBCASE("When AnimationPlayer is not playing") {
REQUIRE_FALSE(player->is_playing());
CHECK(pa->execute(0.01666) == BTTask::SUCCESS);
}
SUBCASE("When AnimationPlayer is playing") {
player->play("test");
REQUIRE(player->is_playing());
CHECK(pa->execute(0.01666) == BTTask::SUCCESS);
CHECK_FALSE(player->is_playing());
}
}
memdelete(dummy);
memdelete(player);
}
} //namespace TestPauseAnimation
#endif // TEST_PAUSE_ANIMATION_H

View File

@ -0,0 +1,95 @@
/**
* test_play_animation.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_PLAY_ANIMATION_H
#define TEST_PLAY_ANIMATION_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/scene/bt_play_animation.h"
#include "scene/animation/animation_player.h"
#include "scene/main/window.h"
#include "scene/resources/animation.h"
#include "scene/resources/animation_library.h"
namespace TestPlayAnimation {
TEST_CASE("[SceneTree][LimboAI] BTPlayAnimation") {
AnimationPlayer *player = memnew(AnimationPlayer);
SceneTree::get_singleton()->get_root()->add_child(player);
player->set_process_callback(AnimationPlayer::AnimationProcessCallback::ANIMATION_PROCESS_IDLE);
Ref<AnimationLibrary> anim_lib = memnew(AnimationLibrary);
Ref<Animation> anim = memnew(Animation);
anim->set_name("test");
anim->set_length(0.1);
anim->set_loop_mode(Animation::LOOP_NONE);
REQUIRE(anim_lib->add_animation("test", anim) == OK);
REQUIRE(player->add_animation_library("", anim_lib) == OK);
REQUIRE(player->has_animation("test"));
Ref<BTPlayAnimation> pa = memnew(BTPlayAnimation);
pa->set_animation_name("test");
Ref<BBNode> player_param = memnew(BBNode);
pa->set_animation_player(player_param);
Node *dummy = memnew(Node);
SceneTree::get_singleton()->get_root()->add_child(dummy);
Ref<Blackboard> bb = memnew(Blackboard);
SUBCASE("When AnimationPlayer doesn't exist") {
player_param->set_saved_value(NodePath("./NotFound"));
ERR_PRINT_OFF;
pa->initialize(dummy, bb);
CHECK(pa->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When AnimationPlayer exists") {
player_param->set_saved_value(player->get_path());
pa->initialize(dummy, bb);
SUBCASE("When not waiting to finish") {
pa->set_await_completion(0.0);
CHECK(pa->execute(0.01666) == BTTask::SUCCESS);
CHECK(player->is_playing());
CHECK(player->get_current_animation() == "test");
}
SUBCASE("When exceeding max wait time") {
pa->set_await_completion(1.0);
CHECK(pa->execute(0.01666) == BTTask::RUNNING);
CHECK(player->is_playing());
CHECK(player->get_current_animation() == "test");
ERR_PRINT_OFF;
CHECK(pa->execute(1.0) == BTTask::SUCCESS);
ERR_PRINT_ON;
}
SUBCASE("When animation finishes playing before wait time runs out") {
pa->set_await_completion(888.0);
CHECK(pa->execute(0.01666) == BTTask::RUNNING);
CHECK(player->is_playing());
CHECK(player->get_current_animation() == "test");
player->seek(888.0, true);
player->notification(Node::NOTIFICATION_INTERNAL_PROCESS);
CHECK_FALSE(player->is_playing());
CHECK_FALSE(player->get_current_animation() == "test");
CHECK(pa->execute(0.01666) == BTTask::SUCCESS);
}
}
memdelete(dummy);
memdelete(player);
}
} //namespace TestPlayAnimation
#endif // TEST_PLAY_ANIMATION_H

91
tests/test_probability.h Normal file
View File

@ -0,0 +1,91 @@
/**
* test_probability.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_PROBABILITY_H
#define TEST_PROBABILITY_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_probability.h"
#include "core/math/math_funcs.h"
namespace TestProbability {
TEST_CASE("[Modules][LimboAI] BTProbability") {
Ref<BTProbability> prob = memnew(BTProbability);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(prob->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction);
prob->add_child(task);
Math::randomize();
SUBCASE("Check if probability meets expectation") {
task->ret_status = BTTask::SUCCESS;
prob->set_run_chance(0.5);
for (int i = 0; i < 1000; i++) {
prob->execute(0.01666);
}
CHECK(task->num_ticks > 450);
CHECK(task->num_ticks < 550);
}
SUBCASE("When probability is 0") {
task->ret_status = BTTask::SUCCESS;
prob->set_run_chance(0.0);
for (int i = 0; i < 1000; i++) {
prob->execute(0.01666);
}
CHECK(task->num_ticks == 0);
}
SUBCASE("When probability is 1") {
task->ret_status = BTTask::SUCCESS;
prob->set_run_chance(1.0);
for (int i = 0; i < 1000; i++) {
prob->execute(0.01666);
}
CHECK(task->num_ticks == 1000);
}
SUBCASE("Test return status") {
prob->set_run_chance(1.0);
task->ret_status = BTTask::SUCCESS;
CHECK(prob->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 1, 1, 1);
task->ret_status = BTTask::RUNNING;
CHECK(prob->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 2, 2, 1);
task->ret_status = BTTask::FAILURE;
CHECK(prob->execute(0.01666) == BTTask::FAILURE);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 2, 3, 2);
}
}
} //namespace TestProbability
#endif // TEST_PROBABILITY_H

View File

@ -0,0 +1,110 @@
/**
* test_random_selector.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_RANDOM_SELECTOR_H
#define TEST_RANDOM_SELECTOR_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_random_selector.h"
namespace TestRandomSelector {
TEST_CASE("[Modules][LimboAI] BTRandomSelector") {
Ref<BTRandomSelector> sel = memnew(BTRandomSelector);
Ref<BTTestAction> task1 = memnew(BTTestAction());
Ref<BTTestAction> task2 = memnew(BTTestAction());
Ref<BTTestAction> task3 = memnew(BTTestAction());
sel->add_child(task1);
sel->add_child(task2);
sel->add_child(task3);
REQUIRE(sel->get_child_count() == 3);
SUBCASE("Expecting RUNNING status when a child task returns RUNNING") {
task1->ret_status = BTTask::FAILURE;
task2->ret_status = BTTask::RUNNING;
task3->ret_status = BTTask::FAILURE;
CHECK(sel->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->is_status_either(BTTask::FAILURE, BTTask::FRESH));
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->is_status_either(BTTask::FAILURE, BTTask::FRESH));
CHECK_ENTRIES_TICKS_EXITS_UP_TO(task1, 1, 1, 1); // * ran no more than once
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 0); // * running - enters and ticks
CHECK_ENTRIES_TICKS_EXITS_UP_TO(task3, 1, 1, 1); // * ran no more than once
SUBCASE("Resuming and failing when all tasks fail") {
task2->ret_status = BTTask::FAILURE;
CHECK(sel->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1); // * ran once
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 1); // * finishes - ticks and exits with FAILURE
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1); // * ran once
}
SUBCASE("Resuming and succeeding when a child task succeeds") {
task2->ret_status = BTTask::SUCCESS;
CHECK(sel->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->is_status_either(BTTask::FAILURE, BTTask::FRESH));
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->is_status_either(BTTask::FAILURE, BTTask::FRESH));
CHECK_ENTRIES_TICKS_EXITS_UP_TO(task1, 1, 1, 1); // * ran no more than once
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 1); // * finishes - ticks and exits with SUCCESS
CHECK_ENTRIES_TICKS_EXITS_UP_TO(task3, 1, 1, 1); // * ran no more than once
}
}
SUBCASE("Verify that tasks are executed in random order") {
task1->ret_status = BTTask::FAILURE;
task2->ret_status = BTTask::FAILURE;
task3->ret_status = BTTask::RUNNING;
int num_tries = 10;
bool is_confirmed = false;
while (!is_confirmed && num_tries--) {
CHECK(sel->execute(0.01666) == BTTask::RUNNING);
int checksum = 0;
if (task1->get_status() == BTTask::FAILURE) {
checksum += 1;
}
if (task2->get_status() == BTTask::FAILURE) {
checksum += 2;
}
if (task3->get_status() == BTTask::RUNNING) {
checksum += 4;
}
is_confirmed = (checksum != 7);
}
CHECK(is_confirmed);
}
}
TEST_CASE("[Modules][LimboAI] Empty BTRandomSelector returns FAILURE") {
Ref<BTRandomSelector> seq = memnew(BTRandomSelector);
CHECK(seq->execute(0.01666) == BTTask::FAILURE);
}
} //namespace TestRandomSelector
#endif // TEST_RANDOM_SELECTOR_H

View File

@ -0,0 +1,110 @@
/**
* test_random_sequence.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_RANDOM_SEQUENCE_H
#define TEST_RANDOM_SEQUENCE_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_random_sequence.h"
namespace TestRandomSequence {
TEST_CASE("[Modules][LimboAI] BTRandomSequence") {
Ref<BTRandomSequence> seq = memnew(BTRandomSequence);
Ref<BTTestAction> task1 = memnew(BTTestAction());
Ref<BTTestAction> task2 = memnew(BTTestAction());
Ref<BTTestAction> task3 = memnew(BTTestAction());
seq->add_child(task1);
seq->add_child(task2);
seq->add_child(task3);
REQUIRE(seq->get_child_count() == 3);
SUBCASE("Expecting RUNNING status when a child task returns RUNNING") {
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::RUNNING;
task3->ret_status = BTTask::SUCCESS;
CHECK(seq->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->is_status_either(BTTask::SUCCESS, BTTask::FRESH));
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->is_status_either(BTTask::SUCCESS, BTTask::FRESH));
CHECK_ENTRIES_TICKS_EXITS_UP_TO(task1, 1, 1, 1); // * ran no more than once
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 0); // * running - enters and ticks
CHECK_ENTRIES_TICKS_EXITS_UP_TO(task3, 1, 1, 1); // * ran no more than once
SUBCASE("Resuming and succeeding when all tasks succeed") {
task2->ret_status = BTTask::SUCCESS;
CHECK(seq->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1); // * ran once
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 1); // * finishes - ticks and exits with SUCCESS
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1); // * ran once
}
SUBCASE("Resuming and failing when a child task fails") {
task2->ret_status = BTTask::FAILURE;
CHECK(seq->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->is_status_either(BTTask::SUCCESS, BTTask::FRESH));
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->is_status_either(BTTask::SUCCESS, BTTask::FRESH));
CHECK_ENTRIES_TICKS_EXITS_UP_TO(task1, 1, 1, 1); // * ran no more than once
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 1); // * finishes - ticks and exits with FAILURE
CHECK_ENTRIES_TICKS_EXITS_UP_TO(task3, 1, 1, 1); // * ran no more than once
}
}
SUBCASE("Verify that tasks are executed in random order") {
task1->ret_status = BTTask::SUCCESS;
task2->ret_status = BTTask::SUCCESS;
task3->ret_status = BTTask::RUNNING;
int num_tries = 10;
bool is_confirmed = false;
while (!is_confirmed && num_tries--) {
CHECK(seq->execute(0.01666) == BTTask::RUNNING);
int checksum = 0;
if (task1->get_status() == BTTask::SUCCESS) {
checksum += 1;
}
if (task2->get_status() == BTTask::SUCCESS) {
checksum += 2;
}
if (task3->get_status() == BTTask::RUNNING) {
checksum += 4;
}
is_confirmed = (checksum != 7);
}
CHECK(is_confirmed);
}
}
TEST_CASE("[Modules][LimboAI] Empty BTRandomSequence returns SUCCESS") {
Ref<BTRandomSequence> seq = memnew(BTRandomSequence);
CHECK(seq->execute(0.01666) == BTTask::SUCCESS);
}
} //namespace TestRandomSequence
#endif // TEST_RANDOM_SEQUENCE_H

105
tests/test_repeat.h Normal file
View File

@ -0,0 +1,105 @@
/**
* test_repeat.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_REPEAT_H
#define TEST_REPEAT_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_repeat.h"
namespace TestRepeat {
TEST_CASE("[Modules][LimboAI] BTRepeat") {
Ref<BTRepeat> rep = memnew(BTRepeat);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(rep->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction);
rep->add_child(task);
SUBCASE("When repeating forever") {
rep->set_times(3);
rep->set_forever(true);
task->ret_status = BTTask::SUCCESS;
for (int i = 1; i <= 100; i++) {
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, i, i, i);
}
}
SUBCASE("When repeated x3 times") {
rep->set_times(3);
rep->set_forever(false);
task->ret_status = BTTask::SUCCESS;
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 1, 1, 1);
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 2, 2, 2);
CHECK(rep->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 3, 3, 3);
}
SUBCASE("When the child task takes more than one tick to finish") {
rep->set_times(2);
rep->set_forever(false);
task->ret_status = BTTask::RUNNING;
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 1, 1, 0);
task->ret_status = BTTask::SUCCESS;
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 1, 2, 1);
task->ret_status = BTTask::RUNNING;
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 2, 3, 1);
task->ret_status = BTTask::SUCCESS;
CHECK(rep->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 2, 4, 2);
}
SUBCASE("When the child task fails") {
rep->set_times(2);
rep->set_forever(false);
task->ret_status = BTTask::FAILURE;
SUBCASE("When set to abort on failure") {
rep->set_abort_on_failure(true);
CHECK(rep->execute(0.01666) == BTTask::FAILURE);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 1, 1, 1);
CHECK(rep->execute(0.01666) == BTTask::FAILURE);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 2, 2, 2);
}
SUBCASE("When not set to abort on failure") {
rep->set_abort_on_failure(false);
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 1, 1, 1);
CHECK(rep->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 2, 2, 2);
}
}
}
} //namespace TestRepeat
#endif // TEST_REPEAT_H

View File

@ -0,0 +1,51 @@
/**
* test_repeat_until_failure.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_REPEAT_UNTIL_FAILURE_H
#define TEST_REPEAT_UNTIL_FAILURE_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_repeat_until_failure.h"
namespace TestRepeatUntilFailure {
TEST_CASE("[Modules][LimboAI] BTRepeatUntilFailure") {
Ref<BTRepeatUntilFailure> rep = memnew(BTRepeatUntilFailure);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(rep->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction);
rep->add_child(task);
SUBCASE("With various return statuses") {
task->ret_status = BTTask::SUCCESS;
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 1, 1, 1);
task->ret_status = BTTask::RUNNING;
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 2, 2, 1);
task->ret_status = BTTask::FAILURE;
CHECK(rep->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 2, 3, 2);
}
}
} //namespace TestRepeatUntilFailure
#endif // TEST_REPEAT_UNTIL_FAILURE_H

View File

@ -0,0 +1,51 @@
/**
* test_repeat_until_success.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_REPEAT_UNTIL_SUCCESS_H
#define TEST_REPEAT_UNTIL_SUCCESS_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_repeat_until_success.h"
namespace TestRepeatUntilSuccess {
TEST_CASE("[Modules][LimboAI] BTRepeatUntilSuccess") {
Ref<BTRepeatUntilSuccess> rep = memnew(BTRepeatUntilSuccess);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(rep->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction);
rep->add_child(task);
SUBCASE("With various return statuses") {
task->ret_status = BTTask::FAILURE;
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 1, 1, 1);
task->ret_status = BTTask::RUNNING;
CHECK(rep->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 2, 2, 1);
task->ret_status = BTTask::SUCCESS;
CHECK(rep->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 2, 3, 2);
}
}
} //namespace TestRepeatUntilSuccess
#endif // TEST_REPEAT_UNTIL_SUCCESS_H

87
tests/test_run_limit.h Normal file
View File

@ -0,0 +1,87 @@
/**
* test_run_limit.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_RUN_LIMIT_H
#define TEST_RUN_LIMIT_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_run_limit.h"
namespace TestRunLimit {
TEST_CASE("[Modules][LimboAI] BTRunLimit") {
Ref<BTRunLimit> lim = memnew(BTRunLimit);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(lim->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction);
lim->add_child(task);
SUBCASE("With run limit set to 2") {
lim->set_run_limit(2);
task->ret_status = BTTask::SUCCESS;
CHECK(lim->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 1, 1, 1); // * task executed
SUBCASE("When the child task succeeds") {
task->ret_status = BTTask::SUCCESS;
CHECK(lim->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 2, 2, 2); // * task executed
}
SUBCASE("When the child task fails") {
task->ret_status = BTTask::FAILURE;
CHECK(lim->execute(0.01666) == BTTask::FAILURE);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 2, 2, 2); // * task executed
}
task->ret_status = BTTask::SUCCESS;
CHECK(lim->execute(0.01666) == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task, 2, 2, 2); // * task not executed
CHECK(lim->execute(0.01666) == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task, 2, 2, 2); // * task not executed
}
SUBCASE("When the child task takes more than one tick to finish") {
lim->set_run_limit(2);
task->ret_status = BTTask::RUNNING;
CHECK(lim->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 1, 1, 0);
task->ret_status = BTTask::SUCCESS;
CHECK(lim->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 1, 2, 1);
task->ret_status = BTTask::RUNNING;
CHECK(lim->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 2, 3, 1);
task->ret_status = BTTask::SUCCESS;
CHECK(lim->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 2, 4, 2);
CHECK(lim->execute(0.01666) == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task, 2, 4, 2);
}
}
} //namespace TestRunLimit
#endif // TEST_RUN_LIMIT_H

141
tests/test_selector.h Normal file
View File

@ -0,0 +1,141 @@
/**
* test_selector.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_SELECTOR_H
#define TEST_SELECTOR_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_selector.h"
namespace TestSelector {
TEST_CASE("[Modules][LimboAI] BTSelector when all return FAILURE") {
Ref<BTSelector> sel = memnew(BTSelector);
Ref<BTTestAction> task1 = memnew(BTTestAction(BTTask::FAILURE));
Ref<BTTestAction> task2 = memnew(BTTestAction(BTTask::FAILURE));
Ref<BTTestAction> task3 = memnew(BTTestAction(BTTask::FAILURE));
sel->add_child(task1);
sel->add_child(task2);
sel->add_child(task3);
REQUIRE(sel->get_child_count() == 3);
// * First execution.
CHECK(sel->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1);
// * Second execution.
CHECK(sel->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2);
CHECK_ENTRIES_TICKS_EXITS(task2, 2, 2, 2);
CHECK_ENTRIES_TICKS_EXITS(task3, 2, 2, 2);
}
TEST_CASE("[Modules][LimboAI] BTSelector when second returns SUCCESS") {
Ref<BTSelector> sel = memnew(BTSelector);
Ref<BTTestAction> task1 = memnew(BTTestAction(BTTask::FAILURE));
Ref<BTTestAction> task2 = memnew(BTTestAction(BTTask::SUCCESS));
Ref<BTTestAction> task3 = memnew(BTTestAction(BTTask::FAILURE));
sel->add_child(task1);
sel->add_child(task2);
sel->add_child(task3);
REQUIRE(sel->get_child_count() == 3);
// * First execution.
CHECK(sel->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0);
// * Second execution.
CHECK(sel->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2);
CHECK_ENTRIES_TICKS_EXITS(task2, 2, 2, 2);
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0);
}
TEST_CASE("[Modules][LimboAI] BTSelector when second returns RUNNING") {
Ref<BTSelector> sel = memnew(BTSelector);
Ref<BTTestAction> task1 = memnew(BTTestAction(BTTask::FAILURE));
Ref<BTTestAction> task2 = memnew(BTTestAction(BTTask::RUNNING));
Ref<BTTestAction> task3 = memnew(BTTestAction(BTTask::FAILURE));
sel->add_child(task1);
sel->add_child(task2);
sel->add_child(task3);
REQUIRE(sel->get_child_count() == 3);
// * First execution.
CHECK(sel->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 0);
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0);
// * Second execution.
CHECK(sel->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 0);
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0);
// * Third execution with second task returning FAILURE.
task2->ret_status = BTTask::FAILURE;
CHECK(sel->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::FAILURE);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::FAILURE);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 3, 1);
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1);
}
} //namespace TestSelector
#endif // TEST_SELECTOR_H

144
tests/test_sequence.h Normal file
View File

@ -0,0 +1,144 @@
/**
* test_sequence.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_SEQUENCE_H
#define TEST_SEQUENCE_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_sequence.h"
namespace TestSequence {
TEST_CASE("[Modules][LimboAI] BTSequence when all return SUCCESS") {
Ref<BTSequence> seq = memnew(BTSequence);
Ref<BTTestAction> task1 = memnew(BTTestAction(BTTask::SUCCESS));
Ref<BTTestAction> task2 = memnew(BTTestAction(BTTask::SUCCESS));
Ref<BTTestAction> task3 = memnew(BTTestAction(BTTask::SUCCESS));
seq->add_child(task1);
seq->add_child(task2);
seq->add_child(task3);
REQUIRE(seq->get_child_count() == 3);
// * First execution.
CHECK(seq->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1);
// * Second execution.
CHECK(seq->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2);
CHECK_ENTRIES_TICKS_EXITS(task2, 2, 2, 2);
CHECK_ENTRIES_TICKS_EXITS(task3, 2, 2, 2);
}
TEST_CASE("[Modules][LimboAI] BTSequence when second returns FAILURE") {
Ref<BTSequence> seq = memnew(BTSequence);
Ref<BTTestAction> task1 = memnew(BTTestAction(BTTask::SUCCESS));
Ref<BTTestAction> task2 = memnew(BTTestAction(BTTask::FAILURE));
Ref<BTTestAction> task3 = memnew(BTTestAction(BTTask::SUCCESS));
seq->add_child(task1);
seq->add_child(task2);
seq->add_child(task3);
REQUIRE(seq->get_child_count() == 3);
// * First execution.
CHECK(seq->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0);
// * Second execution.
CHECK(seq->execute(0.01666) == BTTask::FAILURE);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::FAILURE);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 2, 2, 2);
CHECK_ENTRIES_TICKS_EXITS(task2, 2, 2, 2);
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0);
}
TEST_CASE("[Modules][LimboAI] BTSequence when second returns RUNNING") {
Ref<BTSequence> seq = memnew(BTSequence);
Ref<BTTestAction> task1 = memnew(BTTestAction(BTTask::SUCCESS));
Ref<BTTestAction> task2 = memnew(BTTestAction(BTTask::RUNNING));
Ref<BTTestAction> task3 = memnew(BTTestAction(BTTask::SUCCESS));
seq->add_child(task1);
seq->add_child(task2);
seq->add_child(task3);
REQUIRE(seq->get_child_count() == 3);
// * First execution.
CHECK(seq->execute(0.01666) == BTTask::RUNNING);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::RUNNING);
CHECK(task3->get_status() == BTTask::FRESH);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 1, 0);
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0);
// * Second execution.
CHECK(seq->execute(0.01666) == BTTask::RUNNING);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 2, 0);
CHECK_ENTRIES_TICKS_EXITS(task3, 0, 0, 0);
// * Third execution with second task returning SUCCESS.
task2->ret_status = BTTask::SUCCESS;
CHECK(seq->execute(0.01666) == BTTask::SUCCESS);
CHECK(task1->get_status() == BTTask::SUCCESS);
CHECK(task2->get_status() == BTTask::SUCCESS);
CHECK(task3->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task1, 1, 1, 1);
CHECK_ENTRIES_TICKS_EXITS(task2, 1, 3, 1);
CHECK_ENTRIES_TICKS_EXITS(task3, 1, 1, 1);
}
TEST_CASE("[Modules][LimboAI] BTSequence with no child tasks") {
Ref<BTSequence> seq = memnew(BTSequence);
REQUIRE(seq->get_child_count() == 0);
CHECK(seq->execute(0.01666) == BTTask::SUCCESS);
}
} //namespace TestSequence
#endif // TEST_SEQUENCE_H

View File

@ -0,0 +1,104 @@
/**
* test_set_agent_property.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_SET_AGENT_PROPERTY_H
#define TEST_SET_AGENT_PROPERTY_H
#include "limbo_test.h"
#include "modules/limboai/blackboard/bb_param/bb_param.h"
#include "modules/limboai/blackboard/bb_param/bb_variant.h"
#include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/scene/bt_set_agent_property.h"
#include "core/os/memory.h"
namespace TestSetAgentProperty {
TEST_CASE("[Modules][LimboAI] BTSetAgentProperty") {
Ref<BTSetAgentProperty> sap = memnew(BTSetAgentProperty);
Node *agent = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
sap->initialize(agent, bb);
sap->set_property("process_priority"); // * property that will be set by the task
Ref<BBVariant> value_param = memnew(BBVariant);
value_param->set_value_source(BBParam::SAVED_VALUE);
value_param->set_saved_value(7);
sap->set_value(value_param);
SUBCASE("With integer") {
CHECK(sap->execute(0.01666) == BTTask::SUCCESS);
CHECK(agent->get_process_priority() == 7);
}
SUBCASE("When value is not set") {
sap->set_value(nullptr);
ERR_PRINT_OFF;
CHECK(sap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When property is empty") {
sap->set_property("");
ERR_PRINT_OFF;
CHECK(sap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When property doesn't exist") {
sap->set_property("not_found");
ERR_PRINT_OFF;
CHECK(sap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("With StringName and String") {
value_param->set_saved_value("TestName");
sap->set_property("name");
CHECK(sap->execute(0.01666) == BTTask::SUCCESS);
CHECK(agent->get_name() == "TestName");
}
SUBCASE("With blackboard variable") {
value_param->set_value_source(BBParam::BLACKBOARD_VAR);
value_param->set_variable("priority");
SUBCASE("With proper BB variable") {
bb->set_var("priority", 8);
CHECK(sap->execute(0.01666) == BTTask::SUCCESS);
CHECK(agent->get_process_priority() == 8);
}
SUBCASE("With BB variable of wrong type") {
bb->set_var("priority", "high");
ERR_PRINT_OFF;
CHECK(sap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
CHECK(agent->get_process_priority() == 0);
}
SUBCASE("When BB variable doesn't exist") {
value_param->set_variable("not_found");
ERR_PRINT_OFF;
CHECK(sap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
CHECK(agent->get_process_priority() == 0);
}
SUBCASE("When BB variable isn't set") {
value_param->set_variable("");
ERR_PRINT_OFF;
CHECK(sap->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
CHECK(agent->get_process_priority() == 0);
}
}
memdelete(agent);
}
} //namespace TestSetAgentProperty
#endif // TEST_SET_AGENT_PROPERTY_H

82
tests/test_set_var.h Normal file
View File

@ -0,0 +1,82 @@
/**
* test_set_var.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_SET_VAR_H
#define TEST_SET_VAR_H
#include "core/variant/variant.h"
#include "limbo_test.h"
#include "modules/limboai/blackboard/bb_param/bb_param.h"
#include "modules/limboai/blackboard/bb_param/bb_variant.h"
#include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/bt/tasks/blackboard/bt_set_var.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "tests/test_macros.h"
namespace TestSetVar {
TEST_CASE("[Modules][LimboAI] BTSetVar") {
Ref<BTSetVar> sv = memnew(BTSetVar);
Ref<Blackboard> bb = memnew(Blackboard);
Node *dummy = memnew(Node);
sv->initialize(dummy, bb);
SUBCASE("When variable is not set") {
ERR_PRINT_OFF;
sv->set_variable("");
CHECK(sv->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("With variable set") {
Ref<BBVariant> value = memnew(BBVariant);
sv->set_value(value);
sv->set_variable("var");
SUBCASE("When setting to a provided value") {
value->set_value_source(BBParam::SAVED_VALUE);
value->set_saved_value(123);
CHECK(sv->execute(0.01666) == BTTask::SUCCESS);
CHECK(bb->get_var("var", 0) == Variant(123));
}
SUBCASE("When assigning value of another blackboard variable") {
value->set_value_source(BBParam::BLACKBOARD_VAR);
SUBCASE("BB variable is empty") {
ERR_PRINT_OFF;
value->set_variable("");
CHECK(sv->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("BB variable doesn't exist") {
ERR_PRINT_OFF;
Variant initial_value = Variant(777);
bb->set_var("var", initial_value);
value->set_variable("not_found");
CHECK(sv->execute(0.01666) == BTTask::FAILURE);
CHECK(bb->get_var("var", 0) == initial_value); // * Check initial value is intact.
ERR_PRINT_ON;
}
SUBCASE("BB variable exists") {
value->set_variable("compare_var");
bb->set_var("compare_var", 123);
CHECK(sv->execute(0.01666) == BTTask::SUCCESS);
CHECK(bb->get_var("var", 0) == Variant(123));
}
}
}
}
} //namespace TestSetVar
#endif // TEST_SET_VAR_H

View File

@ -0,0 +1,77 @@
/**
* test_stop_animation.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_STOP_ANIMATION_H
#define TEST_STOP_ANIMATION_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/scene/bt_stop_animation.h"
#include "scene/animation/animation_player.h"
#include "scene/main/window.h"
#include "scene/resources/animation.h"
#include "scene/resources/animation_library.h"
namespace TestStopAnimation {
TEST_CASE("[SceneTree][LimboAI] BTStopAnimation") {
AnimationPlayer *player = memnew(AnimationPlayer);
SceneTree::get_singleton()->get_root()->add_child(player);
player->set_process_callback(AnimationPlayer::AnimationProcessCallback::ANIMATION_PROCESS_IDLE);
Ref<AnimationLibrary> anim_lib = memnew(AnimationLibrary);
Ref<Animation> anim = memnew(Animation);
anim->set_name("test");
anim->set_length(0.1);
anim->set_loop_mode(Animation::LOOP_NONE);
REQUIRE(anim_lib->add_animation("test", anim) == OK);
REQUIRE(player->add_animation_library("", anim_lib) == OK);
REQUIRE(player->has_animation("test"));
Ref<BTStopAnimation> sa = memnew(BTStopAnimation);
Ref<BBNode> player_param = memnew(BBNode);
sa->set_animation_player(player_param);
Node *dummy = memnew(Node);
SceneTree::get_singleton()->get_root()->add_child(dummy);
Ref<Blackboard> bb = memnew(Blackboard);
SUBCASE("When AnimationPlayer doesn't exist") {
player_param->set_saved_value(NodePath("./NotFound"));
ERR_PRINT_OFF;
sa->initialize(dummy, bb);
CHECK(sa->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("When AnimationPlayer exists") {
player_param->set_saved_value(player->get_path());
sa->initialize(dummy, bb);
SUBCASE("When AnimationPlayer is not playing") {
REQUIRE_FALSE(player->is_playing());
CHECK(sa->execute(0.01666) == BTTask::SUCCESS);
}
SUBCASE("When AnimationPlayer is playing") {
player->play("test");
REQUIRE(player->is_playing());
CHECK(sa->execute(0.01666) == BTTask::SUCCESS);
CHECK_FALSE(player->is_playing());
}
}
memdelete(dummy);
memdelete(player);
}
} //namespace TestStopAnimation
#endif // TEST_STOP_ANIMATION_H

73
tests/test_subtree.h Normal file
View File

@ -0,0 +1,73 @@
/**
* test_subtree.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_SUBTREE_H
#define TEST_SUBTREE_H
#include "limbo_test.h"
#include "modules/limboai/bt/behavior_tree.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_subtree.h"
namespace TestSubtree {
TEST_CASE("[Modules][LimboAI] BTSubtree") {
ClassDB::register_class<BTTestAction>();
Ref<BTSubtree> st = memnew(BTSubtree);
Ref<Blackboard> bb = memnew(Blackboard);
Node *dummy = memnew(Node);
SUBCASE("When empty") {
ERR_PRINT_OFF;
st->initialize(dummy, bb);
CHECK(st->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
SUBCASE("With a subtree assigned") {
Ref<BehaviorTree> bt = memnew(BehaviorTree);
Ref<BTTestAction> task = memnew(BTTestAction(BTTask::SUCCESS));
bt->set_root_task(task);
st->set_subtree(bt);
CHECK(st->get_child_count() == 0);
st->initialize(dummy, bb);
CHECK(st->get_child_count() == 1);
CHECK(st->get_child(0) != task);
Ref<BTTestAction> ta = st->get_child(0);
REQUIRE(ta.is_valid());
SUBCASE("When child succeeds") {
ta->ret_status = BTTask::SUCCESS;
CHECK(st->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(ta, BTTask::SUCCESS, 1, 1, 1);
}
SUBCASE("When child fails") {
ta->ret_status = BTTask::FAILURE;
CHECK(st->execute(0.01666) == BTTask::FAILURE);
CHECK_STATUS_ENTRIES_TICKS_EXITS(ta, BTTask::FAILURE, 1, 1, 1);
}
SUBCASE("When child is running") {
ta->ret_status = BTTask::RUNNING;
CHECK(st->execute(0.01666) == BTTask::RUNNING);
CHECK_STATUS_ENTRIES_TICKS_EXITS(ta, BTTask::RUNNING, 1, 1, 0);
}
}
memdelete(dummy);
}
} //namespace TestSubtree
#endif // TEST_SUBTREE_H

215
tests/test_task.h Normal file
View File

@ -0,0 +1,215 @@
/**
* test_task.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_TASK_H
#define TEST_TASK_H
#include "limbo_test.h"
#include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "tests/test_macros.h"
namespace TestTask {
TEST_CASE("[Modules][LimboAI] BTTask") {
SUBCASE("Test with hierarchy") {
Ref<BTTask> task = memnew(BTTask);
Ref<BTTask> child1 = memnew(BTTask);
Ref<BTTask> child3 = memnew(BTTask);
// * add_child, get_child_count & get_child
REQUIRE(task->get_child_count() == 0);
task->add_child(child1);
task->add_child(child3);
REQUIRE(task->get_child_count() == 2);
REQUIRE(task->get_child(0) == child1);
REQUIRE(task->get_child(1) == child3);
// * add_child_at_index
Ref<BTTask> child2 = memnew(BTTask);
task->add_child_at_index(child2, 1);
REQUIRE(task->get_child_count() == 3);
REQUIRE(task->get_child(0) == child1);
REQUIRE(task->get_child(1) == child2);
REQUIRE(task->get_child(2) == child3);
/** Hierarchy:
* task->
* -> child1 (0)
* -> child2 (1)
* -> child3 (2)
*/
SUBCASE("Test has_child()") {
CHECK(task->has_child(child1));
CHECK(task->has_child(child2));
CHECK(task->has_child(child3));
Ref<BTTask> other = memnew(BTTask);
CHECK_FALSE(task->has_child(other));
}
SUBCASE("Test get_child_index()") {
CHECK(task->get_child_index(child1) == 0);
CHECK(task->get_child_index(child2) == 1);
CHECK(task->get_child_index(child3) == 2);
}
SUBCASE("Test get_child_index() with an out-of-hierarchy task") {
Ref<BTTask> other = memnew(BTTask);
CHECK(task->get_child_index(other) == -1);
}
SUBCASE("Test is_descendant_of()") {
Ref<BTTask> grandchild = memnew(BTTask);
child1->add_child(grandchild);
CHECK(child1->has_child(grandchild));
CHECK(child1->get_child_count() == 1);
CHECK(grandchild->is_descendant_of(task));
}
SUBCASE("Test next_sibling()") {
CHECK(child1->next_sibling() == child2);
CHECK(child2->next_sibling() == child3);
CHECK(child3->next_sibling() == nullptr);
}
SUBCASE("Test remove_child()") {
task->remove_child(child2);
REQUIRE(task->get_child_count() == 2);
CHECK(task->get_child(0) == child1);
CHECK(task->get_child(1) == child3);
task->remove_child(child3);
REQUIRE(task->get_child_count() == 1);
CHECK(task->get_child(0) == child1);
task->remove_child(child1);
REQUIRE(task->get_child_count() == 0);
}
SUBCASE("Test remove_child() with an out-of-hierarchy task") {
Ref<BTTask> other = memnew(BTTask);
// * Must not crash.
ERR_PRINT_OFF;
task->remove_child(other);
ERR_PRINT_ON;
}
SUBCASE("Test remove_at_index()") {
task->remove_child_at_index(1);
REQUIRE(task->get_child_count() == 2);
CHECK(task->get_child(0) == child1);
CHECK(task->get_child(1) == child3);
task->remove_child_at_index(1);
REQUIRE(task->get_child_count() == 1);
CHECK(task->get_child(0) == child1);
task->remove_child_at_index(0);
REQUIRE(task->get_child_count() == 0);
}
SUBCASE("Test remove_child_at_index() with an out-of-bounds index") {
// * Must not crash.
ERR_PRINT_OFF;
task->remove_child_at_index(-1);
CHECK(task->get_child_count() == 3);
task->remove_child_at_index(task->get_child_count());
CHECK(task->get_child_count() == 3);
ERR_PRINT_ON;
}
SUBCASE("Test is_root()") {
CHECK(task->is_root());
CHECK_FALSE(child1->is_root());
CHECK_FALSE(child2->is_root());
CHECK_FALSE(child3->is_root());
}
SUBCASE("Test get_root()") {
CHECK(task->get_root() == task);
CHECK(child1->get_root() == task);
CHECK(child2->get_root() == task);
CHECK(child3->get_root() == task);
}
SUBCASE("Test get_parent()") {
CHECK(task->get_parent() == nullptr);
CHECK(child1->get_parent() == task);
CHECK(child2->get_parent() == task);
CHECK(child2->get_parent() == task);
}
SUBCASE("Test initialize()") {
Node *dummy = memnew(Node);
Ref<Blackboard> bb = memnew(Blackboard);
SUBCASE("With valid parameters") {
task->initialize(dummy, bb);
CHECK(task->get_agent() == dummy);
CHECK(task->get_blackboard() == bb);
CHECK(child1->get_agent() == dummy);
CHECK(child1->get_blackboard() == bb);
CHECK(child2->get_agent() == dummy);
CHECK(child2->get_blackboard() == bb);
CHECK(child3->get_agent() == dummy);
CHECK(child3->get_blackboard() == bb);
}
SUBCASE("Test if not crashes when agent is null") {
ERR_PRINT_OFF;
task->initialize(nullptr, bb);
ERR_PRINT_ON;
}
SUBCASE("Test if not crashes when BB is null") {
ERR_PRINT_OFF;
task->initialize(dummy, nullptr);
ERR_PRINT_ON;
}
memdelete(dummy);
}
}
SUBCASE("Test get_elapsed_time()") {
Ref<BTTestAction> task = memnew(BTTestAction);
task->ret_status = BTTask::RUNNING;
CHECK(task->get_elapsed_time() == 0.0);
task->execute(888.0);
CHECK(task->get_elapsed_time() == 0.0); // * delta_time shouldn't contribute to the elapsed_time on the first tick.
task->execute(10.0);
CHECK(task->get_elapsed_time() == 10.0);
task->execute(10.0);
CHECK(task->get_elapsed_time() == 20.0);
SUBCASE("When finishing with SUCCESS or FAILURE") {
task->ret_status = BTTask::SUCCESS;
task->execute(10.0);
CHECK(task->get_elapsed_time() == 0.0);
}
SUBCASE("When cancelled") {
task->cancel();
CHECK(task->get_elapsed_time() == 0.0);
}
}
SUBCASE("Test clone()") {
// * Note: BTTask cannot be duplicated, thus using BTTestAction.
Ref<BTTestAction> task = memnew(BTTestAction);
Ref<BTTestAction> child1 = memnew(BTTestAction);
Ref<BTTestAction> child2 = memnew(BTTestAction);
task->add_child(child1);
task->add_child(child2);
REQUIRE(task->get_child_count() == 2);
REQUIRE(task->get_child(0) == child1);
REQUIRE(task->get_child(1) == child2);
Ref<BTTestAction> cloned = task->clone();
CHECK_FALSE(cloned == task);
REQUIRE(cloned->get_child_count() == 2);
CHECK_FALSE(cloned->get_child(0) == child1);
CHECK_FALSE(cloned->get_child(1) == child2);
}
}
} //namespace TestTask
#endif // TEST_TASK_H

89
tests/test_time_limit.h Normal file
View File

@ -0,0 +1,89 @@
/**
* test_time_limit.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_TIME_LIMIT_H
#define TEST_TIME_LIMIT_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/decorators/bt_time_limit.h"
namespace TestTimeLimit {
TEST_CASE("[Modules][LimboAI] BTTimeLimit") {
Ref<BTTimeLimit> lim = memnew(BTTimeLimit);
SUBCASE("When empty") {
ERR_PRINT_OFF;
CHECK(lim->execute(0.01666) == BTTask::FAILURE);
ERR_PRINT_ON;
}
Ref<BTTestAction> task = memnew(BTTestAction);
lim->add_child(task);
lim->set_time_limit(1.0);
SUBCASE("With a long-running task") {
task->ret_status = BTTask::RUNNING;
CHECK(lim->execute(0.0) == BTTask::RUNNING); // * elapsed 0.0
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 1, 1, 0); // * running
CHECK(lim->execute(0.4) == BTTask::RUNNING); // * elapsed 0.4
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 1, 2, 0); // * running
CHECK(lim->execute(0.4) == BTTask::RUNNING); // * elapsed 0.8
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 1, 3, 0); // * running
SUBCASE("When exceeding the time limit") {
CHECK(lim->execute(0.4) == BTTask::FAILURE); // * elapsed 1.2
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FRESH, 1, 4, 1); // * cancelled & exited
}
SUBCASE("When finishing on time") {
task->ret_status = BTTask::SUCCESS;
CHECK(lim->execute(0.1) == BTTask::SUCCESS); // * elapsed 0.9
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 1, 4, 1); // * succeeded & exited
}
}
SUBCASE("With a quick task") {
task->ret_status = BTTask::SUCCESS;
CHECK(lim->execute(0.01666) == BTTask::SUCCESS);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 1, 1, 1); // * succeeded
task->ret_status = BTTask::FAILURE;
CHECK(lim->execute(0.01666) == BTTask::FAILURE);
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FAILURE, 2, 2, 2); // * failed
}
SUBCASE("If time limit is reset") {
task->ret_status = BTTask::RUNNING;
CHECK(lim->execute(0.0) == BTTask::RUNNING); // * elapsed 0.0
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 1, 1, 0); // * running
CHECK(lim->execute(1.1) == BTTask::FAILURE); // * elapsed 1.1
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::FRESH, 1, 2, 1); // * cancelled due to time limit exceeded
CHECK(lim->execute(0.0) == BTTask::RUNNING); // * elapsed 0.0
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 2, 3, 1); // * running
CHECK(lim->execute(0.8) == BTTask::RUNNING); // * elapsed 0.8
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::RUNNING, 2, 4, 1); // * running
task->ret_status = BTTask::SUCCESS;
CHECK(lim->execute(0.8) == BTTask::SUCCESS); // * elapsed 1.6
CHECK_STATUS_ENTRIES_TICKS_EXITS(task, BTTask::SUCCESS, 2, 5, 2); // * succeeded, despite time limit exceeded
}
}
} //namespace TestTimeLimit
#endif // TEST_TIME_LIMIT_H

120
tests/test_wait_actions.h Normal file
View File

@ -0,0 +1,120 @@
/**
* test_wait_actions.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_WAIT_ACTIONS_H
#define TEST_WAIT_ACTIONS_H
#include "limbo_test.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/utility/bt_random_wait.h"
#include "modules/limboai/bt/tasks/utility/bt_wait.h"
#include "modules/limboai/bt/tasks/utility/bt_wait_ticks.h"
#include "core/math/math_funcs.h"
namespace TestWaitActions {
TEST_CASE("[Modules][LimboAI] BTWait") {
Ref<BTWait> wait = memnew(BTWait);
SUBCASE("With zero duration") {
wait->set_duration(0.0);
CHECK(wait->execute(0.0) == BTWait::SUCCESS);
}
SUBCASE("With one second duration") {
wait->set_duration(1.0);
CHECK(wait->execute(0.0) == BTWait::RUNNING);
CHECK(wait->execute(0.5) == BTWait::RUNNING); // * elapsed 0.5
CHECK(wait->execute(0.5) == BTWait::SUCCESS); // * elapsed 1.0
}
}
TEST_CASE("[Modules][LimboAI] BTWaitTicks") {
Ref<BTWaitTicks> wait = memnew(BTWaitTicks);
SUBCASE("With zero ticks") {
wait->set_num_ticks(0);
CHECK(wait->execute(0.01666) == BTWait::SUCCESS); // * elapsed 0 ticks
}
SUBCASE("With 1 tick") {
wait->set_num_ticks(1);
CHECK(wait->execute(0.01666) == BTWait::RUNNING); // * elapsed 0 ticks
CHECK(wait->execute(0.01666) == BTWait::SUCCESS); // * elapsed 1 tick
}
SUBCASE("With 2 ticks") {
wait->set_num_ticks(2);
CHECK(wait->execute(0.01666) == BTWait::RUNNING); // * elapsed 0 ticks
CHECK(wait->execute(0.01666) == BTWait::RUNNING); // * elapsed 1 tick
CHECK(wait->execute(0.01666) == BTWait::SUCCESS); // * elapsed 2 ticks
}
}
TEST_CASE("[Modules][LimboAI] BTRandomWait") {
Ref<BTRandomWait> wait = memnew(BTRandomWait);
Math::randomize();
SUBCASE("With duration range [0, 0]") {
wait->set_min_duration(0.0);
wait->set_max_duration(0.0);
CHECK(wait->execute(0.01666) == BTWait::SUCCESS);
}
SUBCASE("With certain SUCCESS") {
wait->set_min_duration(0.5);
wait->set_max_duration(1.0);
CHECK(wait->execute(0.00) == BTWait::RUNNING); // * elapsed 0.00
CHECK(wait->execute(0.25) == BTWait::RUNNING); // * elapsed 0.25
CHECK(wait->execute(0.76) == BTWait::SUCCESS); // * elapsed 1.01
}
SUBCASE("With duration range [0.5, 1.0]") {
wait->set_min_duration(0.5);
wait->set_max_duration(1.0);
int num_successes = 0;
int num_running = 0;
int num_failures = 0;
int num_undefined = 0;
for (int i = 0; i < 1000; i++) {
wait->execute(0.00); // * elapsed 0.00
wait->execute(0.75); // * elapsed 0.75
switch (wait->get_status()) {
case BTTask::RUNNING: {
num_running += 1;
} break;
case BTTask::SUCCESS: {
num_successes += 1;
} break;
case BTTask::FAILURE: {
num_failures += 1;
} break;
default: {
num_undefined += 1;
} break;
}
wait->cancel();
}
// * Expected ~500/500 SUCCESS/RUNNING.
CHECK(num_successes > 450);
CHECK(num_successes < 550);
CHECK(num_running > 450);
CHECK(num_running < 550);
CHECK(num_failures == 0);
CHECK(num_undefined == 0);
}
}
} //namespace TestWaitActions
#endif // TEST_WAIT_ACTIONS_H