behavior_tree 0.1.9 → 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
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,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  # Load all files from lib.
4
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
@@ -29,10 +29,6 @@ module BehaviorTree
29
29
  end
30
30
  end
31
31
 
32
- def ensure_after_tick
33
- status_map
34
- end
35
-
36
32
  protected
37
33
 
38
34
  def repeat_while