[QUIZ] Turtle Graphics (#104)

M

Morton Goldberg

Here is my straight-to-the-point answer:

Your solution passes all the unit tests I supplied and is certainly
good enough to reproduce all the sample designs. So you have good
reason to think it's completely correct. However, one of the optional
methods has a problem.
# Turn to face the given point.
def toward(pt)
@heading = atan(pt[0].to_f / pt[1].to_f) / DEG
end

This won't work in all four quadrants.

I apologize for not providing tests good enough to detect the
problem. Here is one that will test all four quadrants.

<code>
# Test go, toward, and distance.
# Verify heading measures angles clockwise from north.
def test_coord_cmnds
nne = [100, 173]
@turtle.go nne
x, y = @turtle.xy
assert_equal(nne, [x.round, y.round])
@turtle.home
@turtle.run { pd; face nne; fd 200 }
assert_equal(30, @turtle.heading.round)
assert_equal([[[0, 0], nne]], snap(@turtle.track))
sse = [100, -173]
@turtle.home
@turtle.run { face sse; fd 200 }
assert_equal(150, @turtle.heading.round)
ssw = [-100, -173]
@turtle.home
@turtle.run { face ssw; fd 200 }
assert_equal(210, @turtle.heading.round)
nnw = [-100, 173]
@turtle.home
@turtle.run { face nnw; fd 200 }
assert_equal(330, @turtle.heading.round)
@turtle.home
assert_equal(500, @turtle.dist([400, 300]).round)
end
</code>

Regards, Morton
 
H

Hidetoshi NAGAI

From: Edwin Fine <[email protected]>
Subject: Re: tk.rb warning in Turtle Graphics (#104)
Date: Sat, 2 Dec 2006 18:28:44 +0900
Message-ID: said:
Well, I found a bizarre and (to me) totally inexplicable way to get rid
of the warning. This is from the quiz turtle_view.rb file. Adding an
explicit return value (any value; I used nil) to the draw method gets
rid of the warning. WTF???

Do you use the method "draw" at the end of callback operation?
If so, the method returns the result to the Tcl/Tk interpreter.
Then, the result (a Ruby's object) is converted to a string.

Usually, an object of TkObject or its subclasses is converted to
its @path value which is a string.
And when pass the string to Tcl/Tk side, @encoding check is required.

nil or a numeric doesn't need @encoding check.
 
E

Edwin Fine

Here's my solution. It passes all the unit tests, including the new
ones. I added a further unit test to try to force divide by zero errors
in the atan(y/x) calculation, and to see what happens if there is
"stupid" input, like two consecutive "face [100,0]" commands... hey,
stuff happens...
I must say, this turned out to be a bit more involved than I thought it
would. I kept tripping over the differences between turtle space angles
and "normal" angles.

As noted elsewhere, I also added a "nil" return to the turtle_viewer.rb
view method to eliminate most pesky Tk warnings. Thanks to Hidetoshi
NAGAI for explaining why this does what it does.

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

NORTH = 0.0
HOME = [0, 0]

alias run instance_eval

def initialize
self.clear
self.pen_up
end

attr_reader :track, :xy, :heading

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

# Set the turtle's heading to <degrees>. Heading is measured CLOCKWISE
from NORTH!
def heading=(degrees)
@heading = validate_degrees(degrees)
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 = true
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_up = false
end

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

# Is the pen down?
def pen_down?
not self.pen_up?
end

# Places the turtle at the origin, facing north, with its pen up.
# The turtle does not draw when it goes home.
def home
@xy = HOME
self.heading = NORTH
end

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

# Turn right through the angle <degrees>.
def right(degrees)
h = self.heading + validate_degrees(degrees)
self.heading = normalize_degrees(h)
end

# Turn left through the angle <degrees>.
def left(degrees)
h = self.heading - validate_degrees(degrees)
self.heading = normalize_degrees(h)
end

# Move forward by <steps> turtle steps.
def forward(steps)
validate_steps(steps)
normal_radians = to_rad(flip_turtle_and_normal(@heading))
new_pt = [@xy[0] + steps * cos(normal_radians),
@xy[1] + steps * sin(normal_radians)]

add_segment_to_track @xy, new_pt if self.pen_down?
@xy = new_pt
end

# Move backward by <steps> turtle steps.
def back(steps)
validate_steps(steps)

normal_radians = to_rad(flip_turtle_and_normal(@heading))
new_pt = [@xy[0] - steps * cos(normal_radians),
@xy[1] - steps * sin(normal_radians)]

if self.pen_down?
add_segment_to_track @xy, new_pt
end

@xy = new_pt
end

# Move to the given point.
def go(pt)
validate_coords(pt)
add_segment_to_track(self.xy, pt) if self.pen_down?
self.xy = pt
end

# Turn to face the given point.
def toward(pt)
validate_coords(pt)
delta_x = (pt[0] - self.xy[0]).to_f
delta_y = (pt[1] - self.xy[1]).to_f
return if delta_x.zero? and delta_y.zero?

# Handle special cases
case
when delta_x.zero? # North or South
self.heading = delta_y < 0.0 ? 180.0 : 0.0
when delta_y.zero? # East or West
self.heading = delta_x < 0.0 ? 270.0 : 90.0
else
# Calcs are done in non-turtle space so we have to flip afterwards
quadrant_adjustment = if delta_x < 0.0 then 180 elsif delta_y <
0.0 then 360.0 else 0.0 end
self.heading = flip_turtle_and_normal(to_deg(atan(delta_y /
delta_x)) + quadrant_adjustment)
end
end

# Return the distance between the turtle and the given point.
def distance(pt)
# Classic Pythagoras
sqrt((pt[0] - @xy[0]) ** 2 + (pt[1] - @xy[1]) ** 2)
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

# Validations

def validate_coords(coords)
unless coords.respond_to? :[] and
coords.respond_to? :length and
coords.length == 2 and
coords[0].kind_of? Numeric and
coords[1].kind_of? Numeric
raise(ArgumentError, "Invalid coords #{coords.inspect}, should be
[num, num]")
end
coords
end

def validate_degrees(degrees)
raise(ArgumentError, "Degrees must be numeric") unless
degrees.kind_of? Numeric
normalize_degrees(degrees)
end

def validate_steps(steps)
raise(ArgumentError, "Steps must be numeric") unless steps.kind_of?
Numeric
end

# Normalizations

# Flip between turtle space degrees and "normal" degrees (symmetrical)
def flip_turtle_and_normal(degrees)
(450.0 - degrees) % 360.0
end

# Normalize degrees to interval [0, 360)
def normalize_degrees(degrees)
degrees += 360.0 while degrees < 0.0
degrees % 360.0
end

def add_segment_to_track(start, finish)
@track << [ start, finish ]
end

def to_rad(deg)
deg * DEG
end

def to_deg(rad)
rad / DEG
end
end
</code>

---------------
Here's the extra test case:

<code>
def test_edge_cases
east = [100, 0]
west = [-100, 0]
north = [0, 100]
south = [0, -100]
@turtle.home
assert_equal(0, @turtle.heading.round)
assert_nothing_raised { @turtle.face [0, 0] }
assert_equal(0, @turtle.heading.round)
assert_nothing_raised { @turtle.face north }
assert_equal(0, @turtle.heading.round)
@turtle.face east
assert_nothing_raised { @turtle.face east }
assert_equal(90, @turtle.heading.round)
@turtle.face south
assert_nothing_raised { @turtle.face south }
assert_equal(180, @turtle.heading.round)
@turtle.face west
assert_nothing_raised { @turtle.face west }
assert_equal(270, @turtle.heading.round)
end
</code>
 
E

Edwin Fine

Hidetoshi said:
Do you use the method "draw" at the end of callback operation?
If so, the method returns the result to the Tcl/Tk interpreter.
Then, the result (a Ruby's object) is converted to a string.

Usually, an object of TkObject or its subclasses is converted to
its @path value which is a string.
And when pass the string to Tcl/Tk side, @encoding check is required.

nil or a numeric doesn't need @encoding check.

Thanks for the explanation. This was driving me nuts :)
 
M

Matthew Moss

My solution, which does pass your updated test_coord_cmnds, Morton...



require "matrix"

class Turtle
include Math # turtles understand math methods
DEG = Math::pI / 180.0
ORIGIN = [0.0, 0.0]
NORTH = 0.0

attr_accessor :track
alias run instance_eval

def initialize
clear
end

attr_reader :xy, :heading

# Place the turtle at [x, y]. The turtle does not draw when it changes
# position.
def xy=(pt)
validate_point(pt)
if pen_up?
@xy = pt
else
pen_up
@xy = pt
pen_down
end
@xy
end

# Set the turtle's heading to <degrees>.
def heading=(degrees)
validate_angle(degrees)
@heading = degrees % 360
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
@segment = nil
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
if pen_up?
@segment = [@xy.dup]
@track << @segment
end
end

# Is the pen up?
def pen_up?
not @segment
end

# Is the pen down?
def pen_down?
not pen_up?
end

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

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

# Turn right through the angle <degrees>.
def right(degrees)
validate_angle(degrees)
self.heading += degrees
end

# Turn left through the angle <degrees>.
def left(degrees)
validate_angle(degrees)
self.heading -= degrees
end

# Move forward by <steps> turtle steps.
def forward(steps)
validate_dist(steps)
go offset(steps)
end

# Move backward by <steps> turtle steps.
def back(steps)
validate_dist(steps)
go offset(-steps)
end

# Move to the given point.
def go(pt)
validate_point(pt)
@xy = pt
@segment << @xy if pen_down?
end

# Turn to face the given point.
def toward(pt)
validate_point(pt)
d = delta(pt)
self.heading = atan2(d[0], d[1]) / DEG
end

# Return the distance between the turtle and the given point.
def distance(pt)
validate_point(pt)
delta(pt).r
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

# Given a heading, build a unit vector in that direction.
def facing
rd = @heading * DEG
Vector[ sin(rd), cos(rd) ]
end

# Offset the current position in the direction of the current
# heading by the specified distance.
def offset(dist)
(Vector[*@xy] + (facing * dist)).to_a
end

# Build a delta vector to the specified point.
def delta(pt)
(Vector[*pt] - Vector[*@xy])
end

def validate_point(pt)
raise ArgumentError unless pt.is_a?(Array)
raise ArgumentError unless pt.size == 2
pt.each { |x| validate_dist(x) }
end

def validate_angle(deg)
raise ArgumentError unless deg.is_a?(Numeric)
end

def validate_dist(dist)
raise ArgumentError unless dist.is_a?(Numeric)
end

private :facing
private :eek:ffset
private :delta
private :validate_point
private :validate_angle
private :validate_dist
end
 
M

Morton Goldberg

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
 
P

Pete Yandell

Here's my solution.

Ok, a couple of comments.

First, your home method doesn't raise the pen as it should.

Second, all that flipping between turtle space angles and normal
angles is unnecessary. Just swap the x and y axes when doing the trig
and you'll get the right result.

Third, Ruby has an atan2 method that does most of what you do in your
toward method.

Fourth, your normalize_degrees method is overkill. Try '-10 % 360' in
irb.

Here's my solution:


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

# 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

# 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

# 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

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

# 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

# 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

# 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

# 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
 
E

Edwin Fine

Pete said:
Ok, a couple of comments.

First, your home method doesn't raise the pen as it should.

You're right. That's what I get for working when too tired... :(.
Second, all that flipping between turtle space angles and normal
angles is unnecessary. Just swap the x and y axes when doing the trig
and you'll get the right result.

I did the x-y swapping in an earlier version of the program, but I feel
the flipping is more intuitive for me. It also makes it easier for me to
see how angles change between turtle space and conventional space.
Third, Ruby has an atan2 method that does most of what you do in your
toward method.

You learn something new every day!
Fourth, your normalize_degrees method is overkill. Try '-10 % 360' in
irb.

Thanks for pointing that out. Language specifics are sometimes quite
subtle. This is why it's good to post to RubyQuiz - I learn to do things
in a better way. Thanks for your feedback.
 
M

Morton Goldberg

Morton Goldberg wrote:

/ ...


@heading %= 360


@heading %= 360 # same solution

In fact, now that I think about it, the entire block:

case
when @heading >= 360.0
@heading -= 360.0 while @heading >= 360.0
when @heading < 0.0
@heading += 360.0 while @heading < 0.0
end

can be replaced with:

@heading %= 360

Good catch. I had forgotten that %= existed.

Regards, Morton
 
E

Edwin Fine

Pete said:
# 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

What is the correct behavior if calling toward(pt) and @xy == pt. In
this case, atan2 returns 0.0 (North in turtle). This means that setting
the turtle to point to where it already is makes it always face North,
which seems wrong. I would think that this should be a no-op (heading
does not change).

irb(main):004:0> Math.atan2(0,0)
=> 0.0

Try this test case.

def test_toward
east = [100, 0]
@turtle.face east
assert_equal(90, @turtle.heading.round)
assert_nothing_raised { @turtle.face [0, 0] }
assert_equal(90, @turtle.heading.round)
end
 
M

Morton Goldberg

Pete said:
# 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

What is the correct behavior if calling toward(pt) and @xy == pt. In
this case, atan2 returns 0.0 (North in turtle). This means that
setting
the turtle to point to where it already is makes it always face North,
which seems wrong. I would think that this should be a no-op (heading
does not change).

irb(main):004:0> Math.atan2(0,0)
=> 0.0

Try this test case.

def test_toward
east = [100, 0]
@turtle.face east
assert_equal(90, @turtle.heading.round)
assert_nothing_raised { @turtle.face [0, 0] }
assert_equal(90, @turtle.heading.round)
end

You bring up a good point here. Commanding the turtle to face the
point where it's located is really an indeterminate operation. I
think there are three reasonable responses to such a command:

1. Raise an error (because an indeterminate operation should be
treated like 0/0).
2. Make it a no-op (as you suggest).
3. Accept the value returned by Math#atan2 (a show of faith in the C
math library :).

Philosophically, I favor the first response because I think this
situation would most likely arise from a programmer error. But it's
not an error that's commonly made. Also, in implementations
maintaining the turtle's location with floats, testing whether or not
@xy is the same as the argument given to toward/face is rather
expensive. So in practice, I take the lazy way out and go with the
atan2 flow.

However, I would not fault an implementation that goes one of the
other routes.

Regards, Morton
 
D

Dema

Thanks for pointing that out. I don't even know how all the sample
drawings were right with that huge bug in the code.

Here is the corrected version that passes your updated tests (sorry, it
took me so long to reply).

Seeing the other solutions I feel that mine is probably not the best,
but perhaps the most concise one. I tried to be very "economic" on the
line count.

(Please James, could you update my solution link on the rubyquiz site?)

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

# Place the turtle at [x, y]. The turtle does not draw when it
changes
# position.
def xy=(coords)
raise ArgumentError if !coords.is_a?(Array) ||
coords.size != 2 ||
coords.any? { |c| !c.is_a?(Numeric) }
@xy = coords
end

# Set the turtle's heading to <degrees>.
def heading=(degrees)
raise ArgumentError if !degrees.is_a?(Numeric)
set_heading(degrees)
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_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_down = true
end

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

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

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

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

# Turn right through the angle <degrees>.
def right(degrees)
set_heading(@heading + degrees)
end

# Turn left through the angle <degrees>.
def left(degrees)
set_heading(@heading - degrees)
end

# Move forward by <steps> turtle steps.
def forward(steps)
dx, dy = calc_delta(steps)
go [ @xy[0] + dx, @xy[1] + dy ]
end

# Move backward by <steps> turtle steps.
def back(steps)
dx, dy = calc_delta(steps)
go [ @xy[0] - dx, @xy[1] - dy ]
end

# Move to the given point.
def go(pt)
track << [ @xy, pt ] if pen_down?
@xy = pt
end

# Turn to face the given point.
def toward(pt)
set_heading 90.0 - atan2(pt[1] - @xy[1], pt[0] - @xy[0]) / DEG
end

# Return the distance between the turtle and the given point.
def distance(pt)
sqrt((@xy[0] - pt[0]) ** 2 + (@xy[1] - pt[1]) ** 2)
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
def set_heading(degrees)
@heading = degrees % 360
end

def calc_delta(steps)
[ sin(heading * DEG) * steps,
cos(heading * DEG) * steps ]
end
end

Morton said:
Here is my straight-to-the-point answer:

Your solution passes all the unit tests I supplied and is certainly
good enough to reproduce all the sample designs. So you have good
reason to think it's completely correct. However, one of the optional
methods has a problem.
# Turn to face the given point.
def toward(pt)
@heading = atan(pt[0].to_f / pt[1].to_f) / DEG
end

This won't work in all four quadrants.

I apologize for not providing tests good enough to detect the
problem. Here is one that will test all four quadrants.

<code>
# Test go, toward, and distance.
# Verify heading measures angles clockwise from north.
def test_coord_cmnds
nne = [100, 173]
@turtle.go nne
x, y = @turtle.xy
assert_equal(nne, [x.round, y.round])
@turtle.home
@turtle.run { pd; face nne; fd 200 }
assert_equal(30, @turtle.heading.round)
assert_equal([[[0, 0], nne]], snap(@turtle.track))
sse = [100, -173]
@turtle.home
@turtle.run { face sse; fd 200 }
assert_equal(150, @turtle.heading.round)
ssw = [-100, -173]
@turtle.home
@turtle.run { face ssw; fd 200 }
assert_equal(210, @turtle.heading.round)
nnw = [-100, 173]
@turtle.home
@turtle.run { face nnw; fd 200 }
assert_equal(330, @turtle.heading.round)
@turtle.home
assert_equal(500, @turtle.dist([400, 300]).round)
end
</code>

Regards, Morton
 
D

Dema

Hi folks,

Just for fun I implemented a quick and dirty version of
turtle_viewer.rb using Java/Swing. It must be run using JRuby 0.9.1.

Just put the file alongside turtle_viewer.rb and call:
jruby jturtle_viewer.rb

Here it is:
# jturtle_viewer.rb

require 'java'
require "lib/turtle"

class TurtleView
DEFAULT_FRAME = [[-200.0, 200.0], [200.0, -200.0]]

attr_accessor :frame

def initialize(turtle, canvas, frame=DEFAULT_FRAME)
@turtle = turtle
@canvas = canvas
@frame = frame
@turtles = []
end

def handle_map_event(w, h)
top_lf, btm_rt = frame
x0, y0 = top_lf
x1, y1 = btm_rt
@x_xform = make_xform(x0, x1, w)
@y_xform = make_xform(y0, y1, h)
end

def draw
g = @canvas.graphics
@turtle.track.each do |seqment|
if seqment.size > 1
pts = seqment.collect { |pt| transform(pt) }
g.drawLine(pts[0][0], pts[0][1], pts[1][0], pts[1][1])
end
end
end

def transform(turtle_pt)
x, y = turtle_pt
[@x_xform.call(x), @y_xform.call(y)]
end

private

def make_xform(u_min, u_max, v_max)
lambda { |u| v_max * (u - u_min) / (u_max - u_min) }
end

end

JFrame = javax.swing.JFrame
JPanel = javax.swing.JPanel
Dimension = java.awt.Dimension
BorderLayout = java.awt.BorderLayout

class TurtleViewer
def initialize(code)
@code = code

root = JFrame.new "Turtle Graphics Viewer"
@canvas = JPanel.new
root.get_content_pane.add @canvas, BorderLayout::CENTER
root.set_default_close_operation(JFrame::EXIT_ON_CLOSE)
root.set_preferred_size Dimension.new(440, 440)
root.set_resizable false
root.pack
root.set_visible true
run_code
end

def run_code
turtle = Turtle.new
view = TurtleView.new(turtle, @canvas)
view.handle_map_event(@canvas.width,
@canvas.height)
turtle.run(@code)
view.draw
end
end

# Commands to be run if no command line argument is given.
CIRCLE_DESIGN = <<CODE
def circle
pd; 90.times { fd 6; rt 4 }; pu
end
18.times { circle; rt 20 }
CODE

if ARGV.size > 0
code = open(ARGV[0]) { |f| f.read }
else
code = CIRCLE_DESIGN
end
TurtleViewer.new(code)


Ruby said:
The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Morton Goldberg

[Editor's Note: You can download the files for this quiz at:

http://rubyquiz.com/turtle.zip

--JEG2]

Turtle Graphics
===============

Turtle graphics is a form of computer graphics based on the ideas of turtle
geometry, a formulation of local (coordinate-free) geometry. As a brief
introduction to turtle graphics, I quote from [1]:

Imagine that you have control of a little creature called a turtle
that exists in a mathematical plane or, better yet, on a computer
display screen. The turtle can respond to a few simple commands:
FORWARD moves the turtle in the direction it is facing some
number of units. RIGHT rotates it clockwise in its place some
number of degrees. BACK and LEFT cause the opposite movements. ...
The turtle can leave a trace of the places it has been: [its
movements] can cause lines to appear on the screen. This is
controlled by the commands PENUP and PENDOWN. When the pen is
down, the turtle draws lines.

For example, the turtle commands to draw a square, 100 units on a side, can be
written (in a Ruby-ized form) as:

pen_down
4.times { forward 100; right 90 }

For more information, see [2] and [3].

This quiz is a bit different from most. If the usual Ruby quiz can be likened to
an essay exam, this one is a fill-in-the-blanks test. I'm supplying you with a
complete turtle graphics package, except -- to give you something to do -- I've
removed the method bodies from the key file, lib/turtle.rb. Your job is to
repair the damage I've done and make the package work again.

Turtle Commands
===============

There are quite a few turtle commands, but that doesn't mean you have to write a
lot of code to solve this quiz. Most of the commands can be implemented in a
couple of lines. It took me a lot longer to write a description of the commands
than it did for me to implement and test all of them.

I use the following format to describe turtle commands:

long_name | short_name <arg>
description ...
Example: ...

All turtle commands take either one argument or none, and not all turtle
commands have both a long name and a short name.

Required Commands
-----------------

These commands are required in the sense that they are needed to reproduce the
sample designs. Actually, you could get away without implementing 'back' and
'left', but implementing them is far easier than trying to write turtle code
without them.

pen_up | pu
Raises the turtle's pen. The turtle doesn't draw (lay down a visible
track) when its pen is up.

pen_down | pd
Lowers the turtle's pen. The turtle draws (lays down a visible track)
when its pen is down.

forward | fd <distance>
Moves the turtle forwards in the direction it is facing.
Example: forward(100) advances the turtle by 100 steps.

back | bk <distance>
Moves the turtle backwards along its line of motion.
back <distance> == forward -<distance>
Example: back(100) backs up the turtle by 100 steps.

right | rt <angle>
Turns the turtle clockwise by <angle> degrees.
Example: right(90) turns the turtle clockwise by a right angle.

left | lt <angle>
Turns the turtle counterclockwise by <angle> degrees.
left <angle> == right -<angle>
Example: left(45) turns the turtle counterclockwise by 45 degrees.

Traditional Commands
--------------------

These commands are not needed to reproduce any of the sample designs, but they
are found in all implementations of turtle graphics that I know of.

home
Places the turtle at the origin, facing north, with its pen up. The
turtle does not draw when it goes home.

clear
Homes the turtle and empties out it's track. Sending a turtle a clear
message essentially reinitializes it.

xy
Reports the turtle's location.
Example: Suppose the turtle is 10 turtle steps north and 15 turtle steps
west of the origin, then xy will return [-15.0, 10.0].

set_xy | xy= <point>
Places the turtle at <point>. The turtle does not draw when this command
is executed, not even if its pen is down. Returns <point>.
Example: Suppose the turtle is at [10.0, 20.0], then self.xy = [50, 80]
moves the turtle to [50.0, 80.0], but no line will drawn between the [10,
20] and [50, 80].

heading
Reports the direction in which the turtle is facing. Heading is measured
in degrees, clockwise from north.
Example: Suppose the turtle is at the origin facing the point [100, 200],
then heading will return 26.565 (approximately).

heading= | set_h <angle>
Sets the turtle's heading to <angle>. <angle> should be given in degrees,
measured clockwise from north. Returns <angle>.
Example: After self.heading = 135 (or set_h(135) which is easier to
write), the turtle will be facing southeast.

pen_up? | pu?
Reports true if the turtle's pen is up and false otherwise.

pen_down? | pd?
Reports true if the turtle's pen is down and false otherwise.

Optional Commands
-----------------

These commands are only found in some implementations of turtle graphics. When
they are implemented, they make the turtle capable of doing global (coordinate)
geometry in addition to local (coordinate-free) geometry.

I used one of these commands, go, to draw the mandala design (see
designs/mandala.tiff and samples/mandala.rb). If you choose not to implement the
optional commands, you might try writing a turtle program for drawing the
mandala design without using go. But, believe me, it is much easier to implement
go than to write such a program.

go <point>
Moves the turtle to <point>.
Example: Suppose the turtle is home (at the origin facing north). After
go([100, 200]), the turtle will be located at [100.0, 200.0] but will
still be facing north. If its pen was down, it will have drawn a line
from [0, 0] to [100, 200].

toward | face <point>
Turns the turtle to face <point>.
Example: Suppose the turtle is at the origin. After toward([100, 200]),
its heading will be 26.565 (approximately).

distance | dist <point>
Reports the distance between the turtle and <point>.
Example: Suppose the turtle is at the origin, then distance([400, 300])
will return 500.0 (approximately).

Interfacing to the Turtle Graphics Viewer
=========================================

Implementing turtle graphics without being able to view what the turtle draws
isn't much fun, so I'm providing a simple turtle graphics viewer. To interface
with the viewer, turtle instances must respond to the message track by returning
an array which the viewer can use to generate a line drawing.

The viewer expects the array returned by track to take the following form:

track ::= [segment, segment, ...] # drawing data
segment ::= [point, point, ...] # points to be joined by line segments
point ::= [x, y] # pair of floats

Example: [[[0.0, 0.0], [200.0, 200.0]], [[200.0, 0.0], [0.0, 200.0]]]

This represents an X located in the upper-right quadrant of the viewer; i.e.,
two line segments, one running from the center of the viewer up to its
upper-right corner and the other running from the center of the top edge down to
the center of the right edge.

[Editor's Note: I added a script to dump your turtle graphics output to PPM
image files, for those that don't have TK up and running. It works identically
to Morton's turtle_viewer.rb, save that it writes output to a PPM image file in
the current directory. For example, to output the included tree image, use
`ruby turtle_ppm_writer.rb samples/tree.rb`. --JEG2]

Unit Tests
==========

I'm including the unit tests which I developed to test turtle commands. For the
purposes of the quiz, you can ignore tests/turtle_view_test.rb. But I hope you
will find the other test suite, tests/turtle_test.rb, helpful. It tests every
one of the turtle commands described above as well as argument checking by the
commands. Don't hesitate to modify any of the unit tests to meet the needs of
your quiz solution.

References
==========

[1] Abelson, H. & A. diSessa, "Turtle Geometry", MIT Press, 1981.
[2] Harvey, B., "Computer Science Logo Style", Chapter 10.
http://www.cs.berkeley.edu/~bh/pdf/v1ch10.pdf
[3] Wikipedia, http://en.wikipedia.org/wiki/LOGO_programming_language
 
M

Morton Goldberg

Thanks for pointing that out. I don't even know how all the sample
drawings were right with that huge bug in the code.

A bug in Turtle#toward has no effect on reproducing the sample
designs because toward is not used in any of the sample turtle
scripts. Turtle#toward is not part of core turtle graphics -- it is
part of the optional non-local-geometry extensions found in some, but
by no means all, turtle graphic packages. Implementing toward is can
be considered an extra-credit exercise.
Morton said:
Your solution passes all the unit tests I supplied and is certainly
good enough to reproduce all the sample designs. So you have good
reason to think it's completely correct. However, one of the optional
methods has a problem.
# Turn to face the given point.
def toward(pt)
@heading = atan(pt[0].to_f / pt[1].to_f) / DEG
end

IMO, Turtle#toward is the most difficult turtle command to get fully
right. The version I posted as part of my solution is correct, but
it's not the best that can be done. A better implementation would
have been:

# 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(atan2(x2 - x1, y2 - y1) / DEG)
end

But that's not what I had when I wrote the quiz -- this version
incorporates an improvement I saw in Matthew Moss' solution.

Regards, Morton
 

Ask a Question

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

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,754
Messages
2,569,527
Members
44,998
Latest member
MarissaEub

Latest Threads

Top