# Small turtle program using instruction strings to draw patterns

A while ago, I wrote a program that combined L-systems and turtles. This was before I understood OOP (I still don't fully understand classes, or if they are even needed here). I tried to add some documentation and included some demos as functions. If I ever get around to it, I hope to include a simple GUI with user-adjustable patterns and parameters (like a typical Java applet).

Generate L-system

The inclusion of V, the alphabet, is for completeness in the formal description of a L-system as a 3-tuple.

import turtle

def l_system(V, w, P, n):
"""Generates an L-system run for n rounds.
They are defined as
G = (V, w, P)

V = The alphabet (tuple, not actually used, can be specified as None)
w = The start (string)
P = The production rules (dictionary for replacement)
"""
# Make sure all production rules are in alphabet
if V:
assert(all(key in V for key in P))

current = w
for i in range(n):
current = [P[x] if x in P else x for x in list(current)]
current = ''.join(current)

return current


Run the turtle

def run_turtle(var, start, rules, iters, angle, startdir=0):
"""Var, start, rules and iters, correspond to (V, w, P, n) of the
l-system function. The distance moved is scaled down from size.
The turtle starts facing startdir.

Instructions are defined as the following:
F, G: Draw forward
M, N: Move forward (don't draw)
[, ]: Push and pop angle and location
+, -: Turn left and right by angle degrees
Variables not described can be used as constants.
"""

# Initialization
terry = turtle.Turtle()
turtle.mode("world") # Coordinate system
terry.pensize(1)
terry.pencolor("blue")
terry.speed(0) # Instant speed
turtle.tracer(0, 0) # Don't draw anything yet (could change in future)
turtle.setup(width=900, height=900, startx=None, starty=None) # Square pixels
terry.hideturtle()

dist = 1
positions = []
angles = []
bounds = [0, 0, 0, 0] # llx, lly, urx, ury

instructions = l_system(var, start, rules, iters)
print("First 50 instructions:\n", instructions[:50])

def update_bounds(bounds):
coords = terry.position()

bounds[0] = min(bounds[0], coords[0])
bounds[1] = min(bounds[1], coords[1])
bounds[2] = max(bounds[2], coords[0])
bounds[3] = max(bounds[3], coords[1])

# Run turtle
terry.left(startdir) # Starting direction
for instr in instructions:
if instr in ('F', 'G'):
terry.forward(dist)
update_bounds(bounds)

elif instr in ('M', 'N'):
terry.penup()
terry.forward(dist)
terry.pendown()
update_bounds(bounds)

elif instr == '[':
positions.append(terry.pos())

elif instr == ']':
terry.penup()
terry.goto(positions.pop())
terry.pendown()

elif instr == '+':
terry.left(angle)

elif instr == '-':
terry.right(angle)

llx, lly, urx, ury = bounds
width = urx - llx
height = ury - lly

if width > height:
y_center = (ury + lly)/2
ury = y_center + width/2
lly = y_center - width/2
else:
x_center = (urx + llx)/2
urx = x_center + height/2
llx = x_center - height/2

print("Bounds:", bounds)
turtle.setworldcoordinates(llx, lly, urx, ury) # Redraw
turtle.update() # Draw everything
turtle.exitonclick()


Demo systems

Ideally this area could be cleaned up.

def right_koch(iters):
run_turtle(('F',), 'F', {'F':'F+F-F-F+F'}, iters, 90)

def dragon_curve(iters):
run_turtle(('X', 'Y'), 'FX', {'X':'X+YF', 'Y':'FX-Y'}, iters, 90)

def sierpinski(iters):
run_turtle(('F', 'G'), 'F', {'F':'G-F-G', 'G':'F+G+F'},  iters, 60)

def plant_1(iters):
run_turtle(('F', 'G'), 'F', {'G':'GG', 'F':'G[+F]-F'}, iters, 45, startdir=90)

def plant_2(iters):
run_turtle(('X', 'F'), 'X', {'X':'F-[[X]+X]+F[+FX]-X', 'F':'FF'},
iters=iters, angle=360-25, startdir=70)

def hilbert_curve(iters):
run_turtle(('A', 'B'), 'A', {'A':'-BF+AFA+FB-', 'B':'+AF-BFB-FA+'},
iters=iters, angle=90)

def koch_island(iters):
run_turtle(('F',), 'F-F-F-F', {'F':'F+FF-FF-F-F+F+FF-F-F+F+FF+FF-F'},
iters=iters, angle=90)

def square_koch(iters):
run_turtle(('F',), 'F-F-F-F', {'F':'FF-F-F-F-FF'}, iters, 90)

def plant_3(iters):
run_turtle(('F', 'X'), 'X', {'X':'F[+X]F[-X]+X', 'F':'FF'}, iters,
20, startdir=90)

def koch_burst(iters):
# Own design
run_turtle(('F'), 'F++F++F++F++F', {'F':'F+F--FF++F-F'}, iters, 72,
startdir=180)

koch_burst(4)


I like the concept of the question (using a domain-specific language to specify a fractal) and your implementation. An object-oriented rewrite could make it better, but it's not bad as it is.

l_system() could be better written as

def l_system(V, w, P, n):
…
cmd_seq = w
for _ in range(n):
cmd_seq = itertools.chain(*(P.get(cmd, cmd) for cmd in cmd_seq))
return ''.join(cmd_seq)


Namely,

• cmd_seq would be more meaningful than current. cmd would be more meaningful than x.
• Use _ for a variable whose value is disregarded.
• String are directly iterable; you don't have to convert them into lists first.
• Dictionary lookup with a default can be done using get(key, default).
• Instead of re-forming the string with each iteration, just chain them, and join the result just once at the end.

Aside from that, I just have a few minor remarks:

• The parameters are l_system(V, w, P, n) but run_turtle(var, start, rules, iters, …) — why not use consistent notation?
• As you said, the l_system() function doesn't really need V. I would prefer to just leave it out altogether.
• In various places, you refer to the turtle's heading as terry.heading(), startdir, and angles. I suggest using consistent terminology based on "heading".
• update_bounds(bounds) takes a bounds array as an explicit parameter, but takes terry through its scope chain. I'd prefer to see either both variables as explicit parameters, or both via the closure.
• Rather than having two separate stacks for positions and angles, I would prefer to see one turtle_state stack that stores (position, heading) tuples.
• The epilogue could use a comment, like # Extend the shorter dimension of the window to make it square
• Do you think the demo functions are fine the way they are, or could they be modified? – qwr Dec 17 '15 at 13:20
• One thing you could do is add another level of indirection, and put all those demo curve definitions in a dictionary or a config file. – 200_success Dec 17 '15 at 16:05

You seem to expect your var (or V) parameter to contain exactly P.keys().

First of, you could always check that using assert set(V) == set(P.keys()), which I find more understandable at first. Also note that assert is a keyword, not a function.

Second, and since you don't make use of V anyway, you could automate this check without having the user build V for you:

• You want every symbol found in start and each rewritten rule to be in "FCMN[]+-" + P.keys();
• [Optional] You want every symbol in P.keys() to appear in start or the rewritten rules so that there is no unused rules;
• [Optional] You want rules to operate on a single-character string, otherwise they won't be used.

You can get rid of both V and var, and check that, in l_system using:

assert all(len(R) == 1 for R in P), "Rules need to apply on 1 character only"
key_set = set(P.keys())
rules_set = set(itertools.chain(w, *P.values()))
assert key_set <= rules_set, "{} Rules Unused".format(key_set - rules_set)
key_set.update("FGMN[]+-")
assert rules_set <= key_set, "{} Symbols without Rules".format(rules_set - key_set)


Note this is just a rough sketch, you could wrap it in an if __debug__: (even if redundant with assert) to avoid building sets in optimized mode, you could turn some of them into warning messages with print("...", file=sys.stderr) and/or some of them into proper exceptions (possibly ValueError).

But the key point here is that you avoid requiring the user to provide two redundant parameters.