Skip to content

Behavior Tree Basic Node Types

We have defined a number of basic node types. These are the building blocks of any behavior tree. They are defined in the vultron.bt.base module.

vultron.bt.base.node_status

This module defines a Behavior Tree Node Status object.

NodeStatus

Bases: Enum

NodeStatus is the return value of a node's tick() method. Nodes can only return one of these values.

Nodes return SUCCESS if they have completed their task successfully. Nodes return FAILURE if they have completed their task unsuccessfully. Nodes return RUNNING if they are still in the process of completing their task.

Source code in vultron/bt/base/node_status.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class NodeStatus(Enum):
    """NodeStatus is the return value of a node's tick() method.
    Nodes can only return one of these values.

    Nodes return SUCCESS if they have completed their task successfully.
    Nodes return FAILURE if they have completed their task unsuccessfully.
    Nodes return RUNNING if they are still in the process of completing their task.
    """

    FAILURE = 0
    SUCCESS = 1
    RUNNING = 2

    def __str__(self):
        return self.name

vultron.bt.base.bt_node

This module provides the base class for all nodes in the Behavior Tree.

It also provides a number of core node types that can be used to build a Behavior Tree.

ActionNode

Bases: LeafNode

ActionNode is the base class for all action nodes in the Behavior Tree. Action nodes are leaf nodes that perform an action. An action node's func() method returns True for success, False for failure, and None for running.

Source code in vultron/bt/base/bt_node.py
331
332
333
334
335
336
337
338
class ActionNode(LeafNode):
    """ActionNode is the base class for all action nodes in the Behavior Tree.
    Action nodes are leaf nodes that perform an action.
    An action node's func() method returns True for success, False for failure, and None for running.
    """

    name_pfx = "a"
    Exc = ActionNodeError

BtNode

BtNode is the base class for all nodes in the Behavior Tree.

