#!/usr/bin/python

"""Classes for creating worlds with Animals in them.

This module provides the following classes:

World, and its subclasses TurtleWorld, TurmiteWorld and AmoebaWorld.

Interpreter and Cell, which can belong to a World.

Animal and its subclasses Turtle, Turmite and Amoeba.


  Copyright 2005 Allen B. Downey

    This file contains wrapper classes I use with tkinter.  It is
    mostly for my own use; I don't support it, and it is not very
    well documented.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, see
    http://www.gnu.org/licenses/gpl.html or write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
    02110-1301 USA
    
"""


from Gui import *

import random
import time
import threading

class World(Gui):
    """the environment where Animals live.  A World usually
    includes a canvas where animals are drawn and a control panel.
    """
    def __init__(self, **kwds):
        Gui.__init__(self, **kwds)
        self.exists = True    # until the user presses Quit

        # list of animals that live in this world
        self.animals = []

        # the interpreter execute code the user types in the
        # control panel
        self.inter = Interpreter(self, top_globals, top_locals)
        
    def quit(self):
        # setting exists tells other threads that the world is gone
        self.exists = False
        Gui.quit(self)

    def register(self, animal):
        """add a new animal to the world"""
        self.animals.append(animal)

    def unregister(self, animal):
        """remove an animal from the world"""
        self.animals.remove(animal)

    def clear(self):
        """undraw and remove all the animals, and anything else
        on the canvas
        """
        for animal in self.animals:
            animal.undraw()
        self.animals = []
        self.canvas.delete('all')

    def step(self):
        """invoke the step method on every animal
        """
        for animal in self.animals:
            animal.step()
        
    def run(self):
        """invoke step intermittently until the user presses
        Quit or Stop
        """
        self.running = 1
        while self.exists and self.running:
            self.step()
            self.update()
            time.sleep(self.delay)

    def stop(self):
        """stop running"""
        self.running = 0

    def map_animals(self, callable):
        """apply the given callable to all animals"""
        map(callable, self.animals)
        
    def run_text(self):
        """get the code from the TextEntry widget in the control
        panel, and execute it"""
        source = self.te_code.get(1.0, END)
        self.inter.run_code(source, '<user-provided code>')

    def run_file(self):
        """read the code from the given file and run it
        """
        filename = self.en_file.get()
        fp = file(filename)
        source = fp.read()
        self.inter.run_code(source, filename)


class Animal:
    """this is an abstract class
    """
    def __init__(self, world):
        """each animal has a location (x, y) and a reference to the
        world it lives in
        """
        self.world = world
        self.x = 0
        self.y = 0
        self.delay = 0
        
    def step(self):
        """subclasses should override this method"""
        pass

    def draw(self):
        """subclasses should override this method"""
        pass

    def undraw(self):
        try:
            # delete the items on the canvas that have my tag...
            self.world.canvas.delete(self.tag)
        except AttributeError:
            # ...assuming the canvas exists
            pass

    def die(self):
        """remove this animal from the world and undraw it"""
        self.world.unregister(self)
        self.undraw()

    def redraw(self):
        """undraw and then redraw this animal"""
        self.undraw()
        self.draw()

    def update(self):
        """update the world and then sleep"""
        self.world.update()
        time.sleep(self.delay)

    def polar(self, x, y, r, theta):
        """convert polar coordinates (r, theta) to cartesian
        coordinates with the origin at x, y"""
        rad = theta * pi/180
        s = sin(rad)
        c = cos(rad)
        return [ x + r * c, y + r * s ]         


class Interpreter:
    """this object encapsulates the environment where user-provided
    code will execute
    """
    def __init__(self, world, gs=None, ls=None):
        self.world = world

        # if the caller didn't provide globals, use the current env
        if gs == None:
            self.globals = globals()
        else:
            self.globals = gs
            
        # if the caller didn't provide locals, use the current env
        if ls == None:
            self.locals = locals()
        else:
            self.locals = ls

        # make sure the environment contains a reference to the world
        self.globals['world'] = world

    def run_code_thread(self, *args):
        """run the given code in a new thread"""
        MyThread(self.run_code, *args)
        
    def run_code(self, source, filename):
        """run the given code in the saved environment"""
        code = compile(source, filename, 'exec')
        try:
            exec code in self.globals, self.locals
        except KeyboardInterrupt:
            self.world.quit()


class MyThread(threading.Thread):
    """this is a wrapper for threading.Thread that improves
    the syntax for creating and starting threads.
    """
    def __init__(self, target, *args):
        threading.Thread.__init__(self, target=target, args=args)
        self.start()



class TurtleWorld(World):

    def __init__(self):
        World.__init__(self)
        self.title('TurtleWorld')
        self.delay = 0.05           # time in seconds to sleep after an update
        self.setup()

    def setup(self):
        """create the GUI"""

        # canvas width and height
        self.ca_width = 400
        self.ca_height = 400

        self.canvas = self.ca(LEFT, width=self.ca_width, height=self.ca_height,
                              bg='white')

        # right frame
        self.fr(LEFT)

        # buttons
        options = dict(sticky=NSEW)

        self.fr(TOP, expand=0)
        self.gr(2, cweights=[1,1])
        self.bu(text='Print canvas', command=self.canvas.dump, **options)
        self.bu(text='Quit', command=self.quit, **options)
        self.bu(text='Make Turtle', command=self.make_turtle, **options)
        self.bu(text='Clear', command=self.clear, **options)
        self.endgr()
        self.endfr()

        # run this code
        self.fr(BOTTOM, expand=0)
        self.te_code = self.te(BOTTOM, fill=X, expand=1, height=10, width=25)
        self.te_code.insert(END, 'world.clear()\n')
        self.te_code.insert(END, 'bob = Turtle(world)\n')
        self.endfr()

        # run file
        self.fr(BOTTOM, expand=0)
        self.gr(2, cweights=[0,1])
        self.bu(LEFT, text='Run file', command=self.run_file, **options)
        self.en_file = self.en(LEFT, text='turtle_code.py', width=5, **options)

        self.la('')
        self.la('')

        self.bu(LEFT, text='Run code', command=self.run_text, **options)
        self.endgr()
        self.endfr()

        # space-filling frame
        self.fr(BOTTOM, fill=BOTH, expand=1)
        self.endfr()

        # leave the right frame open so that Turtles can add TurtleControls
        # self.endfr()

    def setup_run(self):
        """add a row of buttons for run, step, stop and clear"""
        options = dict(fill=X, expand=1)
        self.fr(**options)
        self.bu(LEFT, text='Run', command=self.run, **options)
        self.bu(LEFT, text='Stop', command=self.stop, **options)
        self.bu(LEFT, text='Step', command=self.step, **options)
        self.bu(LEFT, text='Clear', command=self.clear, **options)
        self.endfr()

    def make_turtle(self):
        """create a new turtle and corresponding controller"""
        turtle = Turtle(self)
        control = TurtleControl(turtle)
        turtle.control = control

    def clear(self):
        """undraw and remove all the animals, and anything else
        on the canvas
        """
        for animal in self.animals:
            animal.undraw()
            if hasattr(animal, 'control'):
                animal.control.frame.destroy()
                
        self.animals = []
        self.canvas.delete('all')


