Search code examples
pythonstack-overflowturtle-graphics

Turtle program in Python crashes with stack overflow


I made a program to generate lines in random directions in Python turtle. The way I did it is very repetitive, and it also crashes. My code recurses through functions repetitively. I have found it always crashes at about the 3215th recursion. I don't know if that's relevant. I'm asking if anyone knows why it's crashing and how to stop it. When it crashes, the turtle graphics window and cmd window both randomly close. My code:

import turtle
import random
import sys

sys.setrecursionlimit(100000)

rminlength = 1
rmaxlength = 15
gminlength = 1
gmaxlength = 30
bminlength = 1
bmaxlength = 45

rminangle = 45
rmaxangle = 45
gminangle = 90
gmaxangle = 90
bminangle = 135
bmaxangle = 135

drawspeed = 10000
global recurse
recurse = 0

r = turtle.Turtle()
r.color('red')
r.pensize(3)
r.shape('circle')
r.speed(drawspeed)
r.hideturtle()

g = turtle.Turtle()
g.color('green')
g.pensize(3)
g.shape('circle')
g.speed(drawspeed)
g.hideturtle()

b = turtle.Turtle()
b.color('blue')
b.pensize(3)
b.shape('circle')
b.speed(drawspeed)
b.hideturtle()

#Movement

def rmove():
    if(random.randint(1,2) == 1):
        r.left(random.randint(rminangle,rmaxangle))
        if(random.randint(1,2) == 1):
            r.forward(random.randint(rminlength,rmaxlength))
        else:
            r.backward(random.randint(rminlength,rmaxlength))
    else:
        r.right(random.randint(rminangle,rmaxangle))
        if(random.randint(1,2) == 1):
            r.forward(random.randint(rminlength,rmaxlength))
        else:
            r.backward(random.randint(rminlength,rmaxlength))
    global recurse
    recurse+=1
    print(recurse)
    gmove()

def gmove():
    if(random.randint(1,2) == 1):
        g.left(random.randint(gminangle,gmaxangle))
        if(random.randint(1,2) == 1):
            g.forward(random.randint(gminlength,gmaxlength))
        else:
            g.backward(random.randint(gminlength,gmaxlength))
    else:
        g.right(random.randint(gminangle,gmaxangle))
        if(random.randint(1,2) == 1):
            g.forward(random.randint(gminlength,gmaxlength))
        else:
            g.backward(random.randint(gminlength,gmaxlength))
    global recurse
    recurse+=1
    print(recurse)
    bmove()

def bmove():
    if(random.randint(1,2) == 1):
        b.left(random.randint(bminangle,bmaxangle))
        if(random.randint(1,2) == 1):
            b.forward(random.randint(bminlength,bmaxlength))
        else:
            b.backward(random.randint(bminlength,bmaxlength))
    else:
        b.right(random.randint(bminangle,bmaxangle))
        if(random.randint(1,2) == 1):
            b.forward(random.randint(bminlength,bmaxlength))
        else:
            b.backward(random.randint(bminlength,bmaxlength))
    global recurse
    recurse+=1
    print(recurse)
    rmove()

rmove()
input('Crashed')

