Skip to content

Instantly share code, notes, and snippets.

@asasine
Last active January 29, 2024 16:56
Show Gist options
  • Select an option

  • Save asasine/bc20da23d1954d2f712f215bfacdc659 to your computer and use it in GitHub Desktop.

Select an option

Save asasine/bc20da23d1954d2f712f215bfacdc659 to your computer and use it in GitHub Desktop.
Backchained PPA

Backchained PPA

An example of a postcondition-precondition-action (PPA) that is backchained once to create a more capable tree.

Tested with BehaviorTree.CPP v4.5.1 with plain CMake.

  1. Clone the repo.
  2. Check out the same version: git checkout 4.5.1
  3. Set up dependencies according to the repo's README.
  4. Copy the gtest_ppa.cpp file to the tests/ directory.
  5. Modify tests/CMakeLists.txt to include gtest_ppa.cpp in the BT_TESTS variable.
  6. Modify the target_link_libraries on line 65 to include GTEST::gmock
  7. Build: mkdir build && cd build && cmake --build . --parallel -t behaviortree_cpp_test
  8. Run PPA tests: ./tests/behaviortree_cpp_test --gtest_filter=PPA.*
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "behaviortree_cpp/leaf_node.h"
#include "behaviortree_cpp/bt_factory.h"
namespace BT::test
{
using ::testing::Assign;
using ::testing::DoAll;
using ::testing::Return;
/// @brief Mocked @c BT::LeafNode that allows callers to trivially customize behavior of @c tick() and @c halt().
/// @tparam NodeTypeT The type of leaf node this class is.
template <NodeType NodeTypeT>
class MockLeafNode : public LeafNode
{
public:
MockLeafNode(const std::string& name)
: LeafNode(name, {})
{
}
// Made public for setting actions and expectations
MOCK_METHOD(NodeStatus, tick, (), (override));
MOCK_METHOD(void, halt, (), (override));
NodeType type() const override
{
return NodeTypeT;
}
};
TEST(PPA, EnsureWarm)
{
// This test shows the basic structure of a PPA: a fallback of a postcondition and an action to make that
// postcondition true.
/*
<BehaviorTree ID="EnsureWarm">
<ReactiveFallback>
<IsWarm />
<ReactiveSequence>
<IsHoldingJacket />
<WearJacket />
</ReactiveSequence>
</ReactiveFallback>
</BehaviorTree>
*/
// The final condition of the PPA; the thing that make_warm achieves. For this example, we're only warm after
// WearJacket returns success.
MockLeafNode<NodeType::CONDITION> is_warm("IsWarm");
NodeStatus is_warm_return = NodeStatus::FAILURE;
EXPECT_CALL(is_warm, tick())
.WillRepeatedly([&is_warm_return]() { return is_warm_return; });
// For this example, we already have a jacket
MockLeafNode<NodeType::CONDITION> is_holding_jacket("IsHoldingJacket");
EXPECT_CALL(is_holding_jacket, tick())
.Times(2)
.WillRepeatedly(Return(NodeStatus::SUCCESS));
// Putting the jacket on takes two ticks, and updates the return value of IsWarm
MockLeafNode<NodeType::ACTION> wear_jacket("WearJacket");
EXPECT_CALL(wear_jacket, tick())
.WillOnce(Return(NodeStatus::RUNNING))
.WillOnce(DoAll(Assign(&is_warm_return, NodeStatus::SUCCESS), Return(NodeStatus::SUCCESS)));
ReactiveSequence make_warm("MakeWarm");
make_warm.addChild(&is_holding_jacket);
make_warm.addChild(&wear_jacket);
ReactiveFallback ensure_warm("EnsureWarm");
ensure_warm.addChild(&is_warm);
ensure_warm.addChild(&make_warm);
// first tick: not warm, have a jacket: start wearing it
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::RUNNING);
// second tick: warm (wearing succeeded)
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::SUCCESS);
// third tick: still warm (just the postcondition ticked)
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::SUCCESS);
}
TEST(PPA, EnsureWarmWithEnsureHoldingHacket)
{
// This test backchains on HoldingHacket => EnsureHoldingHacket to iteratively add reactivity and functionality to the tree.
// The general structure of the PPA remains the same.
/*
<BehaviorTree ID="EnsureWarm">
<ReactiveFallback>
<IsWarm />
<ReactiveSequence>
<SubTree ID="EnsureHoldingJacket" />
<WearJacket />
</ReactiveSequence>
</ReactiveFallback>
</BehaviorTree>
<BehaviorTree ID="EnsureHoldingJacket">
<ReactiveFallback>
<IsHoldingJacket />
<ReactiveSequence>
<IsNearCloset />
<GrabJacket />
</ReactiveSequence>
</ReactiveFallback>
</BehaviorTree>
*/
// Same postcondition as first test.
MockLeafNode<NodeType::CONDITION> is_warm("IsWarm");
NodeStatus is_warm_return = NodeStatus::FAILURE; // initially: not warm
EXPECT_CALL(is_warm, tick())
.WillRepeatedly([&is_warm_return]() { return is_warm_return; });
// The new PPA's postcondition is the same condition that we're backchaining on.
MockLeafNode<NodeType::CONDITION> is_holding_jacket("IsHoldingJacket");
NodeStatus is_holding_jacket_return = NodeStatus::FAILURE; // initially: not holding a jacket
EXPECT_CALL(is_holding_jacket, tick())
.WillRepeatedly([&is_holding_jacket_return]() { return is_holding_jacket_return; });
// For this test, we're already near a closet. This condition could be backchained!
MockLeafNode<NodeType::CONDITION> is_near_closet("IsNearCloset");
EXPECT_CALL(is_near_closet, tick())
.Times(2)
.WillRepeatedly(Return(NodeStatus::SUCCESS));
// Same as first test's action: running once, then success, updating this PPA's postcondition.
MockLeafNode<NodeType::ACTION> grab_jacket("GrabJacket");
EXPECT_CALL(grab_jacket, tick())
.WillOnce(Return(NodeStatus::RUNNING))
.WillRepeatedly(DoAll(Assign(&is_holding_jacket_return, NodeStatus::SUCCESS), Return(NodeStatus::SUCCESS)));
// Same as first test.
MockLeafNode<NodeType::ACTION> wear_jacket("WearJacket");
EXPECT_CALL(wear_jacket, tick())
.WillOnce(Return(NodeStatus::RUNNING))
.WillOnce(DoAll(Assign(&is_warm_return, NodeStatus::SUCCESS), Return(NodeStatus::SUCCESS)));
// The new PPA's precondition-action (PA) is the same form as the root PPA's PA. Similar to MakeWarm, this PA ticks
// running twice and updates the postcondition afterwards.
ReactiveSequence grab_jacket_from_closet("GrabJacketFromCloset");
grab_jacket_from_closet.addChild(&is_near_closet);
grab_jacket_from_closet.addChild(&grab_jacket);
// For this example, our precondition is another PPA: it short circuits if we already have a jacket, otherwise it
// fetches one asynchronously.
ReactiveFallback ensure_holding_jacket("EnsureHoldingJacket");
ensure_holding_jacket.addChild(&is_holding_jacket);
ensure_holding_jacket.addChild(&grab_jacket_from_closet);
ReactiveSequence make_warm("MakeWarm");
make_warm.addChild(&ensure_holding_jacket); // Use the new PPA within PA, not the original precondition.
make_warm.addChild(&wear_jacket);
ReactiveFallback ensure_warm("EnsureWarm");
ensure_warm.addChild(&is_warm);
ensure_warm.addChild(&make_warm);
// BUG(755): need to disable exception throwing on reactive control nodes to support backchaining.
ReactiveSequence::EnableException(false);
ReactiveFallback::EnableException(false);
// first tick: not warm, no jacket, near a closet: start grabbing a jacket
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::RUNNING);
// first tick: not warm, has a jacket (grabbing succeeded), not wearing: start wearing
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::RUNNING);
// third tick: warm (wearing succeeded)
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::SUCCESS);
// fourth tick: still warm (just the postcondition ticked)
EXPECT_EQ(ensure_warm.executeTick(), NodeStatus::SUCCESS);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment