python 3 find in list - python-3.x

I am creating a game and unable to detect turtle position in this list
I am using python3 and turtle.
the objective of this code is to create a filled shape when turtle intersect with its's own path
import turtle
t=turtle.Turtle()
t.fillcolor("red")
t.begin_fill()
s=turtle.Screen()
t.penup()
status=True
penstatus=False
t.speed(1)
x=[]
def go1():
t.left(-(t.heading()))
t.left(90)
def go2():
t.left(-(t.heading()))
# t.left(90)
def go3():
t.left(-(t.heading()))
t.left(270)
def go4():
t.left(-(t.heading()))
t.left(180)
def paint():
global penstatus
if penstatus==False:
penstatus=True
t.down()
else:
t.up()
def detect():
global status
a=t.position()
if a in x:
status=False
print("yes")
while status:
global x
s.onkeypress(go1, "Up")
s.onkeypress(go2, "Right")
s.onkeypress(go3, "Down")
s.onkeypress(go4, "Left")
s.onkeypress(paint,"space")
s.listen()
x.append(t.position())
t.fd(5)
detect()
t.end_fill()
s.mainloop()
it works sometimes but result of filling also gets wrong

There are two reasons you're having trouble detecting if the current position is in your list of past positions. The first is you "hop" five pixels at a time so you are potentially crossing the line at a "filled in" segment, not one you were actually positioned on.
The second is that turtle positions are floating point numbers and can be very slightly different when you come back to the same spot. We can fix both problems by not comparing directly but asking if the distance between points is less than our "hop" distance.
My rework of your code below implements this approach. It also changes how your keys work slightly; changes the logic to only include visible lines in the filled graphic; and is completely event-based. It also has a reset, "r", key to start a new drawing. You can back out any changes you don't like, the back position detection is still applicable:
from turtle import Turtle, Screen
DISTANCE = 3
def go_up():
turtle.setheading(90)
def go_right():
turtle.setheading(0)
def go_down():
turtle.setheading(270)
def go_left():
turtle.setheading(180)
def toggle_pen():
if turtle.isdown():
turtle.penup()
else:
turtle.pendown()
turtle.begin_fill() # ignore pending begin_fill, start anew
def reset_drawing():
global positions
turtle.reset()
turtle.fillcolor('red')
turtle.speed('fastest')
turtle.penup()
positions = []
move()
def move():
for position in positions:
if turtle.distance(position) < DISTANCE:
turtle.end_fill()
return
if turtle.isdown():
positions.append(turtle.position())
turtle.forward(DISTANCE)
screen.ontimer(move, 100)
screen = Screen()
screen.onkeypress(go_up, 'Up')
screen.onkeypress(go_right, 'Right')
screen.onkeypress(go_down, 'Down')
screen.onkeypress(go_left, 'Left')
screen.onkeypress(toggle_pen, 'space')
screen.onkeypress(reset_drawing, 'r')
screen.listen()
turtle = Turtle()
positions = None # make sure global is defined
reset_drawing()
screen.mainloop()

Related

How to aviod rubberband become a a line and How to set rubberband only show border line?

