Extra pixels left and top of canvas python turtle - python-3.x

I'm not sure why but on the left and top of a canvas I'm getting weird behaviour while creating my snake game using turtle. There's a border of 10 pixels on the left and on the top.
Main Code
import functools
from turtle import Screen
import time
from Snake import Snake
# initialize screen
win = Screen()
WIDTH = HEIGHT = 800
win.setup(width=WIDTH, height=HEIGHT)
win.bgcolor("black")
canvas = win.getcanvas()
root = canvas.winfo_toplevel()
root.overrideredirect(1)
win.tracer(0)
s = Snake()
def main():
segments = s.init_segments()
win.update()
move_keys = ["Right", "Up", "Left", "Down"]
running = True
inc = 0
for k in move_keys:
win.onkeypress(functools.partial(s.move, segments, inc, k), key=k)
inc += 90
win.listen()
while running:
win.update()
time.sleep(0.1)
s.move(segments, segments[0].heading(), "none")
if s.crashed(win.window_width(), win.window_height(), segments):
running = False
break
win.exitonclick()
main()
Snake Code
from turtle import Turtle
class Snake:
def init_segments(self):
segs = []
x_pos = 0
for i in range(3):
t = Turtle(shape="square")
t.color("white")
t.penup()
t.speed(1)
segs.append(t)
if t != segs[0]:
x_pos -= 20
t.goto(x_pos, 0)
return segs
def __no_turn(self, h, d):
if h - d == -180 or h - d == 180:
return True
else:
return False
def __no_skip(self, k, dir):
if (k == "Right" and dir == 0 or k == "Up" and dir == 90
or k == "Left" and dir == 180 or k == "Down" and dir == 270):
return False
else:
return True
def move(self, t_list , dir, k):
for seg_num in range(len(t_list) - 1, -1, -1):
if self.__no_turn(t_list[0].heading(), dir):
continue
elif seg_num > 0:
if self.__no_skip(k, dir):
t_list[seg_num].goto(t_list[seg_num - 1].position())
else:
t_list[seg_num].setheading(dir)
if self.__no_skip(k, dir):
t_list[seg_num].forward(20)
def crashed(self, window_width, window_height, t_list):
head_position = t_list[0].position()
body_positions = []
hw = window_width/2
hh = window_height/2
for i in range(len(t_list)):
body_positions.append(t_list[i].position())
body_positions.pop(0)
for i in range(len(body_positions)):
if head_position == body_positions[i]:
return True
if head_position[0] >= hw:
print(f"Head Position: {head_position[0]} ... Wall Position: {hw}")
return True
elif head_position[0] <= -hw:
print(f"Head Position: {head_position[0]} ... Wall Position: {-hw}")
return True
elif head_position[1] >= hh:
print(f"Head Position: {head_position[1]} ... Wall Position: {hh}")
return True
elif head_position[1] <= -hh:
print(f"Head Position: {head_position[1]} ... Wall Position: {-hh}")
return True
The print lines in this file were added to try and figure out what was going on.
Please help me understand what's going on.
Thanks.
Edit:
Also I didn't notice it earlier, but there's a 1px border on the bottom and right hand sides.
Second Edit to illustrate the problem further.
I added the code for the pills a while ago. What I do is, I spawn every pill in all at once and hide them because you can't destroy a turtle without clearing the screen and it would be weird to get the turtle position every single time and redraw the turtle on the screen where it had been 0.1 seconds previously. Then I got this result.
As you can see the pills are spawning off screen.
The following is the code that I used in order to spawn in all pills and is similar to the code that I use for determining the edges of the screen.
hw = int(WIDTH/2)
hh = int(HEIGHT/2)
p_list = []
for i in range(-hw, hw, 20):
for j in range(-hh, hh, 20):
p = Pill((i, j))
p.show_pill(segments)
p_list.append(p)
What happens when I reorder the win.update()
As you can see here, the right side now crashes one whole segment in. If you look closely, you'll see in this picture that the white of the snake, covers the extra pixel of space that is not covered in the previous picture where it stops on the last pixel of the circle.
You'll also see in the second picture and this one that there is a row of circles on the bottom that are peeking over the edge of the window's border.