Source code in vultron/bt/base/bt_node.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class BtNode:
    """BtNode is the base class for all nodes in the Behavior Tree."""

    indent_level = 0
    name_pfx = None
    _node_shape = "box"
    _children = None
    _objcount = 0

    def __init__(self):
        BtNode._objcount += 1

        pfx = ""
        if self.name_pfx is not None:
            pfx = f"{self.name_pfx}_"

        # freeze object count into a string at instantiation time
        self.name_sfx = f"_{BtNode._objcount}"

        self.name = f"{pfx}{self.__class__.__name__}{self.name_sfx}"

        self.parent = None
        self.children = []

        self.status = None
        self.bb = None

        self._setup_complete = False

        self.add_children()

    def __enter__(self):
        self.setup()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

    def setup(self) -> None:
        """Sets up the node and its children.

        Returns:
            None
        """
        if self.parent is not None:
            self.bb = self.parent.bb
        for child in self.children:
            child.setup()

        self._setup_complete = True

    def add_child(self, child: "BtNode") -> "BtNode":
        """Adds a child to the node.

        Args:
            child: the child node to add

        Returns:
            the child node
        """
        self.children.append(child)
        child.indent_level = self.indent_level + 1
        child.parent = self
        child.bb = self.bb
        return child

    def add_children(self) -> None:
        """
        Adds children to the node. Loops through the _children list and creates a new instance of each child class.
        """
        if self._children is None:
            return

        for child_class in self._children:
            # instantiate the child class and add it to this node's children
            child = child_class()
            self.add_child(child)

    @property
    def _pfx(self) -> str:
        if self.name_pfx is not None:
            return f"({self.name_pfx})"
        return ""

    def _pre_tick(self, depth: int = 0) -> None:
        """Called before the node is ticked.
         Override this method in your subclass if you need to do something before the node is ticked.
         Does nothing by default.

        Args:
            depth: the node's depth in the tree
        """

    def tick(self, depth: int = 0) -> NodeStatus:
        """Ticks the node.
        Performs the following actions:

        - calls `_pre_tick()`
        - calls `_tick()`
        - calls `_post_tick()`
        - sets the node's status based on the return value of `_tick()`

        Args:
            depth: the node's depth in the tree

        Returns:
            the node's status (as a NodeStatus enum)
        """
        if self.name is not None:
            logger.debug(_indent(depth) + f"{self._pfx} {self.name}")

        with self:
            self._pre_tick(depth=depth)
            status = self._tick(depth)
            self.status = status
            self._post_tick(depth=depth)

        if self.name is not None:
            logger.debug(_indent(depth + 1) + f"= {self.status}")

        return status

    def _post_tick(self, depth: int = 0) -> None:
        """Called after the node is ticked.
        Override this method in your subclass if you need to do something after the node is ticked.
        Does nothing by default.

        Args:
            depth
        """
        pass

    def _tick(self, depth: int = 0) -> NodeStatus:
        """Called by tick().
        Implement this method in your subclass.

        Args:
            depth

        Returns:
            the node's status (as a NodeStatus enum)
        """
        raise NotImplementedError

    @property
    def _node_label(self) -> str:
        if self.name_pfx is not None:
            return f"{self.name_pfx} {self.name}"

        return self.name

    @property
    def _is_leaf_node(self) -> bool:
        """Returns True if the node is a leaf node, False otherwise."""
        return self._children is None or len(self._children) == 0

    def _namestr(self, depth=0) -> str:
        """Returns a string representation of the node's name."""
        return _indent(depth) + f"{self._pfx} {self.name}"

    def to_str(self, depth=0) -> str:
        """Returns a string representation of the tree rooted at this node."""

        namestring = self._namestr(depth) + "\n"

        if self._is_leaf_node:
            # this is a leaf node
            return namestring

        # recurse through children and return a string representation of the tree
        parts = [
            namestring,
        ]
        for child in self.children:
            parts.append(child.to_str(depth + 1))
        return "".join(parts)

    def to_graph(self) -> nx.DiGraph:
        G = nx.DiGraph()

        # add a self node
        # see note *** below
        G.add_node(self.name, shape=self._node_shape)

        # walk the children
        for child in self.children:
            # add an edge from this node to the child node
            G.add_edge(self.name, child.name)
            # the child node will add itself to the graph with its shape because ***
            # create a graph for the child node and add it to this graph
            G = nx.compose(G, child.to_graph())

        return G

    def to_mermaid(self, depth=0, topdown=True) -> str:
        """Returns a string representation of the tree rooted at this node in mermaid format."""

        import re

        if self._is_leaf_node:
            # this is a leaf node, we aren't doing anything with it
            return ""

        parts = []
        if depth == 0:
            # add preamble
            parts.append("```mermaid")
            if topdown:
                parts.append("graph TD")
            else:
                parts.append("graph LR")

        def fixname(nstr: str) -> str:
            # TODO these should be subclass attributes
            nstr = re.sub(r"^>_", "→ ", nstr)
            nstr = re.sub(r"^\^_", "#8645; ", nstr)
            nstr = re.sub(r"^z_", "#127922; ", nstr)
            nstr = re.sub(r"^a_", "#9648; ", nstr)
            nstr = re.sub(r"^c_", "#11052; ", nstr)
            nstr = re.sub(r"^\?_", "? ", nstr)
            nstr = re.sub(r"_\d+$", "", nstr)

            return nstr

        name = fixname(self.name)
        sname = f"{self.__class__.__name__}{self.name_sfx}"
        if depth == 0:
            parts.append(f'  {sname}["{name}"]')

        for child in self.children:
            cname = f"{child.__class__.__name__}{child.name_sfx}"
            parts.append(f'  {cname}["{fixname(child.name)}"]')

            parts.append(f"  {sname} --> {cname}")
            # recurse through children and return a string representation of the tree below this node
            parts.append(child.to_mermaid(depth + 1))

        if depth == 0:
            # add postamble
            parts.append("```")

        parts = [p for p in parts if p != ""]

        return "\n".join(parts)

add_child(child)

Adds a child to the node.

Parameters:

Name Type Description Default
child BtNode

the child node to add

required

Returns:

Type Description
BtNode

the child node

Source code in vultron/bt/base/bt_node.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def add_child(self, child: "BtNode") -> "BtNode":
    """Adds a child to the node.

    Args:
        child: the child node to add

    Returns:
        the child node
    """
    self.children.append(child)
    child.indent_level = self.indent_level + 1
    child.parent = self
    child.bb = self.bb
    return child

add_children()

Adds children to the node. Loops through the _children list and creates a new instance of each child class.

Source code in vultron/bt/base/bt_node.py
108
109
110
111
112
113
114
115
116
117
118
def add_children(self) -> None:
    """
    Adds children to the node. Loops through the _children list and creates a new instance of each child class.
    """
    if self._children is None:
        return

    for child_class in self._children:
        # instantiate the child class and add it to this node's children
        child = child_class()
        self.add_child(child)

setup()

Sets up the node and its children.

Returns:

Type Description
None

None

Source code in vultron/bt/base/bt_node.py
80
81
82
83
84
85
86
87
88
89
90
91
def setup(self) -> None:
    """Sets up the node and its children.

    Returns:
        None
    """
    if self.parent is not None:
        self.bb = self.parent.bb
    for child in self.children:
        child.setup()

    self._setup_complete = True