class Turtle(Animal):
    """a Turtle has a radius (r), heading, pen (True/False for
    active/inactive), and a color.
    """
    def __init__(self, world, delay=0.1):
        Animal.__init__(self, world)
        self.r = 5
        self.heading = 0
        self.pen = True
        self.color = 'red'
        self.delay = delay
        self.draw()
        world.register(self)

    def step(self):
        """default step behavior is forward one pixel"""
        self.fd()

    def draw_line(self, scale, dtheta, **options):
        """draw a line through the center of this turtle,
        with a dtheta angle from the turtle's heading,
        and length 2r"""
        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 draw(self):
        """draw the turtle"""
        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.update()

    def fd(self, dist=1):
        """move 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:
            self.world.canvas.line([p1, p2], fill='black')
        self.redraw()

    def bk(self, dist=1):
        """move the turtle backward by the given distance"""
        self.fd(-dist)

    def rt(self, angle=90):
        """turn right by the given angle"""
        self.heading = self.heading - angle
        self.redraw()

    def lt(self, angle=90):
        """turn left by the given angle"""
        self.heading = self.heading + angle
        self.redraw()

    def pd(self):
        """put the pen down (active)"""
        self.pen = True

    def pu(self):
        """put the pen up (inactive)"""
        self.pen = False

    def set_color(self, color):
        """change the color of the turtle and redraw.
        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()


class TurtleControl:
    """some turtles have a turtle control panel in the GUI"""

    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)
        w.la(text='Turtle Control')

        # forward and back (and the entry that says how far)
        w.fr(TOP)
        w.bu(LEFT, text='bk', command=Callable(self.move_turtle, -1))
        self.en_dist = w.en(LEFT, fill=NONE, expand=0, width=5, text='10')
        w.bu(LEFT, text='fd', command=self.move_turtle)
        w.endfr()

        # other buttons
        w.fr(TOP)
        w.bu(LEFT, text='lt', command=self.turtle.lt)
        w.bu(LEFT, text='rt', command=self.turtle.rt)
        w.bu(LEFT, text='pu', command=self.turtle.pu)
        w.bu(LEFT, text='pd', command=self.turtle.pd)

        colors = 'red', 'orange', 'yellow', 'green', 'blue', 'violet'

        # color menubutton
        w.la(LEFT, text='Color:')
        self.mb = w.mb(LEFT, text=colors[0])
        for color in colors:
            w.mi(self.mb, color, command=Callable(self.color, color))

        w.endfr()
        w.endfr()

    def color(self, color):
        """callback for the menu button: change 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):
        """callback for fd and bk buttons: read the entry and move
        the turtle.  sign should be +1 or -1 for fd or back."""
        dist = int(self.en_dist.get())
        self.turtle.fd(sign*dist)


class TurmiteWorld(World):
    """TurmiteWorld provides a grid of cells that Turmites occupy
    """
    def __init__(self):
        World.__init__(self)
        self.title('TurmiteWorld')
        self.delay = 0.0           # time in seconds to sleep after an update
        self.csize = 6             # cell size

        # cells is the mapping from indices to Cell objects
        self.cells = {}
        self.setup()

    def setup(self):
        self.ca_width = 600
        self.ca_height = 600

        # left frame
        self.fr(LEFT)
        self.canvas = self.ca(width=self.ca_width, height=self.ca_height,
                              bg='white')

        # put a swirly transform at the beginning of the list
        # (before the CanvasTransform)
        # self.canvas.add_transform(SwirlTransform(pi/400), 0)
        self.endfr()

        # right frame
        self.fr(LEFT, fill=BOTH, expand=1)

        options = dict(side=LEFT, fill=X, expand=1)

        self.fr(TOP)
        self.bu(text='Make Turmite', command=self.make_turmite, **options)
        self.bu(text='Print canvas', command=self.canvas.dump, **options)
        self.bu(text='Quit', command=self.quit, **options)
        self.endfr()

        # make the run and stop buttons
        self.fr(TOP)
        self.bu(text='Run', command=self.run, **options)
        self.bu(text='Stop', command=self.stop, **options)
        self.bu(text='Step', command=self.step, **options)
        self.bu(text='Clear', command=self.clear, **options)
        self.endfr()

        # space-filling frame
        self.fr(TOP, expand=1)
        self.endfr()

        # create the text entry for adding code
        self.te_code = self.te(BOTTOM, height=20, width=40)
        self.te_code.insert(END, 't1 = Turmite(world)\n')
        self.te_code.insert(END, 't2 = Turmite(world)\n')
        self.te_code.insert(END, 't3 = Turmite(world)\n')
        self.te_code.insert(END, 't2.lt()\n')
        self.te_code.insert(END, 't3.rt()\n')
        self.te_code.insert(END, 'world.run()\n')

        self.fr(BOTTOM, expand=0)
        self.bu(LEFT, text='Run code', command=self.run_text)
        self.endfr()

    def make_turmite(self):
        """callback for the Make Turmite button"""
        turmite = Turmite(self)

    def clear(self):
        """callback for the Clean button: remove all the animals and
        all the cells"""
        for animal in self.animals:
            animal.undraw()
        for cell in self.cells.values():
            cell.undraw()
        self.animals = []
        self.cells = {}


    def cell_bounds(self, i, j):
        """return the bounds of the cell with indices i, j"""
        p1 = [i, j]
        p2 = [i+1, j]
        p3 = [i+1, j+1]
        p4 = [i, j+1]
        bounds = [[p[0]*self.csize, p[1]*self.csize] for p in [p1, p2, p3, p4]]
        return bounds


    def get_cell(self, i, j):
        """get the cell at i, j, creating it if it doesn't already exist"""
        i, j = int(round(i)), int(round(j))
        cell = self.cells.get((i,j), None)
        
        if cell==None:
            bounds = self.cell_bounds(i, j)
            cell = Cell(self, bounds)
            self.cells[i,j] = cell
        return cell