If you move win.update() after s.move(segments, segments[0].heading(), "none") instead of before it will work (i.e) crash in the same way in all four walls.
If you don't want the head to get burst into the wall when crashing then modify the crash boundary check accordingly.

I've finally figured out why this happens.
First problem: I was previously led to believe that all turtle shapes are created equal. No they're not. While the default size for a turtle might be 20px by 20px, a square and circle are both defaulted to 21px as they need a center point that is not an even number so that it can divide evenly between the 4 quadrants of turtle's co-ordinate system (which has a single pixel serving as the x and y axes).
How to solve:
Make the canvas a multiple of 21px.
Remember that the canvas, when split into 2 parts has half of a turtle on each edge, which counts as a full turtle but the turtle only completes over the canvas, so we're sitting with an extra column and row of turtles.
Remove the last column and row of turtles and move the entire grid over by one turtle.
If done correctly you'll end up with the result below. Unfortunately, 2px at the right and bottom will be cut off as it's used for Turtle's UI. The top and left face a similar problem in that a single row of pixels are used for Turtle's UI
The snake itself is done in a similar manner in that instead of shifting the starting segment 20px I shift it 11px first and then I appended the other parts of the snake. I calculate it's position by removing the 11px that I shifted it initially. This literally solved most of the problems with the snake apart from stuff that Turtle's ui takes away.

Related

How can I simplify my python turtle game to avoid it from taking larger memory as the running time increases?

This is a color-flipping game based on the python turtle module.
The game is composed of a rectangular board divided into a number of flat colored tiles of various colors. The goal of the game is to turn all tiles to the same color by choosing one tile on the board and then changing its color to another color. The newly selected color will spread to all its neighboring tiles which match the original color of the chosen tile. The player continues selecting another tile and changing its color to another until all the tiles within the board flipped to the same color. In addition to the rectangular board, an array of colors is shown below the board. These are the colors to which the player can choose to flip the color of the selected tile.
As I started to run my game, the memory it took up was getting larger and larger as the running time increased. And it's getting slower and slower. I think it might be due to the while loop at the end of my code. But I'm not sure. How can I modify it to make it faster?
from turtle import *
from random import choice
from functools import partial
# set game dimension to be 5 x 5
g_dim = 5
# use random.choice() to create the color for the game
g_game = [choice(['#0000FF', '#FF0000', '#FFFF00', '#008000', '#00FFFF']) for i in range(25)]
# provide the option to flip during the game
optionColor = ['#0000FF', '#FF0000', '#FFFF00', '#008000', '#00FFFF']
# show a set of colors as option for user to flip
def promptColorToFlip(optionColor, # a list that contains a set of the color for user to choose from
height=100, # the height of the option tiles
width=100 # the width of the option tiles
):
# the coordinates of the first tiles
x = -200
y = -200
for i in range(len(optionColor)):
tile = prototype.clone()
tile.goto(i * width + x, -(height+50) + y)
tile.color('white', optionColor[i])
tile.onclick(partial(returnChosenColor, i))
# return the index of the select-to-flip-to color in the optionColor list
def returnChosenColor(userChosenColor, # the index of the select-to-flip-to color in the optionColor list
x, y # take the positional arguments from the onclick() function to avoid errors, no significant meaning
):
global userOption
userOption = userChosenColor
def refreshScreen(game, rows=5, columns=5, height=100, width=100):
x = -200
y = -200
for column in range(columns):
for row in range(rows):
square = prototype.clone()
square.goto(column * (5+width) + x , row * (5+height) + y)
square.onclick(partial(userChosenTile, row, column))
if state['framed'] == row*5+column:
square.color('black', game[row*5+column])
else:
square.color('white', game[row*5+column])
update()
def userChosenTile(ROW, COL, x, y):
global state, R, C
state['framed'] = ROW*5+COL
R = ROW
C = COL
def flipColor(row, col, game, orig, to):
print('excuted')
global userOption, state, R, C
if orig == to:
return game
if row < 0 or row >= g_dim:
return
if col < 0 or col >= g_dim:
return
idx = row*g_dim+col
if game[idx] != orig:
return
print(idx, 'excuted')
game[idx] = to
flipColor(row-1, col, game, orig, to)
flipColor(row+1, col, game, orig, to)
flipColor(row, col-1, game, orig, to)
flipColor(row, col+1, game, orig, to)
state = {'framed':None}
R = None
C = None
userOption = None
return game
# initialize the game status
state = {'framed':None} # stores the number of the last selected tile, which will be framed with a black border
R = None # the row of the last selected tile
C = None # the column of the last selected tile
userOption = None # the index of the select-to-flip-to color in the optionColor list
# create a prototype of the tiles
prototype = Turtle()
prototype.shape('square')
prototype.shapesize(5, 5, 5)
prototype.penup()
# disable auto screen refresh
tracer(False)
# run the game
while True:
# the try and except block here is to prevent error from raising when user terminate the progarm
try:
promptColorToFlip(optionColor)
refreshScreen(g_game)
if state['framed'] is not None and R is not None and C is not None and userOption is not None:
g_game = flipColor(R, C, g_game, g_game[state['framed']], optionColor[userOption])
except:
pass
I haven't fully worked through your somewhat complex code, but I'm pretty sure I see the source of your progressive slowdown as the game runs for a while. It's right here, in your refreshScreen function:
for column in range(columns):
for row in range(rows):
square = prototype.clone()
...
This code makes a new turtle for each square of your board on every frame. Those turtles never go away, so you just keep on stacking up more and more of them, which slows down the performance of the whole game over time. The solution is probably to use only one turtle for each location, and keep references to them so that you can change their colors when necessary, rather than making them anew each time.

