Skip to main content

Pure Python behavior trees framework

Project description

Nodzz: pure Python behavior trees framework

Nodzz is a Python open-source library which provides a framework for behavior trees creation and management. You can use it to implement behavior of your Arduino based robot, chat-bot or whatever else has its own behavior.

Key features

  • Noddz provides implementations of typical behavior trees components (like selector and sequence nodes) and base classes for creating your own;
  • Behavior trees can be assembled and configured from components by using Python API or pydantic and JSON friendly configs;
  • For now Nodzz is designed for building only synchronous applications, async API will be added soon.

Future plans

Tutorial

Installation

git clone https://github.com/duskforge/nodzz.git
cd nodzz
pip install -e .

Theory

Behavior tree is a model of process execution planning. It is mainly applied in robotics and gaming AI. Here are some references for understanding behavior trees basics: Wikipedia article and Introduction to Behavior Trees by Björn Knafla.

From here on, we assume that you are familiar with behavior tree core concepts. Also, here is Nodzz terms agreement:

  • Controller node - a standard name for control flow nodes;
  • Task node - a standard name for leaf nodes;
  • Behavior agent - an entity which behavior is programmed by behavior tree;
  • State - set of variables which represents snapshot of behavior agent "consciousness" in any given moment: it contains both inputs from the interaction with the external environment and results of these inputs processing.

Basic Use

Let's fast forward 40k years and imagine distant planet where one imperial guardsman is serving his dangerous duty of defending the Imperium of Man from its numerous enemies. We will try to model his behavior with Nodzz behavior tree.

Our brave guardsman behavior depends on number of enemies he encounters:

  • No (zero) enemies: he will continue watching;
  • One enemy: he will ruthlessly fight it;
  • Many (more than one) enemies: hi will bravely run away (into the warm embraces of Commissar).

Once we described guardsman behavior model, we can start implementing it in behavior tree.

We must first determine how our character interacts with the external environment. In this particular case he will be awaiting only one input: enemies number. This is the only parameter affecting guardsman behavior. Let's create node which reads enemies number from the command prompt and writes it to the state variable:

All necessary imports:

from nodzz.core import State
from nodzz.nodes.base import NodeBase, NodeStatus
from nodzz.nodes.controllers import SelectorNode, SequenceNode
from nodzz.nodes.tasks import EvaluationNode, EvaluationNodeConfig, More, Less, Equal

Command prompt reading node:

class InputNode(NodeBase):
    """Initialises node.

    Args:
          config: A dict with the following template: {'enemies_num_var': <state_variable_name>}.
    """
    def __init__(self, config):
        super().__init__(config=config)

    def execute(self, state):
        input_str = input('\nType enemies number or "exit" for exit: ')

        # Graceful exit
        if input_str == 'exit':
            exit()

        # Parsing input
        if input_str.isnumeric():
            input_int = int(input_str)
        else:
            input_int = 0  # Default value for any non-numeric input.

        state.vars[self.config['enemies_num_var']] = input_int

        # execute() method should ALWAYS return node NodeStatus
        return NodeStatus.SUCCESS

Also, our guardsman needs to notice his external environment (in this particular case - us) about the decisions he makes. We will create simple node which prints a text that was set in its config:

class OutputNode(NodeBase):
    def __init__(self, config):
        """Initialises node.

        Args:
              config: A dict with the following template: {'output': <text_that_node_instance_will_print>}.
        """
        super().__init__(config=config)

    def execute(self, state):
        print(self.config['output'])

        return NodeStatus.SUCCESS

Let's set name of the state variable where number of detected enemies will be stored:

var_name = 'enemies_num'

Each behavior scenario of the guardsman will be a sequence of two actions: 1. Estimate enemies number; 2. Tell us about the decision he made.

Enemies number estimation will be implemented by initialising of properly configured EvaluationNode. Evaluation node implements one of the typical decision tree tasks: state variable value evaluation. It allows setting basic evaluation operation with config without any additional code writing. Notification will be implemented via OutputNode. The whole behavior scenario (branch) will be implemented as an instance of SequenceNode initialised with the instances of EvaluationNode and OutputNode given in the order of their execution.

Zero enemies scenario implementation:

zero_enemies_check_cfg = EvaluationNodeConfig(conditions={var_name: Less(value=1)})
zero_enemies_check = EvaluationNode(config=zero_enemies_check_cfg)
zero_enemies_behavior = OutputNode(config={'output': 'No enemies detected, keeping watching.'})
zero_enemies_sequence = SequenceNode(zero_enemies_check, zero_enemies_behavior)

One enemy scenario implementation:

one_enemy_check_cfg = EvaluationNodeConfig(conditions={var_name: Equal(value=1)})
one_enemy_check = EvaluationNode(config=one_enemy_check_cfg)
one_enemy_behavior = OutputNode(config={'output': 'An enemy detected, fight!'})
one_enemy_sequence = SequenceNode(one_enemy_check, one_enemy_behavior)

Many enemies scenario implementation:

many_enemies_check_cfg = EvaluationNodeConfig(conditions={var_name: More(value=1)})
many_enemies_check = EvaluationNode(config=many_enemies_check_cfg)
many_enemies_behavior = OutputNode(config={'output': 'Many enemies detected, run away!'})
many_enemies_sequence = SequenceNode(many_enemies_check, many_enemies_behavior)

