Python Turtle `While True` in Event Driven Environment - python

I have read in several posts here on Stack Overflow that "An event-driven environment like turtle should never have while True: as it potentially blocks out events (e.g. keyboard)."
Here is a Python Turtle program that seems to work fine, but which uses the while True: construct.
Could someone please explain why this approach is wrong, what problems is creates and what the correct way is to achieve the same result?
import turtle
import time
def move_snake():
"""
This function updates the position of the snake's head according to its direction.
"""
if head.direction == "up":
head.sety(head.ycor() + 20)
def go_up():
"""
callback for up key.
"""
if head.direction != "down":
head.direction = "up"
# Set up screen
screen = turtle.Screen()
screen.tracer(0) # Disable animation so we can update screen manually.
# Event handlers
screen.listen()
screen.onkey(go_up, "Up")
# Snake head
head = turtle.Turtle()
head.shape("square")
head.penup()
head.direction = "stopped" # Cheeky use of instance property to avoid global variable.
while True:
move_snake()
screen.update()
time.sleep(0.2)
turtle.done()

I can provide a crude example. Run your code above as-is. Start the snake moving. Click on the window's close button. Count the number of lines of error messages you get in the console. It could easily exceed two dozen.
Now try this same experiment with the following code which eliminates the while True::
from turtle import Screen, Turtle
class Head(Turtle):
def __init__(self):
super().__init__(shape="square")
self.penup()
self.direction = "stopped"
def move_snake():
if head.direction == "up":
head.sety(head.ycor() + 20)
screen.update()
screen.ontimer(move_snake, 200)
def go_up():
if head.direction != "down":
head.direction = "up"
# Snake head
head = Head()
# Set up screen
screen = Screen()
screen.tracer(0) # Disable animation so we can update screen manually.
# Event handlers
screen.onkey(go_up, "Up")
screen.listen()
move_snake()
screen.mainloop()
Your count of error messages should drop to zero. This is because the window close event happens in the same event loop as turtle motion.
There are other effects that you'll end up chasing later. This is just a simple, easily visible one.

Related

I can't move two turtle objects independently using the onkeypress function

I would like to move two different turtle objects(slider_1 and slider_2) upwards and downwards using the onkeypress function from Python's turtle module.
My issue is that when I move slider_1 then I can't move slider_2 without releasing the key for slider_1, and vice-versa. How can I fix my code, so that I can control slider_1 and slider_2 independently, without any pauses in movement.
main.py:
from turtle import Screen, Turtle
from slider import Slider_1, Slider_2
HEIGHT = 800
WIDTH = 600
screen = Screen()
screen2 = Screen()
screen.bgcolor("black")
screen.title("PONG")
screen.setup(height=HEIGHT, width=WIDTH)
screen.listen()
slider_1 = Slider_1()
screen.onkeypress(key="Up", fun=slider_1.up)
screen.onkeypress(key="Down", fun=slider_1.down)
slider_2 = Slider_2()
screen.onkeypress(key="w", fun=slider_2.up)
screen.onkeypress(key="s", fun=slider_2.down)
screen.exitonclick()
Slider.py:
from turtle import Turtle
SIZE = 20
SPEED = 10
class Slider(Turtle):
def __init__(self):
super().__init__()
self.penup()
self.speed("fastest")
self.shape("square")
self.shapesize(SIZE)
self.shapesize(stretch_wid=1, stretch_len=3)
# self.move = True
def up(self):
self.setheading(90)
if self.ycor() >= 360:
return
else:
self.forward(SPEED)
def down(self):
self.setheading(270)
if self.ycor() <= -360:
return
else:
self.forward(SPEED)
class Slider_1(Slider):
def __init__(self):
super().__init__()
self.color("white")
self.goto(x=-600 / 2 + SIZE, y=0)
self.setheading(90)
class Slider_2(Slider):
def __init__(self):
super().__init__()
self.color("blue")
self.goto(x=600 / 2 - SIZE, y=0)
self.setheading(90)
The issue here is the hardware key handling. You are only checking for keypress. The keyboard "key repeat" handler only triggers key repeating for the last key that was pressed.
To do what you want, your code is going to get more complicated. You will need to have key press and key release handlers for all 4 of those buttons. The key press handlers will need to set a flag, and the key release handlers will clear the flag. Then, you will need a periodic function (using screen.ontimer) running in a timer that checks the flags: "Is left moving up? Is left moving down? Is right moving up? Is right moving down?" Handle all four possibilities, then exit and wait for the next call.

