# Running Coach (#82)

**URL:** <https://www.ruby-forum.com/t/running-coach-82/95840>\
**Category:** Ruby\
**Created:** [June 15, 2006, 3:48pm UTC](https://www.ruby-forum.com/t/running-coach-82/95840 "2006-06-15T15:48:45Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [June 15, 2006, 3:48pm UTC](https://www.ruby-forum.com/t/running-coach-82/95840/1 "2006-06-15T15:48:45Z")

</div>

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…