class Cell:
    """represents a location in TurmiteWorld"""
    def __init__(self, world, bounds):
        self.world = world

        # options used for a marked cell
        self.marked_options = dict(fill='black', outline='gray80')

        # options used for an unmarked cell
        self.unmarked_options = dict(fill='yellow', outline='gray80')

        # draw the cell unmarked
        self.tag = world.canvas.polygon(bounds, **self.unmarked_options)
        self.marked = False

    def __del__(self):
        """when the cell is deleted, it undraws itself"""
        self.undraw()

    def undraw(self):
        """delete an items with this cell's tag"""
        self.world.canvas.delete(self.tag)
        
    def config_cell(self, **options):
        """configure this cell with the given options"""
        self.world.canvas.itemconfig(self.tag, **options)

    def cget_cell(self, x, y, option):
        """get the configuration of this cell"""
        return self.world.canvas.itemconfig(self.tag, option)

    def mark(self):
        """mark this cell and configure it"""
        self.marked = True
        self.config_cell(**self.marked_options)
        
    def unmark(self):
        """unmark this cell and configure it"""
        self.marked = False
        self.config_cell(**self.unmarked_options)
        
    def is_marked(self):
        return self.marked



class Turmite(Animal):

    def __init__(self, world):
        Animal.__init__(self, world)
        self.dir = 0
        # have to draw yourself before registering!
        self.draw()
        world.register(self)


    def draw(self):
        # get the bounds of the cell
        cell = self.get_cell()
        bounds = world.cell_bounds(self.x, self.y)

        # draw a triangle inside the cell, pointing in the
        # appropriate direction
        bounds = rotate(bounds, self.dir)
        mid = vmid(bounds[1], bounds[2])
        self.tag = self.world.canvas.polygon([bounds[0], mid,
                                                     bounds[3]], fill='red')

    def fd(self, dist=1):
        """move forward"""
        if self.dir==0:
            self.x += dist
        elif self.dir==1:
            self.y += dist
        elif self.dir==2:
            self.x -= dist
        else:
            self.y -=dist
        self.redraw()

    def bk(self, dist=1):
        """move back"""
        self.fd(-dist)

    def rt(self):
        """turn right"""
        self.dir = (self.dir-1) % 4
        self.redraw()

    def lt(self):
        """turn left"""
        self.dir = (self.dir+1) % 4
        self.redraw()

    def get_cell(self):
        """get the cell this turmite is on"""
        return self.world.get_cell(self.x, self.y)
        
    def mark(self):
        """mark the cell this turmite is on"""
        cell = self.get_cell()
        cell.mark()

    def unmark(self):
        """unmark the cell this turmite is on"""
        cell = self.get_cell()
        cell.unmark()

    def toggle(self):
        """flip the cell this turmite is on"""
        if self.is_on_mark():
            self.unmark()
        else:
            self.mark()

    def is_on_mark(self):
        """check whether the cell this turmite is on is marked"""
        cell = self.get_cell()
        return cell.is_marked()

    def step(self):
        """this step function implements the rules for Langton's
        Ant (see http://mathworld.wolfram.com/LangtonsAnt.html)
        """
        if self.is_on_mark():
            self.lt()
        else:
            self.rt()
        self.toggle()
        self.fd()