Why won't Python Turtle respond to my movement inputs?

I'm taking a coding course this semester and for one of the projects it requires us to make a game of any kind. I chose to make a game contained within a box where enemies move on screen travel completely across then appear again at a different x or y coordinate to add some variety. The issue I'm dealing with is following adding the code needed to create "enemies" the keys I set for the movement of the player turtle no longer work, another strange thing is that the player turtle will spin in place right as the program is run. I am at a total loss as to how to make the turtle respond to the code as it worked correctly prior to writing the code for the enemies. I don't believe the code for the enemies is the issue as when it is commented out it still acts the same way. I didn't include the enemies code for the sake of brevity. this code is done in Python 3. Please help if you can!
EDIT: I removed the parenthesis following the window.onkeypress(mov_x,"x") and after adding the code back in for the enemies it won"t respond, it works without the code for the enemies but that kinda removes the point of the game. Thank you for the help!
import turtle
import random
#screen
window = turtle.Screen()
window.title("Final Project Game")
window.bgcolor("gray")
window.setup(width=600,height=600)
#player
t= turtle.Turtle()
t.speed(5)
t.shape("triangle")
t.color("blue")
t.penup()
#player movement
def mov_rt():
t.seth(0)
t.fd(20)
def mov_lt():
t.seth(180)
t.fd(20)
def mov_up():
t.seth(90)
t.fd(20)
def mov_dw():
t.seth(270)
t.fd(20)
window.onkeypress(mov_rt,"d")
window.onkeypress(mov_lt,"a")
window.onkeypress(mov_up,"w")
window.onkeypress(mov_dw,"s")
window.listen()
#enemies
enemies = []
turt_num = turtle.numinput("Final","Number of Enemies", default=5, minval=1,maxval=10)
e_dir= [0,90,180,270]
if turt_num == 1:
e1= turtle.Turtle("square",visible=False)
e1.speed(5)
e1.color("red")
e1.penup()
e1.setpos(random.randint(-290,290),random.randint(-290,290))
e1.seth(random.choice(e_dir))
enemies.append(e1)
e1.st()
elif turt_num == 2:
e1= turtle.Turtle("square",visible=False)
e1.speed(5)
e1.color("red")
e1.penup()
e1.setpos(random.randint(-290,290),random.randint(-290,290))
e1.seth(random.choice(e_dir))
enemies.append(e1)
e2= turtle.Turtle("square",visible=False)
e2.speed(5)
e2.color("red")
e2.penup()
e2.setpos(random.randint(-290,290),random.randint(-290,290))
e2.seth(random.choice(e_dir))
enemies.append(e2)
e1.st()
e2.st()
elif turt_num ==3:
e1= turtle.Turtle("square",visible=False)
e1.speed(5)
e1.color("red")
e1.penup()
e1.setpos(random.randint(-290,290),random.randint(-290,290))
e1.seth(random.choice(e_dir))
enemies.append(e1)
e2= turtle.Turtle("square",visible=False)
e2.speed(5)
e2.color("red")
e2.penup()
e2.setpos(random.randint(-290,290),random.randint(-290,290))
e2.seth(random.choice(e_dir))
enemies.append(e2)
e3= turtle.Turtle("square",visible=False)
e3.speed(5)
e3.color("red")
e3.penup()
e3.setpos(random.randint(-290,290),random.randint(-290,290))
e3.seth(random.choice(e_dir))
enemies.append(e3)
e1.st()
e2.st()
e3.st()
#borders
def border(): #if you hold down the button it wont reappear bc youre still moving while the turtle is trying to move to the desired target
tx, ty= t.pos()
if t.xcor() >295:
t.ht()
t.setpos(-295,ty)
t.st()
if t.xcor() <-295:
t.ht()
t.setpos(295,ty)
t.st()
if t.ycor() >295:
t.ht()
t.setpos(tx,-295)
t.st()
if t.ycor() <-295:
t.ht()
t.setpos(tx,295)
t.st()
#main game loop
while True:
window.update()
border()
turtle.mainloop()
Try to use
window.onkey()
That is a different way to listen and it does the exact same thing.
Also, remember at the end of that code you need
window.listen()
Common beginner's error. Instead of:
window.onkeypress(mov_rt(),"d")
window.onkeypress(mov_lt(),"a")
window.onkeypress(mov_up(),"w")
window.onkeypress(mov_dw(),"s")
Do:
window.onkeypress(mov_rt, "d")
window.onkeypress(mov_lt, "a")
window.onkeypress(mov_up, "w")
window.onkeypress(mov_dw, "s")
That is, you don't want to call your event handler function, you instead want to pass the name of your event hanlder function for the system to call later, when something actually happens.
Below is a rework of your code to address this as well as some other turtle and Python issues:
from turtle import Screen, Turtle
def border():
x, y = turtle.position()
if x > 295:
turtle.hideturtle()
turtle.setx(-295)
turtle.showturtle()
elif x < -295:
turtle.hideturtle()
turtle.setx(295)
turtle.showturtle()
if y > 295:
turtle.hideturtle()
turtle.sety(-295)
turtle.showturtle()
elif y < -295:
turtle.hideturtle()
turtle.sety(295)
turtle.showturtle()
# player movement
def mov_rt():
turtle.setheading(0)
turtle.forward(20)
border()
def mov_lt():
turtle.setheading(180)
turtle.forward(20)
border()
def mov_up():
turtle.setheading(90)
turtle.forward(20)
border()
def mov_dw():
turtle.setheading(270)
turtle.forward(20)
border()
screen = Screen()
screen.title("Final Project Game")
screen.bgcolor('gray')
screen.setup(width=600, height=600)
# player
turtle = Turtle()
turtle.speed('normal')
turtle.shape('triangle')
turtle.color('blue')
turtle.penup()
screen.onkeypress(mov_rt, 'd')
screen.onkeypress(mov_lt, 'a')
screen.onkeypress(mov_up, 'w')
screen.onkeypress(mov_dw, 's')
screen.listen()
screen.mainloop()
Note removal of while True: loop as it has no place in an event-driven environment like turtle -- it potentially blocks events from being processed.

Couldn't get rid of (_tkinter.TclError: bad event type or keysym "UP") problem

I am running a Linux mint for the first time . I tried coding a python problem but for two days I am continiously facing problems due to Linux interface please
This is my code:-
import turtle
import time
boxsize=200
caught=False
score=0
#function that are called in keypress
def up():
mouse.forward(10)
checkbound()
def left():
move.left(45)
def right():
move.right(45)
def back():
mouse.backward(10)
checkbound()
def quitTurtles():
window.bye()
#stop the mouse from leaving the square set size
def checkbound():
global boxsize
if mouse.xcor()>boxsize:
mouse.goto(boxsize, mouse.ycor())
if mouse.xcor()<-boxsize:
mouse.goto(-boxsize, mouse.ycor())
if mouse.ycor()>boxsize:
mouse.goto(mouse.xcor(),boxsize)
if mouse.ycor()<-boxsize:
mouse.goto(mouse.xcor(),-boxsize)
#set up screen
window=turtle.Screen()
mouse=turtle.Turtle()
cat=turtle.Turtle()
mouse.penup()
mouse.penup()
mouse.goto(100,100)
#add key listeners
window.onkeypress(up ,'UP')
window.onkeypress(right ,'left')
window.onkeypress(left ,'Right')
window.onkeypress(back ,'DOWN')
window.onkeypress(quitTurtles, "Escape")
difficulty=window.numinput("difficulty","Enter a difficulty from 1 to 5",minval=1,maxval=5)
window.listen()
#main loop
#note how it changes with difficulty
while not caught:
cat.setheading(cat.towards(mouse))
cat.forward(8+diffficulty)
score=score+1
if cat.distance(mouse)<5:
caught=true
time.sleep(0.2-(0.1*difficulty))
window.textinput("GAME OVER","WELL DONE YOU SCORED:"+str(score*difficulty))
window.bye()
This code has several problems, many of which will keep it from running correctly:
Substituted move for mouse:
def up():
mouse.forward(10)
checkbound()
def left():
move.left(45)
Unnecessary global declaration as boxsize is not assigned:
def checkbound():
global boxsize
In code copy-and-paste, didn't change mouse to cat:
mouse=turtle.Turtle()
cat=turtle.Turtle()
mouse.penup()
mouse.penup()
The difficulty variable not spelled consistently:
cat.forward(8+diffficulty)
time.sleep(0.2-(0.1*difficulty))
Incorrect case for boolean:
caught=true
As noted in comments, total inconsistency in key naming case:
window.onkeypress(right ,'left')
window.onkeypress(left ,'Right')
window.onkeypress(back ,'DOWN')
Bigger picture issues are use of sleep() in an event-driven environment and lack of drawn boundaries so player knows the limits. Rather than address these issues one by one in SO questions, let's rework this code to work within the turtle event environment and be playable as a game:
from turtle import Screen, Turtle
BOX_SIZE = 600
# functions that are called in keypress
def up():
mouse.forward(15)
checkbound()
def left():
mouse.left(45)
def right():
mouse.right(45)
def back():
mouse.backward(15)
checkbound()
def checkbound():
''' stop the mouse from leaving the square set size '''
if mouse.xcor() > BOX_SIZE/2:
mouse.goto(BOX_SIZE/2, mouse.ycor())
elif mouse.xcor() < -BOX_SIZE/2:
mouse.goto(-BOX_SIZE/2, mouse.ycor())
if mouse.ycor() > BOX_SIZE/2:
mouse.goto(mouse.xcor(), BOX_SIZE/2)
elif mouse.ycor() < -BOX_SIZE/2:
mouse.goto(mouse.xcor(), -BOX_SIZE/2)
def move():
global score
cat.setheading(cat.towards(mouse))
cat.forward(2 * difficulty)
score += 1
if cat.distance(mouse) < 5:
screen.textinput("GAME OVER", "WELL DONE YOU SCORED: {}".format(score * difficulty))
screen.bye()
else:
screen.ontimer(move, 200 - 100 * difficulty)
score = 0
# set up screen
screen = Screen()
marker = Turtle()
marker.hideturtle()
marker.penup()
marker.goto(-BOX_SIZE/2, -BOX_SIZE/2)
marker.pendown()
for _ in range(4):
marker.forward(BOX_SIZE)
marker.left(90)
difficulty = int(screen.numinput("difficulty", "Enter a difficulty from 1 to 5", minval=1, maxval=5))
cat = Turtle()
cat.shapesize(2)
cat.penup()
mouse = Turtle()
mouse.penup()
mouse.goto(200, 200)
# add key listeners
screen.onkeypress(up, 'Up')
screen.onkeypress(right, 'Left')
screen.onkeypress(left, 'Right')
screen.onkeypress(back, 'Down')
screen.onkeypress(screen.bye, 'Escape')
screen.listen()
screen.ontimer(move, 1000) # give player a chance to move hand from keyboard to mouse
screen.mainloop()

