gene 0.0.1 → 0.1.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.
- data/Manifest +46 -0
- data/Rakefile +12 -9
- data/TODO +7 -0
- data/gene.gemspec +34 -0
- data/initializers/functional_extensions.rb +44 -0
- data/initializers/module_extensions.rb +14 -0
- data/initializers/object_extensions.rb +25 -0
- data/initializers/range_extensions.rb +9 -0
- data/initializers/runner.rb +15 -0
- data/initializers/symbol_extensions.rb +11 -0
- data/initializers/unbound_method_extensions.rb +3 -0
- data/lib/aligner.rb +34 -0
- data/lib/calculator.rb +42 -0
- data/lib/cell.rb +41 -0
- data/lib/color.rb +9 -0
- data/lib/dsl.rb +16 -0
- data/lib/gene.rb +67 -3
- data/lib/generator.rb +83 -0
- data/lib/geometry.rb +64 -0
- data/lib/hungarian.rb +205 -0
- data/lib/imagine.rb +22 -0
- data/lib/petri.rb +85 -0
- data/lib/point.rb +1 -0
- data/lib/trait.rb +60 -0
- data/tasks/test.rake +23 -0
- data/test/assets/Nova.jpg +0 -0
- data/test/assets/Rex.jpg +0 -0
- data/test/assets/Squares.jpg +0 -0
- data/test/test_helper.rb +6 -0
- data/test/unit/aligner_test.rb +91 -0
- data/test/unit/calculator_test.rb +100 -0
- data/test/unit/cell_test.rb +64 -0
- data/test/unit/color_test.rb +23 -0
- data/test/unit/dsl_test.rb +45 -0
- data/test/unit/functionals_extensions_test.rb +51 -0
- data/test/unit/gene_test.rb +76 -0
- data/test/unit/generator_test.rb +76 -0
- data/test/unit/geometry_test.rb +57 -0
- data/test/unit/hungarian_test.rb +196 -0
- data/test/unit/imagine_test.rb +54 -0
- data/test/unit/module_extensions_test.rb +40 -0
- data/test/unit/object_extensions_test.rb +34 -0
- data/test/unit/petri_test.rb +87 -0
- data/test/unit/range_extensions_test.rb +29 -0
- data/test/unit/symbol_extensions_test.rb +18 -0
- data/test/unit/trait_test.rb +97 -0
- data/test/unit/unbound_method_extensions_test.rb +11 -0
- metadata +118 -30
- data/History.txt +0 -6
- data/Manifest.txt +0 -7
- data/README.txt +0 -48
- data/bin/gene +0 -3
- data/test/test_gene.rb +0 -8
data/Manifest
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
Manifest
|
2
|
+
Rakefile
|
3
|
+
TODO
|
4
|
+
initializers/functional_extensions.rb
|
5
|
+
initializers/module_extensions.rb
|
6
|
+
initializers/object_extensions.rb
|
7
|
+
initializers/range_extensions.rb
|
8
|
+
initializers/runner.rb
|
9
|
+
initializers/symbol_extensions.rb
|
10
|
+
initializers/unbound_method_extensions.rb
|
11
|
+
lib/aligner.rb
|
12
|
+
lib/calculator.rb
|
13
|
+
lib/cell.rb
|
14
|
+
lib/color.rb
|
15
|
+
lib/dsl.rb
|
16
|
+
lib/gene.rb
|
17
|
+
lib/generator.rb
|
18
|
+
lib/geometry.rb
|
19
|
+
lib/hungarian.rb
|
20
|
+
lib/imagine.rb
|
21
|
+
lib/petri.rb
|
22
|
+
lib/point.rb
|
23
|
+
lib/trait.rb
|
24
|
+
tasks/test.rake
|
25
|
+
test/assets/Nova.jpg
|
26
|
+
test/assets/Rex.jpg
|
27
|
+
test/assets/Squares.jpg
|
28
|
+
test/test_helper.rb
|
29
|
+
test/unit/aligner_test.rb
|
30
|
+
test/unit/calculator_test.rb
|
31
|
+
test/unit/cell_test.rb
|
32
|
+
test/unit/color_test.rb
|
33
|
+
test/unit/dsl_test.rb
|
34
|
+
test/unit/functionals_extensions_test.rb
|
35
|
+
test/unit/gene_test.rb
|
36
|
+
test/unit/generator_test.rb
|
37
|
+
test/unit/geometry_test.rb
|
38
|
+
test/unit/hungarian_test.rb
|
39
|
+
test/unit/imagine_test.rb
|
40
|
+
test/unit/module_extensions_test.rb
|
41
|
+
test/unit/object_extensions_test.rb
|
42
|
+
test/unit/petri_test.rb
|
43
|
+
test/unit/range_extensions_test.rb
|
44
|
+
test/unit/symbol_extensions_test.rb
|
45
|
+
test/unit/trait_test.rb
|
46
|
+
test/unit/unbound_method_extensions_test.rb
|
data/Rakefile
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
-
|
1
|
+
require "rubygems"
|
2
|
+
require "rake"
|
3
|
+
require "echoe"
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
Echoe.new("gene", "0.1.0") do |config|
|
6
|
+
config.description = "Sample genetic program in Ruby"
|
7
|
+
config.url = "http://github.com/evansenter/gene"
|
8
|
+
config.author = "Evan Senter"
|
9
|
+
config.email = "evansenter@gmail.com"
|
10
|
+
config.ignore_pattern = ["tmp/*", "script/*"]
|
11
|
+
config.dependencies = ["rmagick"]
|
12
|
+
config.development_dependencies = []
|
10
13
|
end
|
11
14
|
|
12
|
-
#
|
15
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |task| load task }
|
data/TODO
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
* Things aren't working because the image is coming out white! (Cell#draw_image)
|
2
|
+
* Reflesh test coverage in Generator
|
3
|
+
* Think about using the garbage collector...
|
4
|
+
|
5
|
+
require "initializers/runner.rb"
|
6
|
+
petri = Petri.new("./test/assets/Squares.jpg")
|
7
|
+
petri.evolve while petri.dish.first.fitness < 0.7
|
data/gene.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{gene}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Evan Senter"]
|
9
|
+
s.date = %q{2010-07-20}
|
10
|
+
s.description = %q{Sample genetic program in Ruby}
|
11
|
+
s.email = %q{evansenter@gmail.com}
|
12
|
+
s.extra_rdoc_files = ["TODO", "lib/aligner.rb", "lib/calculator.rb", "lib/cell.rb", "lib/color.rb", "lib/dsl.rb", "lib/gene.rb", "lib/generator.rb", "lib/geometry.rb", "lib/hungarian.rb", "lib/imagine.rb", "lib/petri.rb", "lib/point.rb", "lib/trait.rb", "tasks/test.rake"]
|
13
|
+
s.files = ["Manifest", "Rakefile", "TODO", "initializers/functional_extensions.rb", "initializers/module_extensions.rb", "initializers/object_extensions.rb", "initializers/range_extensions.rb", "initializers/runner.rb", "initializers/symbol_extensions.rb", "initializers/unbound_method_extensions.rb", "lib/aligner.rb", "lib/calculator.rb", "lib/cell.rb", "lib/color.rb", "lib/dsl.rb", "lib/gene.rb", "lib/generator.rb", "lib/geometry.rb", "lib/hungarian.rb", "lib/imagine.rb", "lib/petri.rb", "lib/point.rb", "lib/trait.rb", "tasks/test.rake", "test/assets/Nova.jpg", "test/assets/Rex.jpg", "test/assets/Squares.jpg", "test/test_helper.rb", "test/unit/aligner_test.rb", "test/unit/calculator_test.rb", "test/unit/cell_test.rb", "test/unit/color_test.rb", "test/unit/dsl_test.rb", "test/unit/functionals_extensions_test.rb", "test/unit/gene_test.rb", "test/unit/generator_test.rb", "test/unit/geometry_test.rb", "test/unit/hungarian_test.rb", "test/unit/imagine_test.rb", "test/unit/module_extensions_test.rb", "test/unit/object_extensions_test.rb", "test/unit/petri_test.rb", "test/unit/range_extensions_test.rb", "test/unit/symbol_extensions_test.rb", "test/unit/trait_test.rb", "test/unit/unbound_method_extensions_test.rb", "gene.gemspec"]
|
14
|
+
s.homepage = %q{http://github.com/evansenter/gene}
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Gene"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{gene}
|
18
|
+
s.rubygems_version = %q{1.3.7}
|
19
|
+
s.summary = %q{Sample genetic program in Ruby}
|
20
|
+
s.test_files = ["test/test_helper.rb", "test/unit/aligner_test.rb", "test/unit/calculator_test.rb", "test/unit/cell_test.rb", "test/unit/color_test.rb", "test/unit/dsl_test.rb", "test/unit/functionals_extensions_test.rb", "test/unit/gene_test.rb", "test/unit/generator_test.rb", "test/unit/geometry_test.rb", "test/unit/hungarian_test.rb", "test/unit/imagine_test.rb", "test/unit/module_extensions_test.rb", "test/unit/object_extensions_test.rb", "test/unit/petri_test.rb", "test/unit/range_extensions_test.rb", "test/unit/symbol_extensions_test.rb", "test/unit/trait_test.rb", "test/unit/unbound_method_extensions_test.rb"]
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
24
|
+
s.specification_version = 3
|
25
|
+
|
26
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
27
|
+
s.add_runtime_dependency(%q<rmagick>, [">= 0"])
|
28
|
+
else
|
29
|
+
s.add_dependency(%q<rmagick>, [">= 0"])
|
30
|
+
end
|
31
|
+
else
|
32
|
+
s.add_dependency(%q<rmagick>, [">= 0"])
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module FunctionalExtensions
|
2
|
+
def apply(enum)
|
3
|
+
enum.map &self
|
4
|
+
end
|
5
|
+
alias | apply
|
6
|
+
|
7
|
+
def reduce(enum)
|
8
|
+
enum.inject &self
|
9
|
+
end
|
10
|
+
alias <= reduce
|
11
|
+
|
12
|
+
def compose(function)
|
13
|
+
if self.respond_to?(:arity) && self.arity == 1
|
14
|
+
lambda { |*args| self[function[*args]] }
|
15
|
+
else
|
16
|
+
lambda { |*args| self[*function[*args]] }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
alias * compose
|
20
|
+
|
21
|
+
def apply_head(*first)
|
22
|
+
lambda { |*rest| self[*first.concat(rest)] }
|
23
|
+
end
|
24
|
+
alias >> apply_head
|
25
|
+
|
26
|
+
def apply_tail(*last)
|
27
|
+
lambda { |*rest| self[*rest.concat(last)] }
|
28
|
+
end
|
29
|
+
alias << apply_tail
|
30
|
+
|
31
|
+
def memoize
|
32
|
+
cache = {}
|
33
|
+
lambda do |*args|
|
34
|
+
unless cache.has_key?(args)
|
35
|
+
cache[args] = self[*args]
|
36
|
+
end
|
37
|
+
cache[args]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias +@ memoize
|
41
|
+
end
|
42
|
+
|
43
|
+
class Method; include FunctionalExtensions; end
|
44
|
+
class Proc; include FunctionalExtensions; end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Module
|
2
|
+
alias [] instance_method
|
3
|
+
|
4
|
+
def []=(symbol, function)
|
5
|
+
self.instance_eval { define_method(symbol, function) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def alias_method_chain(target, feature)
|
9
|
+
method_name, extension = target.to_s.match(/(\w+)(\W?)/).to_a[1..-1]
|
10
|
+
|
11
|
+
alias_method "#{method_name}_without_#{feature}#{extension}", target
|
12
|
+
alias_method target, "#{method_name}_with_#{feature}#{extension}"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Object
|
2
|
+
def returning(value)
|
3
|
+
yield value
|
4
|
+
value
|
5
|
+
end
|
6
|
+
|
7
|
+
def send_if(condition, method, *arguments)
|
8
|
+
block = arguments.pop if arguments.last.is_a?(Proc) && arguments.length == 1
|
9
|
+
if condition
|
10
|
+
block ? eval("self.#{method} &block") : self.send(method, *arguments)
|
11
|
+
else
|
12
|
+
self
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def assert_at_least(number_required, number_present, message = nil)
|
17
|
+
unless number_present >= number_required
|
18
|
+
raise(ArgumentError, message || "You must provide at least #{number_required} objects (#{number_present} #{number_present == 1 ? 'was' : 'were'} provided)")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def class_metaclass
|
23
|
+
class << self.class; self; end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Pull in necessary libraries.
|
2
|
+
require "RMagick"
|
3
|
+
|
4
|
+
# Get all source files except this one, pulling initializers before the lib_files.
|
5
|
+
initializers = Dir.glob(File.join(File.dirname(__FILE__), "**", "*.rb")) - [__FILE__]
|
6
|
+
lib_files = Dir.glob(File.join(File.dirname(__FILE__), "..", "lib", "**", "*.rb"))
|
7
|
+
|
8
|
+
# Set dynamic autoload on all of them based on their filename.
|
9
|
+
constants = (initializers + lib_files).inject([]) do |constants, file_path|
|
10
|
+
constant = File.basename(file_path, ".rb").split(/_/).map(&:capitalize).join.to_sym
|
11
|
+
autoload(constant, file_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Pull in the initializer files!
|
15
|
+
initializers.map(&method(:require))
|
data/lib/aligner.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Aligner
|
2
|
+
def align_crossover
|
3
|
+
optimal_alignment = Hungarian.new(crossover_map).solve
|
4
|
+
|
5
|
+
returning({ :cell_1 => [], :cell_2 => [] }) do |alignment_hash|
|
6
|
+
optimal_alignment.each do |tuple|
|
7
|
+
alignment_hash[:cell_1] << tuple.first
|
8
|
+
alignment_hash[:cell_2] << tuple.last
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def crossover_map
|
16
|
+
@cells.first.genes.map do |gene_1|
|
17
|
+
@cells.last.genes.map do |gene_2|
|
18
|
+
distance_between(middle_point_of(gene_1), middle_point_of(gene_2))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def distance_between(point_1, point_2)
|
24
|
+
square = lambda { |value| value ** 2 }
|
25
|
+
Math.sqrt(square[point_2.x - point_1.x] + square[point_2.y - point_1.y])
|
26
|
+
end
|
27
|
+
|
28
|
+
def middle_point_of(gene)
|
29
|
+
sum = lambda { |a, b| a + b }
|
30
|
+
average = lambda { |axis| (sum <= gene.polygon.points.map(&axis).map(&:value)) / gene.polygon.num_points.to_f }
|
31
|
+
|
32
|
+
Point.new(average[:x], average[:y])
|
33
|
+
end
|
34
|
+
end
|
data/lib/calculator.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Calculator
|
2
|
+
def self.included(base)
|
3
|
+
base.extend ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def generate_value(max)
|
8
|
+
case max
|
9
|
+
when 0: raise(ArgumentError, "Trying to call Calculator.generate_value(0), which is not supported at this time due to ambiguity of intention")
|
10
|
+
when Float: rand(0)
|
11
|
+
when Fixnum: rand(max)
|
12
|
+
else raise(ArgumentError, "Can't generate values of type #{max.class} in Calculator.generate_value(max)")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_normal_random_variable
|
17
|
+
x = y = sum = 0
|
18
|
+
loop do
|
19
|
+
x = get_uniform_random_variable
|
20
|
+
y = get_uniform_random_variable
|
21
|
+
break if (sum = (x ** 2) + (y ** 2)) < 1
|
22
|
+
end
|
23
|
+
rand(2).zero? ? convert(x, sum) : convert(y, sum)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_uniform_random_variable
|
27
|
+
rand(0) * random_sign_change
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def random_sign_change
|
33
|
+
rand(2).zero? ? 1 : -1
|
34
|
+
end
|
35
|
+
|
36
|
+
def convert(variable, sum)
|
37
|
+
# http://en.wikipedia.org/wiki/Marsaglia_polar_method
|
38
|
+
return 0 if sum.zero?
|
39
|
+
variable * Math.sqrt(-2 * Math.log(sum) / sum)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/cell.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
class Cell < Dsl
|
2
|
+
attr_accessor :fitness
|
3
|
+
attr_reader :genes, :image
|
4
|
+
|
5
|
+
def genes_by_alpha
|
6
|
+
genes.sort { |gene_1, gene_2| gene_2.color.a.value <=> gene_1.color.a.value }
|
7
|
+
end
|
8
|
+
|
9
|
+
def genes_from_alignment_map(alignment)
|
10
|
+
alignment.map { |index| genes[index] }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def finish_init
|
16
|
+
fill_out_genes
|
17
|
+
draw_image
|
18
|
+
end
|
19
|
+
|
20
|
+
def fill_out_genes
|
21
|
+
@genes = num_genes.times.map { |index| (@genes ||= [])[index] || Gene.new }
|
22
|
+
end
|
23
|
+
|
24
|
+
def draw_image
|
25
|
+
@image = Magick::Image.new(image_dimensions.x, image_dimensions.y)
|
26
|
+
pen = Magick::Draw.new
|
27
|
+
|
28
|
+
genes_by_alpha.each do |gene|
|
29
|
+
pen.fill(gene.color.rgba_format)
|
30
|
+
pen.polygon(*gene.hulled_sequence)
|
31
|
+
pen.draw(@image)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(name, *args, &block)
|
36
|
+
case name.to_s
|
37
|
+
when /^gene_(\d+)$/: (@genes ||= [])[$1.to_i] = Gene.new(&block)
|
38
|
+
else super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/color.rb
ADDED
data/lib/dsl.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Dsl
|
2
|
+
def initialize(*args, &block)
|
3
|
+
if block_given?
|
4
|
+
@_original_self = block.binding.eval("self")
|
5
|
+
instance_eval(&block)
|
6
|
+
remove_instance_variable :@_original_self
|
7
|
+
end
|
8
|
+
finish_init if defined? :finish_init
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(name, *args, &block)
|
12
|
+
return Petri.send(name) if Petri.respond_to?(name)
|
13
|
+
|
14
|
+
@_original_self ? @_original_self.send(name, *args, &block) : super
|
15
|
+
end
|
16
|
+
end
|
data/lib/gene.rb
CHANGED
@@ -1,3 +1,67 @@
|
|
1
|
-
class Gene
|
2
|
-
|
3
|
-
|
1
|
+
class Gene < Dsl
|
2
|
+
include Geometry
|
3
|
+
|
4
|
+
attr_reader :polygon, :color
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def finish_init
|
9
|
+
fill_out_polygon
|
10
|
+
fill_out_color
|
11
|
+
initialize_singleton_methods_for_polygon_and_color
|
12
|
+
end
|
13
|
+
|
14
|
+
def fill_out_polygon
|
15
|
+
@polygon ||= []
|
16
|
+
@polygon = num_points.times.map do |index|
|
17
|
+
if polygon[index]
|
18
|
+
returning(polygon[index]) do |point|
|
19
|
+
point.x ||= Trait.new(0...image_dimensions.x)
|
20
|
+
point.y ||= Trait.new(0...image_dimensions.y)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
Point.new(
|
24
|
+
Trait.new(0...image_dimensions.x),
|
25
|
+
Trait.new(0...image_dimensions.y)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def fill_out_color
|
32
|
+
@color ||= Color.new
|
33
|
+
color.each_pair { |channel, trait| color.send(:"#{channel}=", Trait.new(0.0..1.0)) unless trait }
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize_singleton_methods_for_polygon_and_color
|
37
|
+
:points[polygon] = lambda { self }
|
38
|
+
:num_points[polygon] = lambda { size }
|
39
|
+
end
|
40
|
+
|
41
|
+
def point_at(index, x, y)
|
42
|
+
point = (@polygon ||= [])[index] ||= Point.new
|
43
|
+
|
44
|
+
point.x = Trait.new(0...image_dimensions.x) { set_value x }
|
45
|
+
point.y = Trait.new(0...image_dimensions.y) { set_value y }
|
46
|
+
end
|
47
|
+
|
48
|
+
def point_from(index, name, &block)
|
49
|
+
point = (@polygon ||= [])[index] ||= Point.new
|
50
|
+
|
51
|
+
point.send(:"#{name}=", Trait.new(0...image_dimensions.send(:"#{name}"), &block))
|
52
|
+
end
|
53
|
+
|
54
|
+
def color_from(channel, &block)
|
55
|
+
@color ||= Color.new
|
56
|
+
color.send(:"#{channel}=", Trait.new(0.0..1.0, &block))
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing(name, *args, &block)
|
60
|
+
case name.to_s
|
61
|
+
when /^point_(\d+)$/: point_at($1.to_i, *args)
|
62
|
+
when /^point_(\d+)_(\w)$/: point_from($1.to_i, $2, &block)
|
63
|
+
when /^trait_(\w)$/: color_from($1, &block)
|
64
|
+
else super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|