I've made a controller for Python's Turtle. The controller essentially defines an 'instruction set' so that commands can be recorded in a text file and then executed sequentially by the turtle. It acts similarly to (underlying) assembly code in that it executes these instructions in order and can also do simple iteration, and has its own program counter.
This is the Turtle class:
import turtle
class Turtle(object):
def __init__(self, command_filename="", turtle_name="Terry", speed=6):
"""Initialise turtle with filename, name and a speed."""
self.mrTurtle = turtle.Turtle()
self.set_speed(speed)
self.set_name(turtle_name)
if command_filename:
self.load_commands(command_filename)
self.pc = 0
self.loop_counters = {}
def load_commands(self, filename):
"""Load the command file and set self.commands"""
lines_out = []
try:
with open(filename, "r") as inFile:
for line in inFile.readlines():
comment = line.find(';')
if comment != -1:
line = line[:comment]
line = line.strip()
if len(line) > 0:
lines_out.append(line)
except FileNotFoundError:
print(f"File {filename} not found.")
if lines_out[0].startswith("name"):
self.set_name(lines_out[0].split()[1])
lines_out = lines_out[1:]
self.commands = lines_out
self.queue = lines_out
def Run(self):
"""Run all commands in buffer"""
print(f"{self.name} is ready to go!")
self.pc = 0
while self.pc < len(self.commands):
self.run_command(self.commands[self.pc])
self.pc += 1
print(f"{self.name} is finished!")
def Step(self, steps=1):
"""Step through specified number of commands. 1 if no number specified."""
for i in range(steps):
try:
self.run_command(self.commands[self.pc])
self.pc += 1
except IndexError:
self.pc = 0
self.run_command(self.commands[self.pc])
self.pc += 1
def StepThrough(self):
"""Run all commands, but wait for input between each command."""
self.pc = 0
while self.pc < len(self.commands):
input("Press Enter.")
self.run_command(self.commands[self.pc])
self.pc += 1
def queue_command(self, command):
"""Add a command to the buffer"""
self.commands.append(command)
def run_command(self, command):
"""Send 1 command to turtle. Big switch case handles command."""
print(f"{self.name} is trying {command}")
command = command.lower()
try:
command = command.split(" ")
if len(command) > 1:
data = [convert(item) for item in command[1:]]
else:
data = None
command = command[0]
except:
print(f"Error: {self.name} could not perform command {command}")
return None
if command in ["forward", "forwards", "f", "fwd"]:
self.forward(data[0])
elif command in ["backward", "backwards", "back", "b", "bck"]:
self.backward(data[0])
elif command in ["left", "l", "lft"]:
self.turn(-data[0])
elif command in ["right", "r", "turn", "rgt", "trn"]:
self.turn(data[0])
elif command in ["move", "goto", "mv", "mov"]:
self.set_position(data)
elif command in ["reset", "origin", "centre", "cnt"]:
self.set_position([0, 0])
elif command in ["circle", "crc"]:
self.circle(data)
elif command in ["stamp", "print", "prt", "smp"]:
self.stamp()
elif command in ["undo", "und"]:
if data is None:
self.undo(1)
else:
self.undo(data[0])
elif command in ["face", "setheading", "fac"]:
self.face(data[0])
elif command in ["loop", "startloop", "lop"]:
if data[1] in self.loop_counters:
pass
else:
self.loop_counters[data[1]] = [data[0], self.pc]
elif command in ["endloop", "end"]:
loop_name = data[0]
loop_data = self.loop_counters[loop_name]
if loop_data[0] > 1:
self.pc = loop_data[1]
self.loop_counters[loop_name][0] -= 1
else:
del self.loop_counters[loop_name]
elif command in ["colour", "color", "setcolour", "clr"]:
self.colour(data)
elif command in ["penup", "up"]:
self.penup()
elif command in ["pendown"]:
self.pendown()
elif command in ["pen"]:
self.switchpen()
else:
print(f"{self.name} doesn't know how to do {command}")
def forward(self, data):
"""Go forwards a certain amount."""
self.mrTurtle.forward(data)
def backward(self, data):
"""Go backwards"""
self.mrTurtle.backward(data)
def turn(self, data):
"""Turn clockwise specified amount in degrees. Commands 'left' and 'right' both
map to this function. Commands for 'left' are made negative."""
self.mrTurtle.right(data)
def face(self, data):
"""Face a certain heading."""
self.mrTurtle.setheading(data)
def set_position(self, data):
"""Move turtle to coordinates"""
x = data[0]
y = data[1]
self.mrTurtle.setposition(x, y)
def circle(self, data):
"""Draw a circle with the built in turtle circle function."""
radius = data[0]
extent = None
steps = None
if len(data) == 2:
extent = data[1]
if len(data) == 3:
extent = data[1]
steps = data[2]
self.mrTurtle.circle(radius, extent, steps)
def colour(self, data):
"""Modify turtle colour."""
if len(data) == 1:
data = data[0]
elif len(data) == 3:
data = (data[0], data[1], data[2])
else:
return None
self.mrTurtle.colour(data)
def penup(self):
"""Pen up."""
self.mrTurtle.penup()
def pendown(self):
"""Pen down."""
self.mrTurtle.pendown()
def switchpen(self):
"""Flip pen state."""
if self.mrTurtle.isdown():
self.mrTurtle.penup()
else:
self.mrTurtle.pendown()
def stamp(self):
"""Stamp a copy of turtle."""
self.mrTurtle.stamp()
def undo(self, num):
"""Undo specified number of commands"""
for i in range(num):
self.mrTurtle.undo()
def set_speed(self, speed):
"""Set the movement speed of turtle."""
self.mrTurtle.speed(speed)
def set_name(self, name):
"""Set the turtle name"""
self.name = name
def done(self):
"""Forces window to stay open after completion. Function for runturtle.py. No
more commands can be executed after this one."""
turtle.done()
def convert(input):
"""
Converts a string to an int or float.
Converts a list of strings into a list of strings, ints or floats.
Args:
input (str, list[str]): string or list of strings to attempt to convert.
Returns:
(str, int, float, list[...]): Depending on data contained in input.
"""
if isinstance(input, str):
input = [input]
converted = []
for item in input:
try:
if "." in item:
converted.append(float(item))
else:
converted.append(int(item))
except ValueError:
converted.append(item)
if len(converted) == 1:
converted = converted[0]
return converted
Here is what an example of a command file looks like:
name Loopy
loop 10 top ;loops MUST be named at both the top and the bottom of the loop
forwards 40
loop 4 square ;loop syntax is "loop [number of iterations] [loop name]"
left 90
forwards 10
endloop square
left 36
endloop top
My code is contained in this GitHub repo if you want to download and run it. It comes with a second script that can be run from the command line. There are also example files for the turtle that show some of the exciting ways it can be used.
I know a fat elif isn't the best way to go here, as this will be a nightmare to extend and maintain. What are some better options for how to implement this?
I also want to extend this, so it is more of a complete language. I want to implement while loops, some form of memory (likely just storing integers), and selection. What would be the right way of doing this?
If you think of any other features, additions or changes, I would like to hear those too :)