Home > Enterprise >  A few minor problems with my tkinter sand simulation
A few minor problems with my tkinter sand simulation

Time:07-10

I have made a sand simulation in tkinter. Only tkinter. No pygame, no matplotlib, none of that. It works great, it looks great, but nothing in life is perfect.

enter image description here

Minor Problem #1: When I hold down and let the sand fall, the column where the cursor is stays the same color. I haven't been able to track this down yet.

Minor Problem #2: When I create happy little piles of sand, the sides seem to build up from the bottom rather that fall from the top. I suspect this is a rendering issue, but I also haven't found the reason for this.

That's all the problems I've noticed, but if you see any others feel free to let me know.


Code:

from tkinter import *
from random import choice, random
from copy import deepcopy
from colorsys import hsv_to_rgb
 
CELLSIZE = 30
 
AIR = 0
WALL = 1
SAND = 2
 
BG = '#cef'
SANDCOLOR = (45, 45, 86)
WALLCOLOR = (224, 37, 34)
 
TARGETFPS = 100
 
def randomColor(h, s, v):
    h, s, v = (h / 360), s / 100, v / 100
    s  = (random() - 0.5) * 0.1
    v  = (random() - 0.5) * 0.1
    if s < 0: s = 0
    if s > 1: s = 1
    if v < 0: v = 0
    if v > 1: v = 1
    r, g, b = [round(i * 255) for i in hsv_to_rgb(h, s, v)]
    return '#xxx' % (r, g, b)
 
class App:
    def __init__(self):
        global WIDTH, HEIGHT, SANDCOLOR, WALLCOLOR
        
        self.master = Tk()
        self.master.title('Sand Simulation')
        self.master.resizable(0, 0)
        self.master.attributes('-fullscreen', True)
        WIDTH = self.master.winfo_screenwidth() // CELLSIZE
        HEIGHT = self.master.winfo_screenheight() // CELLSIZE
        Width, Height = WIDTH * CELLSIZE, HEIGHT * CELLSIZE
        self.canvas = Canvas(self.master, width=Width, height=Height, bg=BG, highlightthickness=0)
        self.canvas.pack()
 
        self.map = [[AIR] * WIDTH for i in range(HEIGHT)]
        self.colors = [[BG] * WIDTH for i in range(HEIGHT)]
        self.positions = []
        for x in range(WIDTH):
            for y in range(HEIGHT):
                self.positions.append([x, y])
        self.positions.reverse()
 
        self.dragging, self.dragX, self.dragY = False, 0, 0
        self.canvas.bind('<Button-1>', self.mouseDown)
        self.canvas.bind('<B1-Motion>', self.mouseDrag)
        self.canvas.bind('<ButtonRelease-1>', self.mouseUp)
 