tick(depth=0)

Ticks the node. Performs the following actions:

  • calls _pre_tick()
  • calls _tick()
  • calls _post_tick()
  • sets the node's status based on the return value of _tick()

Parameters:

Name Type Description Default
depth int

the node's depth in the tree

0

Returns:

Type Description
NodeStatus

the node's status (as a NodeStatus enum)

Source code in vultron/bt/base/bt_node.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def tick(self, depth: int = 0) -> NodeStatus:
    """Ticks the node.
    Performs the following actions:

    - calls `_pre_tick()`
    - calls `_tick()`
    - calls `_post_tick()`
    - sets the node's status based on the return value of `_tick()`

    Args:
        depth: the node's depth in the tree

    Returns:
        the node's status (as a NodeStatus enum)
    """
    if self.name is not None:
        logger.debug(_indent(depth) + f"{self._pfx} {self.name}")

    with self:
        self._pre_tick(depth=depth)
        status = self._tick(depth)
        self.status = status
        self._post_tick(depth=depth)

    if self.name is not None:
        logger.debug(_indent(depth + 1) + f"= {self.status}")

    return status

to_mermaid(depth=0, topdown=True)

Returns a string representation of the tree rooted at this node in mermaid format.

Source code in vultron/bt/base/bt_node.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def to_mermaid(self, depth=0, topdown=True) -> str:
    """Returns a string representation of the tree rooted at this node in mermaid format."""

    import re

    if self._is_leaf_node:
        # this is a leaf node, we aren't doing anything with it
        return ""

    parts = []
    if depth == 0:
        # add preamble
        parts.append("```mermaid")
        if topdown:
            parts.append("graph TD")
        else:
            parts.append("graph LR")

    def fixname(nstr: str) -> str:
        # TODO these should be subclass attributes
        nstr = re.sub(r"^>_", "→ ", nstr)
        nstr = re.sub(r"^\^_", "#8645; ", nstr)
        nstr = re.sub(r"^z_", "#127922; ", nstr)
        nstr = re.sub(r"^a_", "#9648; ", nstr)
        nstr = re.sub(r"^c_", "#11052; ", nstr)
        nstr = re.sub(r"^\?_", "? ", nstr)
        nstr = re.sub(r"_\d+$", "", nstr)

        return nstr

    name = fixname(self.name)
    sname = f"{self.__class__.__name__}{self.name_sfx}"
    if depth == 0:
        parts.append(f'  {sname}["{name}"]')

    for child in self.children:
        cname = f"{child.__class__.__name__}{child.name_sfx}"
        parts.append(f'  {cname}["{fixname(child.name)}"]')

        parts.append(f"  {sname} --> {cname}")
        # recurse through children and return a string representation of the tree below this node
        parts.append(child.to_mermaid(depth + 1))

    if depth == 0:
        # add postamble
        parts.append("```")

    parts = [p for p in parts if p != ""]

    return "\n".join(parts)

to_str(depth=0)

Returns a string representation of the tree rooted at this node.

Source code in vultron/bt/base/bt_node.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def to_str(self, depth=0) -> str:
    """Returns a string representation of the tree rooted at this node."""

    namestring = self._namestr(depth) + "\n"

    if self._is_leaf_node:
        # this is a leaf node
        return namestring

    # recurse through children and return a string representation of the tree
    parts = [
        namestring,
    ]
    for child in self.children:
        parts.append(child.to_str(depth + 1))
    return "".join(parts)

ConditionCheck

Bases: LeafNode

ConditionCheck is the base class for all condition check nodes in the Behavior Tree. Condition check nodes are leaf nodes that check a condition. A condition check node's func() method returns True for success, False for failure. Although it is possible to return None for running, this is not recommended.

Source code in vultron/bt/base/bt_node.py
341
342
343
344
345
346
347
348
349
350
class ConditionCheck(LeafNode):
    """ConditionCheck is the base class for all condition check nodes in the Behavior Tree.
    Condition check nodes are leaf nodes that check a condition.
    A condition check node's func() method returns True for success, False for failure.
    Although it is possible to return None for running, this is not recommended.
    """

    name_pfx = "c"
    _node_shape = "ellipse"
    Exc = ConditionCheckError

CountTicks

Bases: BtNode

CountTicks is a decorator node that counts the number of times it is ticked.

Source code in vultron/bt/base/bt_node.py
353
354
355
356
357
358
359
360
361
362
363
364
class CountTicks(BtNode):
    """CountTicks is a decorator node that counts the number of times it is ticked."""

    start = 0

    def __init__(self):
        super().__init__()
        self.counter = self.start

    def _tick(self, depth: int = 0) -> NodeStatus:
        self.counter += 1
        return NodeStatus.SUCCESS

