evolvable 1.2.0 → 2.0.0

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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +44 -25
  6. data/README.md +498 -190
  7. data/README_YARD.md +85 -166
  8. data/bin/console +10 -19
  9. data/docs/Evolvable/ClassMethods.html +1233 -0
  10. data/docs/Evolvable/Community/ClassMethods.html +708 -0
  11. data/docs/Evolvable/Community.html +1342 -0
  12. data/docs/Evolvable/CountGene.html +886 -0
  13. data/docs/Evolvable/EqualizeGoal.html +347 -0
  14. data/docs/Evolvable/Error.html +134 -0
  15. data/docs/Evolvable/Evaluation.html +773 -0
  16. data/docs/Evolvable/Evolution.html +616 -0
  17. data/docs/Evolvable/Gene/ClassMethods.html +413 -0
  18. data/docs/Evolvable/Gene.html +522 -0
  19. data/docs/Evolvable/GeneCluster/ClassMethods.html +431 -0
  20. data/docs/Evolvable/GeneCluster.html +280 -0
  21. data/docs/Evolvable/GeneCombination.html +515 -0
  22. data/docs/Evolvable/GeneSpace.html +619 -0
  23. data/docs/Evolvable/Genome.html +1070 -0
  24. data/docs/Evolvable/Goal.html +500 -0
  25. data/docs/Evolvable/MaximizeGoal.html +348 -0
  26. data/docs/Evolvable/MinimizeGoal.html +348 -0
  27. data/docs/Evolvable/Mutation.html +729 -0
  28. data/docs/Evolvable/PointCrossover.html +444 -0
  29. data/docs/Evolvable/Population.html +2826 -0
  30. data/docs/Evolvable/RigidCountGene.html +501 -0
  31. data/docs/Evolvable/Selection.html +594 -0
  32. data/docs/Evolvable/Serializer.html +293 -0
  33. data/docs/Evolvable/UniformCrossover.html +286 -0
  34. data/docs/Evolvable.html +1619 -0
  35. data/docs/_index.html +341 -0
  36. data/docs/class_list.html +54 -0
  37. data/docs/css/common.css +1 -0
  38. data/docs/css/full_list.css +58 -0
  39. data/docs/css/style.css +503 -0
  40. data/docs/file.README.html +750 -0
  41. data/docs/file_list.html +59 -0
  42. data/docs/frames.html +22 -0
  43. data/docs/index.html +750 -0
  44. data/docs/js/app.js +344 -0
  45. data/docs/js/full_list.js +242 -0
  46. data/docs/js/jquery.js +4 -0
  47. data/docs/method_list.html +1302 -0
  48. data/docs/top-level-namespace.html +110 -0
  49. data/evolvable.gemspec +6 -6
  50. data/examples/ascii_art.rb +5 -9
  51. data/examples/stickman.rb +25 -33
  52. data/{examples/hello_world.rb → exe/hello_evolvable_world} +46 -30
  53. data/lib/evolvable/community.rb +190 -0
  54. data/lib/evolvable/count_gene.rb +65 -0
  55. data/lib/evolvable/equalize_goal.rb +2 -9
  56. data/lib/evolvable/evaluation.rb +113 -14
  57. data/lib/evolvable/evolution.rb +38 -15
  58. data/lib/evolvable/gene.rb +124 -25
  59. data/lib/evolvable/gene_cluster.rb +106 -0
  60. data/lib/evolvable/gene_combination.rb +57 -16
  61. data/lib/evolvable/gene_space.rb +111 -0
  62. data/lib/evolvable/genome.rb +32 -4
  63. data/lib/evolvable/goal.rb +19 -24
  64. data/lib/evolvable/maximize_goal.rb +2 -9
  65. data/lib/evolvable/minimize_goal.rb +3 -9
  66. data/lib/evolvable/mutation.rb +87 -41
  67. data/lib/evolvable/point_crossover.rb +24 -4
  68. data/lib/evolvable/population.rb +258 -84
  69. data/lib/evolvable/rigid_count_gene.rb +36 -0
  70. data/lib/evolvable/selection.rb +68 -14
  71. data/lib/evolvable/serializer.rb +46 -0
  72. data/lib/evolvable/uniform_crossover.rb +30 -6
  73. data/lib/evolvable/version.rb +2 -1
  74. data/lib/evolvable.rb +268 -107
  75. metadata +57 -36
  76. data/examples/images/diagram.png +0 -0
  77. data/exe/hello +0 -16
  78. data/lib/evolvable/error/undefined_method.rb +0 -7
  79. data/lib/evolvable/search_space.rb +0 -181
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.9.37
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" />
16
+
17
+ <script type="text/javascript">
18
+ pathId = "";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+
41
+
42
+ <span class="title">Top Level Namespace</span>
43
+
44
+ </div>
45
+
46
+ <div id="search">
47
+
48
+ <a class="full_list_link" id="class_list_link"
49
+ href="class_list.html">
50
+
51
+ <svg width="24" height="24">
52
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
53
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
54
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
55
+ </svg>
56
+ </a>
57
+
58
+ </div>
59
+ <div class="clear"></div>
60
+ </div>
61
+
62
+ <div id="content"><h1>Top Level Namespace
63
+
64
+
65
+
66
+ </h1>
67
+ <div class="box_info">
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+ </div>
80
+
81
+ <h2>Defined Under Namespace</h2>
82
+ <p class="children">
83
+
84
+
85
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="Evolvable.html" title="Evolvable (module)">Evolvable</a></span>
86
+
87
+
88
+
89
+
90
+ </p>
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <div id="footer">
103
+ Generated on Sat May 10 18:39:22 2025 by
104
+ <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
+ 0.9.37 (ruby-3.4.2).
106
+ </div>
107
+
108
+ </div>
109
+ </body>
110
+ </html>
data/evolvable.gemspec CHANGED
@@ -10,11 +10,12 @@ Gem::Specification.new do |spec|
10
10
  spec.summary = 'Add evolutionary behavior to any Ruby object'
