/
cigarette_smokers.rb
executable file
·322 lines (262 loc) · 7.45 KB
/
cigarette_smokers.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
#!/usr/bin/env ruby
require 'rubygems'
require 'bundler/setup'
require 'celluloid/autostart'
#
# THE CIGARETTE SMOKERS PROBLEM
# See http://en.wikipedia.org/wiki/Cigarette_smokers_problem
#
# Three smokers are sitting at a table. One is a tobacco grower, the second a
# papermaker, and the third a matchstick maker. They have all brought with
# them an abundance of their own fare, and they're here to smoke! However, in
# order to smoke each man will need the goods of the other two.
#
# After a great deal of disagreement about the best way to settle the three-way
# exchange needed for any one man to enjoy a cigarette, the three men
# eventually negotiate a rather unusual arrangement. Whenever the table in
# front of them is empty they notify the waitress, who selects two of the men
# at random, taking a single unit of their particular good. She then places
# their goods on the table and gives a wink and a nod to the third man.
#
# If the third man whose goods she did not take is not smoking, he'll take
# the two items and use his own supply of the third to enjoy a cigarette.
# If, however, he's already smoking, he'll smile and nod in return, perhaps
# taking an extra long drag on his cigarette, possibly to the chagrin of his
# compatriates. For you see, the rules are that only the third man in this
# exchange is allowed to remove the items from the table. If he's already
# enjoying a cigarette, the other two are forced to wait.
#
# One way or another, the third man will eventually collect the items from the
# table. When he does, he'll whistle for the waitress, and she'll again select
# two men at random, replenishing the table.
# Each of the three men at the table is a smoker
class Smoker
include Celluloid
# These guys are smoking MACHINES (literally, they're finite state machines!)
class Machine
include Celluloid::FSM
default_state :standing
state :waiting, :to => :procuring do
actor.check_table
end
state :procuring, :to => [:waiting, :smoking] do
if actor.take_items
transition :smoking
else
transition :waiting
end
end
state :smoking do
actor.smoke
end
state :done do
actor.smoking = false
puts "#{actor.name} has finished smoking"
transition :procuring
end
end
def initialize(commodity, rate)
@commodity, @rate = commodity, rate
@table = nil
@cigarette = nil
@match = nil
@smoking = false
@machine = Machine.new
end
attr_accessor :smoking
alias_method :smoking?, :smoking
def name
"#{@commodity} Guy"
end
def inspect
"#<Smoker: #{name}>"
end
# Sit down at the table
def sit(table)
@table = table
@table.async.welcome(Actor.current)
end
def sat_down
@machine.transition :procuring
end
# Obtain this smoker's commodity
def dispense_commodity
@commodity.new
end
# The table should now have the items I need
def notify_ready
if @machine.state == :smoking
puts "#{name} says: I'm good on smokes, thanks"
else
@machine.transition :procuring
end
end
# Check the table for the items we're waiting for
def check_table
return unless @table.empty?
puts "#{name} whistles at waitress... get me smokes!"
@table.waitress.async.whistle # We'll decide what to do when the waitress arrives
end
# Take the items from the table
def take_items
items = @table.items
unless complimentary? items
if items
puts "#{name} eyes the table: #{items.map(&:class).join(' and ')} are useless to me!"
else
puts "#{name} says: The table is empty!"
end
return
end
puts "#{name} takes the items from the table"
items = @table.take
puts "#{name} whistles for the waitress because the table is empty"
@table.waitress.async.whistle
tobacco = find_item items, Tobacco
paper = find_item items, Paper
@match = find_item items, Matches
@cigarette = paper.roll tobacco
true
end
def find_item(items, type)
return @commodity.new if type == @commodity
item = items.find { |i| i.class == type }
raise "can't find any #{type}" unless item
item
end
def smoke
@smoking = true
@match.light
@cigarette.light @match
puts "#{name} enjoys a smoke"
@cigarette.smoke
@machine.transition :done, :delay => @rate * 5
end
def complimentary?(items)
return unless items
klasses = [Tobacco, Paper, Matches] - [@commodity]
items.map { |i| klasses.include?(i.class) }.all?
end
end
# The waitress fills the table
class Waitress
include Celluloid
def initialize(table)
@table = table
@refilling = false
end
# Handle incoming whistles
def whistle
unless @table.smokers.size == 3
puts "Waitress says: Nothing I can do for ya hon, your party isn't completely seated"
return
end
if @table.items
puts "Waitress says: The table already has stuff on it!"
elsif @refilling
puts "Waitress says: I'm WORKIN' on it! Hold your horses!"
else
refill_table
end
end
def refill_table
@refilling = true
begin
smokers = @table.smokers.dup
receiver = smokers.delete_at rand(smokers.size)
puts "#{self.class} decides that #{receiver.name} gets to smoke!"
items = smokers.map(&:dispense_commodity)
puts "Waitress collects #{items.map(&:class).join(' and ')} and puts them on the table"
@table.place(items)
ensure
@refilling = false
end
puts "Waitress tells #{receiver.name} he can smoke now"
receiver.async.notify_ready
note_smokers
end
def note_smokers
active_smokers = @table.smokers.select(&:smoking?)
puts "*** Active smokers: #{active_smokers.map(&:name).join(', ')}"
puts "!!! EVERYBODY SMOKES !!!" if active_smokers.size == @table.smokers.size
end
end
# The table is the central state holder of the system
class Table
include Celluloid
attr_reader :smokers
attr_reader :waitress
attr_reader :items
def initialize
@smokers = []
@waitress = Waitress.new(Actor.current) # Always use Actor.current instead of self!
@items = nil
end
def welcome(smoker)
@smokers << smoker
smoker.async.sat_down
end
def place(items)
abort RuntimeError.new("there's already stuff on the table") if @items
@items = items
puts "Table now contains: #{items.map(&:class).join(', ')}"
return @items
end
def take
raise "there's nothing on the table" unless @items
items = @items
@items = nil
items
end
def empty?
!@items
end
end
#
# The following are effectively data objects passed around between smokers
#
class Tobacco
def burn
carcinogens = [:polonium, :nitrosamine, :benzopyrene]
[:nicotine, *carcinogens]
end
end
class Paper
def roll(tobacco)
Cigarette.new(tobacco, self)
end
def burn; [:ash]; end
end
class Matches
def initialize
@lit = false
end
def light; @lit = true; end
def lit?; @lit; end
end
class Cigarette
def initialize(tobacco, paper)
@lit = false
@tobacco, @paper = tobacco, paper
end
def light(match)
@lit = true if match.lit?
end
def lit?; @lit end
def smoke
raise "not lit" unless @lit
@tobacco.burn + @paper.burn
end
end
#
# Run the simulation
#
smokers = [
Smoker.new(Tobacco, 1.0),
Smoker.new(Paper, 1.5),
Smoker.new(Matches, 0.3) # he smokes faster because he likes to burn things
]
table = Table.new
smokers.each { |smoker| smoker.sit(table) }
# The main thread is done! Sleep forever
sleep