I write a simple app, While drag or scale the MainView, The PartView rubberband will show scene area in PartView.But sometime the rubber-band become a line, and sometime the rubberband disappear.So How to aviod this phenomenon appear?And sometime I want the rubberband only show it's border-line, not contain it's light-blue rectangle,So how can I write code ?
My Code
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import random
import math
r = lambda : random.randint(0, 255)
r255 = lambda : (r(), r(), r())
class Scene(QGraphicsScene):
def __init__(self):
super().__init__()
for i in range(1000):
item = QGraphicsEllipseItem()
item.setRect(0, 0, r(), r())
item.setBrush(QColor(*r255()))
item.setPos(r()*100, r()*100)
self.addItem(item)
class MainView(QGraphicsView):
sigExposeRect = pyqtSignal(QRectF)
def drawBackground(self, painter: QPainter, rect: QRectF) -> None:
super().drawBackground(painter, rect)
self.sigExposeRect.emit(rect)
def wheelEvent(self, event: QWheelEvent) -> None:
factor = math.pow(2.7, event.angleDelta().y()/360)
self.scale(factor, factor)
class PartView(QGraphicsView):
def __init__(self):
super().__init__()
self.r = QRubberBand(QRubberBand.Rectangle, self)
self.r.setWindowOpacity(1)
self.r.show()
class View(QSplitter):
def __init__(self):
super().__init__()
self.m = MainView()
self.m.setMouseTracking(True)
self.m.setDragMode(QGraphicsView.ScrollHandDrag)
self.m.sigExposeRect.connect(self.onExposeRect)
self.p = PartView()
self.m.setScene(Scene())
self.p.setScene(self.m.scene())
self.p.fitInView(self.m.scene().itemsBoundingRect())
self.addWidget(self.m)
self.addWidget(self.p)
def onExposeRect(self, rect: QRectF):
prect = self.p.mapFromScene(rect).boundingRect()
self.p.r.setGeometry(prect)
app = QApplication([])
v = View()
v.show()
app.exec()
My Result
I think the problem is that the qrect passed to the drawBackground method is only includes the portion of the background that wasn't previously in the viewport. Not positive about that though.
Either way I was able to achieve your goal of avoiding only a section of the rubber band being drawn, by sending the area for the entire viewport to the onExposeRect slot.
class MainView(QGraphicsView):
sigExposeRect = pyqtSignal(QRectF)
def drawBackground(self, painter: QPainter, rect: QRectF) -> None:
# Adding this next line was the only change I made
orect = self.mapToScene(self.viewport().geometry()).boundingRect()
super().drawBackground(painter, rect)
self.sigExposeRect.emit(orect) # and passing it to the slot.
def wheelEvent(self, event: QWheelEvent) -> None:
factor = math.pow(2.7, event.angleDelta().y()/360)
self.scale(factor, factor)
A fundamental aspect about Graphics View is its high performance in drawing even thousands of elements.
To achieve this, one of the most important optimization is updating only the portions of the scene that really need redrawing, similar to what item views do, as they normally only redraw the items that actually require updates, instead of always painting the whole visible area, which can be a huge bottleneck.
This is the reason for which overriding drawBackground is ineffective: sometimes, only a small portion of the scene is updated (and, in certain situations, even no update is done at all), and the rect argument of drawBackground only includes that portion, not the whole visible area. The result is that in these situations, the signal will emit a rectangle that will not be consistent with the visible area.
Since the visible area is relative to the viewport of the scroll area, the only safe way to receive updates about that area is to connect to the horizontal and vertical scroll bars (which always work even if they are hidden).
A further precaution is to ensure that the visible rectangle is also updated whenever the scene rect is changed (since that change might not be reflected by the scroll bars), by connecting to the sceneRectChanged signal and also overriding the setSceneRect() of the source view. Considering that the changes in vertical and scroll bars might coincide, it's usually a good idea to delay the signal with a 0-delay QTimer, so that it's only sent once when more changes to the visible area happen at the same time.
Note that since you're not actually using the features of QRubberBand, there's little use in its usage, especially if you also need custom painting. Also, since the rubber band is a child of the view, it will always keep its position even if the preview view is scrolled.
In the following example I'll show two ways of drawing the "fake" rubber band (but choose only one of them, either comment one or the other to test them) that will always be consistent with both the source and target views.
class MainView(QGraphicsView):
sigExposeRect = pyqtSignal(QRectF)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signalDelay = QTimer(self, singleShot=True, interval=0,
timeout=self.emitExposeRect)
# signals might have arguments that collide with the start(interval)
# override of QTimer, let's use a basic lambda that ignores them
self.delayEmit = lambda *args: self.signalDelay.start()
self.verticalScrollBar().valueChanged.connect(self.delayEmit)
self.horizontalScrollBar().valueChanged.connect(self.delayEmit)
def emitExposeRect(self):
topLeft = self.mapToScene(self.viewport().geometry().topLeft())
bottomRight = self.mapToScene(self.viewport().geometry().bottomRight())
self.sigExposeRect.emit(QRectF(topLeft, bottomRight))
def setScene(self, scene):
if self.scene() == scene:
return
if self.scene():
try:
self.scene().sceneRectChanged.disconnect(self.delayEmit)
except TypeError:
pass
super().setScene(scene)
if scene:
scene.sceneRectChanged.connect(self.delayEmit)
def setSceneRect(self, rect):
super().setSceneRect(rect)
self.delayEmit()
def wheelEvent(self, event: QWheelEvent) -> None:
factor = math.pow(2.7, event.angleDelta().y()/360)
self.scale(factor, factor)
class PartView(QGraphicsView):
exposeRect = None
def updateExposeRect(self, rect):
if self.exposeRect != rect:
self.exposeRect = rect
self.viewport().update()
def paintEvent(self, event):
super().paintEvent(event)
if not self.exposeRect:
return
rect = self.mapFromScene(self.exposeRect).boundingRect()
# use either *one* of the following:
# 1. QStyle implementation, imitates QRubberBand
qp = QStylePainter(self.viewport())
opt = QStyleOptionRubberBand()
opt.initFrom(self)
opt.rect = rect
qp.drawControl(QStyle.CE_RubberBand, opt)
# 2. basic QPainter
qp = QPainter(self.viewport())
color = self.palette().highlight().color()
qp.setPen(self.palette().highlight().color())
# for background
bgd = QColor(color)
bgd.setAlpha(40)
qp.setBrush(bgd)
qp.drawRect(rect)
class View(QSplitter):
def __init__(self):
super().__init__()
self.m = MainView()
self.m.setMouseTracking(True)
self.m.setDragMode(QGraphicsView.ScrollHandDrag)
self.p = PartView()
self.m.setScene(Scene())
self.p.setScene(self.m.scene())
self.p.fitInView(self.m.scene().itemsBoundingRect())
self.addWidget(self.m)
self.addWidget(self.p)
self.m.sigExposeRect.connect(self.p.updateExposeRect)
PS: please use single letter variables when they actually make sense (common variables, coordinates, loop placeholders, etc.), not for complex objects, and especially for attributes: there's no benefit in using self.m or self.p, and the only result you get is to make code less readable to you and others.

function as function parameter does not work

import turtle
t = turtle.Turtle()
def vary(shape,dup):
while not out_of_bound():
shape
dup
def out_of_bound():#returns True if turtle is out of screen
out = False
height = (turtle.window_height()-30)/2
width = (turtle.window_width()-30)/2
if abs(t.xcor())>width or abs(t.ycor())>height:
out = True
return out
def linear(direction,distance):#for linear duplication
try:
origin_head = t.heading()
t.seth(direction)
t.up()
t.fd(distance)
t.seth(origin_head)#ensures duplication remains linear
except:
print('Invalid input.')
def circle(radius,steps=None,orient=None,circle_color=None,shape_color=None):#circle or shape in circle, orientation in degrees
try:
t.down()
t.circle(-radius)
t.up()
t.circle(-radius,orient)#set orientation of the shape in circle
t.down()
t.circle(-radius,steps=steps)#draw shape in the circle
t.up()
t.lt(180)
t.circle(radius,orient)#return to default position
t.seth(0)
except:
print('Invalid input.')`enter code here`
above code is my attempt to use the turtle library to create patterns, in this case, in a linear manner. when I call the function, vary(circle(50,4,45),linear(0,100)) the while loop only draws one shape and stops, while the code continues to run. please help.
Calling vary like that will not pass circle and linear as parameters to vary. What vary will receive in this case are the return values of those functions (in this case None for both). If you want to pass a function as a parameter, do not add the parentheses - they result in calling the function, which returns its return value.
Because of the above, the following code:
while not out_of_bound():
shape
dup
Is equivalent to this:
while not out_of_bound():
None
None
which obviously does nothing.
The following code should achieve your goal:
def vary(shape, shape_args, dup, dup_args):
while not out_of_bound():
shape(*shape_args)
dup(*dup_args)
Then call it like this: vary(circle, (50,4,45), linear, (0,100)).

Pyqtgraph: Overloaded AxisItem only showing original data

I'm building an application to view real time data based on the examples for scrolling plots. My x-axis should display time as a formated string. The x-values added to the plot are timestamp floats in seconds. Here is a simplified version of my plot window code.
Everything works in real time and I have no problem showing the values i want to plot, but the labels for my x-axis are only the timestamps not the formated strings. I know that the function formatTimestampToString(val) in the AxisItem overload returns a good string value.
import pyqtgraph as pg
class NewLegend(pg.LegendItem):
def __init__(self, size=None, offset=None):
pg.LegendItem.__init__(self, size, offset)
def paint(self, p, *args):
p.setPen(pg.mkPen(0,0,0)) # outline
p.setBrush(pg.mkBrush(255,255,255)) # background
p.drawRect(self.boundingRect())
class DateAxis(pg.AxisItem):
def tickStrings(self, values, scale, spacing):
strings = []
for val in values:
strings.append(formatTimestampToString(val))
return strings
class PlotWindow(QtWidgets.QWidget):
def __init__(self, iof, num):
QtWidgets.QWidget.__init__(self)
self.setWindowTitle('Plot Window ')
self.resize(1000, 800)
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
"""
... other stuff ...
"""
# Externally updated dict with data
self.data = {}
self.curves = {}
self.plotWidget = pg.GraphicsWindow("Graph Window")
axis = DateAxis(orientation='bottom')
self.plot = self.plotWidget.addPlot(axisItem={'bottom': axis})
self.plot.setAutoPan(x=True)
self.legend = NewLegend(size=(100, 60), offset=(70, 30))
self.legend.setParentItem(self.plot.graphicsItem())
def updatePlots(self):
#update data for the different curves
for data_name, curve in self.curves.items():
if curve != 0:
curve.setData(y=self.data[data_name].values[:], x=self.data[data_name].timestamps[:])
def addCurve(self, data_name):
if data_name not in self.curves:
self.curves[data_name] = self.plot.plot(y=self.data[data_name].values[:], x=self.data[data_name].timestamps[:], pen=pg.mkPen(color=self.randomColor(),width=3))
self.legend.addItem(self.curves[data_name], name=data_name)
def removeCurve(self, data_name):
if data_name in self.curves:
self.plot.removeItem(self.curves[data_name])
self.legend.removeItem(data_name)
del self.curves[data_name]
Am i doing something wrong? Is there a better way to overload the AxisItem? I have also tried to overload the AxisItem with a simple numerical formula just to see if it has any effect on my axis.
Another problem i have is with the LegendItem: I can add and subtract labels with no problem, but it only updates the size of the legend box when adding labels. This means that when I add and remove curves/data in my plot the legend grows, but never shrinks down again. I have tried calling the LegendItem.updateSize() function after removing labels, but nothing happens.
I hope you can help! Thanks
So I found the problem. In self.plot = self.plotWidget.addPlot(axisItem={'bottom': axis}) its supposed to say axisItems, not axisItem.

Class Attribute Variable is not updating