11
11
  spec.homepage = 'https://github.com/mattruzicka/evolvable'
12
12
  spec.license = 'MIT'
13
+ spec.required_ruby_version = ">= 2.6"
13
14
 
14
15
  spec.metadata['homepage_uri'] = spec.homepage
15
- spec.metadata['source_code_uri'] = 'https://github.com/mattruzicka/evolvable'
16
- spec.metadata['changelog_uri'] = 'https://github.com/mattruzicka/evolvable' \
17
- '/blob/master/CHANGELOG.md'
16
+ spec.metadata['source_code_uri'] = spec.homepage
17
+ spec.metadata['changelog_uri'] = 'https://github.com/mattruzicka/evolvable/blob/main/CHANGELOG.md'
18
+ spec.metadata["documentation_uri"] = Evolvable::DOC_URL
18
19
 
19
20
  # Specify which files should be added to the gem when it is released.
20
21
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -25,7 +26,6 @@ Gem::Specification.new do |spec|
25
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
27
  spec.require_paths = ['lib']
27
28
 
28
- spec.add_development_dependency 'bundler', '~> 2.0'
29
- spec.add_development_dependency 'readme_yard'
30
- spec.add_development_dependency 'yard'
29
+ spec.add_development_dependency 'irb', '>= 1.0'
30
+ spec.add_development_dependency 'readme_yard', '>= 0.5'
31
31
  end
@@ -3,14 +3,10 @@ require './examples/ascii_gene'
3
3
  class AsciiArt
4
4
  include Evolvable
5
5
 
6
- class << self
7
- def search_space
8
- { chars: { type: 'AsciiGene', count: 136 } }
9
- end
6
+ gene :chars, type: 'AsciiGene', count: 136
10
7
 
11
- def before_evaluation(population)
12
- population.best_evolvable.to_terminal
13
- end
8
+ def self.before_evaluation(population)
9
+ population.best_evolvable.to_terminal
14
10
  end
15
11
 
16
12
  CLEAR_SEQUENCE = ("\e[1A\r\033[2K" * 14).freeze
@@ -18,7 +14,7 @@ class AsciiArt
18
14
  def to_terminal
19
15
  print(CLEAR_SEQUENCE) unless population.evolutions_count.zero?
20
16
  lines = genes.each_slice(17).flat_map(&:join)
21
- lines[0] = "\n #{lines[0]} #{green_text('Minimalism Score:')} #{value}"
17
+ lines[0] = "\n #{lines[0]} #{green_text('Minimalism Score:')} #{fitness}"
22
18
  lines[1] << " #{green_text('Generation:')} #{population.evolutions_count}"
23
19
  print "\n\n#{lines.join("\n ")}\n\n\n\n #{green_text('Use Ctrl-C to stop')} "
24
20
  end
@@ -27,7 +23,7 @@ class AsciiArt
27
23
  @minimalism_score ||= essence_score + spacial_score - clutter_score
28
24
  end
29
25
 
30
- alias value minimalism_score
26
+ alias fitness minimalism_score
31
27
 
32
28
  private
33
29
 
data/examples/stickman.rb CHANGED
@@ -21,49 +21,41 @@ class Stickman
21
21
  end
22
22
  end
23
23
 
24
- class << self
25
- def search_space
26
- { head: { type: 'HeadGene', count: 1 },
27
- body: { type: 'AsciiGene', count: 1 },
28
- appendages: { type: 'AppendageGene', count: 4 } }
29
- end
30
-
31
- CLEAR_SEQUENCE = ("\e[1A\r\033[2K" * 8).freeze
24
+ gene :head, type: 'HeadGene', count: 1
25
+ gene :body, type: 'AsciiGene', count: 1
26
+ gene :appendages, type: 'AppendageGene', count: 4
32
27
 
33
- def before_evaluation(population)
34
- population.evolvables.each do |evolvable|
35
- puts "\n\n#{evolvable.draw}\n\n"
36
- print green_text(" Rate Stickman #{population.evolutions_count}.#{evolvable.generation_index + 1}: ")
37
- evolvable.value = gets.to_i
38
- print CLEAR_SEQUENCE
39
- end
28
+ CLEAR_SEQUENCE = ("\e[1A\r\033[2K" * 8).freeze
40
29
 
41
- (@best_evolvables ||= []) << population.best_evolvable
42
- animate_best_evolvables if @best_evolvables.count > 1
43
- print "\n\n\n\n\n\n\n #{green_text('Evolve next generation?');} Yes!" \
44
- " #{green_text('...Use Ctrl-C to stop')}#{"\b" * 23}"
45
- gets
30
+ def self.before_evaluation(population)
31
+ population.evolvables.each do |evolvable|
32
+ puts "\n\n#{evolvable.draw}\n\n"
33
+ print green_text(" Rate Stickman #{population.evolutions_count}.#{evolvable.generation_index + 1}: ")
34
+ evolvable.fitness = gets.to_i
46
35
  print CLEAR_SEQUENCE
47
36
  end
48
37
 
49
- def animate_best_evolvables
50
- @best_evolvables.each_with_index do |evolvable, index|
51
- puts "\n\n#{green_text(evolvable.draw)}\n\n"
52
- print green_text(" Generation: #{index}\r\n ")
53
- sleep 0.12
54
- print "#{CLEAR_SEQUENCE}"
55
- end
56
- end
38
+ (@best_evolvables ||= []) << population.best_evolvable
39
+ animate_best_evolvables if @best_evolvables.count > 1
40
+ end
57
41
 
58
- def green_text(text)
59
- "\e[32m#{text}\e[0m"
42
+ def self.animate_best_evolvables
43
+ @best_evolvables.each_with_index do |evolvable, index|
44
+ puts "\n\n#{green_text(evolvable.draw)}\n\n"
45
+ print green_text(" Generation: #{index}\r\n ")
46
+ sleep 0.5
47
+ print "#{CLEAR_SEQUENCE}"
60
48
  end
61
49
  end
62
50
 
51
+ def self.green_text(text)
52
+ "\e[32m#{text}\e[0m"
53
+ end
54
+
63
55
  def draw
64
- canvas.sub!('o', find_gene(:head).to_s)
65
- canvas.sub!('0', find_gene(:body).to_s)
66
- find_genes(:appendages).each { |a| canvas.sub!('-', a.to_s) }
56
+ canvas.sub!('o', head.to_s)
57
+ canvas.sub!('0', body.to_s)
58
+ appendages.each { |a| canvas.sub!('-', a.to_s) }
67
59
  canvas
68
60
  end
69
61
 
@@ -1,27 +1,37 @@
1
- class HelloWorld
2
- include Evolvable
3
-
4
- class CharGene
5
- include Evolvable::Gene
6
-
7
- def self.chars
8
- @chars ||= 32.upto(126).map(&:chr)
9
- end
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Hello World Command Line Demo
4
+ #
5
+ # Make sure you have the gem installed: `gem install evolvable``
6
+ # Run this file directly: `bundle exec hello_evolvable_world`
7
+ #
8
+
9
+ require 'bundler/setup'
10
+ require 'evolvable'
11
+ require 'irb'
12
+
13
+ class CharGene
14
+ include Evolvable::Gene
15
+
16
+ def self.chars
17
+ @chars ||= 32.upto(126).map(&:chr)
18
+ end
10
19
 
11
- def self.ensure_chars(string)
12
- @chars.concat(string.chars - chars)
13
- end
20
+ def self.add_missing_chars(input_string)
21
+ @chars.concat(input_string.chars - @chars)
22
+ end
14
23
 
15
- def to_s
16
- @to_s ||= self.class.chars.sample
17
- end
24
+ def to_s
25
+ @to_s ||= self.class.chars.sample
18
26
  end
27
+ end
28
+
29
+ class HelloWorld
30
+ include Evolvable
19
31
 
20
32
  MAX_STRING_LENGTH = 40
21
33
 
22
- def self.search_space
23
- { char_genes: { type: 'CharGene', count: 1..MAX_STRING_LENGTH } }
24
- end
34
+ gene :char_genes, type: 'CharGene', count: 1..MAX_STRING_LENGTH
25
35
 
26
36
  def self.start_loop(population)
27
37
  loop do
@@ -29,7 +39,7 @@ class HelloWorld
29
39
  prepare_to_exit_loop && break if exit_loop?
30
40
 
31
41
  population.reset_evolvables
32
- population.evolve
42
+ population.evolve_to_goal
33
43
  end
34
44
  end
35
45
 
@@ -48,17 +58,15 @@ class HelloWorld
48
58
  self.target = gets.strip!
49
59
  end
50
60
 
51
- def self.target=(val)
52
- @target = if val.empty?
53
- 'I chose this string :)'
54
- else
55
- CharGene.ensure_chars(val)
56
- val.slice(0...MAX_STRING_LENGTH)
57
- end
61
+ DEFAULT_TARGET = 'Hello Evolvable World'
62
+
63
+ def self.target=(input_string)
64
+ CharGene.add_missing_chars(input_string)
65
+ @target = input_string.empty? ? DEFAULT_TARGET : input_string.slice(0...MAX_STRING_LENGTH)
58
66
  end
59
67
 
60
68
  def self.target
61
- @target ||= 'Hello World!'
69
+ @target ||= DEFAULT_TARGET
62
70
  end
63
71
 
64
72
  def self.before_evolution(population)
@@ -75,13 +83,13 @@ class HelloWorld
75
83
  @to_s ||= genes.join
76
84
  end
77
85
 
78
- def value
79
- @value ||= compute_value
86
+ def fitness
87
+ @fitness ||= compute_fitness
80
88
  end
81
89
 
82
90
  private
83
91
 
84
- def compute_value
92
+ def compute_fitness
85
93
  string = to_s
86
94
  target = self.class.target
87
95
  target_length = target.length
@@ -89,3 +97,11 @@ class HelloWorld
89
97
  (target_length - char_matches) + (target_length - string.length).abs
90
98
  end
91
99
  end
100
+
101
+ population = HelloWorld.new_population(size: 100,
102
+ evaluation: { equalize: 0 },
103
+ mutation: { probability: 0.6 })
104
+ population.evolve_to_goal
105
+ HelloWorld.start_loop(population)
106
+
107
+ binding.irb
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # @readme
6
+ # The `Community` module provides a framework for coordinating multiple evolvable populations
7
+ # under a unified interface. Each population represents a distinct type of evolvable, and
8
+ # each key returns a single evolvable instance drawn from its corresponding population.
9
+ #
10
+ # Communities are ideal for simulations or systems where different components evolve
11
+ # in parallel but interact as part of a larger whole - such as ecosystems, design systems,
12
+ # or modular agents. Evolvables from different populations can co-evolve, influencing each other's fitness.
13
+ #
14
+ # Use the `evolvable_community` macro to declare the set of named populations in the community.
15
+ # Each population will have a corresponding method (e.g., `fish_1`, `plant`, `shrimp`) that
16
+ # returns a single evolvable instance. You can evolve all populations together using the
17
+ # `evolve` method, or per population.
18
+ #
19
+ # **Key Features**
20
+ # - Define a community composed of named populations
21
+ # - Automatically generate accessors for each evolvable instance
22
+ # - Coordinate evolution across populations through a shared interface
23
+ # - Evolve all populations in a single call with `evolve(...)`
24
+ #
25
+ # This `FishTank` example sets up a community with four named populations:
26
+ #
27
+ # ```ruby
28
+ # class FishTank
29
+ # include Evolvable::Community
30
+ #
31
+ # evolvable_community fish_1: Fish,
32
+ # fish_2: Fish,
33
+ # plant: AquariumPlant,
34
+ # shrimp: CleanerShrimp
35
+ #
36
+ # def describe_tank
37
+ # puts "🐟 Fish 1: #{fish_1.name} (#{fish_1.color})"
38
+ # puts "🐟 Fish 2: #{fish_2.name} (#{fish_2.color})"
39
+ # puts "🌿 Plant: #{plant.name} (#{plant.color})"
40
+ # puts "🦐 Shrimp: #{shrimp.name} (#{shrimp.color})"
41
+ # end
42
+ # end
43
+ # ```
44
+ #
45
+ # Initialize the community, describe the tank, and evolve each population:
46
+ #
47
+ # ```ruby
48
+ # tank = FishTank.new_community
49
+ # tank.describe_tank
50
+ # tank.evolve
51
+ # ```
52
+ #
53
+ module Community
54
+ def self.included(base)
55
+ base.extend(ClassMethods)
56
+ end
57
+
58
+ module ClassMethods
59
+ # Creates a new instance of the community
60
+ # @return [Object] A new community instance
61
+ def new_community
62
+ initialize_community
63
+ end
64
+
65
+ # Initializes a new community instance
66
+ # This method exists as a hook for classes that include this module to override
67
+ # and customize the community instantiation process.
68
+ # @return [Object] A new community instance
69
+ def initialize_community
70
+ new
71
+ end
72
+
73
+ def load_community(data)
74
+ Serializer.load(data)
75
+ end
76
+
77
+ attr_accessor :community_config
78
+
79
+ # Configures a class as an evolvable community with specified populations
80
+ # @param community_config [Hash] A hash mapping names to population classes
81
+ # @return [void]
82
+ def evolvable_community(community_config)
83
+ self.community_config = community_config
84
+ community_config.each_key do |name|
85
+ method_name = name.to_sym
86
+ define_method(method_name) do
87
+ instance_variable_get("@#{method_name}") || instance_variable_set("@#{method_name}", find_or_new_evolvable(method_name))
88
+ end
89
+ end
90
+ end
91
+
92
+ # Returns a hash of population instances by name
93
+ # @return [Hash] A hash mapping names to population instances
94
+ def populations_by_name
95
+ @populations_by_name ||= community_config.inject({}) do |hash, (name, population_klass)|
96
+ hash[name.to_sym] = population_klass.new_population(size: 0)
97
+ hash
98
+ end
99
+ end
100
+
101
+ # Adds a population to the community
102
+ # @param name [String, Symbol] The name of the population
103
+ # @param population [Object] The population instance
104
+ # @return [Object] The added population
105
+ def add_population(name, population)
106
+ populations_by_name[name.to_sym] = population
107
+ end
108
+ end
109
+
110
+ # Returns a hash of population instances by name
111
+ # @return [Hash] A hash mapping names to population instances
112
+ def populations_by_name
113
+ @populations_by_name ||= self.class.populations_by_name.dup
114
+ end
115
+
116
+ # Returns an array of population instances
117
+ # @return [Array] An array of population instances
118
+ def populations
119
+ populations_by_name.values
120
+ end
121
+
122
+ # Evolves all populations in the community
123
+ # @return [void]
124
+ def evolve(...)
125
+ populations.each { |population| population.evolve(...) }
126
+ end
127
+
128
+ # Resets the populations hash to an empty hash
129
+ # @return [Hash] An empty hash
130
+ def reset_populations
131
+ @populations_by_name = {}
132
+ end
133
+
134
+ # Adds a population to the community
135
+ # @param name [String, Symbol] The name of the population
136
+ # @param population [Object] The population instance
137
+ # @return [Object] The added population
138
+ def add_population(name, population)
139
+ populations_by_name[name.to_sym] = population
140
+ end
141
+
142
+ # Finds a population by name
143
+ # @param name [String, Symbol] The name of the population
144
+ # @return [Object, nil] The population instance or nil if not found
145
+ def find_population(name)
146
+ populations_by_name[name.to_sym]
147
+ end
148
+
149
+ # Finds an evolvable instance by name
150
+ # @param name [String, Symbol] The name of the evolvable
151
+ # @return [Object, nil] The evolvable instance or nil if not found
152
+ def find_evolvable(name)
153
+ evolvables_by_name[name.to_sym]
154
+ end
155
+
156
+ # Finds an existing evolvable instance by name or creates a new one
157
+ # @param name [String, Symbol] The name of the evolvable
158
+ # @return [Object] The existing or new evolvable instance
159
+ def find_or_new_evolvable(name)
160
+ find_evolvable(name) || new_evolvable(name)
161
+ end
162
+
163
+ # Creates a new evolvable instance from the corresponding population
164
+ # @param name [String, Symbol] The name of the evolvable
165
+ # @return [Object] The new evolvable instance
166
+ def new_evolvable(name)
167
+ evolvables_by_name[name.to_sym] = find_population(name).new_evolvable
168
+ end
169
+
170
+ # Adds an evolvable instance to the community
171
+ # @param name [String, Symbol] The name of the evolvable
172
+ # @param evolvable [Object] The evolvable instance
173
+ # @return [Object] The added evolvable
174
+ def add_evolvable(name, evolvable)
175
+ evolvables_by_name[name.to_sym] = evolvable
176
+ end
177
+
178
+ # Returns a hash of evolvable instances by name
179
+ # @return [Hash] A hash mapping names to evolvable instances
180
+ def evolvables_by_name
181
+ @evolvables_by_name ||= {}
182
+ end
183
+
184
+ # Resets the evolvables hash to an empty hash
185
+ # @return [Hash] An empty hash
186
+ def reset_evolvables
187
+ @evolvables_by_name = {}
188
+ end
189
+ end
190
+ end
@@ -1,10 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
5
+ # The CountGene class handles the dynamic count of genes in evolvable instances.
6
+ # When a gene is defined with a range for `count:` (e.g., `count: 2..8`), a CountGene
7
+ # is created to manage this count, allowing the number of genes to change over
8
+ # successive generations.
9
+ #
10
+ # @example Define a melody with a variable number of notes (4 to 16)
11
+ # class Melody
12
+ # include Evolvable
13
+ #
14
+ # gene :notes, type: NoteGene, count: 4..16
15
+ #
16
+ # # The actual number of notes can change during evolution
17
+ # def play
18
+ # puts "Playing melody with #{notes.count} notes"
19
+ # end
20
+ # end
21
+ #
4
22
  class CountGene