##        self.images = [PhotoImage(file='images/sandButton.png'), PhotoImage(file='images/sandButtonActivated.png'),
##                       PhotoImage(file='images/wallButton.png'), PhotoImage(file='images/wallButtonActivated.png')]
        self.images = [PhotoImage().blank(), PhotoImage().blank(), PhotoImage().blank(), PhotoImage().blank()]
        self.sandButton = self.canvas.create_image(125, 125, anchor='center', image=self.images[1])
        self.wallButton = self.canvas.create_image(125, 325, anchor='center', image=self.images[2])
        self.drawingMode = 'SAND'
 
        self.master.after(round(1 / TARGETFPS * 1000), self.frame)
        self.master.mainloop()
 
    def swapBlocks(self, x1, y1, x2, y2):
        block1 = self.map[y1][x1]
        color1 = self.colors[y1][x1]
        self.map[y1][x1] = self.map[y2][x2]
        self.colors[y1][x1] = self.colors[y2][x2]
        self.map[y2][x2] = block1
        self.colors[y2][x2] = color1
 
    def mouseDown(self, event):
        if 50 < event.x < 200 and 50 < event.y < 200:
            self.drawingMode = 'SAND'
            self.canvas.itemconfig(self.sandButton, image=self.images[1])
            self.canvas.itemconfig(self.wallButton, image=self.images[2])
        elif 50 < event.x < 200 and 250 < event.y < 400:
            self.drawingMode = 'WALL'
            self.canvas.itemconfig(self.sandButton, image=self.images[0])
            self.canvas.itemconfig(self.wallButton, image=self.images[3])
        else:
            self.dragging = True
            
            self.dragX = event.x // CELLSIZE
            self.dragY = event.y // CELLSIZE
 
            if self.dragX > WIDTH - 1: self.dragX = WIDTH - 1
            if self.dragX < 0: self.dragX = 0
            if self.dragY > HEIGHT - 1: self.dragY = HEIGHT - 1
            if self.dragY < 0: self.dragY = 0
 
    def mouseDrag(self, event):
        self.dragX = event.x // CELLSIZE
        self.dragY = event.y // CELLSIZE
 
        if self.dragX > WIDTH - 1: self.dragX = WIDTH - 1
        if self.dragX < 0: self.dragX = 0
        if self.dragY > HEIGHT - 1: self.dragY = HEIGHT - 1
        if self.dragY < 0: self.dragY = 0
 
    def mouseUp(self, event):
        self.dragging = False
 
    def updateParticles(self):
        if self.dragging:
            color = choice(['red', 'white', 'blue'])
            if self.drawingMode == 'SAND':
                self.map[self.dragY][self.dragX] = SAND
                self.colors[self.dragY][self.dragX] = randomColor(SANDCOLOR[0], SANDCOLOR[1], SANDCOLOR[2])
            elif self.drawingMode == 'WALL':
                self.map[self.dragY][self.dragX] = WALL
                self.colors[self.dragY][self.dragX] = randomColor(WALLCOLOR[0], WALLCOLOR[1], WALLCOLOR[2])
 
        for block in self.positions:
            x, y = block
            
            block = self.map[y][x]
 
            if block == SAND:
                if y == HEIGHT - 1:
                    below = WALL
                else:
                    below = self.map[y   1][x]
 
                if below == AIR:
                    self.swapBlocks(x, y, x, y   1)
                else:
                    left, right, belowLeft, belowRight = AIR, AIR, AIR, AIR
                    if y == HEIGHT - 1:
                        belowLeft, belowRight = WALL, WALL
                    else:
                        if x == 0:
                            belowLeft = WALL
                            left = WALL
                        else:
                            belowLeft = self.map[y   1][x - 1]
                            left = self.map[y][x - 1]
 
                        if x == WIDTH - 1:
                            belowRight = WALL
                            right = WALL
                        else:
                            belowRight = self.map[y   1][x   1]
                            right = self.map[y][x   1]
 
                    if belowLeft == AIR and belowRight == AIR:
                        if choice([True, False]):
                            if left == AIR:
                                self.swapBlocks(x, y, x - 1, y   1)
                        else:
                            if right == AIR:
                                self.swapBlocks(x, y, x   1, y   1)
                    else:
                        if belowLeft == AIR and left == AIR:
                            self.swapBlocks(x, y, x - 1, y   1)
                        if belowRight == AIR and right == AIR:
                            self.swapBlocks(x, y, x   1, y   1)
                        
    def renderMap(self, previousMap):
        for block in self.positions:
            x, y = block
            previousBlock = previousMap[y][x]
            currentBlock = self.map[y][x]
 
            x1, y1 = x * CELLSIZE, y * CELLSIZE
            x2, y2 = x1   CELLSIZE, y1   CELLSIZE
 
            if previousBlock == AIR and currentBlock != AIR:
                if currentBlock == WALL: color = self.colors[y][x]
                if currentBlock == SAND: color = self.colors[y][x]
                
                rect = self.canvas.create_rectangle(x1, y1, x2, y2, outline='', fill=color)
                self.canvas.tag_lower(rect)
 
            if previousBlock != AIR and currentBlock == AIR:
                blockAtPosition = self.canvas.find_enclosed(x1, y1, x2, y2)
                self.canvas.delete(blockAtPosition)
 
            if previousBlock != AIR and currentBlock != AIR and previousBlock != currentBlock:
                blockAtPosition = self.canvas.find_enclosed(x1, y1, x2, y2)
                self.canvas.delete(blockAtPosition)
                
                if currentBlock == WALL: color = self.colors[y][x]
                if currentBlock == SAND: color = self.colors[y][x]
                
                rect = self.canvas.create_rectangle(x1, y1, x2, y2, outline='', fill=color)
                self.canvas.tag_lower(rect)
 
        self.canvas.update()
 
    def frame(self):
        previousMap = deepcopy(self.map)
        self.updateParticles()
        self.renderMap(previousMap)
        
        self.master.after(round(1 / TARGETFPS * 1000), self.frame)
 
def main():
    app = App()
 
if __name__ == '__main__':
    main()

Please help me fix any bugs, glitches, etc...

CodePudding user response:

Problem #1 is caused by the fact that your rendering function doesn't create a new rectangle if both the old and new block types are sand. As the sand falls in a column, the first bit of sand causes rectangles to be created with its color, and because another bit of sand always falls in its place on the next frame, it just stays that color. You could compare old and new colors to see which blocks need refreshing, or store which blocks changed.

Problem #2 is related to the order of your positions. You're inserting [x,y] positions into the positions array from the left to the right, one column at a time, each column from top to bottom, and then reversing the result, which gives you an ordering of bottom to top per column with the columns from right to left. So when you iterate through the positions, you're essentially sweeping from right to left as you process the blocks, so any block that falls to the left is going to be reprocessed in the same update, and as it keeps falling to the left it will keep being reprocessed, until it settles, and all of that happened in one frame. So particles will seem to fall to the left instantly.

I bet you don't have this problem with particles falling to the right, which is why on the right side of your image you have some diagonal bands of color: problem #1 is happening there too any time two sand blocks fall to the right in sequence.

You can fix problem #2 by reordering your width and height loops when you set up self.positions. Since blocks always move down one row when they are moved, iterating from bottom to top will always update each particle only once. If you ever introduce anything that makes particles move up, you'll need a better solution.

  • Related