behavior_tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/lib/behavior_tree.rb +4 -0
  3. data/lib/behavior_tree/builder.rb +126 -0
  4. data/lib/behavior_tree/concerns/dsl/dsl.yml +45 -0
  5. data/lib/behavior_tree/concerns/dsl/initial_config.rb +28 -0
  6. data/lib/behavior_tree/concerns/dsl/spell_checker.rb +21 -0
  7. data/lib/behavior_tree/concerns/node_iterators/all_nodes.rb +12 -0
  8. data/lib/behavior_tree/concerns/node_iterators/prioritize_running.rb +19 -0
  9. data/lib/behavior_tree/concerns/tree_structure/algorithms.rb +95 -0
  10. data/lib/behavior_tree/concerns/tree_structure/printer.rb +83 -0
  11. data/lib/behavior_tree/concerns/validations/proc_or_block.rb +17 -0
  12. data/lib/behavior_tree/concerns/validations/single_child.rb +18 -0
  13. data/lib/behavior_tree/control_nodes/control_node_base.rb +93 -0
  14. data/lib/behavior_tree/control_nodes/selector.rb +22 -0
  15. data/lib/behavior_tree/control_nodes/sequence.rb +24 -0
  16. data/lib/behavior_tree/decorator_nodes/condition.rb +46 -0
  17. data/lib/behavior_tree/decorator_nodes/decorator_base.rb +40 -0
  18. data/lib/behavior_tree/decorator_nodes/force_failure.rb +16 -0
  19. data/lib/behavior_tree/decorator_nodes/force_success.rb +16 -0
  20. data/lib/behavior_tree/decorator_nodes/inverter.rb +18 -0
  21. data/lib/behavior_tree/decorator_nodes/repeat_times_base.rb +57 -0
  22. data/lib/behavior_tree/decorator_nodes/repeater.rb +16 -0
  23. data/lib/behavior_tree/decorator_nodes/retry.rb +14 -0
  24. data/lib/behavior_tree/errors.rb +65 -0
  25. data/lib/behavior_tree/node_base.rb +119 -0
  26. data/lib/behavior_tree/node_status.rb +70 -0
  27. data/lib/behavior_tree/single_child_node.rb +29 -0
  28. data/lib/behavior_tree/tasks/nop.rb +38 -0
  29. data/lib/behavior_tree/tasks/task_base.rb +37 -0
  30. data/lib/behavior_tree/tree.rb +35 -0
  31. data/lib/behavior_tree/version.rb +5 -0
  32. metadata +73 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 31f5019e142143d5a29316f05bbd5a5763631041b6bcbba15a453c91dd0d149f