making an endpoint in turtle graphics

I'm making a maze with turtle graphics for a class project and I have one more main thing to complete before I'm finished...
I've created a second "turtle" to make a box at the endpoint. So the objective is to finish the maze and get the turtle in the box. But I am unsure how to make the box an actual endpoint and have a message pop up.
Here is my code:
from turtle import Turtle, Screen
screen = Screen()
screen.setup(650, 850)
screen.title("Turtle Keys")
screen.bgpic('scooby_doo_maze.gif')
move = Turtle(shape="triangle")
move.penup()
move.setx(-150)
move.sety(200)
move.pendown()
move.pensize(5)
box = Turtle(shape="square")
box.hideturtle()
box.speed(0)
box.penup()
box.setx(150)
box.sety(-190)
box.pendown()
box.right(90)
box.forward(100)
box.right(90)
box.forward(100)
box.right(90)
box.forward(100)
box.right(90)
box.forward(100)
def keyUp():
move.forward(12)
def keyLeft():
move.left(90)
def keyRight():
move.right(90)
def keyDown():
move.backward(12)
def keyReset():
move.reset()
move.penup()
move.setx(-150)
move.sety(200)
move.pendown()
move.pensize(5)
screen.onkey(keyUp, "Up")
screen.onkey(keyLeft, "Left")
screen.onkey(keyRight, "Right")
screen.onkey(keyDown, "Down")
screen.onkey(keyReset, "r")
screen.listen()
screen.exitonclick()
We just need to add few features. First, instead of drawing the end point with turtle box, we make turtle box the endpoint by expanding the turtle itself via box.shapesize(). This way, we can use move.distance(box) to determine if move is near the center of box.
Second, we need a function called by all the movement functions to test if above distance is close enough and then invoke the following:
Third, we introduce screen.textinput() to let the play know they've succeeded and offer then the option to play again, or quit. I've reworked your code below to introduce these additions and tweak it a bit for style:
from turtle import Turtle, Screen
screen = Screen()
screen.setup(650, 850)
screen.title("Turtle Keys")
screen.bgpic('scooby_doo_maze.gif')
def insideBox():
if move.distance(box) < 60:
play_again = screen.textinput("Success!", "Play again?")
if play_again and play_again.lower().startswith('y'):
keyReset()
else:
screen.bye()
def keyUp():
move.forward(12)
insideBox()
def keyLeft():
move.left(90)
def keyRight():
move.right(90)
def keyDown():
move.backward(12)
insideBox()
def keyReset():
move.reset()
move.penup()
move.goto(-150, 200)
move.pendown()
move.pensize(5)
screen.listen() # it's here because screen.textinput() unsets it
screen.onkey(keyUp, "Up")
screen.onkey(keyLeft, "Left")
screen.onkey(keyRight, "Right")
screen.onkey(keyDown, "Down")
screen.onkey(keyReset, "r")
move = Turtle(shape="triangle")
keyReset()
box = Turtle(shape="square")
box.color("black", "white")
box.shapesize(5, 5, 5)
box.penup()
box.goto(150, -190)
screen.mainloop()
This is a situation where I would avoid screen.exitonclick() as you need to click the window to get it to listen and easily end up closing it! Using screen.mainloop() should be sufficient and let the user close the window by not choosing to play again or using the window controls.