Interference of canvas items and problem in setting coordinates

I'm working on an animation of a moving object, while drawing it's path.
I want to draw the pixels in which the center of the object went through... but guess what? python decided to set the NW anchor of the image with the coordinates I send, instead of the center. I infer it has something to do with the pixels I draw simultaneously (creating a one pixel rectangle). so the image appear on the right of the path bellow... I want the center of it to be on the top of the pixels... adding the main of the code:
from tkinter import*
import time
dt = 0.01
clock_place = (500, 10)
def round_two(t, t0):
return round((t-t0)*100)/100
def round_three(t, t0):
return round((t-t0)*1000)/1000
# showing 'real time motion' for a known path (also cyclic), with
# parametric representation
def paint_known_path(x_pos, y_pos, t_0):
window = Tk()
canvas = Canvas(window, height=700, width=1000)
canvas.pack()
canvas.config(background='black')
tennis_ball = PhotoImage(file='tennis ball.png')
t = t_0
x = x_pos(t_0)
y = y_pos(t_0)
particle = canvas.create_image(x, y, image=tennis_ball)
clock = canvas.create_text(clock_place, text=round_two(t, t_0),
fill='white')
while True:
canvas.create_rectangle(x, y, x, y, outline='red')
canvas.itemconfig(clock, text=round_two(t, t_0))
t += dt
x = x_pos(t)
y = y_pos(t)
canvas.moveto(particle, x, y)
window.update()
if x == x_pos(t_0) and y == y_pos(t_0):
if t - t_0 > 100*dt:
break
time.sleep(dt)
canvas.create_text((500, 100), text='orbit duration: ' +
str(round_three(t, t_0)), fill='white')
window.mainloop()
It turns out to be quite a bit require, but here is the main completion components.
The first additional part that you need to add:
# print('the ten ball height', tennis_ball.height(), tennis_ball.width())
# tennis ball dimensions
tb_hght = tennis_ball.height()
tb_wdth = tennis_ball.width()
mid_point_x = x + tennis_ball.height() / 2
mid_point_y = y + tennis_ball.width() / 2
Secondly, also needed to add some functions to for x_pos and y_pos like this (these are just example functions to make the code work):
def x_pos(a):
# any function of t,
return 100
def y_pos(a):
# any function of t,
return 100
Furthermore, you need to call the function at the end like this:
paint_known_path(x_pos,y_pos,0)
Finally, need to add the mid_point_x and mid_point_y to the path that is drawn (as these will be the image centre points).

