Velocity Reviews > Ruby > [QUIZ] Turtle Graphics (#104)

# [QUIZ] Turtle Graphics (#104)

Edwin Fine
Guest
Posts: n/a

 12-04-2006
Pete Yandell wrote:
> # 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_nothing_raised { @turtle.face [0, 0] }
end

--
Posted via http://www.ruby-forum.com/.

Morton Goldberg
Guest
Posts: n/a

 12-05-2006
On Dec 4, 2006, at 6:21 PM, Edwin Fine wrote:

> Pete Yandell wrote:
>> # 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_nothing_raised { @turtle.face [0, 0] }
> 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

Dema
Guest
Posts: n/a

 12-06-2006
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:I / 180.0

attr_accessor :track
alias run instance_eval

def initialize
clear
end

# 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>.
raise ArgumentError if !degrees.is_a?(Numeric)
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]
end

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

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

# Turn left through the angle <degrees>.
def left(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_xy xy=
alias face toward
alias dist distance

private
end

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

Morton Goldberg wrote:
> On Dec 3, 2006, at 8:30 PM, Dema wrote:
>
> > 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([[[0, 0], nne]], snap(@turtle.track))
> sse = [100, -173]
> @turtle.home
> @turtle.run { face sse; fd 200 }
> ssw = [-100, -173]
> @turtle.home
> @turtle.run { face ssw; fd 200 }
> nnw = [-100, 173]
> @turtle.home
> @turtle.run { face nnw; fd 200 }
> @turtle.home
> assert_equal(500, @turtle.dist([400, 300]).round)
> end
> </code>
>
> Regards, Morton

Dema
Guest
Posts: n/a

 12-06-2006
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.set_default_close_operation(JFrame::EXIT_ON_C LOSE)
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 Quiz wrote:
> 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
> 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 }
>
>
> 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.
>
> --------------------
>
> 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].
>
> 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).
>
> 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
>
> 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

Morton Goldberg
Guest
Posts: n/a

 12-06-2006
On Dec 5, 2006, at 9:00 PM, Dema wrote:

> 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 Goldberg wrote:
>
>> 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