5
23
  include Gene
6
24
 
7
25
  class << self
26
+ #
27
+ # Combines two count genes to produce a new count gene.
28
+ # The combination strategy is randomly selected from LAMBDAS.
29
+ #
30
+ # @param gene_a [CountGene] The first count gene
31
+ # @param gene_b [CountGene] The second count gene
32
+ # @return [CountGene] A new count gene with a combined count value
33
+ #
8
34
  def combine(gene_a, gene_b)
9
35
  min = gene_a.min_count
10
36
  max = gene_a.max_count
@@ -12,29 +38,68 @@ module Evolvable
12
38
  new(range: gene_a.range, count: count)
13
39
  end
14
40
 
41
+ #
42
+ # Available strategies for combining count genes.
43
+ # These lambdas determine how two count genes are merged during evolution.
44
+ #
45
+ # 1. Random selection with slight mutation (-1, 0, or +1)
46
+ # 2. Average of the two counts
47
+ #
15
48
  LAMBDAS = [->(a, b) { [a, b].sample.count + rand(-1..1) },
16
49
  ->(a, b) { a.count + b.count / 2 }].freeze
17
50
 
51
+ #
52
+ # Selects a random combination strategy from LAMBDAS.
53
+ #
54
+ # @return [Proc] A lambda function for combining two count genes
55
+ #
18
56
  def combination