LeafNode

Bases: BtNode

LeafNode is the base class for all leaf nodes in the Behavior Tree. Leaf nodes are nodes that do not have children.

Source code in vultron/bt/base/bt_node.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
class LeafNode(BtNode):
    """LeafNode is the base class for all leaf nodes in the Behavior Tree.
    Leaf nodes are nodes that do not have children.
    """

    Exc = LeafNodeError

    def __init__(self):
        """
        Raises a LeafNodeError if the node has children.
        """
        if self.__class__._children is not None:
            raise self.Exc("Behavior Tree Leaf Nodes cannot have children")
        super().__init__()

    def func(self) -> Union[bool, None]:
        """
        Override this method in your subclass.
        Return True for success, False for failure, and None for running.
        """

        raise NotImplementedError

    def _tick(self, depth: int = 0) -> NodeStatus:
        """
        Calls the node's func() method and returns the result.

        Args:
            depth

        Returns:
            the node's status (as a NodeStatus enum)
        """

        result = self.func()

        if result is None:
            return NodeStatus.RUNNING
        elif result:
            return NodeStatus.SUCCESS
        return NodeStatus.FAILURE

__init__()

Raises a LeafNodeError if the node has children.

Source code in vultron/bt/base/bt_node.py
295
296
297
298
299
300
301
def __init__(self):
    """
    Raises a LeafNodeError if the node has children.
    """
    if self.__class__._children is not None:
        raise self.Exc("Behavior Tree Leaf Nodes cannot have children")
    super().__init__()

func()

Override this method in your subclass. Return True for success, False for failure, and None for running.

Source code in vultron/bt/base/bt_node.py
303
304
305
306
307
308
309
def func(self) -> Union[bool, None]:
    """
    Override this method in your subclass.
    Return True for success, False for failure, and None for running.
    """

    raise NotImplementedError

SnapshotState

Bases: BtNode

SnapshotState is a decorator node that takes a snapshot of the blackboard and appends it to the STATELOG list.

Source code in vultron/bt/base/bt_node.py
370
371
372
373
374
375
376
377
378
379
380
381
class SnapshotState(BtNode):
    """
    SnapshotState is a decorator node that takes a snapshot of the blackboard and appends it to the STATELOG list.
    """

    name = "Snapshot_state"

    def _tick(self, depth: int = 0) -> NodeStatus:
        global STATELOG
        snapshot = deepcopy(self.bb)
        STATELOG.append(snapshot)
        return NodeStatus.SUCCESS

vultron.bt.base.composites

This module implements Composite Nodes for a Behavior Tree.

FallbackNode

Bases: BtNode

FallbackNode is a composite node that ticks its children in order.

  • If a child returns SUCCESS, the FallbackNode returns SUCCESS.
  • If a child returns RUNNING, the FallbackNode returns RUNNING.
  • If a child returns FAILURE, the FallbackNode ticks the next child.
  • If all children return FAILURE, the FallbackNode returns FAILURE.
Source code in vultron/bt/base/composites.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class FallbackNode(BtNode):
    """FallbackNode is a composite node that ticks its children in order.

    - If a child returns SUCCESS, the FallbackNode returns SUCCESS.
    - If a child returns RUNNING, the FallbackNode returns RUNNING.
    - If a child returns FAILURE, the FallbackNode ticks the next child.
    - If all children return FAILURE, the FallbackNode returns FAILURE.
    """

    name_pfx = "?"

    # _node_shape = "diamond"

    def _tick(self, depth=0):
        for child in self.children:
            child_status = child.tick(depth + 1)

            if child_status == NodeStatus.RUNNING:
                return NodeStatus.RUNNING
            elif child_status == NodeStatus.SUCCESS:
                return NodeStatus.SUCCESS
        return NodeStatus.FAILURE

ParallelNode

Bases: BtNode

ParallelNode is a composite node that ticks its children in parallel.

When subclassing, you must set the m attribute indicating the minimum number of successes. The maximum failures is then calculated as N - m, where N is the number of children.

  • When a child returns SUCCESS, the ParallelNode increments a success counter.
  • When a child returns FAILURE, the ParallelNode increments a failure counter.
  • If the success counter reaches the minimum number of successes, the ParallelNode returns SUCCESS.
  • If the failure counter reaches the maximum number of failures, the ParallelNode returns FAILURE.
  • If neither of the above conditions are met, the ParallelNode returns RUNNING.

Not Fully Implemented

