behavior_tree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []