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.
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
@@ -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' # TODO: Change?
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 = 'Behavior Tree (AI) library for Ruby.'
12
- spec.homepage = 'https://github.com/FeloVilches/Ruby-Behavior-Tree' # TODO: Change?
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
- # Load all files from lib.
4
- # Gem.find_files('behavior_tree/**/*.rb').each { |path| require path }
3
+ require 'set'
5
4
 
6
- Dir.glob(File.join('./lib', '**', '*.rb')).sort.each { |file| require file }
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
- # 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)
61
+ def exec_with_children(node_class, children_type, args, block)
95
62
  stack_children_from_block(block)
96
- final_args = [@stack.pop] + args # @stack.pop is already an Array
97
- final_args.flatten! unless children == :multiple
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 dynamic_method_leaf(node_class, args, block)
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
- dynamic_method_leaf(node_class, args, block)
87
+ exec_leaf(node_class, args, block)
118
88
  else
119
- dynamic_method_with_children(node_class, children, args, block)
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
- 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
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) ? '│ ' : ' ' }.join
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
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../node_base'
4
- require_relative '../concerns/node_iterators/prioritize_running'
5
- require_relative '../concerns/node_iterators/all_nodes'
6
4
 
7
5
  module BehaviorTree
8
6
  # A node that has children (abstract class).