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>
--
Posted via http://www.ruby-forum.com/.