"""This module is part of Swampy, a suite of programs available from allendowney.com/swampy. Copyright 2010 Allen B. Downey Distributed under the GNU General Public License at gnu.org/licenses/gpl.html. """ from Tkinter import TOP, BOTTOM, LEFT, RIGHT, END, LAST, NONE, SUNKEN from Gui import Callable from World import World, Animal, wait_for_user class TurtleWorld(World): """An environment for Turtles and TurtleControls.""" def __init__(self, interactive=False): World.__init__(self) self.title('TurtleWorld') # the interpreter executes user-provided code g = globals() g['world'] = self self.make_interpreter(g) # make the GUI self.setup() if interactive: self.setup_interactive() def setup(self): """Create the GUI.""" # canvas width and height self.ca_width = 400 self.ca_height = 400 self.row() self.canvas = self.ca(width=self.ca_width, height=self.ca_height, bg='white') def setup_interactive(self): """Creates the right frame with the buttons for interactive mode.""" # right frame self.fr() self.gr(2, [1,1], [1,1], expand=0) self.bu(text='Print canvas', command=self.canvas.dump) self.bu(text='Quit', command=self.quit) self.bu(text='Make Turtle', command=self.make_turtle) self.bu(text='Clear', command=self.clear) self.endgr() # run this code self.bu(side=BOTTOM, text='Run code', command=self.run_text, expand=0) self.fr(side=BOTTOM) self.te_code = self.te(height=10, width=25, side=BOTTOM) self.te_code.insert(END, 'world.clear()\n') self.te_code.insert(END, 'bob = Turtle()\n') self.endfr() # run file self.row([0,1], pady=30, side=BOTTOM, expand=0) self.bu(side=LEFT, text='Run file', command=self.run_file) self.en_file = self.en(side=LEFT, text='turtle_code.py', width=5) self.endrow() # leave the right frame open so that Turtles can add TurtleControls # self.endfr() def setup_run(self): """Adds a row of buttons for run, step, stop and clear.""" self.gr(2, [1,1], [1,1], expand=0) self.bu(text='Run', command=self.run) self.bu(text='Stop', command=self.stop) self.bu(text='Step', command=self.step) self.bu(text='Quit', command=self.quit) self.endgr() def make_turtle(self): """Creates a new turtle and corresponding controller.""" turtle = Turtle(self) control = TurtleControl(turtle) turtle.control = control return control def clear(self): """Undraws and remove all the animals, clears the canvas. Also removes any control panels. """ for animal in self.animals: animal.undraw() if hasattr(animal, 'control'): animal.control.frame.destroy() self.animals = [] self.canvas.delete('all') class Turtle(Animal): """Represents a Turtle in a TurtleWorld. Attributes: x: position (inherited from Animal) y: position (inherited from Animal) r: radius of shell heading: what direction the turtle is facing, in degrees. 0 is east. pen: boolean, whether the pen is down color: string turtle color """ def __init__(self, world=None): Animal.__init__(self, world) self.r = 5 self.heading = 0 self.pen = True self.color = 'red' self.pen_color = 'blue' self.draw() def get_x(self): """Returns the current x coordinate.""" return self.x def get_y(self): """Returns the current y coordinate.""" return self.y def get_heading(self): """Returns the current heading in degrees. 0 is east.""" return self.heading def step(self): """Takes a step. Default step behavior is forward one pixel. """ self.fd() def draw(self): """Draws the turtle.""" if not self.world: return self.tag = 'Turtle%d' % id(self) lw = self.r/2 # draw the line that makes the head and tail self._draw_line(2.5, 0, tags=self.tag, width=lw, arrow=LAST) # draw the diagonal that makes two feet self._draw_line(1.8, 40, tags=self.tag, width=lw) # draw the diagonal that makes the other two feet self._draw_line(1.8, -40, tags=self.tag, width=lw) # draw the shell self.world.canvas.circle([self.x, self.y], self.r, self.color, tags=self.tag) self.world.sleep() def _draw_line(self, scale, dtheta, **options): """Draws the lines that make the feet, head and tail. Args: scale: length of the line relative to self.r dtheta: angle of the line relative to self.heading """ r = scale * self.r theta = self.heading + dtheta head = self.polar(self.x, self.y, r, theta) tail = self.polar(self.x, self.y, -r, theta) self.world.canvas.line([tail, head], **options) def fd(self, dist=1): """Moves the turtle foward by the given distance.""" x, y = self.x, self.y p1 = [x, y] p2 = self.polar(x, y, dist, self.heading) self.x, self.y = p2 # if the pen is down, draw a line if self.pen and self.world.exists: self.world.canvas.line([p1, p2], fill=self.pen_color) self.redraw() def bk(self, dist=1): """Moves the turtle backward by the given distance.""" self.fd(-dist) def rt(self, angle=90): """Turns right by the given angle.""" self.heading = self.heading - angle self.redraw() def lt(self, angle=90): """Turns left by the given angle.""" self.heading = self.heading + angle self.redraw() def pd(self): """Puts the pen down (active).""" self.pen = True def pu(self): """Puts the pen up (inactive).""" self.pen = False def set_color(self, color): """Changes the color of the turtle. Note that changing the color attribute doesn't change the turtle on the canvas until redraw is invoked. One way to address that would be to make color a property. """ self.color = color self.redraw() def set_pen_color(self, color): """Changes the pen color of the turtle.""" self.pen_color = color """Add the turtle methods to the module namespace so they can be invoked as simple functions (not methods). """ fd = Turtle.fd bk = Turtle.bk lt = Turtle.lt rt = Turtle.rt pu = Turtle.pu pd = Turtle.pd die = Turtle.die set_color = Turtle.set_color set_pen_color = Turtle.set_pen_color class TurtleControl(object): """Represents the control panel for a turtle. Some turtles have a turtle control panel in the GUI, but not all; it depends on how they were created. """ def __init__(self, turtle): self.turtle = turtle self.setup() def setup(self): w = self.turtle.world self.frame = w.fr(bd=2, relief=SUNKEN, padx=1, pady=1, expand=0) w.la(text='Turtle Control') # forward and back (and the entry that says how far) w.fr(side=TOP) w.bu(side=LEFT, text='bk', command=Callable(self.move_turtle, -1)) self.en_dist = w.en(side=LEFT, fill=NONE, expand=0, width=5, text='10') w.bu(side=LEFT, text='fd', command=self.move_turtle) w.endfr() # other buttons w.fr(side=TOP) w.bu(side=LEFT, text='lt', command=self.turtle.lt) w.bu(side=LEFT, text='rt', command=self.turtle.rt) w.bu(side=LEFT, text='pu', command=self.turtle.pu) w.bu(side=LEFT, text='pd', command=self.turtle.pd) w.endfr() # color menubutton colors = 'red', 'orange', 'yellow', 'green', 'blue', 'violet' w.row([0,1]) w.la('Color:') self.mb = w.mb(text=colors[0]) for color in colors: w.mi(self.mb, text=color, command=Callable(self.set_color, color)) w.endrow() w.endfr() def set_color(self, color): """Changes the color of the turtle and the text on the button.""" self.mb.config(text=color) self.turtle.set_color(color) def move_turtle(self, sign=1): """Reads the entry and moves the turtle. Args: sign: +1 for fd or -1 for back. """ dist = int(self.en_dist.get()) self.turtle.fd(sign*dist) if __name__ == '__main__': tw = TurtleWorld(interactive=True) tw.wait_for_user()