How do I make this program wait for a screen click before starting?

I am attempting to finish a program for a class, and I am doing fairly well. It is a simple turtle-graphics game in python, where you attempt to avoid poison dots and get navigate to a square. However, my program starts immediately, before the user clicks on the screen. How can I fix this? Thanks!
My code:
# This game involves avoiding red poison blobs while attempting to navigate to
# a square. If you hit the blob, you begin to speed up, making it more difficult
# not to hit more. Additionally, you lose a point. If you reach the square you
# get a point.
import turtle
import math
import random
# screen
wn = turtle.Screen()
wn.bgcolor("black")
wn.tracer(3)
# Draw border
pen1 = turtle.Turtle()
pen1.color("white")
pen1.penup()
pen1.setposition(-275,-275)
pen1.pendown()
pen1.pensize(5)
for side in range(4):
pen1.forward(550)
pen1.left(90)
pen1.hideturtle()
# player
player = turtle.Turtle()
player.color("dark green")
player.shape("turtle")
player.penup()
# poisonBlob
maxpoisonBlob = 15
poisonBlob = []
for a in range(maxpoisonBlob):
poisonBlob.append(turtle.Turtle())
poisonBlob[a].color("dark red")
poisonBlob[a].shape("circle")
poisonBlob[a].shapesize(4, 4, 4)
poisonBlob[a].penup()
poisonBlob[a].speed(0)
poisonBlob[a].setposition(random.randint(-255, 255), random.randint(-255, 255))
maxfood = 1
food = []
for a in range(maxfood):
food.append(turtle.Turtle())
food[a].color("light blue")
food[a].shape("square")
food[a].penup()
food[a].speed(0)
food[a].setposition(random.randint(-240, 240), random.randint(-240, 240))
# speed variable
speed = 6.5
def turnleft():
player.left(30)
def turnright():
player.right(30)
def increasespeed():
global speed
speed += 1
def touchPoison(t1, t2):
d = math.sqrt(math.pow(t1.xcor()-t2.xcor(),2) + math.pow(t1.ycor()-t2.ycor(),2))
if d < 50:
return True
else:
return False
def touchfood(z1, z2):
d = math.sqrt(math.pow(z1.xcor()-z2.xcor(),2) + math.pow(z1.ycor()-z2.ycor(),2))
if d < 20:
return True
else:
return False
turtle.listen()
turtle.onkey(turnleft, "Left")
turtle.onkey(turnright, "Right")
def main():
print("Help your turtle navigate red poison blobs while attempting to navigate to the\n"
"food! If you hit the poison, you begin to speed up, making it more difficult\n"
"not to hit more. Additionally, you lose a point. If you reach the square you\n"
"get a point. To navigate, click the screen, and then use the right and left\n"
"arrow keys. Quickly, your turtle is running away!")
score = 0
while True:
player.forward(speed)
# turtle boundary
if player.xcor() > 260 or player.xcor() < -260:
player.right(180)
# turtle boundary
if player.ycor() > 260 or player.ycor() < -260:
player.right(180)
# move poison
for a in range(maxpoisonBlob):
poisonBlob[a].forward(3)
# Poison boundaries
if poisonBlob[a].xcor() > 220 or poisonBlob[a].xcor() < -220:
poisonBlob[a].right(180)
# Poision boundaries
if poisonBlob[a].ycor() > 220 or poisonBlob[a].ycor() < -220:
poisonBlob[a].right(180)
# Poison touching
if touchPoison(player, poisonBlob[a]):
increasespeed()
poisonBlob[a].setposition(random.randint(-230, 230), random.randint(-230, 230))
poisonBlob[a].right(random.randint(0,360))
score -= 1
#Draw score
pen1.undo()
pen1.penup()
pen1.hideturtle()
pen1.setposition(-260, 280)
scorestring = "Score: %s" %score
pen1.write(scorestring, font=("Calibri", 12))
for a in range(maxfood):
#Positive Point Checking
if touchfood(player, food[a]):
food[a].setposition(random.randint(-230, 230), random.randint(-230, 230))
score += 1
#Draw Score
pen1.undo()
pen1.penup()
pen1.hideturtle()
pen1.setposition(-260, 280)
scorestring = "Score: %s" %score
pen1.write(scorestring, font=("Calibri", 12))
if __name__ == "__main__":
main()
Generally we try not to directly answer homework questions, but I will give you some guidance.
I'm not familiar with the library you're using, but the basic idea is that you need to wait until the user clicks the screen before continuing. My understanding is that you want to do so after the print() function in main, so that the user has to read the message and then click to continue.
A quick search turned up this link: Turtle in python- Trying to get the turtle to move to the mouse click position and print its coordinates
This goes into detail of the onscreenclick() event. Judging by your onkey() methods you are already familiar with binding to events, so you should be able to use this knowledge to bind to the onscreenclick() event and then you just need to break your code up a bit so that it doesn't execute your game until the onscreenclick() event has happened.
If that isn't clear enough, help me understand where your confusion is and I'll assist further. Thanks!
You could begin by defining a function where you call your "if" statement and then use turtle.onscreenclick() by inserting your newly defined function in it like so:
def Game(x, y):
if __name__ == "__main__":
main()
turtle.onscreenclick(Game)
Make sure to insert all this at the end!
Now, adding the (x, y) in the parenthesis after the function name in the function definition assigns the spot the user clicks on in the graphics window its corresponding (x, y) coordinates, which in turn activates the function due to the action of clicking on the screen. I did this successfully, so I can assure you it works! I hope this helps! :)

Categories