EDIT: The answer ended up being quite long and I don't want you to forget that you did a really nice job with this program and, especially, in recognising that there probably was a better alternative than tripling the code, one piece per colour.
This is my take on a less repetitive version of the same code. Notice that my suggestions aren't necessarily the best, and you, and I, and other people that eventually chime in, might be able to design an even better program :)
Taking care of the state
You had the right idea: you wanted a container with all the turtles so you didn't have to have a variable for each one of them, but executing that is hard because things are interconnected and you need many of your functions to be aware of many things at once.
On a first iteration I was trying to redesign your code by keeping the three painters in a dictionary that would be then passed around the several functions, but I ended up going using OOP (object-oriented programming) because this is the type of situation where it comes in handy.
We create objects that are connected and related, and let them store their own relationships and etc.
Please notice that this is not the only way to go about doing this.
Your own turtle
First, we need our own turtles that are aware of themselves, so that their .ondrag
can know about their current position and etc.
import turtle
class Painter(turtle.Turtle):
def __init__(self, turtle_app, i, colour):
super().__init__()
self.turtle_app = turtle_app
self.up()
self.speed(0)
self.setheading(90)
self.color(colour)
self.pencolor('black')
self.shape('turtle')
self.shapesize(5, 5, 5)
self.goto(i, 0)
self.ondrag(self.draw)
def draw(self, x, y):
x = self.xcor()
self.goto(x, y)
self.turtle_app.update_screen_colour()
Notice that the __init__
method is the thing that initialises the turtle and does almost all of the things you had in your spawningPainters
method.
The things that are different are that the Painter
itself defines its own draw
method, and that it stores a reference to this turtle_app
which is the "main controller" of your app:
class TurtleApp:
def __init__(self):
self.spawn_painters()
def spawn_painters(self):
self.painters = {}
for i, colour in enumerate(["red", "green", "blue"], start=1):
self.painters[colour[0]] = Painter(self, i, colour)
def update_screen_colour(self):
r = max(min(self.painters["r"].ycor(), 255) , 0)
g = max(min(self.painters["g"].ycor(), 255), 0)
b = max(min(self.painters["b"].ycor(), 255), 0)
turtle.bgcolor(int(r), int(g), int(b))
def reset(self):
for painter in self.painters.values():
painter.goto(painter.xcor(), 0)
self.update_screen_colour()
The TurtleApp
object stores the painters in a dictionary indexed by the first letter of the colour and provides the helper methods you had to reset positions, update screen colour, etc.
Using a "proper main" like another answer suggested (and which is a good suggestion!) your code ends with
if __name__ == "__main__":
turtle.setworldcoordinates(0, 0, 4, 255)
turtle.colormode(255)
turtle.tracer(False)
turtle.bgcolor(0,0,0)
ta = TurtleApp()
turtle.onkeypress(ta.reset, 'c')
turtle.listen()
turtle.tracer(True)
turtle.mainloop()
Notice the two lines that I separated, where you create your TurtleApp
and then tell turtle
to call the TurtleApp.reset
if the user presses the "c"
key.
This has been tested and seems to be working as your original one was.
Remove irrelevant variables
Try to make your code as self-sufficient as possible.
You have dedicated variables to store the x
position of each painter, and that is perfectly fine, but notice that as soon as you initialise it, you can always query its x
position with painter.xcor()
, which you know because you did the same for y
with painter.ycor()
when updating the background colour.
Now, if you know the x
position of your painter, you can always prevent it from moving to the left or right by doing something like painter.goto(painter.xcor(), y)
in the draw
function, you don't need to keep the x_red
, x_blue
, and x_green
variables with you :)
Enumerate
enumerate
(which I used in the for
loop to initialise the painters) is really helpful when you need to iterate over a set of values (in my case, the colours) and also know their index (which you use to set the x
position in the beginning).
If you don't know enumerate
, you can read about it in the docs or in this tutorial. Notice that I also used the often-overlooked start
optional argument to tell enumerate
to start counting from 1
instead of 0
.
I did one other thing:
PEP 8 naming conventions
Python usually prefers snake_case
names, so that is why all my functions have capitalisation that differs from your original ones.
Final code proposal
Now everything together:
import turtle
class Painter(turtle.Turtle):
def __init__(self, turtle_app, i, colour):
super().__init__()
self.turtle_app = turtle_app
self.up()
self.speed(0)
self.setheading(90)
self.color(colour)
self.pencolor('black')
self.shape('turtle')
self.shapesize(5, 5, 5)
self.goto(i, 0)
self.ondrag(self.draw)
def draw(self, x, y):
x = self.xcor()
self.goto(x, y)
self.turtle_app.update_screen_colour()
class TurtleApp:
def __init__(self):
self.spawn_painters()
def spawn_painters(self):
self.painters = {}
for i, colour in enumerate(["red", "green", "blue"], start=1):
self.painters[colour[0]] = Painter(self, i, colour)
def update_screen_colour(self):
r = max(min(self.painters["r"].ycor(), 255) , 0)
g = max(min(self.painters["g"].ycor(), 255), 0)
b = max(min(self.painters["b"].ycor(), 255), 0)
turtle.bgcolor(int(r), int(g), int(b))
def reset(self):
for painter in self.painters.values():
painter.goto(painter.xcor(), 0)
self.update_screen_colour()
if __name__ == "__main__":
turtle.setworldcoordinates(0, 0, 4, 255)
turtle.colormode(255)
turtle.tracer(False)
turtle.bgcolor(0,0,0)
ta = TurtleApp()
turtle.onkeypress(ta.reset, 'c')
turtle.listen()
turtle.tracer(True)
turtle.mainloop()
```