Python interpreter for turtle graphics (LOGO)¶
Do you remember LOGO - procedural language from the ‘60 to draw programatically? So here is an interpreter made in about 250 lines of Python. It recognizes the following commands:
- F n – move forward n steps,
- L n – turn left n degrees, n might be negative
- R n – turn right n degrees, n might be negative
- U – pen up
- D – pen down
- JUMP x y – turtle jump to the given location
- SLOWER n – make steps smaller by n
- FASTER n – make steps larger by n
- REPEAT n [commands] – repeat given commands n times
- DEF name := [commands] – define a procedure named name
The parameter n is mandatory. Commands in [] block must be separated with a semicolon. Only one instruction per
line is allowed except a [] block, which must appear in a single line.
I’ve created this demo for my “Object oriented programming and design” course. It’s purpose was to present object-oriented design patterns in a concise form, without distracting ourselves on details. Most notably, code parsing is quite simplified and there is no syntax error reporting. The program implements the following patterns:
- Command – each LOGO command is an object, derived from
TurtleCommandbase class- Factory – the commands are produced by
Logoclass; I omitted object makers class hierarchy here for clarity and used just lamda functions- Dispatch – the factory uses a dispatch (based on a Python dictonary) to join command names (strings) with respective makers (lambda expressions)
- Interpreter – well … it interprets the command, right? The
Turtleclass is used here as interpreter context- Strategy – the turtle can draw on anything that implements
GraphicDeviceinterface; the program provides also a basic strategy:EchoDevice- Bridge –
EchoDeviceis not very useful - it just prints output on the screen. This HTML page uses VisuaLife to make graphics.visualife.core.HtmlViewporthowever has different interface thanEchoDeviceThe natural way for turtle to draw a line is to use line_to(x, y) command since theTurtleclass know only its actual location;visualife.core.HtmlViewportclass on the other hand offers line_to(x1, y1, x2, y2) method.VisualifeBridgeclass translates fromGraphicDevicetoHtmlViewportinterface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | from math import sin, cos, pi
from browser import document
from visualife.core import HtmlViewport
class Turtle:
def __init__(self):
"""Default turtle constructor"""
self.x, self.y, self.a, self.pen, self.speed = 0, 0, 0, True, 1
def __str__(self): return "%d %d" % (self.x, self.y)
class TurtleCommand:
"""Base class for a turtle command"""
def __str__(self):
raise NotImplemented
def __call__(self, turtle, drawing):
raise NotImplemented
class R(TurtleCommand):
def __init__(self, angle):
"""Turn right by a given angle"""
self.angle = angle
def __call__(self, turtle, drawing):
turtle.a -= self.angle
def __str__(self):
return "R " + str(self.angle)
class L(TurtleCommand):
def __init__(self, angle):
"""Turn left by a given angle"""
self.angle = angle
def __call__(self, turtle, drawing):
turtle.a += self.angle
def __str__(self):
return "L " + str(self.angle)
class A(TurtleCommand):
def __init__(self, angle):
"""Set a given angle (in degrees)"""
self.angle = angle
def __call__(self, turtle, drawing):
turtle.a = self.angle
def __str__(self):
return "A " + str(self.angle)
class U(TurtleCommand):
def __call__(self, turtle, drawing):
turtle.pen = False
def __str__(self):
return "U"
class D(TurtleCommand):
def __call__(self, turtle, drawing):
turtle.pen = True
def __str__(self):
return "D"
class F(TurtleCommand):
def __init__(self, n_steps=1):
"""Go forward command"""
self.n_steps = n_steps
def __call__(self, turtle, drawing):
turtle.y += self.n_steps * sin(turtle.a / 180.0 * pi) * turtle.speed
turtle.x += self.n_steps * cos(turtle.a / 180.0 * pi) * turtle.speed
if turtle.pen:
drawing.line_to(turtle.x, turtle.y)
else:
drawing.move_to(turtle.x, turtle.y)
def __str__(self):
return "F " + str(self.n_steps) + "" if self.n_steps != 1 else "F"
class J(TurtleCommand):
def __init__(self, params):
"""Turtle jumps to an arbitrary position given in absolute coordinates"""
self.xy = [float(token) for token in params.split()]
def __call__(self, turtle, drawing):
turtle.x, turtle.y = self.xy[0], self.xy[1]
drawing.move_to(turtle.x, turtle.y)
def __str__(self):
return "J " + str(self.xy[0]) + " " + str(self.xy[1])
class Faster(TurtleCommand):
def __init__(self, f):
"""Increase turtle speed by a given value"""
self.f = f
def __call__(self, turtle, drawing):
turtle.speed += self.f
def __str__(self):
return "FASTER " + str(self.f)
class Slower(TurtleCommand):
def __init__(self, f):
"""Decrease turtle speed by a given value"""
self.f = f
def __call__(self, turtle, drawing):
turtle.speed += self.f
def __str__(self):
return "SLOWER " + str(self.f)
class Procedure(TurtleCommand):
def __init__(self, name):
"""Procedure is a group of commands"""
self.__commands = []
self.__name = name
def do_next(self, next_command):
self.__commands.append(next_command)
return self
def __call__(self, turtle, drawing):
for cmd in self.__commands:
cmd(turtle, drawing)
def __str__(self):
out = self.__name+" := ["
for cmd in self.__commands:
out += str(cmd)+" "
return out + "]"
class Repeat(TurtleCommand):
def __init__(self, n, cmd):
"""Repeats a command multiple times"""
self.__command = cmd
self.__n = n
@property
def n(self):
return self.__n
def __call__(self, turtle, drawing):
for i in range(self.__n):
self.__command(turtle, drawing)
def __str__(self):
return "REPEAT " + str(self.__n) + " " + str(self.__command)
class GraphicDevice:
def line_to(self, x, y):
raise NotImplemented
def move_to(self, x, y):
raise NotImplemented
class EchoDevice(GraphicDevice):
def line_to(self, x, y):
print("line to %.1f %.1f" %( x, y))
def move_to(self, x, y):
print("move to %.1f %.1f" %( x, y))
class Logo:
def __init__(self, device=EchoDevice()):
self.__makers = {}
self.__turtle = Turtle()
self.__device = device
self.add_command("R", lambda angle: R(float(angle)))
self.add_command("L", lambda angle: L(float(angle)))
self.add_command("A", lambda angle: A(float(angle)))
self.add_command("F", lambda steps: F(float(steps)))
self.add_command("FASTER", lambda value: Faster(float(value)))
self.add_command("SLOWER", lambda value: Slower(float(value)))
self.add_command("U", U())
self.add_command("D", D())
self.add_command("J", lambda value: J(value))
self.add_command("JUMP", lambda value: J(value))
self.add_command("DEF", self.make_procedure)
self.add_command("REPEAT", self.make_repeat)
self.__loop_cnt = 0
@property
def turtle(self): return self.__turtle
def draw(self, *cmds):
for ci in cmds:
if isinstance(ci, str): # ---------- If it's a string, use factory to produce a command
cmd = self.produce_command(ci)
if not ci.startswith("DEF") and cmd: # ---------- Execute, if it's not a definition
cmd(self.__turtle, self.__device)
elif ci: # ---------- Execute a command
if ci: ci(self.__turtle, self.__device)
def add_command(self, name, cmd_maker):
self.__makers[name] = cmd_maker
def make_procedure(self, procedure_string):
name = procedure_string.split(maxsplit=1)[0]
p = Procedure(name)
cmnds = procedure_string[procedure_string.find('[')+1:procedure_string.find(']')].split(";")
for cmd in cmnds:
p.do_next(self.produce_command(cmd))
self.__makers[name] = p # ---------- here we register the new procedure in this factory
return p
def make_repeat(self, repeat_string):
tokens = repeat_string.split(maxsplit=1)
if tokens[1].find('[') >= 0:
tokens[1] = "DEF loop_" + str(self.__loop_cnt) + " := " + tokens[1]
self.__loop_cnt += 1
return Repeat(int(tokens[0]), self.produce_command(tokens[1]))
def produce_command(self, a_string):
if a_string.strip() == "" or a_string[0] == '#': return None
tokens = a_string.split(maxsplit=1)
if isinstance(self.__makers[tokens[0]], TurtleCommand):
return self.__makers[tokens[0].strip()]
else:
return self.__makers[tokens[0].strip()](*tokens[1:])
class VisualifeBridge(GraphicDevice):
def __init__(self, viewport):
self.__viewport = viewport
self.__x, self.__y = 0, 0
self.__n_lines = 0
def line_to(self, x, y):
self.__viewport.line("line-%d" % self.__n_lines, self.__x, self.__y, x, y)
self.__n_lines += 1
self.__x, self.__y = x, y
def move_to(self, x, y):
self.__x, self.__y = x, y
def run(evt=None):
drawing.clear()
logo = Logo(VisualifeBridge(drawing))
logo.turtle.speed = 2
commands = document["logo1"].value.splitlines()
logo.draw(*commands)
drawing.close()
def load(evt):
name = evt.target.id
document["logo1"].value = programs[name]
run()
programs = {}
programs["dandelion"] = """
JUMP 1 1
REPEAT 4 [F 199; R -90]
JUMP 160 160
DEF one_side := [F 10; R 90]
DEF square := [REPEAT 4 one_side]
REPEAT 36 [R 10; square]
A 80
F 100
A -75
F 110
SLOWER 0.1
REPEAT 36 [R 10; square]
JUMP 10 360
FASTER 0.2
DEF diamond := [F 3; L 20; F 3; L 160; F 3; L 20; F 3]
REPEAT 20 [A -130; diamond; A -80; diamond; A 0; F 7]
"""
programs["spiral"] = """
JUMP 0 0
REPEAT 4 [F 194; R -90]
JUMP 200 200
DEF one_side := [F 8.5; R 91]
DEF square := [REPEAT 4 one_side]
REPEAT 200 [R 5; square; FASTER 0.09]
"""
programs["stars"] = """
JUMP 0 0
REPEAT 4 [F 194; R -90]
JUMP 200 200
REPEAT 126 [F 50; R 154]
JUMP 44 100
REPEAT 120 [F 50; R 134]
JUMP 125 240
REPEAT 120 [F 50; R 124; FASTER 0.007]
"""
drawing = HtmlViewport(document['svg'], 400, 400)
drawing.style = "stroke:black; stroke-width: 0.25;"
document["dandelion"].bind("click", load)
document["spiral"].bind("click", load)
document["stars"].bind("click", load)
document["redraw"].bind("click", run)
document["logo1"].value = programs["dandelion"]
run()
|