Python factory, where object parameters can be specified gradually? - python-3.x

Assuming we have the following object (in reality many more parameters):
class Room:
def __init__(self, h, w, color):
self.h = h
self.w = w
self.color = color
print(f'Room is {self.h}*{self.w} and painted in {self.color}')
Room(0, 1, 'red') # Room is 0*1 and painted in red
Further assume we need to create this object hundreds of times. A few of the parameters need to be changed each time. However, most of the parameters stay consistent within one "session". Therefore, I would like to fix those parameters at the beginning of my session, to make the subsequent calls much simpler.
My current approach looks like this. Surely there is something better out there?
class RoomFactory:
def __init__(self, h=None, w=None, color=None):
self.h = h
self.w = w
self.color = color
# TODO: Get arguments from Room.__init__ automatically?
def create(self, **kwargs):
kwargs_already_set = {k: v for k, v in self.__dict__.items() if v}
return Room(**kwargs_already_set, **kwargs)
# Define some parameters at session beginning. Which parameters depends on session!
rf = RoomFactory()
rf.h = 5
rf.w = 5
# Later, create hundreds of objects with different remaining arguments
rf.create(color='green') # Room is 5*5 and painted in green
rf.create(color='black') # Room is 5*5 and painted in black

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.

How to append the same variable multiple times that defines a class

Here is the class NPC:
class NPC:
def __init__(self):
self.x = randint(0,800)
self.y = randint(0,60)
self.velocity_x = 5
self.drop_y = 60
self.img = "my image path here"
npc = NPC()
num_npc = 5
list = []
for i in range(num_npc):
list.append(npc)
In the game loop only one image is shown and is stationary.
I'm working on trying to write old code to be object oriented and can't figure out the best way to render the npcs
Below is the old code I was using and it worked as expected
npc_img = []
npc_x = []
npc_y = []
npc_vel_x = []
npc_vel_y = []
num_of_npc = 5
for i in range(num_of_npc):
npc_img.append("my img path")
npc_x.append(random.randint(0, 800))
npc_y.append(random.randint(0, 60))
npc_vel_x.append(4)
npc_vel_y.append(40)
Your code is pretty much correct already. However the way you are creating instances of NPC objects is not quite correct. I guess you meant to add 5 NPCs to the list, not 5 references to the same NPC object. That is what your question title says though!
npc = NPC()
...
for i in range(num_npc):
list.append(npc) # <<-- HERE, same object, 5 times
The code should call the NPC constructor in the loop, rather than outside it.
for i in range( num_npc ):
new_npc = NPC()
list.append( new_npc )
While you're rewriting code, it might be worth keeping the co-ordinates and image dimensions in a Pygame Rect, since this allows for easy collision detection and other nice things.
Something like:
class NPC:
def __init__(self):
self.image = pygame.image.load( "image path here" ).convert_alpha()
self.rect = self.image.get_rect()
self.rect.x = randint(0,800)
self.rect.y = randint(0,60)
self.velocity_x = 5
self.drop_y = 60
def draw( self, screen ):
screen.blit( self.image, self.rect )
def hitBy( self, arrow_rect ):
hit = self.rect.colliderect( arrow_rect )
return hit
If I understood correctly, something like this should work:
class NPC:
def __init__(self):
self.x = randint(0,800)
self.y = randint(0,60)
self.velocity_x = 5
self.drop_y = 60
self.image_list = []
self_image_load_dict = {}
def add_image(self, image_path):
self.image_list.append(image_path)
def load_images(self):
self.image_load_dict[]
for i in len(self.get_image_list()):
self.image_load_dict[i] = pygame.image.load(self.get_image_list()[i])
def get_image_list(self):
return self.image_list
def get_image_load_dict(self):
return self.image_load_dict
I used fstring so it would be easier to load images and keep track of the image number:
npc = NPC()
for i in range(NUMBER_OF_NPC):
npc.add_image(f"image_path_{i}")
Now you have image_paths in the object's list, I assume you want to load them, hence the load_images method.
NOTE: If needed you can create additional method(s) for loading images. E.g. if you have animations for "left" and "right" movement
I hope this answers your question, if I omitted something please say in comment.

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