19
57
  LAMBDAS.sample
20
58
  end
21
59
  end
22
60
 
61
+ #
62
+ # Initializes a new CountGene instance.
63
+ #
64
+ # @param range [Range] The valid range for the count value
65
+ # @param count [Integer, nil] Optional initial count value, randomly selected from range if nil
66
+ #
23
67
  def initialize(range:, count: nil)
24
68
  @range = range
25
69
  @count = count
26
70
  end
27
71
 
72
+ #
73
+ # The valid range for the count value.
74
+ #
75
+ # @return [Range] The range of possible count values
76
+ #
28
77
  attr_reader :range
29
78
 
79
+ #
80
+ # Returns the current count value.
81
+ # If not already set, randomly selects a value from the range.
82
+ #
83
+ # @return [Integer] The number of genes to create
84
+ #
30
85
  def count
31
86
  @count ||= rand(@range)
32
87
  end
33
88
 
89
+ #
90
+ # The minimum allowed count value.
91
+ #
92
+ # @return [Integer] The minimum value from the range
93
+ #
34
94
  def min_count
35
95
  @range.min
36
96
  end
37
97
 
98
+ #
99
+ # The maximum allowed count value.
100
+ #
101
+ # @return [Integer] The maximum value from the range
102
+ #
38
103
  def max_count
39
104
  @range.max
40
105
  end
@@ -12,18 +12,11 @@ module Evolvable
12
12
  end
13
13
 
14
14
  def evaluate(evolvable)
15
- -(evolvable.value - value).abs
15
+ -(evolvable.fitness - value).abs
16
16
  end
17
17
 
18
18
  def met?(evolvable)
19
- evolvable.value == value
19
+ evolvable.fitness == value
20
20
  end
21
21
  end
22
-
23
- #
24
- # @deprecated
25
- # Will be removed in 2.0.
26
- # Use {EqualizeGoal} instead
27
- #
28
- class Goal::Equalize < EqualizeGoal; end
29
22
  end