This quiz turns out to be a little bit of work, if you want to get some
decent
feedback to the user. Adam S. hammered out a reasonably complete
solution
though, so let’s have a look at it:
$CheerThreshold = 6 #decrease to get more random encouragement
$LongThreshold = 120 #minimum time to be considered a "long" run
class Phase
attr_reader :action, :seconds
def initialize action, time
@action = action.downcase
@seconds = time.to_i
end
end
# ...
We can see some setup work here for variables that allow users to tweak
the
output. We also have the trivial Phase class definition, which is just
a data
class for linking actions and times.
Here’s the main event loop:
class Coach
def initialize filename
File.open(filename) {|f|
@rawdata = f.read.split("\n")
}
@duration = 0
@runs = @longs = @walks = 0
@encouragometer = 0
@step = [30,15,10,5,5]
end
def coach
build_timeline
say summarize(2)
say start_prompt
@time = Time.now
@target_time = @time
while (phase = @phases.shift)
update_summary phase
narrate_phase phase
if @phases.size > 0
say transition(@phases[0].action)
say summarize(rand(2))
end
end
say finish_line
end
# ...
There’s nothing too interesting about initialize() which is just
assigning
defaults to the instance variables. Have a look at the coach() method
though.
This is the process the application runs through, and I really like how
well it
reads. It builds up the timeline of events, hits user with a summary
and
starting prompt, then launches into Phase processing. Each Phase is
narrated to
the user, and then the code transitions naturally to the next Phase.
Finally
the code sends the finish line message to indicate a successful workout.
Let’s see what narrating a phase involves:
# ...
def narrate_phase phase
say what_to_do_for(phase)
@target_time += phase.seconds
delta = (@target_time - Time.now).to_i
stepidx = 0
while (delta > 0)
stepidx+=1 if delta < @step[stepidx]+1
wait_time = delta % @step[stepidx]
wait_time += @step[stepidx] if wait_time <= 0
wait(wait_time)
delta = (@target_time - Time.now).to_i
encourage_maybe
say whats_left(phase.action,delta) if delta > 0
end
end
# ...
Obviously, this method is mostly about time management. It breaks a
Phase down
into smaller chunks, so that it can provide encouragement frequently and
inform
the user of what is left to be done.
Note the clever output messages here again that read so naturally:
what_to_do_for(), encourage_maybe(), and whats_left().
# ...
def update_summary phase
@duration -= phase.seconds
@runs -= 1 if phase.action == 'run'
@longs -= 1 if phase.action == 'run' and phase.seconds >=
$LongThreshold
@walks -= 1 if phase.action == ‘walk’
end
def build_timeline
@phases = @rawdata.map {|command|
p = Phase.new(*command.split)
@duration += p.seconds
@runs += 1 if p.action == 'run'
@longs += 1 if p.action == 'run' and p.seconds >= $LongThreshold
@walks += 1 if p.action == 'walk'
p
}
end
# ...
These two methods are quite similar save that one adds and the other
subtracts.
First, build_timeline() constructs the Phase objects from the import
file. As
it goes through, it counts things like the total number of walks and
runs a
person needs to complete. Then, update_summary() runs inside each Phase
of the
event loop ticking off the walks and runs the user has completed.
Here’s the say() method that would eventually need to be replaced with
speech
programming:
# ...
def say s
puts s
#todo: replace with speech
end
# ...
Now, take a look at this:
# ...
def wait n
if $DEBUG
puts "...waiting #{n} seconds..."
@target_time -= n
else
$stdout.flush
sleep(n)
end
end
# ...
This is obviously the delay method and it mainly just calls sleep().
However, I
like how it can be set to just explain what the pause would have been,
in $DEBUG
mode. That makes testing the application much more pleasant.
Two more helper methods:
# ...
def encourage_maybe
@encouragometer += rand(3)
if (@encouragometer > $CheerThreshold)
say cheer
@encouragometer = 0
end
end
def timesay secs
secs = secs.to_i
s = ""
if secs > 60
min = secs/60
secs -= min*60
s += "#{min} minute"
s += 's' if min > 1
s += ' and ' if secs > 0
end
if secs > 0
s += "#{secs} second"
s += 's' if secs > 1
end
s
end
# ...
There’s the definition for the encourage_maybe() call I pointed out
earlier. It
just randomly decides if a cheer should be emitted.
The other method, timesay(), is a helper like we are use to in Rails.
It just
humanizes the output of some number of seconds by breaking it into
minutes and
seconds.
Next the code has several output methods, of which I’ll just show a
couple:
# ...
# All the phrases should be below this line, not mixed up in the logic
def what_to_do_for phase
s = "#{phase.action} for #{timesay(phase.seconds)} \n"
s += "You are almost done" if @phases.size == 1
s
end
def whats_left act, time
timestr = timesay(time)
s = [
"You have #{timestr} more to #{act}",
"#{act} for #{timestr} more",
"only #{timestr} left of #{act}ing",
"You have #{timestr} more to #{act}",
"#{timestr} left in this phase",
"There are #{timestr} until the next activity"
]
s[rand(s.size)]
end
# ... several more output routines not shown ...
end
# ...
You can see that these methods just use simple conditional logic or
random picks
to vary the program’s output. With several of these methods, the end
result is
a fairly good mix of prompts for the user.
Here’s the last line that turns it into a solution:
# ...
Coach.new(ARGV[0]||"week3.txt").coach
My thanks to those who stole the time from their busy running schedules
to code
up a solution. These scripts should have us all in shape by RubyConf!
Tomorrow, we will try an extremely common computerism, but see if we can
handle
it a little better than the usual treatment…