ipycanvas displaying final stroke_lines thoughout animation

So I was playing with animating some Bezier curves - just part of learning how to use ipycanvas (0,10,2) -- The animation I produced is really hurting my head. What I expected to see was a set of straight lines between 4 Bezier control points "bouncing" around the canvas with the Bezier curve moving along with them.
I did get the moving Bezier curve -- BUT the control points stayed static. Even stranger they were static in the final position and the curve came to meet them.
Now sometimes Python's structures and references can get a little tricky and so you can sometimes get confusing results if you are not really thinking it through -- and this totally could be what's going on - but I am at a loss.
So to make sure I was not confused I printed the control points (pts) at the beginning and then displayed them to the canvas. This confirmed my suspicion. Through quantum tunneling or some other magic time travel the line canvas.stroke_lines(pts) reaches into the future and grabs the pts array as it will exist in the future and keeps the control points in their final state.
Every other use of pts uses the current temporal state.
So what I need to know is A) The laws of physics are safe and I am just too dumb to understand my own code. B) There is some odd bug in ipycanvas that I should report. C) How to monetize this time-traveling code -- like, could we use it to somehow factor large numbers?
from ipycanvas import Canvas, hold_canvas
import numpy as np
def rgb_to_hex(rgb):
if len(rgb) == 3:
return '#%02x%02x%02x' % rgb
elif len(rgb) == 4:
return '#%02x%02x%02x%02x' % rgb
def Bezier4(t, pts):
p = t**np.arange(0, 4,1)
M=np.matrix([[0,0,0,1],[0,0,3,-3],[0,3,-6,3],[1,-3,3,-1]])
return np.asarray((p*M*pts))
canvas = Canvas(width=800, height=800)
display(canvas) # display the canvas in the output cell..
pts = np.random.randint(50, 750, size=[4, 2]) #choose random starting point
print(pts) #print so we can compare with ending state
d = np.random.uniform(-4,4,size=[4,2]) #some random velocity vectors
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3))) #some random color
canvas.font = '16px serif' #font for displaying the changing pts array
with hold_canvas(canvas):
for ani in range(300):
#logic to bounce the points about...
for n in range(0,len(pts)):
pts[n]=pts[n] + d[n]
if pts[n][0] >= 800 or pts[n][0] <= 0 :
d[n][0] = - d[n][0]
if pts[n][1] >= 800 or pts[n][1] <= 0 :
d[n][1] = - d[n][1]
#calculate the points needed to display a bezier curve
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,15)]
#begin display output....
canvas.clear()
#first draw bezier curve...
canvas.stroke_style = c
canvas.stroke_lines(B)
#Now draw control points
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
canvas.stroke_lines(pts)
#print the control points to the canvas so we can see them move
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.stroke_text(str(pts), 10, 32)
canvas.sleep(20)
In all seriousness, I have tried to think through what can be happening and I am coming up blank. Since ipycanvas is talking to the browser/javascript maybe all of the data for the frames are rendered first and the array used to hold the pts data for the stroke_lines ends up with the final values... Whereas the B array is recreated in each loop... It's a guess.
There are two ways to get the code to behave as expected and avoid the unsightly time-traveling code. The first way is to switch the location of the line with hold_canvas(canvas): to inside the loop. This however renders the canvas.sleep(20) line rather useless.
canvas = Canvas(width=800, height=800)
display(canvas)
pts = np.random.randint(50, 750, size=[4, 2])
print(pts)
d = np.random.uniform(-8,8,size=[4,2])
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3)))
canvas.font = '16px serif'
#with hold_canvas(canvas):
for ani in range(300):
with hold_canvas(canvas):
for n in range(0,len(pts)):
if pts[n][0] > 800 or pts[n][0] < 0 :
d[n][0] = -d[n][0]
if pts[n][1] > 800 or pts[n][1] < 50 :
d[n][1] = -d[n][1]
pts[n]=pts[n] + d[n]
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,25)]
canvas.clear()
canvas.stroke_style = c
canvas.stroke_lines(B)
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
#pts2 = np.copy(pts)
canvas.stroke_lines(pts)
canvas.fill_style = rgb_to_hex((255,255,255, 150))
canvas.fill_circles(pts.T[0], pts.T[1],np.array([4]*4))
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.fill_text(str(pts), 10, 32)
sleep(20/1000)
#canvas.sleep(20)
In this version, the control lines are updated as expected. This version is a little more "real time" and thus the sleep(20/1000) is needed to
The other way to do it would be just to ensure that a copy of pts is made and passed to canvas.stroke_lines:
canvas = Canvas(width=800, height=800)
display(canvas)
pts = np.random.randint(50, 750, size=[4, 2])
print(pts)
d = np.random.uniform(-8,8,size=[4,2])
c = rgb_to_hex(tuple(np.random.randint(75, 255,size=3)))
canvas.font = '16px serif'
with hold_canvas(canvas):
for ani in range(300):
#with hold_canvas(canvas):
for n in range(0,len(pts)):
if pts[n][0] > 800 or pts[n][0] < 0:
d[n][0] = -d[n][0]
if pts[n][1] > 800 or pts[n][1] < 50:
d[n][1] = -d[n][1]
pts[n]=pts[n] + d[n]
B = [(Bezier4(i, pts)).ravel() for i in np.linspace(0,1,35)]
canvas.clear()
canvas.stroke_style = c
canvas.stroke_lines(B)
canvas.stroke_style = rgb_to_hex((255,255,128, 50))
pts2 = np.copy(pts)
canvas.stroke_lines(pts2)
canvas.fill_style = rgb_to_hex((255,255,255, 150))
canvas.fill_circles(pts.T[0], pts.T[1],np.array([4]*4))
canvas.stroke_style = rgb_to_hex((255,255,128, 150))
canvas.fill_text(str(pts), 10, 32)
#sleep(20/1000)
canvas.sleep(20)
I could not actually find the data passed between the python and the browser but it seems pretty logical that what is happening is that python is finishing its work (and ani loop) before sending the widget instructions on what to draw, and the pts values sent are the final ones.
(yes I know there is a bug in the bouncing logic)

