smcty 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 066404cac54ab6ea59d05f19655509a73fe25879
4
+ data.tar.gz: 9fb49f6ed6a8d580f4f63d8558ce012b5ebc1fab
5
+ SHA512:
6
+ metadata.gz: 79c2493d00915a80980b58991bd7f13a38fdb5f0fd5ebb79884c29f09cbcb584d359824b01085a5a7488a1a458c5bd5d27659f130cae8cda89751e0d13a9e969
7
+ data.tar.gz: dde05154fcd122b9f13c0907716bfb8f0cebfdcad6b4a8756bcdf8238ab3407deb760266b556bcbb20eabb848fa1504b4bc075ad38ac8c294c05a011abd7e5a3
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'smcty'
4
+ Smcty::Planner.start(ARGV[0])
@@ -0,0 +1 @@
1
+ require 'smcty/planner'
@@ -0,0 +1,26 @@
1
+ module Smcty
2
+ class Allocation
3
+ attr_reader :store, :resource, :amount
4
+
5
+ def initialize(store, resource, amount)
6
+ @store = store
7
+ @resource = resource
8
+ @amount = amount
9
+ end
10
+
11
+ def valid?
12
+ @store.valid?(self)
13
+ end
14
+
15
+ def get
16
+ @store.get(self)
17
+ end
18
+
19
+ def to_hash
20
+ {
21
+ "resource" => @resource.name,
22
+ "amount" => @amount
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,61 @@
1
+ module Smcty
2
+ class Configuration
3
+ attr_reader :created_at, :store, :scheduler
4
+
5
+ def initialize(store)
6
+ @created_at = Time.new
7
+ @store = store
8
+ @factories = {}
9
+ @scheduler = Scheduling.new(self)
10
+ end
11
+
12
+ def register_factory(factory)
13
+ @factories[factory.name] = factory
14
+ end
15
+
16
+ def resource_by_name(resource_name)
17
+ @factories.values.each do |factory|
18
+ resource = factory.resource_by_name(resource_name)
19
+ return resource if resource
20
+ end
21
+ nil
22
+ end
23
+
24
+ def resources
25
+ result = []
26
+ @factories.values.each do |f|
27
+ result += f.resources
28
+ end
29
+ result
30
+ end
31
+
32
+ def factory_for(resource)
33
+ @factories.values.each do |factory|
34
+ found = factory.resource_by_name(resource.name)
35
+ return factory if found
36
+ end
37
+ nil
38
+ end
39
+
40
+ def factories
41
+ @factories.keys.sort
42
+ end
43
+
44
+ def factory(name)
45
+ @factories[name]
46
+ end
47
+
48
+ def to_s
49
+ "Configuration created at #{@created_at}"
50
+ end
51
+
52
+ def to_hash
53
+ {
54
+ "store" => @store.to_hash,
55
+ "factories" => @factories.values.map{|f| f.to_hash},
56
+ "scheduling" => @scheduler.to_hash
57
+ }
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,178 @@
1
+ require 'json'
2
+
3
+ require 'smcty/store'
4
+ require 'smcty/configuration'
5
+ require 'smcty/factory'
6
+ require 'smcty/resource'
7
+
8
+ module Smcty
9
+ class Configurator
10
+ attr_reader :configuration
11
+
12
+ def initialize(config_path)
13
+ @config_path = config_path
14
+ @configuration = Configurator.read_configuration(config_path)
15
+ end
16
+
17
+ def save
18
+ Configurator.write_configuration(@configuration, @config_path)
19
+ end
20
+
21
+ def self.read_configuration(path)
22
+ file = File.read(path)
23
+ data_hash = JSON.parse(file)
24
+
25
+ if (data_hash && data_hash != {})
26
+ store_hash = read_value(data_hash, "store", true)
27
+ store_name = read_value(store_hash, "name", true)
28
+ store_capacity = read_value(store_hash, "capacity", true)
29
+ store = Store.new(store_name, store_capacity)
30
+
31
+ configuration = Configuration.new(store)
32
+
33
+ deferred_dependencies = []
34
+
35
+ factories_list = read_value(data_hash, "factories", true)
36
+ factories_list.each do |factory_hash|
37
+ factory_name = read_value(factory_hash, "name", true)
38
+ factory_capacity = read_value(factory_hash, "capacity", true)
39
+ factory_sequential = read_value(factory_hash, "sequential", false, false)
40
+
41
+ factory = Factory.new(factory_name, factory_capacity, factory_sequential)
42
+ configuration.register_factory(factory)
43
+
44
+ factory_resources = read_value(factory_hash, "resources", true)
45
+ factory_resources.each do |resources_hash|
46
+ resource_name = read_value(resources_hash, "name", true)
47
+ resource_description = read_value(resources_hash, "description", true)
48
+ resource_time = read_value(resources_hash, "time", true)
49
+
50
+ resource = Resource.new(resource_name, resource_description)
51
+ factory.register_resource(resource, resource_time)
52
+
53
+ resource_deps = read_value(resources_hash, "deps", false, [])
54
+ resource_deps.each do |dependency_hash|
55
+ dependency_name = read_value(dependency_hash, "name", true)
56
+ dependency_amount = read_value(dependency_hash, "amount", true)
57
+
58
+ if dep_resource = configuration.resource_by_name(dependency_name)
59
+ resource.register_dependency(dep_resource, dependency_amount)
60
+ else
61
+ deferred_dependencies << {resource: resource,
62
+ dependency_name: dependency_name,
63
+ dependency_amount: dependency_amount
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # post process deferred dependencies
71
+ deferred_dependencies.each do |deferred_dep|
72
+ resource = deferred_dep[:resource]
73
+ dependency_name = deferred_dep[:dependency_name]
74
+ amount = deferred_dep[:dependency_amount]
75
+
76
+ dependent_resource = configuration.resource_by_name(dependency_name)
77
+ unless dependent_resource
78
+ raise "Cannot fullfill dependency from #{resource.name} -> #{dependency_name}"
79
+ end
80
+
81
+ resource.register_dependency(dependent_resource, amount)
82
+ end
83
+
84
+ # post process stock of store
85
+ read_value(store_hash, "stock", false, []).each do |stock_hash|
86
+ resource_name = read_value(stock_hash, "name", true)
87
+ resource_amount = read_value(stock_hash, "amount", true)
88
+
89
+ resource = configuration.resource_by_name(resource_name) || Resource.new(resource_name, "unmanaged")
90
+ store.put(resource, resource_amount)
91
+ end
92
+
93
+ # post process the scheduling information
94
+ scheduling_hash = read_value(data_hash, "scheduling", true)
95
+ read_value(scheduling_hash, "projects", false, []).each do |project_hash|
96
+ project_name = read_value(project_hash, "name", true)
97
+ project = Project.new(project_name)
98
+ read_value(project_hash, "requirements", true).each do |req|
99
+ resource_name = read_value(req, "resource", true)
100
+ amount = read_value(req, "amount", true).to_i
101
+ resource = configuration.resource_by_name(resource_name)
102
+ unless resource
103
+ raise "Cannot register requirement for unknown resource #{resource_name}"
104
+ end
105
+ project.add_requirement(resource, amount)
106
+ end
107
+
108
+ job_dict = {}
109
+ job_dependencies = {}
110
+ jobs = []
111
+
112
+ # first pass to create the single jobs
113
+ read_value(project_hash, "jobs", true).each do |job_hash|
114
+ reference = read_value(job_hash, "id", true)
115
+ resource_name = read_value(job_hash, "resource", true)
116
+ resource = configuration.resource_by_name(resource_name)
117
+ unless resource
118
+ raise "Cannot create job for unknown resource #{resource_name}"
119
+ end
120
+ job = Job.new(resource)
121
+ job_dict[reference] = job
122
+ jobs << job
123
+
124
+ allocation_hash = read_value(job_hash, "allocation", false, nil)
125
+ if allocation_hash
126
+ amount = read_value(allocation_hash, "amount", true).to_i
127
+ allocation = store.allocate(resource, amount)
128
+ job.allocate(allocation) if allocation
129
+ end
130
+
131
+ production_hash = read_value(job_hash, "production", false, nil)
132
+ if production_hash
133
+ start_time = read_value(production_hash, "start_time", true).to_i
134
+ duration = read_value(production_hash, "duration", true).to_i
135
+ production = Production.new(resource, duration, start_time)
136
+ job.produce(production)
137
+ end
138
+
139
+ dependency_list = read_value(job_hash, "dependent_jobs", false, nil)
140
+ job_dependencies[job] = dependency_list if dependency_list
141
+ end
142
+
143
+ # second pass to connect dependent jobs
144
+ job_dependencies.keys.each do |job|
145
+ job_dependencies[job].each do |dep_ref|
146
+ dep_job = job_dict[dep_ref]
147
+ unless dep_job
148
+ raise "job references another job which is not known (#{dep_ref})"
149
+ end
150
+ job.add_dependent(dep_job)
151
+ end
152
+ end
153
+
154
+ configuration.scheduler.load_project(project, jobs)
155
+ end
156
+
157
+ configuration
158
+ end
159
+ end
160
+
161
+ def self.write_configuration(configuration, path)
162
+ File.open(path,"w") do |f|
163
+ f.write(configuration.to_hash.to_json)
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def self.read_value(hash, key, force, default=nil)
170
+ value = hash[key]
171
+ if force && (value == nil)
172
+ raise "The hash #{hash} does not contain the key #{key}"
173
+ end
174
+ value || default
175
+ end
176
+
177
+ end
178
+ end
@@ -0,0 +1,229 @@
1
+ require 'smcty/configurator'
2
+ require 'smcty/output'
3
+ require 'smcty/helpers'
4
+ require 'smcty/production'
5
+ require 'smcty/project'
6
+ require 'smcty/scheduling'
7
+
8
+ module Smcty
9
+ class Console
10
+
11
+ # Initialize the console with the input stream (e.g. $stdin)
12
+ # and a path to smcty configuration file.
13
+ def initialize(input_stream, config_path)
14
+ @in = input_stream
15
+ @configurator = Configurator.new(config_path)
16
+ @configuration = @configurator.configuration
17
+ @productions = {}
18
+
19
+ end
20
+
21
+ def prompt!
22
+ run = true
23
+ while run
24
+ "-> ".display
25
+ run = dispatch(@in.gets.chomp)
26
+ end
27
+ end
28
+
29
+ # Commands
30
+ #
31
+ # quit
32
+ #
33
+ # store -> list the inventory of the store
34
+ # store put #item #amount
35
+ #
36
+ # factory -> list the registered factories
37
+ # factory #name -> list the resources and the production of the named factory
38
+ #
39
+ # resources -> list all managed resources
40
+ #
41
+ # project add #label [#resource:#amount]+
42
+ # project #label
43
+ #
44
+ # produce #resource
45
+ #
46
+ # pick #production-number
47
+ #
48
+ # next
49
+ #
50
+ def dispatch(input)
51
+ continue = true
52
+ commands = input.split(" ")
53
+ case commands[0].downcase
54
+ when "store"
55
+ continue = process_store(commands[1..-1])
56
+ when "factory"
57
+ continue = process_factory(commands[1..-1])
58
+ when "quit"
59
+ continue = process_quit
60
+ when "resources"
61
+ continue = process_resources
62
+ when "project"
63
+ continue = process_projects(commands[1..-1])
64
+ when "produce"
65
+ continue = process_production(commands[1..-1])
66
+ when "pick"
67
+ continue = process_pick(commands[1..-1])
68
+ when "next"
69
+ continue = process_next
70
+ when "save"
71
+ continue = process_save
72
+ when "scheduling"
73
+ continue = process_scheduling
74
+ else
75
+ puts "unknown command: #{input}"
76
+ end
77
+ return continue
78
+ end
79
+
80
+ def process_scheduling
81
+ @configuration.scheduler.print_job_lists
82
+ true
83
+ end
84
+
85
+ def process_next
86
+ puts "DO > #{@configuration.scheduler.next}"
87
+ true
88
+ end
89
+
90
+ def process_projects(commands)
91
+ if commands.size == 1
92
+ puts "print the project"
93
+ elsif commands.size > 2
94
+ unless commands[0] == "add"
95
+ puts "unknow command #{commands[0]} for project is not known"
96
+ return true
97
+ end
98
+ project = Project.new(commands[1])
99
+ requirements = commands[2..-1]
100
+ requirements.each do |r|
101
+ items = r.split(":")
102
+ r_name = items[0]
103
+ amount = items[1].to_i
104
+ resource = @configuration.resource_by_name(r_name)
105
+ unless resource
106
+ puts "the resource #{resource_name} is not known"
107
+ return true
108
+ end
109
+ project.add_requirement(resource, amount)
110
+ end
111
+ @configuration.scheduler.plan_project(project)
112
+ puts "planned the project"
113
+ else
114
+ puts "not enough parameters for project command"
115
+ end
116
+ true
117
+ end
118
+
119
+ def process_pick(commands)
120
+ if commands.size != 1
121
+ puts "please pass the number of production to pick"
122
+ else
123
+ number = commands[0].to_i
124
+ production = @productions[number]
125
+ unless production
126
+ puts "the production number #{commands[0]} is not known"
127
+ return true
128
+ end
129
+ unless production.finished?
130
+ puts "the production is not finished yet"
131
+ return true
132
+ end
133
+ if @configuration.store.free_capacity > 0
134
+ factory = @configuration.factory_for(production.resource)
135
+ factory.pick(production)
136
+ @productions.delete(number)
137
+ @configuration.store.put(production.resource, 1)
138
+ puts "the item was stored"
139
+ else
140
+ puts "no storage left"
141
+ end
142
+ end
143
+ true
144
+ end
145
+
146
+ def process_production(commands)
147
+ if commands.size != 1
148
+ puts "please name the resource to produce"
149
+ else
150
+ resource_name = commands[0]
151
+ resource = @configuration.resource_by_name(resource_name)
152
+ unless resource
153
+ puts "the resource #{resource_name} is not known"
154
+ return true
155
+ end
156
+ factory = @configuration.factory_for(resource)
157
+ allocations = []
158
+ resource.dependent_resources.each do |r|
159
+ allocation = @configuration.store.allocate(r, resource.dependent_resource_amount(r))
160
+ unless allocation
161
+ puts "Not enough of #{r.name} to produce #{resource_name}"
162
+ return true
163
+ end
164
+ allocations << allocation
165
+ end
166
+ production = factory.produce(resource, allocations)
167
+ @productions[production.object_id] = production
168
+ puts "Now producing: #{production.object_id} in factory: #{factory.name}"
169
+ end
170
+ true
171
+ end
172
+
173
+ def process_resources
174
+ list_resources(@configuration.resources)
175
+ true
176
+ end
177
+
178
+ def process_quit
179
+ puts "Storing configuration back to file and exit."
180
+ @configurator.save
181
+ false
182
+ end
183
+
184
+ def process_save
185
+ puts "Storing configuration back to file"
186
+ @configurator.save
187
+ true
188
+ end
189
+
190
+ def process_store(commands)
191
+ if commands.size == 0
192
+ list_inventory(@configuration.store)
193
+ elsif commands.size == 3
194
+ resource = @configuration.resource_by_name(commands[1])
195
+ amount = commands[2].to_i
196
+ if resource && amount > 0
197
+ case commands[0].downcase
198
+ when "put"
199
+ puts "put #{commands[2]} units of #{commands[1]} to store"
200
+ result = @configuration.store.put(resource, amount)
201
+ puts "new stock is: #{@configuration.store.stock(resource)}"
202
+ else
203
+ puts "operator on store was not recognized"
204
+ end
205
+ else
206
+ puts "resource '#{commands[1]}' or amount '#{commands[2]}' not valid"
207
+ end
208
+ else
209
+ puts "command not recognized"
210
+ end
211
+ true
212
+ end
213
+
214
+ def process_factory(commands)
215
+ if commands.size == 0
216
+ list_factories(@configuration.factories)
217
+ else
218
+ factory = @configuration.factory(commands[0])
219
+ if factory
220
+ list_factory(factory)
221
+ else
222
+ puts "The factory '#{commands[0]}' is not registered"
223
+ end
224
+ end
225
+ true
226
+ end
227
+
228
+ end
229
+ end
@@ -0,0 +1,105 @@
1
+ module Smcty
2
+ class Factory
3
+ attr_reader :name, :capacity, :productions, :sequential
4
+
5
+ def initialize(name, capacity, sequential=false)
6
+ @name = name
7
+ @capacity = capacity
8
+ @resources = {}
9
+ @production_times = {}
10
+ @productions = []
11
+ @sequential = sequential
12
+ end
13
+
14
+ def free_capacity
15
+ capacity - @productions.size
16
+ end
17
+
18
+ def register_resource(resource, production_time)
19
+ @production_times[resource] = production_time
20
+ @resources[resource.name] = resource
21
+ end
22
+
23
+ def resources
24
+ @production_times.keys
25
+ end
26
+
27
+ def production_time(resource)
28
+ @production_times[resource]
29
+ end
30
+
31
+ def resource_by_name(name)
32
+ @resources[name]
33
+ end
34
+
35
+ def produce(resource, allocations=[])
36
+ unless free_capacity > 0
37
+ raise "The factory has no capacity for further production"
38
+ end
39
+
40
+ unless Factory.check_preconditions(resource, allocations)
41
+ raise "The preconditions for this production aren't met"
42
+ end
43
+
44
+ time = @production_times[resource]
45
+ unless time
46
+ raise "The resource #{resource.name} is not registered with this factory"
47
+ end
48
+
49
+ # in case of a sequential production the time must be extended
50
+ # find latest end time of a preceeding production
51
+ if sequential && latest = @productions.sort{|x,y| y.end_time <=> x.end_time}.first
52
+ time += (latest.end_time - Time.now).to_i
53
+ end
54
+
55
+ allocations.each{ |a| a.get }
56
+ production = Production.new(resource, time)
57
+ @productions << production
58
+ production
59
+ end
60
+
61
+ def pick(production)
62
+ if production && production.finished?
63
+ @productions.delete(production)
64
+ end
65
+ end
66
+
67
+ def to_s
68
+ "Factory '#{@name}' with capacity of #{@capacity} is responsible for #{@resources.keys.join(", ")}"
69
+ end
70
+
71
+ def to_hash
72
+ {
73
+ "name" => @name,
74
+ "capacity" => @capacity,
75
+ "sequential" => @sequential,
76
+ "resources" => @resources.values.map{|r| r.to_hash(@production_times[r])}
77
+ }
78
+ end
79
+
80
+ private
81
+
82
+ def self.check_preconditions(resource, allocations)
83
+ allocated_resources = {}
84
+ allocations.each do |a|
85
+ if a.valid?
86
+ depot = allocated_resources[a.resource] || {amount: 0}
87
+ depot[:amount] += a.amount
88
+ allocated_resources[a.resource] = depot
89
+ end
90
+ end
91
+
92
+ resource.dependent_resources.each do |dr|
93
+ required_amount = resource.dependent_resource_amount(dr)
94
+ depot = allocated_resources[dr]
95
+ if depot && depot[:amount] >= required_amount
96
+ depot[:amount] -= required_amount
97
+ else
98
+ return false
99
+ end
100
+ end
101
+ return true
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,16 @@
1
+ def natural_time(time)
2
+ unit = time
3
+
4
+ # given unit in seconds
5
+ hours = unit / 3600
6
+ unit = unit - (hours * 3600)
7
+
8
+ minutes = unit / 60
9
+ seconds = unit - (minutes * 60)
10
+
11
+ result = (hours > 0) ? "#{hours}h " : ""
12
+ result += (minutes > 0) ? "#{minutes}m " : ""
13
+ result += "#{seconds}s"
14
+
15
+ result.strip
16
+ end
@@ -0,0 +1,72 @@
1
+ module Smcty
2
+ class Job
3
+ attr_reader :resource, :allocation, :production, :dependent_jobs
4
+
5
+ def initialize(resource)
6
+ @resource = resource
7
+ @used = false
8
+ @dependent_jobs = []
9
+ end
10
+
11
+ def allocate(allocation)
12
+ @allocation = allocation
13
+ @production = nil
14
+ end
15
+
16
+ def produce(production)
17
+ @allocation = nil
18
+ @production = production
19
+ end
20
+
21
+ def add_dependent(job)
22
+ @dependent_jobs << job
23
+ end
24
+
25
+ def reset_dependent_jobs
26
+ @dependent_jobs = []
27
+ end
28
+
29
+ def dependencies?
30
+ @dependent_jobs.size > 0
31
+ end
32
+
33
+ def allocated?
34
+ @allocation != nil
35
+ end
36
+
37
+ def in_production?
38
+ @production != nil && !@production.finished?
39
+ end
40
+
41
+ def ready?
42
+ @production != nil && @production.finished?
43
+ end
44
+
45
+ def new?
46
+ @allocation == nil && @production == nil && @dependent_jobs.size == 0
47
+ end
48
+
49
+ def allocated_dependencies?
50
+ if dependencies?
51
+ @dependent_jobs.each do |job|
52
+ return false unless job.allocated?
53
+ end
54
+ return true
55
+ else
56
+ return false
57
+ end
58
+ end
59
+
60
+ def to_hash
61
+ core_hash = {
62
+ "id" => self.object_id,
63
+ "resource" => @resource.name,
64
+ }
65
+ core_hash["allocation"] = @allocation.to_hash if @allocation
66
+ core_hash["production"] = @production.to_hash if @production
67
+ core_hash["dependent_jobs"] = @dependent_jobs.map{|j| j.object_id} if @dependent_jobs.size > 0
68
+ core_hash
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,71 @@
1
+ def list_inventory(store)
2
+ puts "---------------------------------------------------------------------------------------"
3
+ puts store.to_s
4
+ puts "---------------------------------------------------------------------------------------"
5
+ puts "Current inventory:"
6
+ puts ""
7
+ store.inventory.each do |item|
8
+ puts "\t#{item.name}: #{store.available_stock(item)} items (allocated: #{store.allocated_stock(item)})"
9
+ end
10
+ end
11
+
12
+ def list_resources(resource_list)
13
+ puts "-------------------------------------------------------------------"
14
+ puts "Registered Resources:"
15
+ puts ""
16
+ resource_list.each do |r|
17
+ puts "\t#{r.to_s}"
18
+ end
19
+ end
20
+
21
+ def list_project(project)
22
+ puts "-------------------------------------------------------------------"
23
+ puts "Project #{project.name}"
24
+ puts ""
25
+ project.resources.each do |r|
26
+ puts "\t#{r.name} - #{project.amount(r)}"
27
+ end
28
+ end
29
+
30
+ def list_factories(factory_list)
31
+ puts "-------------------------------------------------------------------"
32
+ puts "Registered Factories:"
33
+ puts ""
34
+ factory_list.each do |f|
35
+ puts "\t#{f.to_s}"
36
+ end
37
+ end
38
+
39
+ def list_factory(factory)
40
+ puts "-------------------------------------------------------------------"
41
+ puts factory.to_s
42
+ puts "sequential: #{factory.sequential}"
43
+ puts "-------------------------------------------------------------------"
44
+ puts "Available resources:"
45
+ puts ""
46
+ factory.resources.each do |resource|
47
+ puts "\t#{resource.to_s} (Time: #{natural_time(factory.production_time(resource))})"
48
+ end
49
+ puts ""
50
+ puts "Running production"
51
+ factory.productions.each do |production|
52
+ line = "\t#{production.object_id} -> #{production.resource.name}"
53
+ line += " / started at: #{production.start_time}"
54
+ line += " / duration: #{production.duration}"
55
+ line += " / delivery at: #{production.start_time + production.duration}"
56
+ line + " / finished: #{production.finished?}"
57
+ puts line
58
+ end
59
+ end
60
+
61
+ def list_configuration(configuration)
62
+ puts "==================================================================="
63
+ puts "Configuration created at: #{configuration.created_at}"
64
+ puts "==================================================================="
65
+ puts ""
66
+ list_inventory(configuration.store)
67
+ configuration.factories.each do |factory_name|
68
+ puts ""
69
+ outline_factory(configuration.factory(factory_name))
70
+ end
71
+ end
@@ -0,0 +1,13 @@
1
+ require 'smcty/console'
2
+
3
+ module Smcty
4
+ class Planner
5
+ def self.start(path)
6
+ puts "Starting Simcity Production Assistent"
7
+ puts "> load configuration from #{path}"
8
+
9
+ console = Console.new($stdin, path)
10
+ console.prompt!
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ module Smcty
2
+ class Production
3
+ attr_reader :start_time, :duration, :resource
4
+
5
+ def initialize(resource, duration, start_time=nil)
6
+ @start_time = start_time ? Time.at(start_time) : Time.now
7
+ @duration = duration
8
+ @resource = resource
9
+ end
10
+
11
+ def finished?
12
+ end_time <= Time.now
13
+ end
14
+
15
+ def end_time
16
+ @start_time + duration
17
+ end
18
+
19
+ def to_s
20
+ "Production of #{resource.name} since #{@start_time} takes #{natural_time(@duration)} (Finished: #{finished?})"
21
+ end
22
+
23
+ def to_hash
24
+ {
25
+ "start_time" => @start_time.to_i,
26
+ "duration" => @duration,
27
+ "resource" => @resource.name
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ module Smcty
2
+ class Project
3
+ attr_reader :name
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ @requirements = {}
8
+ end
9
+
10
+ def resources
11
+ @requirements.keys
12
+ end
13
+
14
+ def amount(resource)
15
+ @requirements[resource]
16
+ end
17
+
18
+ def add_requirement(resource, amount)
19
+ @requirements[resource] = amount
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ module Smcty
2
+ class Resource
3
+ attr_reader :name, :description
4
+
5
+ def initialize(name, description)
6
+ @name = name
7
+ @description = description
8
+ @dependencies = {}
9
+ end
10
+
11
+ def register_dependency(resource, amount)
12
+ @dependencies[resource] = amount
13
+ end
14
+
15
+ def dependent_resources
16
+ @dependencies.keys
17
+ end
18
+
19
+ def dependent_resource_amount(resource)
20
+ @dependencies[resource]
21
+ end
22
+
23
+ def to_s
24
+ "Resource '#{@name}' (#{@description}) with these dependencies: #{dependency_list}"
25
+ end
26
+
27
+ def to_hash(time)
28
+ {
29
+ "name" => @name,
30
+ "description" => @description,
31
+ "time" => time,
32
+ "deps" => @dependencies.keys.map{|r| {"name" => r.name, "amount" => @dependencies[r]}}
33
+ }
34
+ end
35
+
36
+ private
37
+
38
+ def dependency_list
39
+ items = []
40
+ @dependencies.keys.each do |resource|
41
+ items << "#{resource.name}: #{@dependencies[resource]}"
42
+ end
43
+ items.join(", ")
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,198 @@
1
+ require 'smcty/job'
2
+
3
+ module Smcty
4
+ class Scheduling
5
+
6
+ def initialize(configuration)
7
+ @configuration = configuration
8
+ @projects = {}
9
+ end
10
+
11
+ def plan_project(project)
12
+ jobs = []
13
+ @projects[project] = jobs
14
+
15
+ # 1. split the requirements into jobs
16
+ project.resources.each do |r|
17
+ project.amount(r).times do
18
+ jobs << Job.new(r)
19
+ end
20
+ end
21
+
22
+ # 2. work through initial job list and either
23
+ # - allocate available resources
24
+ # - add jobs to produce dependent jobs
25
+ # and iterate until the list does not change any more.
26
+ #
27
+ prev_size = 0
28
+ while (jobs.size != prev_size) do
29
+ prev_size = jobs.size
30
+ jobs.select{|j| j.new? }.each do |job|
31
+ if store.available_stock(job.resource) >= 1
32
+ job.allocate(store.allocate(job.resource, 1))
33
+ else
34
+ job.resource.dependent_resources.each do |dr|
35
+ job.resource.dependent_resource_amount(dr).times do
36
+ djob = Job.new(dr)
37
+ job.add_dependent(djob)
38
+ jobs << djob
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def next
47
+ puts "determine the next action"
48
+ # any project finished?
49
+ action = project_ready
50
+ return action if action
51
+ # any pure request?
52
+ action = something_dependent_to_produce
53
+ return action if action
54
+ # anything to pickup?
55
+ action = something_to_pick
56
+ return action if action
57
+ # anything that can be produced?
58
+ action = something_pure_to_produce
59
+ if action
60
+ return action
61
+ else
62
+ return "wait"
63
+ end
64
+ end
65
+
66
+ def project_ready
67
+ puts " -> is there a project ready for delivery?"
68
+ @projects.keys.each do |project|
69
+ ready = true
70
+ @projects[project].each do |job|
71
+ unless job.allocated?
72
+ ready = false
73
+ break
74
+ end
75
+ end
76
+
77
+ if ready
78
+ finish_project(project)
79
+ # we are done and the project can be finished.
80
+ return "finish #{project.name}"
81
+ end
82
+ end
83
+ nil
84
+ end
85
+
86
+ def finish_project(project)
87
+ # get all the allocated resources
88
+ @projects[project].each do |job|
89
+ job.allocation.get
90
+ end
91
+ # remove the project from scheduling
92
+ @projects.delete(project)
93
+ end
94
+
95
+ def something_pure_to_produce
96
+ puts " -> is there something without dependencies to produce?"
97
+ @projects.keys.each do |project|
98
+ @projects[project].each do |job|
99
+ if job.new?
100
+ factory = @configuration.factory_for(job.resource)
101
+ if factory.free_capacity > 0
102
+ job.produce(factory.produce(job.resource))
103
+ return "produce #{job.resource.name}"
104
+ end
105
+ end
106
+ end
107
+ end
108
+ nil
109
+ end
110
+
111
+ def something_to_pick
112
+ puts " -> is there something to pick?"
113
+ @projects.keys.each do |project|
114
+ @projects[project].each do |job|
115
+ if job.ready? && store.free_capacity > 0
116
+ @configuration.factory_for(job.resource).pick(job.production)
117
+ store.put(job.resource, 1)
118
+ job.allocate(store.allocate(job.resource, 1))
119
+ return "pick #{job.resource.name}"
120
+ end
121
+ end
122
+ end
123
+ nil
124
+ end
125
+
126
+ def something_dependent_to_produce
127
+ puts " -> is there something with dependencies ready for production?"
128
+ @projects.keys.each do |project|
129
+ @projects[project].each do |job|
130
+ if job.allocated_dependencies?
131
+
132
+ # extract the requirements
133
+ requirements = job.dependent_jobs.map{|j| j.allocation }
134
+ # produce the resource
135
+
136
+ factory = @configuration.factory_for(job.resource)
137
+
138
+ if factory.free_capacity > 0
139
+ puts "go for a complex production: #{job.resource}"
140
+ job.produce(factory.produce(job.resource, requirements))
141
+
142
+ # remove the dependent jobs
143
+ job.dependent_jobs.each{|j| @projects[project].delete(j)}
144
+ # reset the job dependency
145
+ job.reset_dependent_jobs
146
+
147
+ return "produce #{job.resource.name}"
148
+ end
149
+ end
150
+ end
151
+ end
152
+ nil
153
+ end
154
+
155
+ # debugging
156
+ def print_job_lists
157
+ @projects.keys.each do |project|
158
+ puts
159
+ puts "Project: #{project.name}"
160
+ puts ""
161
+ @projects[project].each_with_index do |job, idx|
162
+ puts "\t Step-#{idx} - #{job_line(job)}"
163
+ end
164
+ puts
165
+ end
166
+ end
167
+
168
+ def job_line(job)
169
+ "#{job.resource.name}: allocation: #{job.allocation} production: #{job.production} dependent: #{job.dependent_jobs.map{|j| j.resource.name}} new: #{job.new?} ready: #{job.ready?} allocated dependencies: #{job.allocated_dependencies?}"
170
+ end
171
+ # debugging
172
+
173
+ def load_project(project, job_list)
174
+ @projects[project] = job_list
175
+ end
176
+
177
+ def to_hash
178
+ {
179
+ projects: @projects.keys.map{|p| project_hash(p, @projects[p])}
180
+ }
181
+ end
182
+
183
+ def project_hash(project, job_list)
184
+ {
185
+ "name" => project.name,
186
+ "requirements" => project.resources.map{|r| {"resource" => r.name, "amount" => project.amount(r)}},
187
+ "jobs" => job_list.map{|j| j.to_hash}
188
+ }
189
+ end
190
+
191
+ private
192
+
193
+ def store
194
+ @configuration.store
195
+ end
196
+
197
+ end
198
+ end
@@ -0,0 +1,115 @@
1
+ require 'smcty/allocation'
2
+ require 'set'
3
+
4
+ module Smcty
5
+ class Store
6
+ attr_reader :name, :capacity
7
+
8
+ def initialize(name, capacity)
9
+ @name = name
10
+ @capacity = capacity
11
+ @storage = {}
12
+ @allocations = Set.new
13
+ end
14
+
15
+ def free_capacity
16
+ @capacity - total_available_stock - total_allocated_stock
17
+ end
18
+
19
+ def total_available_stock
20
+ result = 0
21
+ @storage.values.each do |value|
22
+ result += value
23
+ end
24
+ result
25
+ end
26
+
27
+ def total_allocated_stock
28
+ result = 0
29
+ @allocations.each do |alloc|
30
+ result += alloc.amount
31
+ end
32
+ result
33
+ end
34
+
35
+ def available_stock(resource)
36
+ @storage[resource] || 0
37
+ end
38
+
39
+ def allocated_stock(resource)
40
+ result = 0
41
+ @allocations.each do |alloc|
42
+ result += alloc.amount if resource == alloc.resource
43
+ end
44
+ result
45
+ end
46
+
47
+ def total_stock(resource)
48
+ available_stock(resource) + allocated_stock(resource)
49
+ end
50
+
51
+ def put(resource, _amount=1)
52
+ # at least one item of the resource
53
+ amount = [1, _amount].max
54
+
55
+ unless free_capacity >= amount
56
+ raise "The store is full - Cannot put #{amount} items of #{resource.name}"
57
+ end
58
+
59
+ in_stock = available_stock(resource)
60
+ @storage[resource] = in_stock + amount
61
+ amount
62
+ end
63
+
64
+ def allocate(resource, _amount)
65
+ # at least one item of the resource
66
+ amount = [1, _amount].max
67
+
68
+ in_stock = available_stock(resource)
69
+ unless in_stock >= amount
70
+ raise "#{in_stock} is not enough of #{resource.name} to allocate #{amount} items"
71
+ end
72
+
73
+ @storage[resource] = in_stock - amount
74
+ allocation = Allocation.new(self, resource, amount)
75
+ @allocations.add(allocation)
76
+ allocation
77
+ end
78
+
79
+ def get(allocation)
80
+ @allocations.delete(allocation)
81
+ end
82
+
83
+ def free(allocation)
84
+ put(allocation.resource, allocation.amount)
85
+ @allocations.delete(allocation)
86
+ end
87
+
88
+ def valid?(allocation)
89
+ @allocations.include?(allocation)
90
+ end
91
+
92
+ def inventory
93
+ @storage.keys
94
+ end
95
+
96
+ def to_s
97
+ "Store #{@name} has a total capacity of #{@capacity} whereas the free capacity is #{free_capacity}"
98
+ end
99
+
100
+ def to_hash
101
+ {
102
+ "name" => @name,
103
+ "capacity" => @capacity,
104
+ "stock" => @storage.keys.map{|k| {"name" => k.name, "amount" => total_stock(k)}}
105
+ }
106
+ end
107
+
108
+ private
109
+
110
+ def enforce(lower, amount, upper)
111
+ [[lower, amount].max, upper].min
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,3 @@
1
+ module Smcty
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smcty
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Markus Kuhnt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-25 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Plan your production based on scheduled plans to optimize throughput.
14
+ email:
15
+ - markus.kuhnt@gmail.com
16
+ executables:
17
+ - smcty
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/smcty
22
+ - lib/smcty.rb
23
+ - lib/smcty/allocation.rb
24
+ - lib/smcty/configuration.rb
25
+ - lib/smcty/configurator.rb
26
+ - lib/smcty/console.rb
27
+ - lib/smcty/factory.rb
28
+ - lib/smcty/helpers.rb
29
+ - lib/smcty/job.rb
30
+ - lib/smcty/output.rb
31
+ - lib/smcty/planner.rb
32
+ - lib/smcty/production.rb
33
+ - lib/smcty/project.rb
34
+ - lib/smcty/resource.rb
35
+ - lib/smcty/scheduling.rb
36
+ - lib/smcty/store.rb
37
+ - lib/smcty/version.rb
38
+ homepage:
39
+ licenses:
40
+ - MIT
41
+ metadata: {}
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 1.3.6
56
+ requirements: []
57
+ rubyforge_project:
58
+ rubygems_version: 2.4.5.1
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: A production planning software for simcity(tm)
62
+ test_files: []