Quiz 104 -- Solution
====================

Here is what turtle.rb looked like before I messed with it to produce  
Quiz 104.

<code>
# An implementation of Turtle Procedure Notation (TPN) as described in
# H. Abelson & A. diSessa, "Turtle Geometry", MIT Press, 1981.
#
# Turtles navigate by traditional geographic coordinates: X-axis  
pointing
# east, Y-axis pointing north, and angles measured clockwise from the
# Y-axis (north) in degrees.

class Turtle
    include Math
    DEG = Math::PI / 180.0
    ORIGIN = [0.0, 0.0]

    alias run instance_eval
    attr_accessor :track
    attr_reader :xy, :heading

    def degree
       DEG
    end

    ###
    # Turtle primitives
    ###
</code>

I explicitly define a writer for @xy to get the Logo-like argument  
checking that I wanted. Also, I decided to maintain @xy as an array  
of floats to minimize the accumulation of position errors in long  
tracks.

<code>
    # Place the turtle at [x, y]. The turtle does not draw when it  
changes
    # position.
    def xy=(coords)
       if coords.size != 2
          raise(ArgumentError, "turtle needs two coordinates")
       end
       x, y = coords
       must_be_number(x, 'x-coordinate')
       must_be_number(y, 'y-coordinate')
       @xy = x.to_f, y.to_f
    end
</code>

Similarly, I explicitly define a writer for @heading. But it's not  
just for argument checking: I also use it to constrain @heading to  
the interval [0.0, 360.0).

<code>
    # Set the turtle's heading to <degrees>.
    def heading=(degrees)
       must_be_number(degrees, 'heading')
       @heading = degrees.to_f
       case
       when @heading >= 360.0
          @heading -= 360.0 while @heading >= 360.0
       when @heading < 0.0
          @heading += 360.0 while @heading < 0.0
       end
       @heading
    end

    # Raise the turtle's pen. If the pen is up, the turtle will not  
draw;
    # i.e., it will cease to lay a track until a pen_down command is  
given.
    def pen_up
       @pen = :up
    end
</code>

When the pen goes down, a new track segment must be added. Initially,  
the segment contains only a single point. If the pen goes up before  
another point is added to the segment, the segment ends up with just  
one point. Such singleton segments are skipped when the track is  
processed by in the view.

<code>
    # Lower the turtle's pen. If the pen is down, the turtle will draw;
    # i.e., it will lay a track until a pen_up command is given.
    def pen_down
       @pen = :down
       @track << [@xy]
    end

    # Is the pen up?
    def pen_up?
       @pen == :up
    end

    # Is the pen down?
    def pen_down?
       @pen == :down
    end

    ###
    # Turtle commands
    ###

    # Place the turtle at the origin, facing north, with its pen up.
    # The turtle does not draw when it goes home.
    def home
       pen_up
       self.xy = ORIGIN
       self.heading = 0.0
    end

    # Home the turtle and empty out it's track.
    def clear
       home
       self.track = []
    end

    alias initialize clear

    # Turn right through the angle <degrees>.
    def right(degrees)
       must_be_number(degrees, 'turn')
       self.heading = heading + degrees.to_f
    end

    # Turn left through the angle <degrees>.
    def left(degrees)
       right(-degrees)
    end
</code>

This is one of two places in the code where it actually has to do  
some trigonometry -- Turtle#toward below is the other.

<code>
    # Move forward by <steps> turtle steps.
    def forward(steps)
       must_be_number(steps, 'distance')
       angle = heading * DEG
       x, y = xy
       self.xy = [x + steps * sin(angle), y + steps * cos(angle)]
       track.last << xy if pen_down?
    end

    # Move backward by <steps> turtle steps.
    def back(steps)
       forward(-steps)
    end

    # Move to the given point.
    def go(pt)
       self.xy = pt
       track.last << xy if pen_down?
    end
</code>

In Turtle#toward, the expression atan2(y2 - y1, x2 - x1) computes the  
slope angle of the line between pt and xy. Math#atan2 is better here  
than Math#atan because atan2 handles the four quadrant cases  
automatically. Once the slope angle is known, it is easily converted  
into a heading.

<code>
    # Turn to face the given point.
    def toward(pt)
       x2, y2 = pt
       must_be_number(x2, 'pt.x')
       must_be_number(y2, 'pt.y')
       x1, y1 = xy
       set_h(90.0 - atan2(y2 - y1, x2 - x1) / DEG)
    end
</code>

Turtle#distance is easy to implement providing one remembers the  
existence of Math#hypot.

<code>
    # Return the distance between the turtle and the given point.
    def distance(pt)
       x2, y2 = pt
       must_be_number(x2, 'pt.x')
       must_be_number(y2, 'pt.y')
       x1, y1 = xy
       hypot(x2 - x1, y2 - y1)
    end

    # Traditional abbreviations for turtle commands.
    alias fd forward
    alias bk back
    alias rt right
    alias lt left
    alias pu pen_up
    alias pd pen_down
    alias pu? pen_up?
    alias pd? pen_down?
    alias set_h heading=
    alias set_xy xy=
    alias face toward
    alias dist distance

private

    # Raise an exception if <val> is not a number.
    def must_be_number(val, name)
       if !val.respond_to?(:to_f)
          raise(ArgumentError, "#{name} must be a number")
       end
    end
end
</code>

Now that you've seen the code, let me discuss some of the  
implementation decisions I made.

The first issue I had to deal with was how to reconcile the way  
turtles measure angles with the way Ruby/Math measures angles.  
Turtles, you recall, (following the conventions of geography/ 
navigation) measure angles clockwise from north in degrees, while the  
Math module (following mathematical conventions) measures angles  
counterclockwise from east in radians. Since the Turtle class  
includes Math, there are advantages to following mathematical  
conventions when maintaining the turtle's orientation internal to the  
class, However, influenced by Logo, I chose to use the navigator's  
notion of angle and to reconcile turtle angles to Math angles each  
time I actually did some trig.

I also considered overriding the trig functions with methods that  
would accept angles in degrees as their arguments. In the end, I  
decided not to, but I still find myself thinking, from time to time,  
that I should go back to the code and do it.

The next issue I settled was: what, if any, argument checking should  
I do? I settled on accepting any argument that responds to to_f,  
raising ArgumentError for those that don't, and providing Logo-like  
error messages. The private method Turtle#must_be_number takes care  
of this.

The last major issue was: how should I maintain the turtle's state?  
That is, what instance variables should the class have? My choices were:

@xy                turtle location
@heading           turtle orientation
@pen               pen state (up or down)
@track             array needed to interface with Ruby/Tk

One last remark. Over the years I have built up a good-sized  
collection of Logo turtle graphics programs. One of reasons I wanted  
a Ruby turtle graphics capability was to convert this collection to  
Ruby. I had the feeling that Ruby would prove to be a better Logo  
than Logo. Well, I've performed the conversion and I'm convinced I  
was right: the Ruby versions of the Logo programs are simpler, easier  
to understand, and often considerably shorter than their Logo  
counterparts.

Regards, Morton