In the current implementation, the ParallelNode does not actually tick its children in parallel. Instead, it ticks them in a random order and counts the results until either SUCCESS or FAILURE is indicated as above. This is good enough for demonstration purposes, but it should be replaced with a proper parallel implementation.

Source code in vultron/bt/base/composites.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class ParallelNode(BtNode):
    """
    ParallelNode is a composite node that ticks its children in parallel.

    When subclassing, you must set the `m` attribute indicating the minimum number of successes.
    The maximum failures is then calculated as `N - m`, where N is the number of children.

    - When a child returns SUCCESS, the ParallelNode increments a success counter.
    - When a child returns FAILURE, the ParallelNode increments a failure counter.
    - If the success counter reaches the minimum number of successes, the ParallelNode returns SUCCESS.
    - If the failure counter reaches the maximum number of failures, the ParallelNode returns FAILURE.
    - If neither of the above conditions are met, the ParallelNode returns RUNNING.

    !!! warning "Not Fully Implemented"

        In the current implementation, the ParallelNode does not actually tick its children in parallel.
        Instead, it ticks them in a random order and counts the results until either SUCCESS or FAILURE is
        indicated as above. This is good enough for demonstration purposes, but it should be replaced with
        a proper parallel implementation.
    """

    # todo this needs to have a policy for how to handle failures/successes in the children
    # e.g., a Sequence-like policy where all children must succeed, or a Selector-like policy where only one child needs to succeed
    # or just actually implement the parallelism as described in the docstring

    name_pfx = "="
    m = 1

    # _node_shape = "parallelogram"

    @property
    def N(self):
        return len(self.children)

    def set_min_successes(self, m):
        self.m = m

    def _tick(self, depth=0):
        successes = 0
        failures = 0

        # todo: this is a kludge because I'm too lazy to do real parallelism yet
        # randomize order so we don't get stuck
        children = list(self.children)
        random.shuffle(children)

        for child in children:
            child_status = child.tick(depth + 1)
            if child_status == NodeStatus.SUCCESS:
                successes += 1
            elif child_status == NodeStatus.FAILURE:
                failures += 1

            if successes >= self.m:
                return NodeStatus.SUCCESS
            elif failures > (self.N - self.m):
                return NodeStatus.FAILURE
        return NodeStatus.RUNNING

SequenceNode

Bases: BtNode

SequenceNode is a composite node that ticks its children in order.

  • If a child returns SUCCESS, the SequenceNode ticks the next child.
  • If a child returns RUNNING, the SequenceNode returns RUNNING.
  • If a child returns FAILURE, the SequenceNode returns FAILURE.
  • If all children return SUCCESS, the SequenceNode returns SUCCESS.
Source code in vultron/bt/base/composites.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class SequenceNode(BtNode):
    """SequenceNode is a composite node that ticks its children in order.

    - If a child returns SUCCESS, the SequenceNode ticks the next child.
    - If a child returns RUNNING, the SequenceNode returns RUNNING.
    - If a child returns FAILURE, the SequenceNode returns FAILURE.
    - If all children return SUCCESS, the SequenceNode returns SUCCESS.
    """

    name_pfx = ">"

    # _node_shape = "rarrow"

    def _tick(self, depth=0):
        for child in self.children:
            child_status = child.tick(depth + 1)
            if child_status == NodeStatus.RUNNING:
                return NodeStatus.RUNNING
            elif child_status == NodeStatus.FAILURE:
                return NodeStatus.FAILURE
        return NodeStatus.SUCCESS

vultron.bt.base.decorators

This module defines a number of Behavior Tree Decorator Nodes.

BtDecorator

Bases: BtNode

BtDecorator is the base class for all decorators in the Behavior Tree.

Source code in vultron/bt/base/decorators.py
25
26
27
28
29
30
class BtDecorator(BtNode):
    """
    BtDecorator is the base class for all decorators in the Behavior Tree.
    """

    name_pfx = "d"

ForceFailure

Bases: BtDecorator

ForceFailure decorator returns FAILURE no matter what the child node returns.

Source code in vultron/bt/base/decorators.py
 94
 95
 96
 97
 98
 99
100
101
102
103
class ForceFailure(BtDecorator):
    """ForceFailure decorator returns FAILURE no matter what the child node returns."""

    name_pfx = "F"

    def _tick(self, depth=0):
        only_child = self.children[0]
        child_status = only_child.tick(depth + 1)
        logger.debug(f"Child {only_child.name} returns {child_status}")
        return NodeStatus.FAILURE

ForceRunning

Bases: BtDecorator

ForceRunning decorator returns RUNNING no matter what the child node returns.