class AmoebaWorld(World):
    """AmoebaWorld is a microscope slide with hash marks where
    Amoebas trace parametric equations
    """

    def __init__(self):
        World.__init__(self)
        self.title('AmoebaWorld')
        self.ca_width = 400             # canvas width and height
        self.ca_height = 400
        self.animals = []               # a list of Amoebas
        self.thread = None

        # create the canvas
        self.fr(LEFT)
        self.canvas = self.ca(width=self.ca_width, height=self.ca_height,
                              bg='white', transforms=[])
        trans = CanvasTransform(self.canvas, [20, 20])
        self.canvas.add_transform(trans)
        self.endfr()

        # draw the grid
        dash = {True:'', False:'.'}
        (xmin, xmax) = (-10, 10)
        (ymin, ymax) = (-10, 10)
        for x in range(xmin, xmax+1, 1):
            self.canvas.line([[x, ymin], [x, ymax]], dash=dash[x==0])
        for y in range(ymin, ymax+1, 1):
            self.canvas.line([[xmin, y], [xmax, y]], dash=dash[y==0])

    def control_panel(self):
        self.fr(LEFT, fill=BOTH, expand=1)

        # run, stop, quit buttons
        self.gr(2, cweights=[1,1], expand=0)
        options = dict(sticky=NSEW)
        self.bu(text='Clear', command=self.clear, **options)
        self.bu(text='Quit', command=self.quit, **options)
        self.bu(text='Run', command=self.run_thread, **options)
        self.bu(text='Stop', command=self.stop, **options)
        self.endgr()
        self.la(TOP)

        # end time entry
        self.fr(fill=X, expand=0)
        self.la(LEFT, text='end time')
        self.en_end = self.en(LEFT, width=5, fill=X, expand=1, text='10')
        self.la(LEFT, text='seconds')
        self.endfr()
        self.la(TOP)

        # entries for x(t) and y(t)

        self.gr(2, cweights=[0,1], fill=X, expand=0)
        self.en_xoft = self.entry('x(t) = ')
        self.la('')
        self.la('')
        self.en_yoft = self.entry('y(t) = ')
        self.endgr()

        # space-filling frame
        self.fr(expand=1)
        self.endfr()

        self.endfr()

    def clear(self):
        """undraw and remove all the animals, and anything else
        on the canvas
        """
        for animal in self.animals:
            animal.undraw()
        self.canvas.delete('slime')

    def entry(self, label):
        """make the entries for the equations x(t) and y(t)
        """
        self.la(LEFT, text=label)
        entry = self.en(LEFT, width=5, sticky=EW, text=' t')
        return entry

    def run_thread(self):
        """execute AmoebaWorld.run in a new thread"""
        
        # if there is already a thread, kill it and wait for it to die
        if self.thread:
            self.running = 0
            self.thread.join()

        # find out how long to run
        end = self.en_end.get()
        end = int(end)

        # create a thread and start it
        self.thread = MyThread(self.run, end)

    def run(self, end=10):
        """count from 0 to end seconds in 0.1 second increments.
        At each step, compute the location of the Amoebas and update.
        """
        self.running = 1
        start_time = time.time()
        t = 0
        while self.exists and self.running and t < end:
            for amoeba in self.animals:
                x = amoeba.xoft(t)
                y = amoeba.yoft(t)
                print 't = %.1f   x = %.1f   y = %.1f' % (t, x, y)
                amoeba.redraw(x, y)
            time.sleep(0.1)
            t = time.time() - start_time
            
    def xoft(self, t):
        """evaluate the expression in the x(t) entry"""
        return eval(self.en_xoft.get())

    def yoft(self, t):
        """evaluate the expression in the y(t) entry"""
        return eval(self.en_yoft.get())

        
