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