[SUMMARY] Turtle Graphics (#104)

Discussion in 'Ruby' started by Ruby Quiz, Dec 7, 2006.

  1. Ruby Quiz

    Ruby Quiz Guest

    I'm going to move my standard thank you note right to the beginning of this
    summary, because it's very important this time. Morton put in a lot of work
    prepping this problem so it would be Ruby Quiz size and fun at the same time.
    He even nursed me through my additions. Thank you Morton! More thanks to those
    who fiddled with the problem, showing Morton how much we appreciate his efforts.

    Alright, let's get to the solutions.

    Solving this problem isn't too tricky. The main issue is to have the Turtle
    track its state which consists of where it currently is, which way it is facing,
    and if the pen is currently up or down. Then you need to make the methods that
    alter this state functional. A surprising number of the methods have trivial
    implementations, but you do need a little trigonometry for some.

    Let's walk through Pete Yandell's turtle.rb file to see how a solution comes
    together. Here's the start of the code:

    class Turtle
    include Math # turtles understand math methods
    DEG = Math::pI / 180.0

    attr_accessor :track
    alias run instance_eval

    def initialize
    clear
    end

    attr_reader :xy, :heading

    # ...

    The only line in there not provided by the quiz is the call to clear() in
    initialize(). We'll look at what that does in just a moment, but first let's
    talk a little about what the quiz gave us for free.

    We've already decided a little trig is needed so the functions of the Math
    Module are included for us. Now those Math methods expect arguments in radians,
    but our Turtle is going to work with degrees. The conversion formula is radians
    = degrees * (PI / 180) and that's exactly what the DEG constant sets up for us.

    Skipping down, we see that instance_eval() is given a new name, so we can invoke
    Turtle code more naturally. This tells us how our object will be used. Because
    user code is evaluated in the context of this object, it will have access to all
    the methods we are about to build and even the methods borrowed from Math.

    The rest of the code provides accessors to the elements of Turtle state we
    identified earlier. Since they are there, we might as well take the hint and
    tuck our instance data away in them. We still need to figure out how to track
    the pen's up/down state though. Finally, The track() method provides access to
    the Turtle path we are to construct. The viewer will call this to decide what
    to render.

    I'll jump ahead in the code now, to show you that clear() method and another
    method it makes use of:

    # ...

    # Homes the turtle and empties out it's track.
    def clear
    @track = []
    home
    end

    # Places the turtle at the origin, facing north, with its pen up.
    # The turtle does not draw when it goes home.
    def home
    @heading = 0.0
    @xy = [0.0, 0.0]
    @pen_is_down = false
    end

    # ...

    As you can see, clear() resets the Turtle to the beginning state (by calling
    home()) and clears any drawing that has been done. The constructor called this
    method to ensure all the state variables would be set before we run() any code.

    We can now see that pen state will be tracked via a boolean instance variable as
    well. Here are the methods that expose that to the user:

    # ...

    # 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_is_down = false
    end

    # 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_is_down = true
    @track << [@xy]
    end

    # Is the pen up?
    def pen_up?
    !@pen_is_down
    end

    # Is the pen down?
    def pen_down?
    @pen_is_down
    end

    # ...

    Most of those should be obvious implementations. The surprise, if any, comes
    from the fact that pen_down() puts a point on the track. This makes sense
    though, if you think about it. If you touch a pen to a piece of paper you have
    made a mark, even though you have not yet drawn a line. The Turtle should
    function the same way.

    Here are the other setters for our Turtle's state:

    # ...

    # Place the turtle at [x, y]. The turtle does not draw when it changes
    # position.
    def xy=(coords)
    raise ArgumentError unless is_point?(coords)
    @xy = coords
    end

    # Set the turtle's heading to <degrees>.
    def heading=(degrees)
    raise ArgumentError unless degrees.is_a?(Numeric)
    @heading = degrees % 360
    end

    # ...

    These should be pretty straight-forward as well. I haven't shown it yet, but
    is_point?() just validates that we received sensible parameters. Beyond the
    checks, these methods just make assignments, save that heading=() restricts the
    parameter to a value between 0 and 359.

    We've got the state, so it's time to get the Turtle moving. Let's start with
    turns:

    # ...

    # Turn right through the angle <degrees>.
    def right(degrees)
    raise ArgumentError unless degrees.is_a?(Numeric)
    @heading += degrees
    @heading %= 360
    end

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

    # ...

    The right() method is the workhorse here. It validates, adds the requested
    number of degrees, and trims the heading if we have passed 360. Pete then
    wisely reuses the code by defining left() in terms of a negative right() turn.
    Two for the price of one.

    We can turn, so it's time to mix in a little motion:

    # ...

    # Move forward by <steps> turtle steps.
    def forward(steps)
    raise ArgumentError unless steps.is_a?(Numeric)
    @xy = [ @xy.first + sin(@heading * DEG) * steps,
    @xy.last + cos(@heading * DEG) * steps ]
    @track.last << @xy if @pen_is_down
    end

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

    # ...

    Remember your trig? We have the angle (@heading) and the length of the
    hypotenuse of a right triangle (steps). What we need are the lengths of the
    other two sides which would be the distance we moved along the X and Y axes.
    Note the use of DEG here to convert degrees to into the expected radians.

    Once you accept how forward() calculates the new location, drawing the line is
    almost a let down. The point where we were will already be on the track, either
    from a previous line draw or from a pen_down() call. Just adding the new point
    to that segment that contains the last point ensures that a line will be drawn
    to connect them.

    Again, we see that back() is just a negative forward().

    Here are the rest of the Turtle movement commands:

    # ...

    # Move to the given point.
    def go(pt)
    raise ArgumentError unless is_point?(pt)
    @xy = pt
    @track.last << @xy if @pen_is_down
    end

    # Turn to face the given point.
    def toward(pt)
    raise ArgumentError unless is_point?(pt)
    @heading = atan2(pt.first - @xy.first, pt.last - @xy.last) /
    DEG % 360
    end

    # Return the distance between the turtle and the given point.
    def distance(pt)
    raise ArgumentError unless is_point?(pt)
    return sqrt( (pt.first - @xy.first) ** 2 +
    (pt.last - @xy.last) ** 2 )
    end

    # ...

    go() is just forward() without needing to calculate the new point. (In fact,
    forward() could have called go() with the new point for even more aggregation
    goodness.) toward() uses an arc tangent calculation to change headings and
    distance() uses the Pythagorean theorem to tell you how many steps the given
    point is from where you are.

    Here's the final bit of code:

    # ...

    # 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

    def is_point?(pt)
    pt.is_a?(Array) and pt.length == 2 and
    pt.first.is_a?(Numeric) and pt.last.is_a?(Numeric)
    end

    end

    Those aliases were provided with the quiz and is_point?() is the helper method
    used to check the passed arguments to xy=(), go(), toward(), and distance().

    If you slot that file into the provided quiz project and start running samples,
    you should see pretty pictures and I'm a real sucker for pretty pictures.
    Thanks again Morton. Great quiz idea!

    Tomorrow we will tackle a fun algorithmic problem for us tournament players...
     
    Ruby Quiz, Dec 7, 2006
    #1
    1. Advertising

  2. Ruby Quiz

    Edwin Fine Guest

    Re: Turtle Graphics (#104)

    > # Move forward by <steps> turtle steps.
    > def forward(steps)
    > raise ArgumentError unless steps.is_a?(Numeric)
    > @xy = [ @xy.first + sin(@heading * DEG) * steps,
    > @xy.last + cos(@heading * DEG) * steps ]
    > @track.last << @xy if @pen_is_down
    > end


    I think that you should mention a subtlety here. The calculation of a
    new point is usually

    [x,y] = [x_old + dist * cos(theta), y_old + dist * sin(theta)]

    Because the turtle geometry is 90 degrees out of phase and rotating in
    the other direction, Pete has flipped the x and y so the new point is

    [x,y] = [x_old + dist * sin(theta), y_old + dist * cos(theta)]

    Another subtlety is that Pete used atan2, which handles division by zero
    and the ambiguity of which quadrant you are in.

    (Well, it was subtle to me!!)

    --
    Posted via http://www.ruby-forum.com/.
     
    Edwin Fine, Dec 7, 2006
    #2
    1. Advertising

  3. Re: Turtle Graphics (#104)

    On Dec 7, 2006, at 11:15 AM, Edwin Fine wrote:

    >> # Move forward by <steps> turtle steps.
    >> def forward(steps)
    >> raise ArgumentError unless steps.is_a?(Numeric)
    >> @xy = [ @xy.first + sin(@heading * DEG) * steps,
    >> @xy.last + cos(@heading * DEG) * steps ]
    >> @track.last << @xy if @pen_is_down
    >> end

    >
    > I think that you should mention a subtlety here. The calculation of a
    > new point is usually
    >
    > [x,y] = [x_old + dist * cos(theta), y_old + dist * sin(theta)]
    >
    > Because the turtle geometry is 90 degrees out of phase and rotating in
    > the other direction, Pete has flipped the x and y so the new point is
    >
    > [x,y] = [x_old + dist * sin(theta), y_old + dist * cos(theta)]
    >
    > Another subtlety is that Pete used atan2, which handles division by
    > zero
    > and the ambiguity of which quadrant you are in.
    >
    > (Well, it was subtle to me!!)


    Both terrific points. Thanks for bringing them up!

    James Edward Gray II
     
    James Edward Gray II, Dec 7, 2006
    #3
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Brent W. Hughes

    Python and Turtle Graphics

    Brent W. Hughes, Jul 19, 2004, in forum: Python
    Replies:
    4
    Views:
    1,607
    Lee Harr
    Jul 20, 2004
  2. Ruby Quiz

    [QUIZ] Turtle Graphics (#104)

    Ruby Quiz, Dec 1, 2006, in forum: Ruby
    Replies:
    34
    Views:
    570
    Morton Goldberg
    Dec 6, 2006
  3. David Tran
    Replies:
    0
    Views:
    129
    David Tran
    Dec 6, 2006
  4. David Tran
    Replies:
    0
    Views:
    124
    David Tran
    Dec 7, 2006
  5. Adam Funk
    Replies:
    7
    Views:
    236
    Adam Funk
    Feb 6, 2013
Loading...

Share This Page