Source code in vultron/bt/base/decorators.py
106
107
108
109
110
111
112
113
114
115
class ForceRunning(BtDecorator):
    """ForceRunning decorator returns RUNNING no matter what the child node returns."""

    name_pfx = "R"

    def _tick(self, depth=0):
        only_child = self.children[0]
        child_status = only_child.tick(depth + 1)
        logger.debug(f"Child {only_child.name} returns {child_status}")
        return NodeStatus.RUNNING

ForceSuccess

Bases: BtDecorator

ForceSuccess decorator returns SUCCESS no matter what the child node returns.

Source code in vultron/bt/base/decorators.py
82
83
84
85
86
87
88
89
90
91
class ForceSuccess(BtDecorator):
    """ForceSuccess decorator returns SUCCESS no matter what the child node returns."""

    name_pfx = "S"

    def _tick(self, depth=0):
        only_child = self.children[0]
        child_status = only_child.tick(depth + 1)
        logger.debug(f"Child {only_child.name} returns {child_status}")
        return NodeStatus.SUCCESS

Invert

Bases: BtDecorator

Inverts the result of the child node.

  • If the child node returns SUCCESS, the Invert decorator will return FAILURE.
  • If the child node returns FAILURE, the Invert decorator will return SUCCESS.
  • If the child node returns RUNNING, the Invert decorator will return RUNNING.
Source code in vultron/bt/base/decorators.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Invert(BtDecorator):
    """Inverts the result of the child node.

    - If the child node returns SUCCESS, the Invert decorator will return FAILURE.
    - If the child node returns FAILURE, the Invert decorator will return SUCCESS.
    - If the child node returns RUNNING, the Invert decorator will return RUNNING.
    """

    name_pfx = "^"

    def _tick(self, depth=0):
        only_child = self.children[0]
        child_status = only_child.tick(depth + 1)

        if child_status == NodeStatus.FAILURE:
            return NodeStatus.SUCCESS
        elif child_status == NodeStatus.SUCCESS:
            return NodeStatus.FAILURE
        return NodeStatus.RUNNING

LoopDecorator

Bases: BtDecorator

LoopDecorator is the base class for all decorators that loop.

Source code in vultron/bt/base/decorators.py
118
119
120
121
122
123
124
125
126
127
128
129
130
class LoopDecorator(BtDecorator):
    """LoopDecorator is the base class for all decorators that loop."""

    name_pfx = "l"
    reset = False

    def __init__(self):
        super().__init__()
        self.count = 0

    def _pre_tick(self, depth=0):
        if self.reset:
            self.count = 0

RepeatN

Bases: LoopDecorator

Repeat up to n times until the child returns failure or running. When subclassing RepeatN, set the n class variable to the number of repeats.

Source code in vultron/bt/base/decorators.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
class RepeatN(LoopDecorator):
    """
    Repeat up to n times until the child returns failure or running.
    When subclassing RepeatN, set the `n` class variable to the number of repeats.
    """

    n = 1

    def _tick(self, depth=0):
        only_child = self.children[0]
        for i in range(self.n):
            child_status = only_child.tick(depth + 1)
            self.count += 1
            if child_status == NodeStatus.SUCCESS:
                continue
            if child_status == NodeStatus.FAILURE:
                return NodeStatus.FAILURE
            if child_status == NodeStatus.RUNNING:
                return NodeStatus.RUNNING

        return NodeStatus.SUCCESS

RepeatUntilFail

Bases: LoopDecorator

Repeat until the child returns FAILURE, then return SUCCESS.

Source code in vultron/bt/base/decorators.py
178
179
180
181
182
183
184
185
186
187
188
189
class RepeatUntilFail(LoopDecorator):
    """
    Repeat until the child returns FAILURE, then return SUCCESS.
    """

    def _tick(self, depth=0):
        only_child = self.children[0]
        while True:
            child_status = only_child.tick(depth + 1)
            self.count += 1
            if child_status == NodeStatus.FAILURE:
                return NodeStatus.SUCCESS

RetryN

Bases: LoopDecorator

Retry up to n times until the child returns success or running. When subclassing RetryN, set the n class variable to the number of retries.

Source code in vultron/bt/base/decorators.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
class RetryN(LoopDecorator):
    """
    Retry up to n times until the child returns success or running.
    When subclassing RetryN, set the `n` class variable to the number of retries.
    """

    n = 1

    def _tick(self, depth=0):
        only_child = self.children[0]
        for i in range(self.n):
            child_status = only_child.tick(depth + 1)
            self.count += 1
            if child_status == NodeStatus.FAILURE:
                continue
            if child_status == NodeStatus.SUCCESS:
                return NodeStatus.SUCCESS
            if child_status == NodeStatus.RUNNING:
                return NodeStatus.RUNNING
        return NodeStatus.FAILURE