4
+ data.tar.gz: 6b4f1de6262f8837974101a42ed0f101559a9d9e5de8b5e1645a8d082a3d57c7
5
+ SHA512:
6
+ metadata.gz: c0e4a9526e181ac5f3f3d9f2e789ac288f7ba5a45cdcb68b2b9a54f315d02d911802f7aa1b7ca3d74bd5f4b5ed2a33802e925215bdd1b6a0c495177f40ff5b41
7
+ data.tar.gz: d0a55cad6cecf95ae803bb83c5b00f99575a002508d5bd909129cc580382d5aaf23c5123d01be15c05069fc846386df8a3fd010adb46fa21a7ac46a6b494273d
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load all files from lib.
4
+ Dir[File.join(__dir__, '../', 'lib', 'behavior_tree', '**', '*.rb')].sort.each { |file| require file }
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './concerns/dsl/spell_checker'
4
+ require_relative './concerns/dsl/initial_config'
5
+
6
+ module BehaviorTree
7
+ # DSL for building a tree.
8
+ class Builder
9
+ @node_type_mapping = {}
10
+ class << self
11
+ include Dsl::SpellChecker
12
+ include Dsl::InitialConfig
13
+
14
+ def build(&block)
15
+ # Stack of lists. When a method like 'sequence' is executed, the resulting
16
+ # sequence object will be stored in the last list. Then, the whole list will
17
+ # be retrieved as the node children.
18
+ @stack = []
19
+
20
+ stack_children_from_block(block)
21
+ tree_main_nodes = @stack.pop
22
+
23
+ raise DSLStandardError, 'Tree main node should be a single node' if tree_main_nodes.count > 1
24
+
25
+ raise 'Tree structure is incorrect. Probably a problem with the library.' unless @stack.empty?
26
+
27
+ BehaviorTree::Tree.new tree_main_nodes.first
28
+ end
29
+
30
+ # Don't validate class_name, because in some situations the user wants it to be evaluated
31
+ # in runtime.
32
+ def register(node_name, class_name, children: :none)
33
+ valid_children_values = %i[none single multiple]
34
+ raise "Children value must be in: #{valid_children_values}" unless valid_children_values.include?(children)
35
+
36
+ node_name = node_name.to_sym
37
+ raise RegisterDSLNodeAlreadyExistsError, node_name if @node_type_mapping.key?(node_name)
38
+
39
+ @node_type_mapping[node_name] = {
40
+ class: class_name,
41
+ children: children
42
+ }
43
+ end
44
+
45
+ def register_alias(original, alias_key)
46
+ unless @node_type_mapping.key?(original)
47
+ raise "Cannot register alias for '#{original}', since it doesn't exist."
48
+ end
49
+ raise RegisterDSLNodeAlreadyExistsError, alias_key if @node_type_mapping.key?(alias_key)
50
+ raise 'Alias key cannot be empty' if alias_key.to_s.empty?
51
+
52
+ @node_type_mapping[original][:alias] = alias_key
53
+ @node_type_mapping[alias_key] = @node_type_mapping[original].dup
54
+ @node_type_mapping[alias_key][:alias] = original
55
+ end
56
+
57
+ private
58
+
59
+ def stack(obj)
60
+ @stack.last << obj
61
+ end
62
+
63
+ # Execute @stack.pop after executing this method to
64
+ # extract what was pushed.
65
+ def stack_children_from_block(block)
66
+ @stack << []
67
+ instance_eval(&block)
68
+ end
69
+
70
+ def chain(node)
71
+ unless node.is_a?(NodeBase)
72
+ raise DSLStandardError, "The 'chain' keyword must be used to chain a node or subtree, not a #{node.class}"
73
+ end
74
+
75
+ stack node
76
+ end
77
+
78
+ def respond_to_missing?(method_name, _include_private)
79
+ @node_type_mapping.key? method_name
80
+ end
81
+
82
+ # Convert a class name with namespace into a constant.
83
+ # It returns the class itself if it's already a class.
84
+ # @param class_name [String]
85
+ # @return [Class]
86
+ def constantize(class_name)
87
+ return class_name if class_name.is_a?(Class)
88
+
89
+ class_name.split('::').compact.inject(Object) { |o, c| o.const_get c }
90
+ rescue NameError
91
+ nil
92
+ end
93
+
94
+ def dynamic_method_with_children(node_class, children, args, block)
95
+ stack_children_from_block(block)
96
+ final_args = [@stack.pop] + args # @stack.pop is already an Array
97
+ final_args.flatten! unless children == :multiple
98
+ stack node_class.new(*final_args)
99
+ end
100
+
101
+ def dynamic_method_leaf(node_class, args, block)
102
+ stack node_class.new(*args, &block)
103
+ end
104
+
105
+ def method_missing(name, *args, &block)
106
+ # Find by name or alias.
107
+ node_class_name = @node_type_mapping.dig name, :class
108
+
109
+ node_class = constantize(node_class_name)
110
+
111
+ raise_node_type_not_exists name if node_class.nil?
112
+
113
+ children = @node_type_mapping.dig(name.to_sym, :children)
114
+
115
+ # Nodes that have children are executed differently from leaf nodes.
116
+ if children == :none
117
+ dynamic_method_leaf(node_class, args, block)
118
+ else
119
+ dynamic_method_with_children(node_class, children, args, block)
120
+ end
121
+ end
122
+ end
123
+
124
+ BehaviorTree::Builder.initial_config
125
+ end
126
+ end
@@ -0,0 +1,45 @@
1
+ dsl:
2
+ nodes:
3
+ -
4
+ keyword: re_try
5
+ class_name: BehaviorTree::Decorators::Retry
6
+ children: single
7
+ -
8
+ keyword: inverter
9
+ class_name: BehaviorTree::Decorators::Inverter
10
+ children: single
11
+ -
12
+ keyword: repeater
13
+ class_name: BehaviorTree::Decorators::Repeater
14
+ children: single
15
+ -
16
+ keyword: force_failure
17
+ class_name: BehaviorTree::Decorators::ForceFailure
18
+ children: single
19
+ -
20
+ keyword: force_success
21
+ class_name: BehaviorTree::Decorators::ForceSuccess
22
+ children: single
23
+ -
24
+ keyword: condition
25
+ class_name: BehaviorTree::Decorators::Condition
26
+ children: single
27
+ -
28
+ keyword: sequence
29
+ class_name: BehaviorTree::Sequence
30
+ children: multiple
31
+ -
32
+ keyword: selector
33
+ class_name: BehaviorTree::Selector
34
+ children: multiple
35
+ -
36
+ keyword: task
37
+ class_name: BehaviorTree::TaskBase
38
+ children: none
39
+ aliases:
40
+ sequence: seq
41
+ selector: sel
42
+ repeater: rep
43
+ inverter: inv
44
+ condition: cond
45
+ task: t
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module BehaviorTree
6
+ module Dsl
7
+ # Executes the initial registration of nodes.
8
+ module InitialConfig
9
+ def dsl_config
10
+ @dsl_config ||= YAML.load_file(File.join(__dir__, 'dsl.yml'))['dsl']
11
+ end
12
+
13
+ def initial_config
14
+ dsl_config['nodes'].each do |node|
15
+ BehaviorTree::Builder.register(
16
+ node['keyword'].to_sym,
17
+ node['class_name'],
18
+ children: node['children'].to_sym
19
+ )
20
+ end
21
+
22
+ dsl_config['aliases'].each do |k, v|
23
+ BehaviorTree::Builder.register_alias(k.to_sym, v.to_sym)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module Dsl
5
+ # Helpers for spellchecking, and correcting user input in the DSL builder.
6
+ module SpellChecker
7
+ def raise_node_type_not_exists(missing_method)
8
+ suggestion = most_similar_name missing_method
9
+ method_alias = @node_type_mapping.dig suggestion, :alias
10
+ raise NodeTypeDoesNotExistError.new(missing_method, suggestion, method_alias)
11
+ end
12
+
13
+ def most_similar_name(name)
14
+ return nil if (defined? DidYouMean).nil?
15
+
16
+ DidYouMean::SpellChecker.new(dictionary: @node_type_mapping.keys)
17
+ .correct(name)&.first
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module NodeIterators
5
+ # Iterates all nodes, without skipping or re-ordering.
6
+ module AllNodes
7
+ def all_nodes
8
+ @children.each
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module NodeIterators
5
+ # If there's at least one node with 'running' status, then iterate starting from there, in order.
6
+ # Else, iterate all nodes.
7
+ module PrioritizeRunning
8
+ def prioritize_running
9
+ idx = @children.find_index { |child| child.status.running? }.to_i
10
+
11
+ Enumerator.new do |y|
12
+ @children[idx..].each do |child|
13
+ y << child
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module TreeStructure
5
+ # Basic tree algorithms.
6
+ module Algorithms
7
+ def repeated_nodes
8
+ visited = Set.new
9
+ repeated_nodes = Set.new
10
+
11
+ dfs = ->(node) {
12
+ break repeated_nodes << node if visited.include?(node)
13
+
14
+ visited << node
15
+
16
+ node.children.each(&dfs)
17
+ }
18
+
19
+ dfs.(chainable_node)
20
+
21
+ repeated_nodes
22
+ end
23
+
24
+ def uniq_nodes?
25
+ repeated_nodes.empty?
26
+ end
27
+
28
+ def cycle?
29
+ current_path = Set.new
30
+
31
+ dfs = ->(node) {
32
+ break true if current_path.include?(node)
33
+
34
+ current_path << node
35
+ result = node.children.any?(&dfs)
36
+ current_path.delete node
37
+
38
+ result
39
+ }
40
+
41
+ dfs.(chainable_node)
42
+ end
43
+
44
+ def each_node(traversal_type = TRAVERSAL_TYPES.first, &block)
45
+ return enum_for(:each_node, traversal_type) unless block_given?
46
+
47
+ raise ArgumentError, "Traversal type must be in: #{TRAVERSAL_TYPES}" unless TRAVERSAL_TYPES.any?(traversal_type)
48
+
49
+ send("#{traversal_type}_node_yielder", &block)
50
+ nil
51
+ end
52
+
53
+ private
54
+
55
+ def breadth_node_yielder
56
+ queue = [[chainable_node, 0, self]]
57
+ idx = 0
58
+ depth = 0
59
+ until queue.empty?
60
+ node, depth, parent_node = queue.shift # Remove first
61
+ # Enqueue node with depth and parent.
62
+ queue.concat(node.children.map { |child| [child, depth + 1, node] })
63
+ yield(node, depth, idx, parent_node)
64
+ idx += 1
65
+ end
66
+ end
67
+
68
+ def depth_postorder_node_yielder
69
+ idx = 0
70
+
71
+ dfs = ->(node, depth, parent_node) {
72
+ node.children.each { |child| dfs.(child, depth + 1, node) }
73
+ yield(node, depth, idx, parent_node)
74
+ idx += 1
75
+ }
76
+
77
+ dfs.(chainable_node, 0, self)
78
+ end
79
+
80
+ def depth_preorder_node_yielder
81
+ idx = 0
82
+
83
+ dfs = ->(node, depth, parent_node) {
84
+ yield(node, depth, idx, parent_node)
85
+ idx += 1
86
+ node.children.each { |child| dfs.(child, depth + 1, node) }
87
+ }
88
+
89
+ dfs.(chainable_node, 0, self)
90
+ end
91
+
92
+ TRAVERSAL_TYPES = %i[depth_postorder depth_preorder breadth].freeze
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorized_string'
4
+
5
+ module BehaviorTree
6
+ module TreeStructure
7
+ # Algorithm to print tree.
8
+ module Printer
9
+ def print
10
+ puts '∅' # Style for the root node.
11
+ tree_lines.each { |line| puts line }
12
+ puts ''
13
+ puts cycle_string
14
+ puts uniq_nodes_string
15
+ puts size_string
16
+ end
17
+
18
+ private
19
+
20
+ def tree_lines
21
+ # Store which depth values must continue to display a vertical line.
22
+ vertical_lines_continues = Set.new
23
+
24
+ each_node(:depth_preorder).map do |node, depth, _global_idx, parent_node|
25
+ # Parent's last child?
26
+ last_child = node == parent_node.children.last
27
+
28
+ last_child ? vertical_lines_continues.delete(depth) : vertical_lines_continues << depth
29
+
30
+ space = (0...depth).map { |d| vertical_lines_continues.include?(d) ? '│ ' : ' ' }.join
31
+ connector = last_child ? '└─' : '├─'
32
+
33
+ "#{space}#{connector}#{class_simple_name(node)} #{status_string(node)} #{tick_count_string(node)}"
34
+ end
35
+ end
36
+
37
+ def size_string
38
+ "Tree has #{size - 1} nodes."
39
+ end
40
+
41
+ def cycle_string
42
+ "Cycles: #{bool_yes_no(cycle?)}."
43
+ end
44
+
45
+ def uniq_nodes_string
46
+ "All nodes are unique object refs: #{bool_yes_no(uniq_nodes?)}."
47
+ end
48
+
49
+ def bool_yes_no(bool)
50
+ bool ? 'yes' : 'no'
51
+ end
52
+
53
+ def status_string(node)
54
+ if node.status.success?
55
+ ColorizedString['success'].colorize(:blue)
56
+ elsif node.status.running?
57
+ ColorizedString['running'].colorize(:light_green)
58
+ elsif node.status.failure?
59
+ ColorizedString['failure'].colorize(:red)
60
+ end
61
+ end
62
+
63
+ def tick_count_string(node)
64
+ count = node.tick_count
65
+ color = count.zero? ? :light_red : :light_black
66
+ ColorizedString["(#{node.tick_count} ticks)"].colorize(color)
67
+ end
68
+
69
+ # Copied from Rails' ActiveSupport.
70
+ def snake_case(str)
71
+ str.gsub(/::/, '/')
72
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
73
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
74
+ .tr('-', '_')
75
+ .downcase
76
+ end
77
+
78
+ def class_simple_name(node)
79
+ snake_case(node.class.name.split('::').last)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module Validations
5
+ # Validates that only one (procedure or block) is present. None present is also valid.
6
+ module ProcOrBlock
7
+ private
8
+
9
+ def validate_proc!(procedure, block)
10
+ return if block.nil? && procedure.nil?
11
+ return if block.is_a?(Proc) ^ procedure.is_a?(Proc)
12
+
13
+ raise ArgumentError, 'Pass a lambda/proc or block to a condition decorator, but not both'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module Validations
5
+ # Validates that a node has a single node child.
6
+ module SingleChild
7
+ private
8
+
9
+ def validate_single_child!(child)
10
+ raise InvalidLeafNodeError if child.nil?
11
+ return if child.is_a?(NodeBase)
12
+
13
+ err = "This node can only have a #{NodeBase.name} object as a child. Attempted to assign #{child.class}."
14
+ raise TypeError, err
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../node_base'
4
+ require_relative '../concerns/node_iterators/prioritize_running'
5
+ require_relative '../concerns/node_iterators/all_nodes'
6
+
7
+ module BehaviorTree
8
+ # A node that has children (abstract class).
9
+ class ControlNodeBase < NodeBase
10
+ include NodeIterators::PrioritizeRunning
11
+ include NodeIterators::AllNodes
12
+
13
+ def initialize(children = [])
14
+ raise IncorrectTraversalStrategyError, nil.class if traversal_strategy.nil?
15
+ raise IncorrectTraversalStrategyError, traversal_strategy unless respond_to?(traversal_strategy, true)
16
+
17
+ super()
18
+ @children = children
19
+ end
20
+
21
+ def <<(child)
22
+ @children << child
23
+ @children.flatten! # Accepts array of children too.
24
+ @children.map!(&:chainable_node)
25
+ end
26
+
27
+ def halt!
28
+ validate_non_leaf!
29
+
30
+ super
31
+
32
+ @children.each(&:halt!)
33
+ end
34
+
35
+ protected
36
+
37
+ def on_tick
38
+ raise NotImplementedError, 'Must implement control logic'
39
+ end
40
+
41
+ def validate_non_leaf!
42
+ raise InvalidLeafNodeError if @children.empty?
43
+ end
44
+
45
+ def tick_each_children(&block)
46
+ return enum_for(:tick_each_children) unless block_given?
47
+
48
+ validate_non_leaf!
49
+
50
+ Enumerator.new do |y|
51
+ enum = send(traversal_strategy)
52
+ validate_enum!(enum)
53
+
54
+ enum.each do |child|
55
+ child.tick!
56
+ y << child
57
+ end
58
+ end.each(&block)
59
+ end
60
+
61
+ private
62
+
63
+ def traversal_strategy
64
+ self.class.traversal_strategy
65
+ end
66
+
67
+ # Keep it simple, because it's executed everytime it ticks.
68
+ def validate_enum!(enum)
69
+ raise IncorrectTraversalStrategyError, enum unless enum.respond_to? :each
70
+ end
71
+
72
+ class << self
73
+ def traversal_strategy
74
+ @traversal_strategy ||= ancestors.find do |constant|
75
+ next if constant == self
76
+ next unless constant.is_a? Class
77
+ next unless constant.respond_to? :traversal_strategy
78
+ next if constant.traversal_strategy.nil?
79
+
80
+ break constant.traversal_strategy
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def children_traversal_strategy(traversal_strategy)
87
+ @traversal_strategy = traversal_strategy
88
+ end
89
+ end
90
+
91
+ children_traversal_strategy :prioritize_running
92
+ end
93
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './control_node_base'
4
+
5
+ module BehaviorTree
6
+ # A selector node.
7
+ class Selector < ControlNodeBase
8
+ def on_tick
9
+ tick_each_children do |child|
10
+ return status.running! if child.status.running?
11
+
12
+ # Both self and children have the status set to success.
13
+ return halt! if child.status.success?
14
+ end
15
+
16
+ # Halt, but set success only to children, not to self.
17
+ # Self status must be overriden to failure.
18
+ halt!
19
+ status.failure!
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ # A sequence node.
5
+ class Sequence < ControlNodeBase
6
+ def on_tick
7
+ tick_each_children do |child|
8
+ return status.running! if child.status.running?
9
+
10
+ if child.status.failure?
11
+ halt!
12
+
13
+ # Halt, but set success only to children, not to self.
14
+ # Self status must be overriden to failure.
15
+ status.failure!
16
+ return
17
+ end
18
+ end
19
+
20
+ # Both self and children have the status set to success.
21
+ halt!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './decorator_base'
4
+ require_relative '../concerns/validations/proc_or_block'
5
+
6
+ module BehaviorTree
7
+ module Decorators
8
+ # Applies a condition that will decide whether to tick the decorated node or not.
9
+ class Condition < DecoratorBase
10
+ include Validations::ProcOrBlock
11
+
12
+ def initialize(child, procedure = nil, &block)
13
+ validate_proc!(procedure, block)
14
+
15
+ super(child)
16
+
17
+ @conditional_block = block_given? ? block : procedure
18
+ end
19
+
20
+ protected
21
+
22
+ def should_tick?
23
+ return false unless @conditional_block.is_a?(Proc)
24
+
25
+ if @conditional_block.lambda?
26
+ args = [@context, self].take @conditional_block.arity
27
+ @conditional_block.(*args)
28
+ else
29
+ instance_eval(&@conditional_block)
30
+ end
31
+ end
32
+
33
+ def status_map
34
+ if @tick_prevented
35
+ status.failure!
36
+ else
37
+ self.status = child.status
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :context
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../single_child_node'
4
+
5
+ module BehaviorTree
6
+ module Decorators
7
+ # Base class for a decorator node.
8
+ class DecoratorBase < SingleChildNodeBase
9
+ def on_tick
10
+ super
11
+ decorate
12
+ end
13
+
14
+ def ensure_after_tick
15
+ status_map
16
+ end
17
+
18
+ def halt!
19
+ super
20
+
21
+ status_map
22
+ end
23
+
24
+ protected
25
+
26
+ # Decorate behavior. Retry, repeat, etc.
27
+ # Leave empty if there's no extra behavior to add.
28
+ # Default behavior is to do nothing additional.
29
+ # @return [void]
30
+ def decorate; end
31
+
32
+ # This method must change the self node status in function
33
+ # of the child status. The default behavior is to copy its status.
34
+ # @return [void]
35
+ def status_map
36
+ self.status = child.status
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module Decorators
5
+ # Returns always failure when the child is not running.
6
+ class ForceFailure < DecoratorBase
7
+ protected
8
+
9
+ def status_map
10
+ return status.running! if child.status.running?
11
+
12
+ status.failure!
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module Decorators
5
+ # Returns always success when the child is not running.
6
+ class ForceSuccess < DecoratorBase
7
+ protected
8
+
9
+ def status_map
10
+ return status.running! if child.status.running?
11
+
12
+ status.success!
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './decorator_base'
4
+
5
+ module BehaviorTree
6
+ module Decorators
7
+ # Returns the inverted child status.
8
+ class Inverter < DecoratorBase
9
+ protected
10
+
11
+ def status_map
12
+ return status.running! if child.status.running?
13
+ return status.failure! if child.status.success?
14
+ return status.success! if child.status.failure?
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module Decorators
5
+ # While the node status is <repeat_while>, tick it again up to N times.
6
+ # Interrupt the loop when the <repeat_while> condition fails.
7
+ # If the child returns running, this node returns running too.
8
+ # The count is resetted when the loop is interrupted or finished.
9
+ # N is the total times to be ticked, and it includes the initial tick (the
10
+ # original tick that all nodes have in common).
11
+ class RepeatTimesBase < DecoratorBase
12
+ def initialize(child, max)
13
+ validate_max!(max)
14
+ super(child)
15
+
16
+ @max = max
17
+ reset_remaining_attempts
18
+ end
19
+
20
+ def decorate
21
+ while repeat_while || child.status.running?
22
+ break if child.status.running?
23
+
24
+ @remaining_attempts -= 1
25
+ break unless @remaining_attempts.positive?
26
+
27
+ child.tick!
28
+ break if child.status.running?
29
+ end
30
+ end
31
+
32
+ def ensure_after_tick
33
+ status_map
34
+ end
35
+
36
+ protected
37
+
38
+ def repeat_while
39
+ raise NotImplementedError
40
+ end
41
+
42
+ private
43
+
44
+ def reset_remaining_attempts
45
+ @remaining_attempts = @max
46
+ end
47
+
48
+ def validate_max!(max)
49
+ return if max.is_a?(Integer) && max.positive?
50
+
51
+ raise ArgumentError, 'Number of repetitions must be a positive integer.'
52
+ end
53
+ end
54
+
55
+ private_constant :RepeatTimesBase
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './repeat_times_base'
4
+
5
+ module BehaviorTree
6
+ module Decorators
7
+ # Repeat N times while child has success status.
8
+ class Repeater < RepeatTimesBase
9
+ protected
10
+
11
+ def repeat_while
12
+ child.status.success?
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ module Decorators
5
+ # Repeat N times while child has failure status.
6
+ class Retry < RepeatTimesBase
7
+ protected
8
+
9
+ def repeat_while
10
+ child.status.failure?
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ # Exception for when the children traversal strategy is incorrect.
5
+ class IncorrectTraversalStrategyError < StandardError
6
+ def initialize(value)
7
+ err = [
8
+ "Strategy for iterating children nodes must return an object which has an 'each' method.",
9
+ "Attempted to use strategy: #{value}."
10
+ ]
11
+ super err.join ' '
12
+ end
13
+ end
14
+
15
+ # Exception raised when the main node of a tree is of invalid type.
16
+ class InvalidTreeMainNodeError < StandardError
17
+ def initialize(node_type)
18
+ super "Cannot chain #{node_type} to the root node of a tree. Valid types are: #{Tree::CHILD_VALID_CLASSES}."
19
+ end
20
+ end
21
+
22
+ # Exception for control nodes without children.
23
+ class InvalidLeafNodeError < StandardError
24
+ def initialize
25
+ super 'This node cannot be a leaf node.'
26
+ end
27
+ end
28
+
29
+ # Exception for when a node has an incorrect status value.
30
+ class IncorrectStatusValueError < StandardError
31
+ def initialize(value)
32
+ super "Incorrect status value. A node cannot have '#{value}' status."
33
+ end
34
+ end
35
+
36
+ # Exception for incorrect node type when using the DSL builder.
37
+ class NodeTypeDoesNotExistError < StandardError
38
+ def initialize(missing_method, suggestion, method_alias)
39
+ suggestion = suggestion.to_s
40
+ method_alias = method_alias.to_s
41
+
42
+ err = ["Node type '#{missing_method}' does not exist."]
43
+ unless suggestion.empty?
44
+ alias_text = method_alias.empty? ? '' : " (alias of #{method_alias})"
45
+ err << "Did you mean '#{suggestion}'#{alias_text}?"
46
+ end
47
+
48
+ super err.join ' '
49
+ end
50
+ end
51
+
52
+ # Exception for misuse of the DSL builder.
53
+ class DSLStandardError < StandardError
54
+ def initialize(message)
55
+ super "Cannot build tree (DSL Builder): #{message}"
56
+ end
57
+ end
58
+
59
+ # Exception for when trying to register a DSL keyword that already exists.
60
+ class RegisterDSLNodeAlreadyExistsError < StandardError
61
+ def initialize(node_type)
62
+ super "Cannot register node '#{node_type}', it already exists."
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './concerns/tree_structure/algorithms'
4
+
5
+ module BehaviorTree
6
+ # A node (abstract class).
7
+ class NodeBase
8
+ include TreeStructure::Algorithms
9
+ attr_reader :status, :tick_count, :ticks_running, :arbitrary_storage
10
+
11
+ def initialize
12
+ @status = NodeStatus.new NodeStatus::SUCCESS
13
+ @tick_count = 0
14
+ @ticks_running = 0
15
+ @context = nil
16
+
17
+ @status.subscribe { |prev, curr| on_status_change(prev, curr) }
18
+
19
+ @arbitrary_storage = {}
20
+ end
21
+
22
+ def context=(context)
23
+ @context = context
24
+
25
+ # Propagate context.
26
+ children.each do |child|
27
+ child.context = context
28
+ end
29
+ end
30
+
31
+ def size
32
+ 1 + children.map(&:size).sum
33
+ end
34
+
35
+ def tick!
36
+ @tick_count += 1
37
+ @tick_prevented = !should_tick?
38
+
39
+ unless @tick_prevented
40
+ status.running!
41
+ on_tick
42
+ @ticks_running += 1
43
+ end
44
+
45
+ ensure_after_tick
46
+
47
+ # NOTE: Make sure this method returns nil. Since 'ensure_after_tick' might return
48
+ # the node status, it generates some error in IRB (unknown cause).
49
+ #
50
+ # This error can be replicated by pasting a valid status object in IRB, such as by doing:
51
+ # BehaviorTree.const_get(:NodeStatus).new(:__running__) # Valid, but IRB crashes.
52
+ nil
53
+ end
54
+
55
+ def children
56
+ if @children.is_a?(Array)
57
+ @children
58
+ elsif @child.is_a?(NodeBase)
59
+ [@child]
60
+ else
61
+ []
62
+ end
63
+ end
64
+
65
+ def chainable_node
66
+ self
67
+ end
68
+
69
+ def []=(key, value)
70
+ @arbitrary_storage[key] = value
71
+ end
72
+
73
+ def [](key)
74
+ @arbitrary_storage[key]
75
+ end
76
+
77
+ def status=(other_status)
78
+ status.running! if other_status.running?
79
+ status.failure! if other_status.failure?
80
+ status.success! if other_status.success?
81
+ end
82
+
83
+ def halt!
84
+ status.success!
85
+ end
86
+
87
+ protected
88
+
89
+ # If this value is false, @tick_prevented will be set to true, which can be handled in other
90
+ # tick callbacks.
91
+ def should_tick?
92
+ true
93
+ end
94
+
95
+ def on_tick; end
96
+
97
+ def ensure_after_tick; end
98
+
99
+ def on_started_running; end
100
+
101
+ def on_finished_running; end
102
+
103
+ private
104
+
105
+ # Always prev != curr (states that are set to the same aren't notified).
106
+ # The fact that it's set to 0 means that setting to running must be done before
107
+ # increasing the counts (so that @ticks_running becomes 1 after the whole tick lifecycle).
108
+ def on_status_change(prev, curr)
109
+ if prev == NodeStatus::RUNNING
110
+ on_finished_running
111
+ elsif curr == NodeStatus::RUNNING
112
+ @ticks_running = 0
113
+ on_started_running
114
+ end
115
+ end
116
+ end
117
+
118
+ private_constant :NodeBase
119
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ # Status for nodes.
5
+ class NodeStatus
6
+ SUCCESS = :__success__
7
+ RUNNING = :__running__
8
+ FAILURE = :__failure__
9
+
10
+ def initialize(value)
11
+ set(value)
12
+ end
13
+
14
+ def set(value)
15
+ return if value == @value
16
+ raise IncorrectStatusValueError, value unless [SUCCESS, RUNNING, FAILURE].include?(value)
17
+
18
+ prev = @value
19
+
20
+ @value = value
21
+
22
+ # NOTE: Make sure to notify after having set the @value above, so that the new status is already set.
23
+ @subscriber&.(prev, value)
24
+ end
25
+
26
+ def subscribe(&subscriber)
27
+ @subscriber = subscriber
28
+ end
29
+
30
+ def ==(other)
31
+ to_sym == other.to_sym
32
+ end
33
+
34
+ def inspect
35
+ to_sym
36
+ end
37
+
38
+ def to_sym
39
+ return :success if success?
40
+ return :running if running?
41
+ return :failure if failure?
42
+ end
43
+
44
+ def success!
45
+ set(SUCCESS)
46
+ end
47
+
48
+ def running!
49
+ set(RUNNING)
50
+ end
51
+
52
+ def failure!
53
+ set(FAILURE)
54
+ end
55
+
56
+ def success?
57
+ @value == SUCCESS
58
+ end
59
+
60
+ def running?
61
+ @value == RUNNING
62
+ end
63
+
64
+ def failure?
65
+ @value == FAILURE
66
+ end
67
+ end
68
+
69
+ private_constant :NodeStatus
70
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './node_base'
4
+ require_relative './concerns/validations/single_child'
5
+
6
+ module BehaviorTree
7
+ # A node that has a single child (abstract class).
8
+ class SingleChildNodeBase < NodeBase
9
+ include Validations::SingleChild
10
+ attr_reader :child
11
+
12
+ def initialize(child)
13
+ validate_single_child! child
14
+ super()
15
+ @child = child.chainable_node
16
+ end
17
+
18
+ def on_tick
19
+ @child.tick!
20
+ end
21
+
22
+ def halt!
23
+ super
24
+ @child.halt!
25
+ end
26
+ end
27
+
28
+ private_constant :SingleChildNodeBase
29
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './task_base'
4
+
5
+ module BehaviorTree
6
+ # An empty task that does not do anything.
7
+ # It requires N ticks to complete.
8
+ # It can be set to end with failure.
9
+ class Nop < TaskBase
10
+ def initialize(necessary_ticks = 1, completes_with_failure: false)
11
+ raise ArgumentError, 'Should need at least one tick' if necessary_ticks < 1
12
+
13
+ super()
14
+ @necessary_ticks = necessary_ticks
15
+ @completes_with_status = completes_with_failure ? NodeStatus::FAILURE : NodeStatus::SUCCESS
16
+ reset_remaining_attempts
17
+ end
18
+
19
+ def on_tick
20
+ @remaining_ticks -= 1
21
+ return if @remaining_ticks.positive?
22
+
23
+ status.set @completes_with_status
24
+ reset_remaining_attempts
25
+ end
26
+
27
+ def halt!
28
+ super
29
+ reset_remaining_attempts
30
+ end
31
+
32
+ private
33
+
34
+ def reset_remaining_attempts
35
+ @remaining_ticks = @necessary_ticks
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../node_base'
4
+ require_relative '../concerns/validations/proc_or_block'
5
+
6
+ module BehaviorTree
7
+ # A task (leaf) node.
8
+ class TaskBase < NodeBase
9
+ include Validations::ProcOrBlock
10
+
11
+ def initialize(procedure = nil, &block)
12
+ validate_proc!(procedure, block)
13
+
14
+ super()
15
+
16
+ @task_block = block_given? ? block : procedure
17
+ end
18
+
19
+ def on_tick
20
+ raise 'Node should be set to running' unless status.running?
21
+ return unless @task_block.is_a?(Proc)
22
+
23
+ if @task_block.lambda?
24
+ args = [@context, self].take @task_block.arity
25
+ @task_block.(*args)
26
+ else
27
+ instance_eval(&@task_block)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :context
34
+ end
35
+
36
+ Task = TaskBase
37
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ # Root node of the tree.
5
+ # This is the class that must be instantiated by the user.
6
+ class Tree < SingleChildNodeBase
7
+ include TreeStructure::Algorithms
8
+ include TreeStructure::Printer
9
+ attr_reader :context
10
+
11
+ CHILD_VALID_CLASSES = [
12
+ Decorators::DecoratorBase, ControlNodeBase, TaskBase
13
+ ].freeze
14
+
15
+ def initialize(child)
16
+ super(child) if child.nil? # Cannot be leaf, raise error.
17
+
18
+ if CHILD_VALID_CLASSES.any? { |node_class| child.is_a?(NodeBase) && child.chainable_node.is_a?(node_class) }
19
+ super(child)
20
+ return
21
+ end
22
+
23
+ raise InvalidTreeMainNodeError, child.class
24
+ end
25
+
26
+ def chainable_node
27
+ @child
28
+ end
29
+
30
+ def ensure_after_tick
31
+ # Copy the main node status to self.
32
+ self.status = child.status
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BehaviorTree
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: behavior_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Felo Vilches
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-07-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - felovilches@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - "./lib/behavior_tree.rb"
21
+ - "./lib/behavior_tree/builder.rb"
22
+ - "./lib/behavior_tree/concerns/dsl/dsl.yml"
23
+ - "./lib/behavior_tree/concerns/dsl/initial_config.rb"
24
+ - "./lib/behavior_tree/concerns/dsl/spell_checker.rb"
25
+ - "./lib/behavior_tree/concerns/node_iterators/all_nodes.rb"
26
+ - "./lib/behavior_tree/concerns/node_iterators/prioritize_running.rb"
27
+ - "./lib/behavior_tree/concerns/tree_structure/algorithms.rb"
28
+ - "./lib/behavior_tree/concerns/tree_structure/printer.rb"
29
+ - "./lib/behavior_tree/concerns/validations/proc_or_block.rb"
30
+ - "./lib/behavior_tree/concerns/validations/single_child.rb"
31
+ - "./lib/behavior_tree/control_nodes/control_node_base.rb"
32
+ - "./lib/behavior_tree/control_nodes/selector.rb"
33
+ - "./lib/behavior_tree/control_nodes/sequence.rb"
34
+ - "./lib/behavior_tree/decorator_nodes/condition.rb"
35
+ - "./lib/behavior_tree/decorator_nodes/decorator_base.rb"
36
+ - "./lib/behavior_tree/decorator_nodes/force_failure.rb"
37
+ - "./lib/behavior_tree/decorator_nodes/force_success.rb"
38
+ - "./lib/behavior_tree/decorator_nodes/inverter.rb"
39
+ - "./lib/behavior_tree/decorator_nodes/repeat_times_base.rb"
40
+ - "./lib/behavior_tree/decorator_nodes/repeater.rb"
41
+ - "./lib/behavior_tree/decorator_nodes/retry.rb"
42
+ - "./lib/behavior_tree/errors.rb"
43
+ - "./lib/behavior_tree/node_base.rb"
44
+ - "./lib/behavior_tree/node_status.rb"
45
+ - "./lib/behavior_tree/single_child_node.rb"
46
+ - "./lib/behavior_tree/tasks/nop.rb"
47
+ - "./lib/behavior_tree/tasks/task_base.rb"
48
+ - "./lib/behavior_tree/tree.rb"
49
+ - "./lib/behavior_tree/version.rb"
50
+ homepage: https://github.com/FeloVilches/Ruby-Behavior-Tree
51
+ licenses:
52
+ - MIT
53
+ metadata: {}
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.6.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.2.3
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Behavior Tree (AI) library for Ruby.
73
+ test_files: []