behavior_tree 0.1.7 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +2 -2
- data/README.md +815 -23
- data/Rakefile +77 -0
- data/behavior_tree.gemspec +4 -4
- data/lib/behavior_tree.rb +3 -29
- data/lib/behavior_tree/builder.rb +15 -45
- data/lib/behavior_tree/concerns/dsl/randomizer.rb +78 -0
- data/lib/behavior_tree/concerns/dsl/registration.rb +35 -0
- data/lib/behavior_tree/concerns/dsl/spell_checker.rb +2 -0
- data/lib/behavior_tree/concerns/dsl/utils.rb +22 -0
- data/lib/behavior_tree/concerns/node_iterators/prioritize_running.rb +8 -2
- data/lib/behavior_tree/concerns/tree_structure/printer.rb +31 -8
- data/lib/behavior_tree/control_nodes/control_node_base.rb +0 -2
- data/lib/behavior_tree/decorator_nodes/condition.rb +0 -1
- data/lib/behavior_tree/decorator_nodes/inverter.rb +0 -2
- data/lib/behavior_tree/decorator_nodes/repeat_times_base.rb +0 -4
- data/lib/behavior_tree/decorator_nodes/repeater.rb +0 -2
- data/lib/behavior_tree/errors.rb +8 -0
- data/lib/behavior_tree/node_base.rb +22 -10
- data/lib/behavior_tree/node_status.rb +1 -1
- data/lib/behavior_tree/single_child_node.rb +0 -3
- data/lib/behavior_tree/tasks/task_base.rb +0 -3
- data/lib/behavior_tree/version.rb +1 -1
- metadata +11 -8
data/Rakefile
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
require 'rubocop/rake_task'
|
4
4
|
require 'bundler/gem_tasks'
|
5
5
|
require 'rspec/core/rake_task'
|
6
|
+
require 'bundler/setup'
|
7
|
+
require 'behavior_tree'
|
8
|
+
require 'io/console'
|
6
9
|
|
7
10
|
RSpec::Core::RakeTask.new(:spec)
|
8
11
|
|
@@ -12,3 +15,77 @@ desc 'Run RuboCop'
|
|
12
15
|
RuboCop::RakeTask.new(:lint) do |task|
|
13
16
|
task.options = ['--fail-level', 'autocorrect']
|
14
17
|
end
|
18
|
+
|
19
|
+
desc 'Run RuboCop for specs'
|
20
|
+
RuboCop::RakeTask.new(:spec_lint) do |task|
|
21
|
+
task.requires << 'rubocop-rspec'
|
22
|
+
end
|
23
|
+
|
24
|
+
# Utils for the visualize Rake task.
|
25
|
+
# NOTE: Don't use it for anything other than the Rake task.
|
26
|
+
class VisualizeUtils
|
27
|
+
class << self
|
28
|
+
def random_seed
|
29
|
+
@random_seed ||= if ENV['random_seed'].nil?
|
30
|
+
(Time.now.to_f * 1000).to_i ^ Process.pid
|
31
|
+
else
|
32
|
+
ENV['random_seed'].to_i
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def sleep_time
|
37
|
+
result = ENV['sleep'].nil? ? 0.5 : ENV['sleep'].to_f
|
38
|
+
raise 'Sleep time must be a positive float value' if result <= 0
|
39
|
+
|
40
|
+
result
|
41
|
+
end
|
42
|
+
|
43
|
+
def print_random_seed_info
|
44
|
+
puts ''
|
45
|
+
puts "Generate the same tree by adding: random_seed=#{random_seed}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def random_tree
|
49
|
+
@random_tree ||= BehaviorTree::Builder.build_random_tree(recursion_amount: 2)
|
50
|
+
end
|
51
|
+
|
52
|
+
def update_and_draw
|
53
|
+
$stdout.clear_screen
|
54
|
+
random_tree.print
|
55
|
+
print_random_seed_info
|
56
|
+
random_tree.tick!
|
57
|
+
end
|
58
|
+
|
59
|
+
def setup_console
|
60
|
+
srand(random_seed)
|
61
|
+
$stdout.sync = true
|
62
|
+
$stdout.clear_screen
|
63
|
+
|
64
|
+
trap 'INT' do
|
65
|
+
puts ''
|
66
|
+
exit 0
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
namespace :visualize do
|
73
|
+
desc 'Visualize a random tree animated (optional parameters sleep=0.5 random_seed=12345678)'
|
74
|
+
task :auto do
|
75
|
+
VisualizeUtils.setup_console
|
76
|
+
loop do
|
77
|
+
VisualizeUtils.update_and_draw
|
78
|
+
sleep VisualizeUtils.sleep_time
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
desc 'Visualize a random tree, press key to tick (optional parameter random_seed=12345678)'
|
83
|
+
task :manual do
|
84
|
+
VisualizeUtils.setup_console
|
85
|
+
loop do
|
86
|
+
VisualizeUtils.update_and_draw
|
87
|
+
puts 'Press Enter key to tick. Press CTRL+C (SIGINT) to exit.'
|
88
|
+
$stdin.getc
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/behavior_tree.gemspec
CHANGED
@@ -3,13 +3,13 @@
|
|
3
3
|
require_relative 'lib/behavior_tree/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name = 'behavior_tree'
|
6
|
+
spec.name = 'behavior_tree'
|
7
7
|
spec.version = BehaviorTree::VERSION
|
8
8
|
spec.authors = ['Felo Vilches']
|
9
9
|
spec.email = ['felovilches@gmail.com']
|
10
10
|
|
11
|
-
spec.summary = '
|
12
|
-
spec.homepage = 'https://github.com/FeloVilches/Ruby-Behavior-Tree'
|
11
|
+
spec.summary = 'A robust and customizable Ruby gem for creating Behavior Trees.'
|
12
|
+
spec.homepage = 'https://github.com/FeloVilches/Ruby-Behavior-Tree'
|
13
13
|
spec.license = 'MIT'
|
14
14
|
spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
|
15
15
|
|
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
# spec.metadata['changelog_uri'] = 'TODO: Put your gem's CHANGELOG.md URL here.'
|
20
20
|
|
21
21
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
22
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|assets)/}) }
|
23
23
|
end
|
24
24
|
spec.bindir = 'exe'
|
25
25
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
data/lib/behavior_tree.rb
CHANGED
@@ -1,32 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
# Gem.find_files('behavior_tree/**/*.rb').each { |path| require path }
|
5
|
-
|
6
|
-
# Dir.glob(File.join('**', '*.rb')).sort.each { |file| require_relative file }
|
7
|
-
|
8
|
-
# Dir[File.join('.', 'behavior_tree', '**', '*.rb')].sort.each { |file| require_relative file }
|
3
|
+
require 'set'
|
9
4
|
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
require_relative './behavior_tree/concerns/tree_structure/printer'
|
14
|
-
require_relative './behavior_tree/concerns/tree_structure/algorithms'
|
15
|
-
require_relative './behavior_tree/tasks/task_base'
|
16
|
-
require_relative './behavior_tree/single_child_node'
|
17
|
-
require_relative './behavior_tree/decorator_nodes/decorator_base'
|
18
|
-
require_relative './behavior_tree/decorator_nodes/repeater'
|
19
|
-
require_relative './behavior_tree/decorator_nodes/retry'
|
20
|
-
require_relative './behavior_tree/decorator_nodes/inverter'
|
21
|
-
require_relative './behavior_tree/decorator_nodes/force_success'
|
22
|
-
require_relative './behavior_tree/decorator_nodes/force_failure'
|
23
|
-
require_relative './behavior_tree/decorator_nodes/condition'
|
24
|
-
require_relative './behavior_tree/errors'
|
25
|
-
require_relative './behavior_tree/control_nodes/control_node_base'
|
26
|
-
require_relative './behavior_tree/control_nodes/selector'
|
27
|
-
require_relative './behavior_tree/control_nodes/sequence'
|
28
|
-
require_relative './behavior_tree/node_status'
|
29
|
-
require_relative './behavior_tree/tasks/nop'
|
30
|
-
require_relative './behavior_tree/builder'
|
31
|
-
require_relative './behavior_tree/version'
|
32
|
-
require_relative './behavior_tree/tree'
|
5
|
+
# Load all files from lib.
|
6
|
+
Dir[File.join(__dir__, 'behavior_tree', '**', '*.rb')].sort.each { |file| require_relative file }
|
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
require_relative './concerns/dsl/spell_checker'
|
4
4
|
require_relative './concerns/dsl/initial_config'
|
5
|
+
require_relative './concerns/dsl/randomizer'
|
6
|
+
require_relative './concerns/dsl/registration'
|
7
|
+
require_relative './concerns/dsl/utils'
|
5
8
|
|
6
9
|
module BehaviorTree
|
7
10
|
# DSL for building a tree.
|
@@ -10,6 +13,9 @@ module BehaviorTree
|
|
10
13
|
class << self
|
11
14
|
include Dsl::SpellChecker
|
12
15
|
include Dsl::InitialConfig
|
16
|
+
include Dsl::Randomizer
|
17
|
+
include Dsl::Registration
|
18
|
+
include Dsl::Utils
|
13
19
|
|
14
20
|
def build(&block)
|
15
21
|
# Stack of lists. When a method like 'sequence' is executed, the resulting
|
@@ -27,33 +33,6 @@ module BehaviorTree
|
|
27
33
|
BehaviorTree::Tree.new tree_main_nodes.first
|
28
34
|
end
|
29
35
|
|
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
36
|
private
|
58
37
|
|
59
38
|
def stack(obj)
|
@@ -79,26 +58,17 @@ module BehaviorTree
|
|
79
58
|
@node_type_mapping.key? method_name
|
80
59
|
end
|
81
60
|
|
82
|
-
|
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)
|
61
|
+
def exec_with_children(node_class, children_type, args, block)
|
95
62
|
stack_children_from_block(block)
|
96
|
-
|
97
|
-
|
63
|
+
children_nodes = @stack.pop
|
64
|
+
raise DSLStandardError, "Node #{node_class} has no children." if children_nodes.empty?
|
65
|
+
|
66
|
+
final_args = [children_nodes] + args # @stack.pop is already an Array
|
67
|
+
final_args.flatten! unless children_type == :multiple
|
98
68
|
stack node_class.new(*final_args)
|
99
69
|
end
|
100
70
|
|
101
|
-
def
|
71
|
+
def exec_leaf(node_class, args, block)
|
102
72
|
stack node_class.new(*args, &block)
|
103
73
|
end
|
104
74
|
|
@@ -114,9 +84,9 @@ module BehaviorTree
|
|
114
84
|
|
115
85
|
# Nodes that have children are executed differently from leaf nodes.
|
116
86
|
if children == :none
|
117
|
-
|
87
|
+
exec_leaf(node_class, args, block)
|
118
88
|
else
|
119
|
-
|
89
|
+
exec_with_children(node_class, children, args, block)
|
120
90
|
end
|
121
91
|
end
|
122
92
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BehaviorTree
|
4
|
+
module Dsl
|
5
|
+
# Generates random trees.
|
6
|
+
module Randomizer
|
7
|
+
def build_random_tree(recursion_amount: 10)
|
8
|
+
raise ArgumentError, 'Recursion amount must be greater than 0' if recursion_amount < 1
|
9
|
+
|
10
|
+
build do
|
11
|
+
send(%i[sel seq].sample) do
|
12
|
+
rand(3..5).times { recurse(recursion_amount).() }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def recurse(recursions_left)
|
20
|
+
return random_leaf_blocks.sample if recursions_left.zero?
|
21
|
+
|
22
|
+
recursions_left -= 1
|
23
|
+
|
24
|
+
# Repeated values in order to increase the weight for some type of nodes.
|
25
|
+
%i[
|
26
|
+
control decorated condition
|
27
|
+
control decorated condition
|
28
|
+
leaf
|
29
|
+
].map { |type| send("random_#{type}_blocks", recursions_left) }
|
30
|
+
.concat
|
31
|
+
.flatten
|
32
|
+
.sample
|
33
|
+
end
|
34
|
+
|
35
|
+
def random_control_blocks(recursions_left)
|
36
|
+
[
|
37
|
+
proc { sel { rand(2..3).times { recurse(recursions_left).() } } },
|
38
|
+
proc { seq { rand(2..3).times { recurse(recursions_left).() } } }
|
39
|
+
]
|
40
|
+
end
|
41
|
+
|
42
|
+
def random_decorated_blocks(recursions_left)
|
43
|
+
[
|
44
|
+
proc { force_success(&recurse(recursions_left)) },
|
45
|
+
proc { force_failure(&recurse(recursions_left)) },
|
46
|
+
proc { inv(&recurse(recursions_left)) },
|
47
|
+
proc { re_try(20, &recurse(recursions_left)) },
|
48
|
+
proc { repeater(20, &recurse(recursions_left)) }
|
49
|
+
]
|
50
|
+
end
|
51
|
+
|
52
|
+
def random_condition_blocks(recursions_left)
|
53
|
+
[
|
54
|
+
proc {
|
55
|
+
cond(-> { rand > 0.2 }, &recurse(recursions_left))
|
56
|
+
},
|
57
|
+
proc {
|
58
|
+
cond(-> { rand > 0.8 }, &recurse(recursions_left))
|
59
|
+
}
|
60
|
+
]
|
61
|
+
end
|
62
|
+
|
63
|
+
def random_leaf_blocks(_recursions_left = nil)
|
64
|
+
task = proc do
|
65
|
+
task do
|
66
|
+
# Weights.
|
67
|
+
running_w = 3
|
68
|
+
success_w = 1
|
69
|
+
failure_w = 2
|
70
|
+
new_status = (([:running] * running_w) + ([:success] * success_w) + ([:failure] * failure_w)).sample
|
71
|
+
status.send("#{new_status}!")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
[task]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BehaviorTree
|
4
|
+
module Dsl
|
5
|
+
# Register DSL commands.
|
6
|
+
module Registration
|
7
|
+
# Don't validate class_name, because in some situations the user wants it to be evaluated
|
8
|
+
# in runtime.
|
9
|
+
def register(node_name, class_name, children: :none)
|
10
|
+
valid_children_values = %i[none single multiple]
|
11
|
+
raise "Children value must be in: #{valid_children_values}" unless valid_children_values.include?(children)
|
12
|
+
|
13
|
+
node_name = node_name.to_sym
|
14
|
+
raise RegisterDSLNodeAlreadyExistsError, node_name if @node_type_mapping.key?(node_name)
|
15
|
+
|
16
|
+
@node_type_mapping[node_name] = {
|
17
|
+
class: class_name,
|
18
|
+
children: children
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def register_alias(original, alias_key)
|
23
|
+
unless @node_type_mapping.key?(original)
|
24
|
+
raise "Cannot register alias for '#{original}', since it doesn't exist."
|
25
|
+
end
|
26
|
+
raise RegisterDSLNodeAlreadyExistsError, alias_key if @node_type_mapping.key?(alias_key)
|
27
|
+
raise 'Alias key cannot be empty' if alias_key.to_s.empty?
|
28
|
+
|
29
|
+
@node_type_mapping[original][:alias] = alias_key
|
30
|
+
@node_type_mapping[alias_key] = @node_type_mapping[original].dup
|
31
|
+
@node_type_mapping[alias_key][:alias] = original
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -4,6 +4,8 @@ module BehaviorTree
|
|
4
4
|
module Dsl
|
5
5
|
# Helpers for spellchecking, and correcting user input in the DSL builder.
|
6
6
|
module SpellChecker
|
7
|
+
private
|
8
|
+
|
7
9
|
def raise_node_type_not_exists(missing_method)
|
8
10
|
suggestion = most_similar_name missing_method
|
9
11
|
method_alias = @node_type_mapping.dig suggestion, :alias
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BehaviorTree
|
4
|
+
module Dsl
|
5
|
+
# Helpers for DSL.
|
6
|
+
module Utils
|
7
|
+
private
|
8
|
+
|
9
|
+
# Convert a class name with namespace into a constant.
|
10
|
+
# It returns the class itself if it's already a class.
|
11
|
+
# @param class_name [String]
|
12
|
+
# @return [Class]
|
13
|
+
def constantize(class_name)
|
14
|
+
return class_name if class_name.is_a?(Class)
|
15
|
+
|
16
|
+
class_name.split('::').compact.inject(Object) { |o, c| o.const_get c }
|
17
|
+
rescue NameError
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -5,15 +5,21 @@ module BehaviorTree
|
|
5
5
|
# If there's at least one node with 'running' status, then iterate starting from there, in order.
|
6
6
|
# Else, iterate all nodes.
|
7
7
|
module PrioritizeRunning
|
8
|
+
private
|
9
|
+
|
8
10
|
def prioritize_running
|
9
|
-
|
11
|
+
@first_running_idx = children.find_index { |child| child.status.running? }.to_i if must_recompute_idx?
|
10
12
|
|
11
13
|
Enumerator.new do |y|
|
12
|
-
|
14
|
+
children[@first_running_idx..].each do |child|
|
13
15
|
y << child
|
14
16
|
end
|
15
17
|
end
|
16
18
|
end
|
19
|
+
|
20
|
+
def must_recompute_idx?
|
21
|
+
!@first_running_idx || !children[@first_running_idx].status.running?
|
22
|
+
end
|
17
23
|
end
|
18
24
|
end
|
19
25
|
end
|
@@ -7,12 +7,16 @@ module BehaviorTree
|
|
7
7
|
# Algorithm to print tree.
|
8
8
|
module Printer
|
9
9
|
def print
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
lines = []
|
11
|
+
lines << '∅' # Style for the root node.
|
12
|
+
lines += tree_lines
|
13
|
+
lines << ''
|
14
|
+
lines << cycle_string
|
15
|
+
lines << uniq_nodes_string
|
16
|
+
lines << size_string
|
17
|
+
lines << tree_tick_count_string
|
18
|
+
|
19
|
+
puts lines.join "\n"
|
16
20
|
end
|
17
21
|
|
18
22
|
private
|
@@ -27,7 +31,7 @@ module BehaviorTree
|
|
27
31
|
|
28
32
|
last_child ? vertical_lines_continues.delete(depth) : vertical_lines_continues << depth
|
29
33
|
|
30
|
-
space = (0...depth).map { |d| vertical_lines_continues.include?(d) ? '│
|
34
|
+
space = (0...depth).map { |d| vertical_lines_continues.include?(d) ? '│ ' : ' ' }.join
|
31
35
|
connector = last_child ? '└─' : '├─'
|
32
36
|
|
33
37
|
"#{space}#{connector}#{class_simple_name(node)} #{status_string(node)} #{tick_count_string(node)}"
|
@@ -66,6 +70,10 @@ module BehaviorTree
|
|
66
70
|
ColorizedString["(#{node.tick_count} ticks)"].colorize(color)
|
67
71
|
end
|
68
72
|
|
73
|
+
def tree_tick_count_string
|
74
|
+
"Tree has been ticked #{tick_count} times."
|
75
|
+
end
|
76
|
+
|
69
77
|
# Copied from Rails' ActiveSupport.
|
70
78
|
def snake_case(str)
|
71
79
|
str.gsub(/::/, '/')
|
@@ -76,7 +84,22 @@ module BehaviorTree
|
|
76
84
|
end
|
77
85
|
|
78
86
|
def class_simple_name(node)
|
79
|
-
snake_case(node.class.name.split('::').last)
|
87
|
+
pretty_name snake_case(node.class.name.split('::').last)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Changes the name of some classes (maps it to a better name).
|
91
|
+
# Mapping is simply based on taste.
|
92
|
+
def pretty_name(name)
|
93
|
+
case name
|
94
|
+
when 'task_base'
|
95
|
+
'task'
|
96
|
+
when 'force_success'
|
97
|
+
'forcesuccess'
|
98
|
+
when 'force_failure'
|
99
|
+
'forcefailure'
|
100
|
+
else
|
101
|
+
name
|
102
|
+
end
|
80
103
|
end
|
81
104
|
end
|
82
105
|
end
|