From: Edwin Fine Date: 2006-12-04T18:28:25+09:00 Subject: Re: tk.rb warning in Turtle Graphics (#104) 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. 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 . 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 . def right(degrees) h = self.heading + validate_degrees(degrees) self.heading = normalize_degrees(h) end # Turn left through the angle . def left(degrees) h = self.heading - validate_degrees(degrees) self.heading = normalize_degrees(h) end # Move forward by 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 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 --------------- Here's the extra test case: 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 -- Posted via http://www.ruby-forum.com/.