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.
- checksums.yaml +7 -0
- data/lib/behavior_tree.rb +4 -0
- data/lib/behavior_tree/builder.rb +126 -0
- data/lib/behavior_tree/concerns/dsl/dsl.yml +45 -0
- data/lib/behavior_tree/concerns/dsl/initial_config.rb +28 -0
- data/lib/behavior_tree/concerns/dsl/spell_checker.rb +21 -0
- data/lib/behavior_tree/concerns/node_iterators/all_nodes.rb +12 -0
- data/lib/behavior_tree/concerns/node_iterators/prioritize_running.rb +19 -0
- data/lib/behavior_tree/concerns/tree_structure/algorithms.rb +95 -0
- data/lib/behavior_tree/concerns/tree_structure/printer.rb +83 -0
- data/lib/behavior_tree/concerns/validations/proc_or_block.rb +17 -0
- data/lib/behavior_tree/concerns/validations/single_child.rb +18 -0
- data/lib/behavior_tree/control_nodes/control_node_base.rb +93 -0
- data/lib/behavior_tree/control_nodes/selector.rb +22 -0
- data/lib/behavior_tree/control_nodes/sequence.rb +24 -0
- data/lib/behavior_tree/decorator_nodes/condition.rb +46 -0
- data/lib/behavior_tree/decorator_nodes/decorator_base.rb +40 -0
- data/lib/behavior_tree/decorator_nodes/force_failure.rb +16 -0
- data/lib/behavior_tree/decorator_nodes/force_success.rb +16 -0
- data/lib/behavior_tree/decorator_nodes/inverter.rb +18 -0
- data/lib/behavior_tree/decorator_nodes/repeat_times_base.rb +57 -0
- data/lib/behavior_tree/decorator_nodes/repeater.rb +16 -0
- data/lib/behavior_tree/decorator_nodes/retry.rb +14 -0
- data/lib/behavior_tree/errors.rb +65 -0
- data/lib/behavior_tree/node_base.rb +119 -0
- data/lib/behavior_tree/node_status.rb +70 -0
- data/lib/behavior_tree/single_child_node.rb +29 -0
- data/lib/behavior_tree/tasks/nop.rb +38 -0
- data/lib/behavior_tree/tasks/task_base.rb +37 -0
- data/lib/behavior_tree/tree.rb +35 -0
- data/lib/behavior_tree/version.rb +5 -0
- 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,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,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,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
|
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: []
|