RunningIsFailure

Bases: BtDecorator

RunningIsFailure decorator returns FAILURE if the child node returns RUNNING. Otherwise, it returns the result of the child node.

Source code in vultron/bt/base/decorators.py
54
55
56
57
58
59
60
61
62
63
64
65
class RunningIsFailure(BtDecorator):
    """RunningIsFailure decorator returns FAILURE if the child node returns RUNNING.
    Otherwise, it returns the result of the child node.
    """

    def _tick(self, depth=0):
        only_child = self.children[0]
        child_status = only_child.tick(depth + 1)

        if child_status == NodeStatus.SUCCESS:
            return NodeStatus.SUCCESS
        return NodeStatus.FAILURE

RunningIsSuccess

Bases: BtDecorator

RunningIsSuccess decorator returns SUCCESS if the child node returns RUNNING. Otherwise, it returns the result of the child node.

Source code in vultron/bt/base/decorators.py
68
69
70
71
72
73
74
75
76
77
78
79
class RunningIsSuccess(BtDecorator):
    """RunningIsSuccess decorator returns SUCCESS if the child node returns RUNNING.
    Otherwise, it returns the result of the child node.
    """

    def _tick(self, depth=0):
        only_child = self.children[0]
        child_status = only_child.tick(depth + 1)

        if child_status == NodeStatus.FAILURE:
            return NodeStatus.FAILURE
        return NodeStatus.SUCCESS

vultron.bt.base.fuzzer

This module provides fuzzer node classes for a Behavior Tree. It is intended to be used for testing, simulation, and debugging.

Many of the classes provided are subclasses of the WeightedSuccess class. Their names are based in part on the Words of Estimative Probability from Wikipedia and the Probability Survey (github) by Mauboussin and Mauboussin.

AlmostAlwaysFail

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.10.

