answer-factory 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +21 -0
- data/Rakefile +29 -0
- data/Thorfile +79 -0
- data/VERSION +1 -0
- data/_spikes/old_vs_new_dominated_by?.rb +45 -0
- data/config/database.yml +9 -0
- data/lib/answer-factory.rb +14 -0
- data/lib/answers/answer.rb +126 -0
- data/lib/answers/batch.rb +49 -0
- data/lib/factories/factory.rb +53 -0
- data/lib/factories/workstation.rb +33 -0
- data/lib/operators/basic_operators.rb +240 -0
- data/lib/operators/evaluators.rb +113 -0
- data/lib/operators/samplers_and_selectors.rb +131 -0
- data/pkg/nudgegp-0.0.1.gem +0 -0
- data/readme.md +29 -0
- data/spec/answer_spec.rb +412 -0
- data/spec/batch_spec.rb +98 -0
- data/spec/config_spec.rb +94 -0
- data/spec/factories/factory_spec.rb +86 -0
- data/spec/factories/workstation_spec.rb +139 -0
- data/spec/operators/any_one_sampler_spec.rb +39 -0
- data/spec/operators/dominated_quantile_spec.rb +111 -0
- data/spec/operators/duplicate_genomes_spec.rb +35 -0
- data/spec/operators/evaluators/program_point_evaluator_spec.rb +43 -0
- data/spec/operators/evaluators/test_case_evaluator_spec.rb +129 -0
- data/spec/operators/infrastructure_spec.rb +45 -0
- data/spec/operators/most_dominated_subset_spec.rb +47 -0
- data/spec/operators/nondominated_subset_spec.rb +103 -0
- data/spec/operators/pointCrossover_spec.rb +60 -0
- data/spec/operators/pointDeletion_spec.rb +62 -0
- data/spec/operators/pointMutation_spec.rb +77 -0
- data/spec/operators/random_guess_spec.rb +77 -0
- data/spec/operators/resample_and_clone_spec.rb +60 -0
- data/spec/operators/resample_values_spec.rb +135 -0
- data/spec/operators/uniformBackboneCrossover_spec.rb +67 -0
- data/spec/spec_helper.rb +14 -0
- metadata +201 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2009 William Tozier
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |gemspec|
|
6
|
+
gemspec.name = "answer-factory"
|
7
|
+
gemspec.summary = "Genetic Programming in the Nudge language"
|
8
|
+
gemspec.description = "The pragmaticgp gem provides a simple framework for building, running and managing genetic programming experiments which automatically discover algorithms and equations to solve user-defined problems."
|
9
|
+
gemspec.email = "bill@vagueinnovation.com"
|
10
|
+
gemspec.homepage = "http://github.com/Vaguery/PragGP"
|
11
|
+
gemspec.authors = ["Bill Tozier", "Trek Glowacki", "Jesse Sielaff"]
|
12
|
+
|
13
|
+
gemspec.required_ruby_version = '>= 1.9.1'
|
14
|
+
|
15
|
+
gemspec.add_dependency('nudge', '>= 0.2')
|
16
|
+
gemspec.add_dependency('thor', '>= 0.13')
|
17
|
+
gemspec.add_dependency('couchrest', '>= 0.33')
|
18
|
+
gemspec.add_dependency('fakeweb', '>= 0.33')
|
19
|
+
gemspec.add_dependency('sinatra', '>= 0.9.4')
|
20
|
+
gemspec.add_dependency('activesupport', '>= 2.3.5')
|
21
|
+
|
22
|
+
#files
|
23
|
+
gemspec.files.exclude('_spikes/**')
|
24
|
+
end
|
25
|
+
|
26
|
+
Jeweler::GemcutterTasks.new
|
27
|
+
rescue LoadError
|
28
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
29
|
+
end
|
data/Thorfile
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'lib/answer-factory'
|
3
|
+
|
4
|
+
class Extend_Nudge < Thor::Group
|
5
|
+
include Thor::Actions
|
6
|
+
|
7
|
+
# Define arguments and options
|
8
|
+
argument :project_name
|
9
|
+
class_option :test_framework, :default => :rspec
|
10
|
+
desc "Creates a new project folder structure for Nudge types, instructions and specs"
|
11
|
+
|
12
|
+
|
13
|
+
def self.source_root
|
14
|
+
File.dirname(__FILE__)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_project_folder
|
18
|
+
dirname = "#{Extend_Nudge.source_root}/#{project_name}"
|
19
|
+
puts dirname
|
20
|
+
if Dir.exist?(dirname) then
|
21
|
+
puts "project directory 'dirname' already exists"
|
22
|
+
else
|
23
|
+
empty_directory(dirname)
|
24
|
+
empty_directory("#{dirname}/lib")
|
25
|
+
empty_directory("#{dirname}/lib/instructions")
|
26
|
+
empty_directory("#{dirname}/lib/interpreter/types")
|
27
|
+
empty_directory("#{dirname}/spec")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
class New_Nudge_Type < Thor::Group
|
34
|
+
include Thor::Actions
|
35
|
+
|
36
|
+
# Define arguments and options
|
37
|
+
argument :typename_root
|
38
|
+
class_option :test_framework, :default => :rspec
|
39
|
+
desc "Creates a new NudgeType class definition file, typical instructions, and rspec files"
|
40
|
+
|
41
|
+
|
42
|
+
def self.source_root
|
43
|
+
File.dirname(__FILE__)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.type_name(string)
|
47
|
+
string.camelize + "Type"
|
48
|
+
end
|
49
|
+
|
50
|
+
def nudge_gem_path
|
51
|
+
Nudge.gem_root
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_lib_file
|
55
|
+
@camelcased_type_name = New_Nudge_Type.type_name(typename_root)
|
56
|
+
filename = "#{@camelcased_type_name}.rb"
|
57
|
+
template("#{nudge_gem_path}/templates/nudge_type_class.erb", "#{New_Nudge_Type.source_root}/lib/types/#{filename}")
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_lib_spec
|
61
|
+
@camelcased_type_name = New_Nudge_Type.type_name(typename_root)
|
62
|
+
filename = "#{@camelcased_type_name}_spec.rb"
|
63
|
+
template("#{nudge_gem_path}/templates/nudge_type_spec.erb", "#{New_Nudge_Type.source_root}/spec/#{filename}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_instructions
|
67
|
+
suite = ["define", "equal_q", "duplicate", "flush", "pop",
|
68
|
+
"random", "rotate", "shove", "swap", "yank", "yankdup"]
|
69
|
+
|
70
|
+
suite.each do |inst|
|
71
|
+
@core = "#{typename_root}_#{inst}"
|
72
|
+
filename = "#{@core}.rb"
|
73
|
+
@instname = "#{@core.camelize}Instruction"
|
74
|
+
@type = typename_root
|
75
|
+
@camelized_type = New_Nudge_Type.type_name(typename_root)
|
76
|
+
template("#{nudge_gem_path}/templates/nudge_#{inst}_instruction.erb", "#{New_Nudge_Type.source_root}/lib/instructions/#{filename}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'nudge'
|
2
|
+
require '../lib/answer-factory'
|
3
|
+
include Nudge
|
4
|
+
include NudgeGP
|
5
|
+
|
6
|
+
|
7
|
+
d1 = Answer.new("")
|
8
|
+
d2 = Answer.new("")
|
9
|
+
|
10
|
+
t1 = Time.now
|
11
|
+
100000.times do
|
12
|
+
d1.scores = {"a" => rand(20)-10, "b" => rand(20)-10,"c" => rand(20)-10,"d" => rand(20)-10,"e" => rand(20)-10,"e1" => rand(20)-10,"e2" => rand(20)-10,"e3" => rand(20)-10,"e4" => rand(20)-10,"e5" => rand(20)-10,"e6" => rand(20)-10,"edfsgdf" => rand(20)-10,"se" => rand(20)-10}
|
13
|
+
d2.scores = {"a" => rand(20)-10, "b" => rand(20)-10,"c" => rand(20)-10,"d" => rand(20)-10,"e" => rand(20)-10,"e1" => rand(20)-10,"e2" => rand(20)-10,"e3" => rand(20)-10,"e4" => rand(20)-10,"e5" => rand(20)-10,"e6" => rand(20)-10,"edfsgdf" => rand(20)-10,"se" => rand(20)-10}
|
14
|
+
d1.dominated_by?(d2)
|
15
|
+
end
|
16
|
+
puts "#{Time.now - t1} sec from the new one"
|
17
|
+
|
18
|
+
|
19
|
+
class Answer
|
20
|
+
def dominated_by?(other, template = self.known_criteria)
|
21
|
+
return false unless (known_criteria == other.known_criteria)
|
22
|
+
|
23
|
+
noWorse = true
|
24
|
+
somewhatBetter = false
|
25
|
+
template.each do |score|
|
26
|
+
if self.scores[score] && other.scores[score]
|
27
|
+
noWorse &&= (self.scores[score] >= other.scores[score])
|
28
|
+
somewhatBetter ||= (self.scores[score] > other.scores[score])
|
29
|
+
else
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return noWorse && somewhatBetter
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
t1 = Time.now
|
39
|
+
100000.times do
|
40
|
+
d1.scores = {"a" => rand(20)-10, "b" => rand(20)-10,"c" => rand(20)-10,"d" => rand(20)-10,"e" => rand(20)-10,"e1" => rand(20)-10,"e2" => rand(20)-10,"e3" => rand(20)-10,"e4" => rand(20)-10,"e5" => rand(20)-10,"e6" => rand(20)-10,"edfsgdf" => rand(20)-10,"se" => rand(20)-10}
|
41
|
+
d2.scores = {"a" => rand(20)-10, "b" => rand(20)-10,"c" => rand(20)-10,"d" => rand(20)-10,"e" => rand(20)-10,"e1" => rand(20)-10,"e2" => rand(20)-10,"e3" => rand(20)-10,"e4" => rand(20)-10,"e5" => rand(20)-10,"e6" => rand(20)-10,"edfsgdf" => rand(20)-10,"se" => rand(20)-10}
|
42
|
+
d1.dominated_by?(d2)
|
43
|
+
end
|
44
|
+
puts "#{Time.now - t1} sec from the old one"
|
45
|
+
|
data/config/database.yml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# realy wants to use CouchDB version 0.11 or higher
|
2
|
+
|
3
|
+
# edit this to fit your particular CouchDB configuration
|
4
|
+
main_database:
|
5
|
+
db_root: http://127.0.0.1:5984 # URI of your CouchDB server
|
6
|
+
db_name: my_factory # root database handling your AnswerFactory:
|
7
|
+
tag_filter: _design/routing/_view/by_tag # don't edit this unless you also
|
8
|
+
# change how it's used in the codebase!
|
9
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
$: << File.join(File.dirname(__FILE__), "/../lib")
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'nudge'
|
5
|
+
|
6
|
+
require 'answers/answer'
|
7
|
+
require 'answers/batch'
|
8
|
+
|
9
|
+
require 'operators/basic_operators'
|
10
|
+
require 'operators/samplers_and_selectors'
|
11
|
+
require 'operators/evaluators'
|
12
|
+
|
13
|
+
require 'factories/factory'
|
14
|
+
require 'factories/workstation'
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module NudgeGP
|
4
|
+
class Answer
|
5
|
+
attr_accessor :scores, :tags
|
6
|
+
attr_reader :draft_blueprint, :program, :timestamp, :ancestors
|
7
|
+
attr_reader :initialization_options, :progress
|
8
|
+
|
9
|
+
|
10
|
+
def initialize(blueprint, options = {})
|
11
|
+
raise ArgumentError unless
|
12
|
+
blueprint.kind_of?(String) || blueprint.kind_of?(NudgeProgram)
|
13
|
+
build_from_blueprint!(blueprint)
|
14
|
+
|
15
|
+
@scores = Hash.new do |hash, key|
|
16
|
+
raise ArgumentError, "scores must use symbols as keys" unless key.kind_of?(Symbol)
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
@timestamp = Time.now
|
20
|
+
@initialization_options = options
|
21
|
+
@progress = options[:progress] || 0
|
22
|
+
@ancestors = options[:ancestors] || []
|
23
|
+
@tags = Set.new(options[:tags]) || Set.new
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def build_from_blueprint!(blueprint)
|
28
|
+
if blueprint.kind_of?(String)
|
29
|
+
@draft_blueprint = blueprint
|
30
|
+
@program = NudgeProgram.new(blueprint)
|
31
|
+
else
|
32
|
+
@program = blueprint
|
33
|
+
@draft_blueprint = @program.blueprint
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def blueprint
|
39
|
+
@program.blueprint
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def parses?
|
44
|
+
@program.parses?
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def known_criteria
|
49
|
+
@scores.keys.sort
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def score_vector(ordering = known_criteria)
|
54
|
+
ordering.collect {|k| @scores[k]}
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def dominated_by?(other_answer, comparison_criteria = self.known_criteria)
|
59
|
+
|
60
|
+
return false unless (known_criteria & comparison_criteria) ==
|
61
|
+
(other_answer.known_criteria & comparison_criteria)
|
62
|
+
|
63
|
+
could_be_identical = true
|
64
|
+
|
65
|
+
comparison_criteria.each do |score|
|
66
|
+
return false if (my_score = self.scores[score]) < (other_score = other_answer.scores[score])
|
67
|
+
|
68
|
+
if could_be_identical
|
69
|
+
could_be_identical &&= (my_score == other_score)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
return !could_be_identical
|
74
|
+
rescue NoMethodError
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def points
|
80
|
+
@program.points
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def delete_point_or_clone(which)
|
85
|
+
((1..self.points).include?(which)) ?
|
86
|
+
self.program.delete_point(which) :
|
87
|
+
self.program.deep_copy
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def replace_point_or_clone(which, object)
|
92
|
+
if object.kind_of?(String)
|
93
|
+
prog = NudgeProgram.new(object)
|
94
|
+
if !prog.parses?
|
95
|
+
raise(ArgumentError, "Replacement point cannot be parsed")
|
96
|
+
else
|
97
|
+
new_point = prog.linked_code
|
98
|
+
end
|
99
|
+
elsif object.kind_of?(ProgramPoint)
|
100
|
+
new_point = object
|
101
|
+
else
|
102
|
+
raise(ArgumentError, "Program points cannot be replaced by #{object.class} objects")
|
103
|
+
end
|
104
|
+
|
105
|
+
((1..self.points).include?(which)) ?
|
106
|
+
self.program.replace_point(which, new_point) :
|
107
|
+
self.program.deep_copy
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def add_tag(new_tag)
|
113
|
+
raise ArgumentError, "#{new_tag} is not a Symbol" unless new_tag.kind_of?(Symbol)
|
114
|
+
@tags.add(new_tag)
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def remove_tag(old_tag)
|
119
|
+
@tags.delete(old_tag)
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
def data
|
124
|
+
{'blueprint' => self.blueprint, 'tags' => self.tags, 'scores' => self.scores, 'timestamp' => @timestamp}
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module NudgeGP
|
2
|
+
# A Batch is simply an Array of Answers, with validation for all assignment methods
|
3
|
+
class Batch < Array
|
4
|
+
|
5
|
+
def self.[](*args)
|
6
|
+
raise ArgumentError unless args.inject(true) {|anded, a| anded & a.kind_of?(Answer)}
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def []=(index, obj)
|
12
|
+
raise ArgumentError unless obj.kind_of?(Answer)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def <<(obj)
|
18
|
+
raise ArgumentError unless obj.kind_of?(Answer)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def initialize(*args)
|
24
|
+
raise ArgumentError unless args.inject(true) {|anded, a| anded & a.kind_of?(Answer)}
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def bulk_save!(couchdb_uri)
|
30
|
+
raise ArgumentError, "#{couchdb_uri} is not a String" unless couchdb_uri.kind_of?(String)
|
31
|
+
|
32
|
+
db = CouchRest.database!(couchdb_uri)
|
33
|
+
db.bulk_save(self.data)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def self.load_tagged_answers(couchdb_uri, tag)
|
38
|
+
raise ArgumentError, "#{couchdb_uri} is not a String" unless couchdb_uri.kind_of?(String)
|
39
|
+
raise ArgumentError, "#{tag} is not a String" unless tag.kind_of?(String)
|
40
|
+
db = CouchRest.database(couchdb_uri) # add the view document and key here
|
41
|
+
return Batch.new
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def data
|
46
|
+
self.collect {|i| i.data}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module NudgeGP
|
2
|
+
class Factory
|
3
|
+
require 'open-uri'
|
4
|
+
require 'configatron'
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :instruction_library, :type_library
|
8
|
+
attr_accessor :workstation_names
|
9
|
+
attr_reader :original_options_hash
|
10
|
+
|
11
|
+
|
12
|
+
def initialize(name = "my_factory", options = {})
|
13
|
+
@name = name
|
14
|
+
@original_options_hash = options
|
15
|
+
@instruction_library = options[:instruction_library] || Instruction.all_instructions
|
16
|
+
@type_library = options[:type_library] || NudgeType.all_types
|
17
|
+
@workstation_names = Array.new
|
18
|
+
|
19
|
+
self.configure!
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def couch_available?
|
24
|
+
open(self.configatron.couchdb_uri).status
|
25
|
+
true
|
26
|
+
rescue StandardError
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def configure!
|
32
|
+
self.configure_constants!
|
33
|
+
self.configure_paths!
|
34
|
+
self.configure_databases!
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def configure_constants!
|
39
|
+
self.configatron.factory_name = self.name
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def configure_paths!
|
44
|
+
self.configatron.factory_root = File.expand_path("#{File.dirname(__FILE__)}/../..")
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def configure_databases!
|
49
|
+
self.configatron.configure_from_yaml("#{self.configatron.factory_root}/config/database.yml")
|
50
|
+
self.configatron.couchdb_uri = "#{self.configatron.main_database.db_root}/#{self.name}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module NudgeGP
|
2
|
+
class Workstation
|
3
|
+
attr_reader :name, :capacity, :couchdb_uri, :factory_name
|
4
|
+
attr_accessor :downstream_stations
|
5
|
+
attr_accessor :answers
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(name, options = {})
|
9
|
+
raise ArgumentError, "#{name} is not a Symbol" unless name.kind_of?(Symbol)
|
10
|
+
@name = name
|
11
|
+
@factory_name = options[:factory_name] || 'factory_name'
|
12
|
+
@couchdb_uri = options[:couchdb_uri] || "http://127.0.0.1:5984/#{@factory_name}"
|
13
|
+
@capacity = options[:capacity] || 100
|
14
|
+
@downstream_stations = Array.new
|
15
|
+
@answers = Array.new
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def transfer_answer(which, where)
|
20
|
+
raise ArgumentError, "#{where} is not a Symbol" unless where.kind_of?(Symbol)
|
21
|
+
which.remove_tag(self.name)
|
22
|
+
which.add_tag(where)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def cycle
|
27
|
+
self.receive!
|
28
|
+
self.build!
|
29
|
+
self.ship!
|
30
|
+
self.scrap!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|