Solution

  • Whenever you call a function, you're using memory in a finite region of data allocated to the program called the stack. If these functions never resolve and continue calling other functions, the stack memory is never reclaimed. Eventually, your program runs out of memory. This is called a stack overflow.

    Here's the actual error message from your program:

      ...
      File "a.py", line 100, in bmove
        rmove()
      File "a.py", line 64, in rmove
        gmove()
      File "a.py", line 82, in gmove
        bmove()
      File "a.py", line 99, in bmove
        print(recurse)
    MemoryError: stack overflow
    

    You've attempted to solve the problem by increasing the recursion limit that was set by Python, but this only postpones the inevitable. Even if some calls did resolve, increasing this number is an unsafe way to write code because it makes assumptions about stack size rather than rewriting program logic to ensure calls resolve and that the stack doesn't grow out of control.

    Because recursion is unnecessary to obtain the sequential turtle movement you're going for, let's re-write your program to use loops rather than function calls to direct the turtles:

    import turtle
    import random
    import sys
    
    
    rminlength = 1
    rmaxlength = 15
    gminlength = 1
    gmaxlength = 30
    bminlength = 1
    bmaxlength = 45
    
    rminangle = 45
    rmaxangle = 45
    gminangle = 90
    gmaxangle = 90
    bminangle = 135
    bmaxangle = 135
    
    drawspeed = 10000
    
    r = turtle.Turtle()
    r.color('red')
    r.pensize(3)
    r.shape('circle')
    r.speed(drawspeed)
    r.hideturtle()
    
    g = turtle.Turtle()
    g.color('green')
    g.pensize(3)
    g.shape('circle')
    g.speed(drawspeed)
    g.hideturtle()
    
    b = turtle.Turtle()
    b.color('blue')
    b.pensize(3)
    b.shape('circle')
    b.speed(drawspeed)
    b.hideturtle()
    
    #Movement
    
    def rmove():
        if(random.randint(1,2) == 1):
            r.left(random.randint(rminangle,rmaxangle))
            if(random.randint(1,2) == 1):
                r.forward(random.randint(rminlength,rmaxlength))
            else:
                r.backward(random.randint(rminlength,rmaxlength))
        else:
            r.right(random.randint(rminangle,rmaxangle))
            if(random.randint(1,2) == 1):
                r.forward(random.randint(rminlength,rmaxlength))
            else:
                r.backward(random.randint(rminlength,rmaxlength))
    
    def gmove():
        if(random.randint(1,2) == 1):
            g.left(random.randint(gminangle,gmaxangle))
            if(random.randint(1,2) == 1):
                g.forward(random.randint(gminlength,gmaxlength))
            else:
                g.backward(random.randint(gminlength,gmaxlength))
        else:
            g.right(random.randint(gminangle,gmaxangle))
            if(random.randint(1,2) == 1):
                g.forward(random.randint(gminlength,gmaxlength))
            else:
                g.backward(random.randint(gminlength,gmaxlength))
    
    def bmove():
        if(random.randint(1,2) == 1):
            b.left(random.randint(bminangle,bmaxangle))
            if(random.randint(1,2) == 1):
                b.forward(random.randint(bminlength,bmaxlength))
            else:
                b.backward(random.randint(bminlength,bmaxlength))
        else:
            b.right(random.randint(bminangle,bmaxangle))
            if(random.randint(1,2) == 1):
                b.forward(random.randint(bminlength,bmaxlength))
            else:
                b.backward(random.randint(bminlength,bmaxlength))
    
    while 1: # loop infinitely
        rmove()
        gmove()
        bmove()
    

    Specifically, the recursive calls were removed and a while 1: infinite loop was added.


    As you mention, there is a lot of repetition in the code. Writing a class to encapsulate your turtle logic offers a significant cleanup opportunity and makes the program easily extensible to handle arbitrary numbers of additional turtles:

    import turtle
    from random import choice
    from random import randint
    
    
    class Turtle:
        def __init__(
            self, color, min_len, max_len, angle, speed=10, pensize=3
        ):
            self.min_len = min_len
            self.max_len = max_len
            self.angle = angle
            self.turt = turtle.Turtle()
            self.turt.color(color)
            self.turt.pensize(pensize)
            self.turt.speed(speed)
            self.turt.hideturtle()
    
        def move(self):
            choice((self.turt.left, self.turt.right))(self.angle)
            dir_func = choice((self.turt.forward, self.turt.backward))
            dir_func(randint(self.min_len, self.max_len))
    
    
    if __name__ == "__main__":
        turtles = [
            Turtle("red", 1, 15, 45),
            Turtle("green", 1, 30, 90),
            Turtle("blue", 1, 45, 135)
        ]
    
        while 1:
            for turt in turtles:
                turt.move()
    

    Happy turtling!