Source code in vultron/bt/base/fuzzer.py
184
185
186
187
class AlmostAlwaysFail(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.10."""

    success_rate = 1.0 / 10.0

AlmostAlwaysSucceed

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.90.

Source code in vultron/bt/base/fuzzer.py
136
137
138
139
class AlmostAlwaysSucceed(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.90."""

    success_rate = 9.0 / 10.0

AlmostCertainlyFail

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.07.

Source code in vultron/bt/base/fuzzer.py
190
191
192
193
class AlmostCertainlyFail(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.07."""

    success_rate = 7.0 / 100.0

AlmostCertainlySucceed

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.93.

Source code in vultron/bt/base/fuzzer.py
130
131
132
133
class AlmostCertainlySucceed(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.93."""

    success_rate = 93.0 / 100.0

AlwaysFail

Bases: FuzzerNode

Always returns NodeStatus.FAILURE

Source code in vultron/bt/base/fuzzer.py
40
41
42
43
44
class AlwaysFail(FuzzerNode):
    """Always returns NodeStatus.FAILURE"""

    def _tick(self, depth=0):
        return NodeStatus.FAILURE

AlwaysRunning

Bases: FuzzerNode

Always returns NodeStatus.RUNNING

Source code in vultron/bt/base/fuzzer.py
47
48
49
50
51
class AlwaysRunning(FuzzerNode):
    """Always returns NodeStatus.RUNNING"""

    def _tick(self, depth=0):
        return NodeStatus.RUNNING

AlwaysSucceed

Bases: FuzzerNode

Always returns NodeStatus.SUCCESS

Source code in vultron/bt/base/fuzzer.py
33
34
35
36
37
class AlwaysSucceed(FuzzerNode):
    """Always returns NodeStatus.SUCCESS"""

    def _tick(self, depth=0):
        return NodeStatus.SUCCESS

FortyNineInFifty

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.98.

Source code in vultron/bt/base/fuzzer.py
112
113
114
115
class FortyNineInFifty(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.98."""

    success_rate = 49.0 / 50.0

NineteenInTwenty

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.95.

Source code in vultron/bt/base/fuzzer.py
124
125
126
127
class NineteenInTwenty(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.95."""

    success_rate = 19.0 / 20.0

NinetyNineInOneHundred

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.99.

Source code in vultron/bt/base/fuzzer.py
106
107
108
109
class NinetyNineInOneHundred(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.99."""

    success_rate = 99.0 / 100.0

OftenFail

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.30.

Source code in vultron/bt/base/fuzzer.py
172
173
174
175
class OftenFail(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.30."""

    success_rate = 3.0 / 10.0

OftenSucceed

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.70.

Source code in vultron/bt/base/fuzzer.py
148
149
150
151
class OftenSucceed(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.70."""

    success_rate = 7.0 / 10.0

OneInFifty

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.02.

Source code in vultron/bt/base/fuzzer.py
208
209
210
211
class OneInFifty(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.02."""

    success_rate = 1.0 / 50.0

OneInOneHundred

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.01.

Source code in vultron/bt/base/fuzzer.py
214
215
216
217
class OneInOneHundred(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.01."""

    success_rate = 1.0 / 100.0

OneInThirty

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.033333.

Source code in vultron/bt/base/fuzzer.py
202
203
204
205
class OneInThirty(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.033333."""

    success_rate = 1.0 / 30.0

OneInTwenty

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.05.

Source code in vultron/bt/base/fuzzer.py
196
197
198
199
class OneInTwenty(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.05."""

    success_rate = 1.0 / 20.0

OneInTwoHundred

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.005.

Source code in vultron/bt/base/fuzzer.py
220
221
222
223
class OneInTwoHundred(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.005."""

    success_rate = 1.0 / 200.0

OneNinetyNineInTwoHundred

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.995.

Source code in vultron/bt/base/fuzzer.py
100
101
102
103
class OneNinetyNineInTwoHundred(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.995."""

    success_rate = 199.0 / 200.0

ProbablyFail

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.3333.

Source code in vultron/bt/base/fuzzer.py
166
167
168
169
class ProbablyFail(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.3333."""

    success_rate = 1.0 / 3.0

ProbablySucceed

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.6667.

Source code in vultron/bt/base/fuzzer.py
154
155
156
157
class ProbablySucceed(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.6667."""

    success_rate = 2.0 / 3.0

RandomActionNodeWithRunning

Bases: FuzzerNode

Returns a random NodeStatus, including NodeStatus.RUNNING, with equal probability.

Source code in vultron/bt/base/fuzzer.py
54
55
56
57
58
class RandomActionNodeWithRunning(FuzzerNode):
    """Returns a random NodeStatus, including NodeStatus.RUNNING, with equal probability."""

    def _tick(self, depth=0):
        return random.choice(list(NodeStatus))

SuccessOrRunning

Bases: FuzzerNode

Returns NodeStatus.SUCCESS or NodeStatus.RUNNING with equal probability.

Source code in vultron/bt/base/fuzzer.py
61
62
63
64
65
class SuccessOrRunning(FuzzerNode):
    """Returns NodeStatus.SUCCESS or NodeStatus.RUNNING with equal probability."""

    def _tick(self, depth=0):
        return random.choice((NodeStatus.SUCCESS, NodeStatus.RUNNING))

TwentyNineInThirty

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.96667.

Source code in vultron/bt/base/fuzzer.py
118
119
120
121
class TwentyNineInThirty(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.96667."""

    success_rate = 29.0 / 30.0

UniformSucceedFail

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.50.

Source code in vultron/bt/base/fuzzer.py
160
161
162
163
class UniformSucceedFail(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.50."""

    success_rate = 1.0 / 2.0

UsuallyFail

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.25.

Source code in vultron/bt/base/fuzzer.py
178
179
180
181
class UsuallyFail(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.25."""

    success_rate = 1.0 / 4.0

UsuallySucceed

Bases: WeightedSuccess

Returns NodeStatus.SUCCESS with a probability of 0.75.

Source code in vultron/bt/base/fuzzer.py
142
143
144
145
class UsuallySucceed(WeightedSuccess):
    """Returns NodeStatus.SUCCESS with a probability of 0.75."""

    success_rate = 3.0 / 4.0

WeightedSuccess

Bases: FuzzerNode

Returns NodeStatus.SUCCESS with a probability of success_rate. Otherwise, returns NodeStatus.FAILURE.

When subclassing, set the success_rate class attribute to the desired probability of success.

Source code in vultron/bt/base/fuzzer.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class WeightedSuccess(FuzzerNode):
    """Returns NodeStatus.SUCCESS with a probability of success_rate.
    Otherwise, returns NodeStatus.FAILURE.

    When subclassing, set the `success_rate` class attribute to the desired
    probability of success.
    """

    success_rate = 0.5
    name_pfx = "z"

    # _node_shape = "invtrapezium"

    def _tick(self, depth=0):
        if random.random() < self.success_rate:
            return NodeStatus.SUCCESS
        return NodeStatus.FAILURE

    def _namestr(self, depth=0):
        base = super()._namestr(depth)
        return f"{base} p=({self.success_rate})"