Recursion with Turtles

Get to know the Turtle

We'll learn about a new graphics library today, Turtle Graphics.
We will write programs that control the turtle's movements. The turtle leaves a trail behind.

In [1]:
from turtle import *

setup(400,400, 0, 0) # open the Turtle graphics window
fd(100) # move turtle forward 100 pixels
In [2]:
lt(90)  # change direction by 90 degrees left
fd(100) # move forward 100 pixels
In [3]:
pensize(5)      # change pen size
pencolor('red') # change pen color
lt(90)
fd(100)

Your Turn: Complete the drawing of the square in the cell below.

In [4]:
# solution
lt(90)
fd(100)

Clear the screen for new drawings

In [5]:
# use clear() to erase the canvas
clear()

Notice that the turtle's properties (pen size and color haven't changed). If in addition to clearing
the drawing we want to return the turtle in its original state, use reset().

In [6]:
reset()

Here is a new example (shown in the slides):

In [7]:
fd(100)
lt(60)
shape('turtle')
pencolor('red')
fd(150)
rt(15)
pencolor('blue')
bk(100)
pu()
bk(50)
pd()
pensize(5)
bk(250)
pensize(1)
home()

If what you saw was to fast, we can change the speed with which the turtle moves, with
the method speed(). Let's add the line speed(1) at the start of the block above, and rerun the example.

In [2]:
clear()
reset()

Looping Turtles

Loops can be used in conjunction with turtles to make interesting designs.
Let's build together the function polygon(), which will produce different shapes as shown in slide 13-4.

In [3]:
def polygon(numSides, sideLength):
    '''Draws a polygon with the specified number 
     of sides, each with the specified length.'''
    # solution
    angle = 360.0/numSides   #angle to turn at each side
    for i in range(numSides):
        fd(sideLength)
        lt(angle)
In [10]:
# Draw a triangle, allow user to click on canvas to close window
# 0,0 sets the position of the canvas at the top-left corner
clear()
reset() 
polygon(3,100)
In [12]:
# Draw a square
clear()
reset() 
polygon(4,100)
In [14]:
# Draw a pentagon
clear()
reset() 
polygon(5,75)
In [16]:
# Draw an approximate circle
clear()
reset() 
polygon(100, 3)

In addition to the simple shapes we saw above, with loops we can create more interesting shapes too,
such as geometric flowers. Examples are shown in the slides on page 13-5.
Use the helper function polygon() that you defined previously.

In [31]:
def polyFlow(numPetals, petalSides, petalLen):
    '''Draws "flowers" with numPetals arranged around
    a center point. Each petal is a polygon with
    petalSides sides of length petalLen.'''
    # solution
    angle = 360.0/numPetals   #angle to turn after each petal
    for i in range(numPetals):
        polygon(petalSides, petalLen)
        lt(angle)
In [ ]:
# Example 1: square petals
clear()
reset() 
polyFlow(7,4,80)
In [33]:
# Example 2: pentagon petals
clear()
reset() 
polyFlow(10,5,75)
In [ ]:
# Example 3: hexagon petals
clear()
reset() 
polyFlow(11,6,60)

Spiraling Turtles: A recursion example

Let's try to write together the recursive function spiral(), which creates the images shown in the slide 13-6.

In [20]:
def spiral(sideLen, angle, scaleFactor, minLength):
    '''sideLen is the length of the current side;
       angle is the amount the turtle turns left to draw the next side;
       scaleFactor is the multiplicative factor by which to scale the next side 
       (it is between 0.0 and 1.0);
       minLength is the smallest side length that the turtle will draw.
    '''
    # solution
    if sideLen >= minLength:
        fd(sideLen)
        lt(angle)
        spiral(sideLen*scaleFactor, angle, scaleFactor, minLength)

To test the function with large values, we need to move the turtle from (0,0) in the center
of the canvas to some other point. However, whenever the turtle moves, it leaves a trail behind.
This is why we used the methods pu() and pd() (pen up and pen down).

To keep everything clean, we'll wrap it all in the function.

In [26]:
def drawSpiral(sideLen, angle, scaleFactor, minLength):
    clear()
    reset() 
    padding = 100
    width = sideLen + padding
    height = sideLen + padding
    pu()
    goto(-height/2+padding/2, -width/2+padding/2)
    pd()
    spiral(sideLen, angle, scaleFactor, minLength)
    
drawSpiral(625, 90, 0.7, 100)

Let's see the examples shown in slide 13-6:

In [28]:
drawSpiral(200,90,0.9,10)
In [35]:
drawSpiral(200,72,0.97,10)
In [30]:
drawSpiral(200,121,0.95,15)