I am creating a game where the player is a ship that has to dodge meteors that fall. I have 2 classes, the ship and the meteors. The meteors "fall" by having their canvas objects moved down their y axis and their y coordinates are subtracted by the number as the move function. I have an if statement that detects whether the meteors have fallen passed the border of the canvas, and it deletes those meteors, and creates new meteors at the top of the screen, thus making it seem that there are multiple meteors that are falling. The ship class has a similar function that detects if the ship has gone passed the sides of the canvas, which triggers the death function. I have a function in the meteor class that detects whether it is overlapping, with the help from Sneaky Turtle's answer. Now, it's almost finished, I just have one problem. After 3 "rounds" the meteors should get faster. How I implemented this was by having an if statement that checks if a variable is over 3. If not, the variable adds 1. When it reaches 3, it resets and adds (speed amount) to the speed attribute of the meteor,which is used when it moves. the problem is it only works on the first "wave" after that, the speed attribute stays the same. All the sound functions are commented off so that I don't have to upload the files.
Code:
from random import *
from tkinter import *
from time import *
print('''****Meteor Run****
Don't let the meteors hit you!
A-Left D-Right ''')
sleep(1.25)
#from game_sounds import*
root=Tk()
c = Canvas(width=800,height=600,bg="#37061a")
c.pack()
m1=0
m2=0
m3=0
m4=0
m5=0
m6=0
m7=0
m8=0
direction=0
speed=0
score = 0
cont=True
class ship:
def __init__(self,x1,y1,x2,y2):
self.x1=x1
self.y1=y1
self.x2=x2
self.y2=y2
self.hitbox3=387.5 + x1
self.shape=c.create_polygon(353+x1,380+y1,387.5+x1,310+y1,
420+x1,380+y1,fill="Blue")
def move(self):
global direction
if direction=="L":
self.x1 = self.x1-10
self.hitbox3 = self.hitbox3-10
c.move(self.shape,-10,0)
sleep(0.001)
root.update()
if direction=="R":
self.x1 = self.x1+10
self.hitbox3 = self.hitbox3+10
c.move(self.shape,10,0)
root.update()
self.test_lost_in_space()
sleep(0.001)
def death(self):
root.destroy()
print("You Lost!")
print("Score:",score)
# death_sound()
def test_lost_in_space(self):
if self.hitbox3<=0:
self.death()
if self.hitbox3 >=800:
self.death()
def ship_explode(self):
overlap = c.find_overlapping(353+self.x1,380+self.y1,420+self.x1,310+self.y1)
if overlap != (self.shape,):
self.death()
class meteor:
def __init__(self,x1,y1):
self.x1=x1
self.y1=y1
self.hitbox=89+x1
self.speed=.75
self.shape =c.create_polygon(1+x1,50+y1,34+x1,23+y1,67+x1,23+y1,
89+x1,57+y1,64+x1,71+y1,27+x1,71+y1,fill="brown")
def meteor_return(self):
global m1
global m2
global m3
global m4
global m5
global m6
global m7
global m8
global speed
global score
if self.y1 >=600:
c.delete(self)
m1=meteor(randrange(0,700),randrange(6,12))
m2=meteor(randrange(0,700),randrange(6,12))
m3=meteor(randrange(0,700),randrange(6,12))
m4=meteor(randrange(0,700),randrange(6,12))
m5=meteor(randrange(0,700),randrange(6,12))
m6=meteor(randrange(0,700),randrange(6,12))
m7=meteor(randrange(0,700),randrange(6,12))
m8=meteor(randrange(0,700),randrange(6,12))
if speed!=3:
speed=speed +1
score = score + 1
# lvl_up()
if speed==3:
speed=0
self.speed= self.speed + .5
print(self.speed)
score = score + 5
# lvl_up_2()
def meteor_fall(self):
global speed
self.y1 = self.y1 + self.speed
c.move(self.shape,0,self.speed)
root.update()
self.meteor_return()
# ship1.ship_explode()
def ship_move(event):
global direction
if event.keysym=="a":
direction="L"
ship1.move()
if event.keysym=="d":
direction="R"
ship1.move()
ship1 =ship(0,0,0,0)
m1=meteor(randrange(0,200),randrange(6,12))
m2=meteor(randrange(200,400),randrange(6,12))
m3 =meteor(randrange(400,600),randrange(6,12))
m4=meteor(randrange(600,800),randrange(6,12))
m5 =meteor(randrange(400,600),randrange(6,12))
m6=meteor(randrange(600,800),randrange(6,12))
m7 =meteor(randrange(400,600),randrange(6,12))
m8=meteor(randrange(600,800),randrange(6,12))
c.bind_all("<KeyPress-a>",ship_move)
c.bind_all("<KeyPress-d>",ship_move)
while cont ==True:
m1.meteor_fall()
m2.meteor_fall()
m3.meteor_fall()
m4.meteor_fall()
m5.meteor_fall()
m6.meteor_fall()
m7.meteor_fall()
m8.meteor_fall()
c.bind_all("<KeyPress-a>",ship_move)
c.bind_all("<KeyPress-d>",ship_move)
ship1.death()
Instead of comparing the x and y coordinates of each meteor and seeing whether they are within the bounds of the ships co-ordinates, I would use find_overlapping to detect what actually overlaps the Ship.
If you have nothing on your canvas except the meteors and ship, you could implement something like:
ship_coords = c.coords(self.shape)
overlap = c.find_overlapping(*ship_coords)
if overlap != (self.shape, ):
#Code to run when the meteors collide with the ship.
...
Where (self.shape, ) is the tuple returned from the coordinates you pass to find_overlapping. I recommend reading documentation on the Tkinter canvas, it seems like you have just started learning! Hopefully this helps for the moment however.
If you need to specifically detect what items are overlapping with your ship, then there are plenty of other questions and answers on Stack Overflow about find_overlapping.