All initialised scenarios nodes will be composed in the scenario selector - an instance of SelectorNode. We will need to fill enemies number state variable before starting traversing scenarios, so the root node of the guardsman behavior tree will be SequenceNode instance initialised with the scenario selector instance InputNode instance given in the order of their execution.

behavior_selector = SelectorNode(zero_enemies_sequence, one_enemy_sequence, many_enemies_sequence)
input_node = InputNode(config={'enemies_num_var': var_name})
root_sequence = SequenceNode(input_node, behavior_selector)

There are some final steps to finish behavior tree initialisation, get state instance and start tree execution:

root_sequence.prepare(node_id='root')
new_state = State()

while True:
    root_sequence.execute(state=new_state)

The whole snippet will be (this script is complete, it should run "as is"):

from nodzz.core import State
from nodzz.nodes.base import NodeBase, NodeStatus
from nodzz.nodes.controllers import SelectorNode, SequenceNode
from nodzz.nodes.tasks import EvaluationNode, EvaluationNodeConfig, More, Less, Equal


class InputNode(NodeBase):
    """Initialises node.

    Args:
          config: A dict with the following template: {'enemies_num_var': <state_variable_name>}.
    """
    def __init__(self, config):
        super().__init__(config=config)

    def execute(self, state):
        input_str = input('\nType enemies number or "exit" for exit: ')

        # Graceful exit
        if input_str == 'exit':
            exit()

        # Parsing input
        if input_str.isnumeric():
            input_int = int(input_str)
        else:
            input_int = 0  # Default value for any non-numeric input.

        state.vars[self.config['enemies_num_var']] = input_int

        # execute() method should ALWAYS return node NodeStatus
        return NodeStatus.SUCCESS


class OutputNode(NodeBase):
    def __init__(self, config):
        """Initialises node.

        Args:
              config: A dict with the following template: {'output': <text_that_node_instance_will_print>}.
        """
        super().__init__(config=config)

    def execute(self, state):
        print(self.config['output'])

        return NodeStatus.SUCCESS


var_name = 'enemies_num'


zero_enemies_check_cfg = EvaluationNodeConfig(conditions={var_name: Less(value=1)})
zero_enemies_check = EvaluationNode(config=zero_enemies_check_cfg)
zero_enemies_behavior = OutputNode(config={'output': 'No enemies detected, keeping watching.'})
zero_enemies_sequence = SequenceNode(zero_enemies_check, zero_enemies_behavior)

one_enemy_check_cfg = EvaluationNodeConfig(conditions={var_name: Equal(value=1)})
one_enemy_check = EvaluationNode(config=one_enemy_check_cfg)
one_enemy_behavior = OutputNode(config={'output': 'An enemy detected, fight!'})
one_enemy_sequence = SequenceNode(one_enemy_check, one_enemy_behavior)

many_enemies_check_cfg = EvaluationNodeConfig(conditions={var_name: More(value=1)})
many_enemies_check = EvaluationNode(config=many_enemies_check_cfg)
many_enemies_behavior = OutputNode(config={'output': 'Many enemies detected, run away!'})
many_enemies_sequence = SequenceNode(many_enemies_check, many_enemies_behavior)

behavior_selector = SelectorNode(zero_enemies_sequence, one_enemy_sequence, many_enemies_sequence)
input_node = InputNode(config={'enemies_num_var': var_name})
root_sequence = SequenceNode(input_node, behavior_selector)

root_sequence.prepare(node_id='root')
new_state = State()

while True:
    root_sequence.execute(state=new_state)

The result will be:

Type enemies number or "exit" for exit: 0
No enemies detected, keeping watching.

Type enemies number or "exit" for exit: 1
An enemy detected, fight!

Type enemies number or "exit" for exit: 2
Many enemies detected, run away!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nodzz-0.1.0.tar.gz (15.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nodzz-0.1.0-py3-none-any.whl (14.0 kB view details)

Uploaded Python 3

File details

Details for the file nodzz-0.1.0.tar.gz.

File metadata

  • Download URL: nodzz-0.1.0.tar.gz
  • Upload date:
  • Size: 15.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/4.6.1 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.61.2 CPython/3.8.10

File hashes

Hashes for nodzz-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9d2775fd56ca6ab3d03289d14af9fe2ed3408df3c4f1c85ae6aa7044d2d960ba
MD5 bda293a105f1e707b8f6e5b17201d39d
BLAKE2b-256 63cbaa43dbe62179d234e27d2bfa3cba56b02ad7c036e13feec3e3e960547742

See more details on using hashes here.

File details

Details for the file nodzz-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: nodzz-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 14.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/4.6.1 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.61.2 CPython/3.8.10

File hashes

Hashes for nodzz-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ce81c54842be58fd481443f50263bd29f16b4ec657595a0c40b7590722bc073d
MD5 1a3f4f064a73635978fc67d73ad728b8
BLAKE2b-256 d72809651d6a8c1afe0e629867c83525739940c0b4eef9ce1e131c8f12696443

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page