class Amoeba(Animal):
    """a soft, round animal that lives in AmoebaWorld"""
    
    def __init__(self, world, xoft=None, yoft=None):
        self.world = world

        # xoft and yoft are functions that compute the location
        # of the Amoeba as a function of time
        self.xoft = xoft or self.xoft
        self.yoft = yoft or self.yoft

        # size and color
        self.size = 0.5
        self.color1 = 'violet'
        self.color2 = 'medium orchid'
        world.register(self)

    def xoft(self, t):
        """a simple function that computes the Amoeba's x position"""
        return t

    def yoft(self, t):
        """a simple function that computes the Amoeba's y position"""
        return t

    # NOTE: the interfaces for draw and redraw are different from
    # other animals.  I pass x and y as parameters because I wanted
    # to avoid using attributes.  Students haven't seen attributes
    # yet when they work with AmoebaWorld.

    def redraw(self, x, y):
        """erase the Amoeba and redraw at location x, y"""
        self.undraw()
        self.draw(x, y)

    def draw(self, x, y):
        """draw the Amoeba"""

        # thetas is the sequence of angles used to compute the perimeter
        thetas = range(0, 360, 30)
        coords = self.poly_coords(x, y, thetas, self.size)

        self.tag = 'Amoeba%d' % id(self)
        slime = 'lavender'

        # draw the slime outline which will be left behind
        self.world.canvas.polygon(coords, fill=slime, outline=slime,
                                  tags='slime')

        # draw the outer perimeter
        self.world.canvas.polygon(coords,
            fill=self.color1, outline=self.color2, tags=self.tag)

        # draw the perimeter of the nucleus
        coords = self.poly_coords(x, y, thetas, self.size/2)
        self.world.canvas.polygon(coords,
            fill=self.color2, outline=self.color1, tags=self.tag)

    def poly_coords(self, x, y, thetas, size):
        """compute the coordinates of a polygon centered around x,y,
        with a radius of approximately size, but with random variation
        """
        rs = [size+random.uniform(0, size) for theta in thetas]
        coords = [self.polar(x, y, r, theta) for (r, theta) in zip(rs, thetas)]
        return coords


class GuiAmoeba(Amoeba):
    """there are two kinds of Amoebas: for a regular Amoeba, xoft
    and yoft are functions that compute coordinates as a function of
    time.  For a GuiAmoeba, xoft and yoft use methods from
    AmoebaWorld to read expressions for x(t) and y(t) from the GUI.
    """    
    def xoft(self, t):
        return self.world.xoft(t)

    def yoft(self, t):
        return self.world.yoft(t)


# the following are some random linear-algebra utilities
# written as functions (not methods)

def vadd(p1, p2):
    return [x+y for x,y in zip(p1, p2)]

def vscale(p, s):
    return [x*s for x in p]

def vmid(p1, p2):
    return vscale(vadd(p1, p2), 0.5)

def rotate(v, n=1):
    n %= len(v)
    return v[n:] + v[:n]

# add the turtle functions to the global dictionary
# 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

# top_globals and top_locals are used to create the
# Interpreter that executes user-provided code.
# It is a good idea to provide a top-level environment
# so that user-defined functions are top-level functions.

top_globals = globals()
top_locals = locals()

if __name__ == '__main__':
    world = TurtleWorld()
    world.mainloop()