Python3 Tkinter and Pillow: How to rotate an image while it is on a canvas

I want to move a turtle around a canvas by clicking on the canvas, and the turtle should point in the direction it is moving. The moving part works, but the rotate function causes the image to become distorted and mangled.
What am I doing wrong and how do I fix this?
I have a class that adds an image of a turtle to a canvas:
class TurtleImage:
"""
A Turtle image that will be placed on the canvas that is given in the ctor.
The turtle can be moved around the canvas with the move() method.
"""
def __init__(self, canvas : Canvas):
self.__turtle_file : str = self.__find_file("turtle_example/turtle.png")
self.__canvas = canvas
self.__pilImage : PngImagePlugin.PngImageFile = PILImage.open(self.__turtle_file)
self.__pilTkImage : ImageTk.PhotoImage = ImageTk.PhotoImage(self.__pilImage)
self.__turtle_id : int = canvas.create_image(100,100, image=self.__pilTkImage)
self.__is_moving = False
This class also has a method to animate the turtle moving around the canvas. It moves the turtle by 1 pixel in the x direction, y direction, or both, and then scehdules itself to be called again after a time delay determined by the speed parameter. It also should rotate the turtle so it is pointing in the right direction:
def move(self, dest_x : int, dest_y :int, speed : float = 0.1):
self.__is_moving = True
delay_ms = math.floor(1/speed)
current_x, current_y = self.__canvas.coords(self.__turtle_id)
delta_x = 1 if current_x < dest_x else -1 if current_x > dest_x else 0
delta_y = 1 if current_y < dest_y else -1 if current_y > dest_y else 0
angle = math.atan2(delta_y,delta_x)
self.__rotate(angle)
if (delta_x, delta_y) != (0, 0):
self.__canvas.move(self.__turtle_id, delta_x, delta_y)
if (current_x, current_y) != (dest_x, dest_y):
self.__canvas.after(delay_ms, self.move, dest_x, dest_y, speed)
else:
self.__is_moving = False
Because the canvas does not have the ability to rotate its objects, I must replace the object with a rotated version of itself:
def __rotate(self, angle : float):
self.__pilImage = self.__pilImage.rotate(angle)
self.__pilTkImage = ImageTk.PhotoImage(self.__pilImage)
self.__replace_image(self.__pilTkImage)
def __replace_image(self, new_image : ImageTk.PhotoImage):
self.__canvas.itemconfig(self.__turtle_id, image = new_image)
The moving around works fine, but the rotate function causes the image to become distorted and mangled, and it gets worse every time it is called.
Can you tell me why this isn't working, and what I need to do to fix it?
Here's a screenshot of said turtle, before and after rotating:
I've found the sollution, I have to reopen the file:
def __rotate(self, angle : float):
self.__pilImage = PILImage.open(self.__turtle_file)
self.__pilImage = self.__pilImage.rotate(angle)
self.__pilTkImage = ImageTk.PhotoImage(self.__pilImage)
self.__replace_image(self.__pilTkImage)
I don't know why though.

How can I use the random module in python turtle?

I'm trying to make a game like the original snake and I want the "food" to go to random places but I'm not really sure how to get it to work with it in a class. There are some other errors in the code but want to focus on the player and the food right now.
import turtle
import random
"""-------"""
t=turtle.Turtle()
s=turtle.Screen()
cube=turtle.Turtle
"""------------"""
WIDTH, HEIGHT=300, 300
DISTANCE=5
"""--------------"""
s.setup(WIDTH,HEIGHT)
"""------------"""
t.width(1)
s.bgcolor("dark green")
"""-----------------"""
class Border():
def check():
x, y = t.position()
if not -WIDTH / 2 < x < WIDTH / 2 or not -HEIGHT / 2 < y < HEIGHT / 2:
t.undo() # undo error
t.speed(0)
t.left(180) # turn around
t.forward(10) # redo movement but in new direction
t.speed(3)
"""-------------"""
randint=random.randint
x=random.randint(cube.xcor, 0)
y=random.randint(0,cube.ycor)
"""---------------"""
class Food():
def block():
cube.color("red")
for i in range(4):
cube.goto(x, -x, y, -y)
cube.begin_fill()
cube.forward(20)
cube.right(90)
cube.end_fill()
"""---------------"""
class Player():
def move_up():
player=False
while player==False:
for i in range(1):
t.forward(DISTANCE)
t.clear()
def move_left():
t.speed(0)
t.left(90)
t.speed(3)
def move_right():
t.speed(0)
t.right(90)
t.speed(3)
"""------------"""
collistion_check=Border()
player1=Player()
s.onkey(Player.move_up,"up")
s.onkey(Player.move_left,"left")
s.onkey(Player.move_right,"right")
s.listen()
Generally speaking, your program is a disaster. As far as your immediate problem with random numbers, this code has an issue:
randint=random.randint
x=random.randint(cube.xcor, 0)
y=random.randint(0,cube.ycor)
Syntax-wise, this should be something more like:
from random import randint
...
x = randint(cube.xcor(), 0)
y = randint(0, cube.ycor())
I.e. it's not clear why you set the variable randint since you don't use it and xcor() and ycor() are methods and require parenthesis.
If your goal is to locate the food randomly on the screen, I'd go with:
x = randint(cube.xcor()/2 - WIDTH/2, WIDTH/2 - cube.xcor()/2)
y = randint(cube.ycor()/2 - HEIGHT/2, HEIGHT/2 - cube.ycor()/2)
Adding variables as you see fit to reduce the redundancies. The biggest problem I see with the remainder of your code is this method:
def move_up():
player=False
while player==False:
for i in range(1):
t.forward(DISTANCE)
t.clear()
It's not clear how player ever becomes True so once you hit the up arrow, you're into an infinite loop. But I assume you'll focus on this once you get the food issue resolved.

Resources