Invariant Spiraling

A function is invariant relative to an object’s state if the state of the object is the same
before and after the function is invoked.

Let's write the following function together. It's very similar to spiral(), we just need to
undo the operations we did before the recursion.

In [38]:
def spiralBack(sideLen, angle, scaleFactor, minLength):
    '''Draws a spiral. The state of the turtle (position,
       direction) after drawing the spiral should be the 
       same as before drawing the spiral. '''
    # solution
    if sideLen >= minLength:
        fd(sideLen)
        lt(angle)
        spiralBack(sideLen*scaleFactor, angle, scaleFactor, minLength)
        pu()
        rt(angle)   
        bk(sideLen)
        pd()  # the last four lines retrace position without drawing
In [39]:
clear()
reset() 
spiralBack(200,95,0.93,10)

How would we modify spiralBack so it alternates colors on each line segment? Modify the function and then try it below.

In [ ]:
clear()
reset() 
spiralBack(200,95,0.93,10, 'red', 'blue')

Modify zigzag to become invariant

In [41]:
def zigzag(num, length):
    '''Draws the specified number of zigzags with the specified length.'''
    if num>0:
        lt(45)
        fd(length)
        rt(90)
        fd(length*2)
        lt(90)
        fd(length)
        rt(45)
        zigzag(num-1, length)
        # solution
        pu()
        lt(45)
        bk(length)
        rt(90)
        bk(length*2)
        lt(90)
        bk(length)
        rt(45)
        pd()    # re-trace steps to get to invariant
        
clear()
reset() 
zigzag(5,20)      

Koch curves and snowflakes

Define the koch and snowflake functions as indicated on the slides.

In [64]:
def koch(levels, size):
    # solution
    if levels == 0:
        fd(size)
    else:
        koch(levels-1, int(size/3))
        lt(60)
        koch(levels-1, int(size/3))
        rt(120)
        koch(levels-1, int(size/3))
        lt(60)
        koch(levels-1, int(size/3))
        
In [70]:
#invoke
clear()
reset() 
koch(2, 150)
In [72]:
def snowflake(levels, size):
    # solution: just make a triange of koch curves!
    for i in range(3):
        koch(levels, size)
        rt(120)
    
#invoke
clear()
reset() 
snowflake(2, 150) 

Trees

Now we will use the idea of invariant functions to create more sofisticated turtle designs,
again using recursion. Slide 13-40 shows some designs created with the trees() recursive function.

In [73]:
def tree(levels, trunkLen, angle, shrinkFactor):
    '''levels is the number of branches on any path from the root to a leaf
       trunkLen is the length of the base trunk of the tree
       angle is the angle from the trunk for each subtree
       shrinkFactor is the shrinking factor for each subtree'''
    # solution
    if levels > 0:
        fd(trunkLen)
        rt(angle)   # move into direction to draw right sub-tree
        tree(levels-1, trunkLen*shrinkFactor, angle, shrinkFactor)
        lt(angle*2)   # move into direction to draw left sub-tree
        tree(levels-1, trunkLen*shrinkFactor, angle, shrinkFactor)
        rt(angle)    # move into starting direction 
        pu()
        bk(trunkLen)   # move into starting position
        pd()
In [ ]:
clear()
reset() 
lt(90) # Face north first to grow the tree upwards
tree(7, 75, 30, 0.8)

Random Trees

In [ ]:
def treeRandom(trunkLen, minLength, thickness, minThickness, 
               minAngle, maxAngle, minShrink, maxShrink):
    if (trunkLen < minLength) or (thickness < minThickness): # Base case
        pass # Do nothing
    else:
        angle1 = random.uniform(minAngle, maxAngle)
        angle2 = random.uniform(minAngle, maxAngle)
        shrink1 = random.uniform(minShrink, maxShrink)
        shrink2 = random.uniform(minShrink, maxShrink)
        pensize(thickness)
        fd(trunkLen)
        rt(angle1)
        treeRandom(trunkLen*shrink1, minLength, thickness*shrink1,
                   minThickness, minAngle, maxAngle, minShrink, maxShrink)
        lt(angle1 + angle2)
        treeRandom(trunkLen*shrink2, minLength, thickness*shrink2, 
                   minThickness, minAngle, maxAngle, minShrink, maxShrink)
        rt(angle2)
        pensize(thickness)
        bk(trunkLen)
In [ ]:
clear()
reset() 
lt(90) # Face north first to grow the tree upwards
treeRandom(120, 3, 20, 1, 15, 55, 0.5, 0.8)
In [4]:
exitonclick()  # close canvas
In [ ]: