behavior_tree 0.1.6 → 0.1.10
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 +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +2 -2
- data/README.md +809 -23
- data/Rakefile +77 -0
- data/behavior_tree.gemspec +4 -4
- data/lib/behavior_tree.rb +3 -3
- 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/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/tree.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,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
# Gem.find_files('behavior_tree/**/*.rb').each { |path| require path }
|
3
|
+
require 'set'
|
5
4
|
|
6
|
-
|
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
|
@@ -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
|