_tkinter.TclError:unknown option "23" Code works with few object, but not with many

To start off I am super new at using Tkinter,
The problem I am having is that my code works if I have only one of the object type. It will interact correctly if it is the only one of that tag type. So if I have one 'boat' and 100 'shells' each time it executes, it does so correctly.
The code detects if there is collision between two objects and then changes the currently selected item's color to a random. So as long as there is only one tag type currently it will work correctly. So if I click and drag the 'boat' into a 'shell' it will switch it's color. Then if I take 1 of the 100 'shell's and do the same I get this error.
I do not understand why it works correctly when there is only one object of a given type and to interacts a infinite amount of other objects but when there is more than one of a tag type it fails.
It correctly selects the id number for the selected object so I am just lost right now and appreciate any help.
Follows is the error I receive and the code I am using. It is just the vital parts needed to preform the needed task. The collision code is the same as in the code though.
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python34\lib\tkinter\__init__.py", line 1487, in __call__
return self.func(*args)
File "H:/Charles Engen/PycharmProjects/Battleship/BattleShipGUI.py", line 112, in on_token_motion
self.collision_detection(event)
File "H:/Charles Engen/PycharmProjects/Battleship/BattleShipGUI.py", line 85, in collision_detection
self.canvas.itemconfig(current_token, outline=_random_color())
File "C:\Python34\lib\tkinter\__init__.py", line 2385, in itemconfigure
return self._configure(('itemconfigure', tagOrId), cnf, kw)
File "C:\Python34\lib\tkinter\__init__.py", line 1259, in _configure
self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
_tkinter.TclError: unknown option "22"
import tkinter as tk
from random import randint
HEIGHT = 400
WIDTH = 680
def _random_color():
'''Creates a random color sequence when called'''
random_color = ("#"+("%06x" % randint(0, 16777215)))
return random_color
class Board(tk.Tk):
'''Creates a Board Class'''
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.menu_item = tk.Menu(self)
self.file_menu = tk.Menu(self.menu_item, tearoff=0)
self.file_menu.add_command(label='New', command=self.new_game)
self.file_menu.add_command(label='Exit', command=self.quit)
self.config(menu=self.file_menu)
self.canvas = tk.Canvas(width=WIDTH, height=HEIGHT)
self.canvas.pack(fill='both', expand=True)
# adds variable that keeps track of location
self._token_location_data = {"x": 0, "y": 0, "item": None}
def _create_object(self, coord, fcolor, color, token_name):
'''Creates an object with a tag, each object is assigned the ability to be clicked and dragged'''
(x, y) = coord
if token_name == 'boat':
points = [10+x, 10+y, 20+x, 20+y, 110+x, 20+y, 120+x, 10+y, 80+x,
10+y, 80+x, 0+y, 60+x, 0+y, 60+x, 10+y]
self.canvas.create_polygon(points, outline=fcolor, fill=color, width=3, tag=token_name)
elif token_name == 'shell':
self.canvas.create_oval(0+x, 0+y, 10+x, 10+y, outline=fcolor, fill=color, width=3, tag=token_name)
self.canvas.tag_bind(token_name, '<ButtonPress-1>', self.on_token_button_press)
self.canvas.tag_bind(token_name, '<ButtonRelease-1>', self.on_token_button_press)
self.canvas.tag_bind(token_name, '<B1-Motion>', self.on_token_motion)
def collision_detection(self, event):
'''This function tracks any collision between the boat and shell objects'''
# I will upgrade this to take any object collision
token = self.canvas.gettags('current')[0]
current_token = self.canvas.find_withtag(token)
x1, y1, x2, y2 = self.canvas.bbox(token)
overlap = self.canvas.find_overlapping(x1, y1, x2, y2)
for item in current_token:
for over in overlap:
if over != item:
# Changes the color of the object that is colliding.
self.canvas.itemconfig(current_token, outline=_random_color())
# The following three functions are required to just move the tokens
def on_token_button_press(self, event):
'''Adds ability to pick up tokens'''
# Stores token item's location data
self._token_location_data['item'] = self.canvas.find_closest(event.x, event.y)[0]
self._token_location_data['x'] = event.x
self._token_location_data['y'] = event.y
def on_token_button_release(self, event):
'''Adds ability to drop token'''
# Resets the drag
self._token_location_data['item'] = self.canvas.find_closest(event.x, event.y)
self._token_location_data['x'] = event.x
self._token_location_data['y'] = event.y
def on_token_motion(self, event):
'''Adds ability to keep track of location of tokens as you drag them'''
# Computes how much the object has moved
delta_x = event.x - self._token_location_data['x']
delta_y = event.y - self._token_location_data['y']
# move the object the appropriate amount
self.canvas.move(self._token_location_data['item'], delta_x, delta_y)
# record the new position
self._token_location_data['x'] = event.x
self._token_location_data['y'] = event.y
# Detects collision between objects
self.collision_detection(event)
def new_game(self):
'''Creates new game by deleting the current canvas and creating new objects'''
# Deletes current canvas
self.canvas.delete('all')
# runs the create board mechanism
self._generate_board()
# adds code to create a shell and boat on the screen from the appropriate function
for i in range(1):
self._create_object((410, 15*i), _random_color(), _random_color(), 'boat')
for i in range(4):
for j in range(10):
self._create_object((590+(i*10), 15*j), _random_color(), _random_color(), 'shell')
def main():
app = Board()
app.mainloop()
if __name__ == '__main__':
main()
To run the code, I removed the call to the non-existent ._generate_board. After this, I get the same error but with unknown option '3'.
The exception is caused by passing to self.canvas.itemconfig a tuple of ids, which you misleadingly call current_token, instead of a tag or id. A singleton is tolerated because of the _flatten call, but anything more becomes an error. I am rather sure that it is not a coincidence that '3' is the second member of the shell tuple. Passing token instead stops the exception. Also, there should be a break after itemconfig is called the first time.
With this, however, the shells are treated as a group, and the bounding box incloses all shells and overlap includes all shells. This is why moving a single shell away from the others is seen as a collision. At this point, all shells are randomized to a single new color if one is moved. To fix this, token should be set to the single item set in on_token_button_press, instead of a tag group. This implements your todo note. Here is the result.
def collision_detection(self, event):
'''Detect collision between selected object and others.'''
token = self._token_location_data['item']
x1, y1, x2, y2 = self.canvas.bbox(token)
overlap = self.canvas.find_overlapping(x1, y1, x2, y2)
for over in overlap:
if over != token:
# Changes the color of the object that is colliding.
self.canvas.itemconfig(token, outline=_random_color())
break
A minor problem is that you do the tag binds for 'shell' for each shell (in _create_object. Instead, put the tag binds in new so they are done once.
for tag in 'boat', 'shell':
self.canvas.tag_bind(tag, '<ButtonPress-1>', self.on_token_button_press)
self.canvas.tag_bind(tag, '<ButtonRelease-1>', self.on_token_button_press)
self.canvas.tag_bind(tag, '<B1-Motion>', self.on_token_motion)

Resources