Fatal Python error: Cannot recover from stack overflow. During Flood Fill - python

I've come to a dead end, and after excessive (and unsuccessful) Googling, I need help.
I'm building a simple PyQt4 Widget where it lies out a grid of 60x80 squares, each initialized to None. If the user clicks on that box it changes color based on how many times left-clicked, defined by this list:
self.COLORS=[
(0, 0, 255), #WATER
(255, 210, 128), #SAND
(0, 128, 0), #GREEN
(255, 255, 0), #YELLOW
(255, 165, 0), #ORANGE
(255, 0, 0) #RED
]
If the user right clicks, it flood fills an area, using the common recursive flood fill algo. This works perfectly for small spaces, however if the space is large enough the program fails with the error Fatal Python error: Cannot recover from stack overflow. I have no idea how to fix this, perhaps a flood fill that isn't recursive?
All squares and subsequent color codes are stored in self.cells so by setting self.cells[(y,x)]=1 would set cell (y,x) to the Sand color.
Here is the program in whole.
import sys
from PyQt4 import QtGui, QtCore
class Example(QtGui.QWidget):
def __init__(self, cell_size=10, swidth=800, sheight=600):
QtGui.QWidget.__init__(self)
self.resize(swidth,sheight)
self.cell_size = cell_size
self.height = sheight
self.width = swidth
self.columns = self.width // self.cell_size
self.rows = self.height // self.cell_size
self.COLORS=[
(0, 0, 255), #WATER
(255, 210, 128), #SAND
(0, 128, 0), #GREEN
(255, 255, 0), #YELLOW
(255, 165, 0), #ORANGE
(255, 0, 0) #RED
]
self.cells = {(x,y):None for x in range(1,self.columns+1) for y in range(1,self.rows+1)}
def translate(self,pixel_x, pixel_y):
"Translate pixel coordinates (pixel_x,pixel_y), into grid coordinates"
x = pixel_x * self.columns // self.width + 1
y = pixel_y * self.rows // self.height + 1
return x,y
def check_cell(self,x,y):
if self.cells[(x,y)] <= 0:
self.cells[(x,y)]=0
elif self.cells[(x,y)] >= len(self.COLORS)-1:
self.cells[(x,y)]=len(self.COLORS)-1
else:
pass
def draw_cell(self, qp, col, row):
x1,y1 = (col-1) * self.cell_size, (row-1) * self.cell_size
x2,y2 = (col-1) * self.cell_size + self.cell_size, (row-1) * self.cell_size + self.cell_size
qp.drawRect(x1, y1, x2-x1, y2-y1)
def color_cell(self, qp, col, row):
qp.setBrush(QtGui.QColor(*self.COLORS[self.cells[(col,row)]]))
self.draw_cell(qp, col, row)
def draw_grid(self, qp):
qp.setPen(QtGui.QColor(128,128,128)) # gray
# Horizontal lines
for i in range(self.rows):
qp.drawLine(0, i * self.cell_size, self.width, i * self.cell_size)
# Vertical lines
for j in range(self.columns):
qp.drawLine(j * self.cell_size, 0, j * self.cell_size, self.height)
def set_all(self, type):
self.cells = {(x,y):type for x in range(1,self.columns+1) for y in range(1,self.rows+1)}
self.repaint()
def fill(self, x, y, type):
print(x,y)
if x < 1 or x >= self.columns+1 or y < 1 or y >= self.rows+1:
return
if self.cells[(x,y)] != None:
return
self.cells[(x,y)] = type
self.repaint()
self.fill(x+1, y, type)
self.fill(x-1, y, type)
self.fill(x, y+1, type)
self.fill(x, y-1, type)
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
self.draw_grid(qp)
for row in range(1, self.rows+1):
for col in range(1, self.columns+1):
if self.cells[(col,row)] != None:
self.color_cell(qp, col, row)
qp.end()
def drawPoints(self, qp):
size = self.size()
for i in range(1000):
x = random.randint(1, size.width()-1)
y = random.randint(1, size.height()-1)
qp.drawPoint(x, y)
def mousePressEvent(self, e):
x,y = self.translate(e.pos().x(),e.pos().y())
if e.button() == QtCore.Qt.LeftButton:
if self.cells[(x,y)] == None:
self.cells[(x,y)]=0
else:
self.cells[(x,y)]+=1
self.check_cell(x,y)
elif e.button() == QtCore.Qt.RightButton:
self.fill(x,y,0)
'''
if self.cells[(x,y)] == None:
self.cells[(x,y)]=0
else:
self.cells[(x,y)]-=1
self.check_cell(x,y)
'''
else: pass
self.repaint()
def save(self):
return self.cells
def open(self, new_cells):
self.cells=new_cells
self.repaint()
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Can anyone help diagnose the problem or perhaps point in a direction to fix it?

You are using a stack-based forest fire algorithm, known to eat a lot of stack, so it's better to avoid it.
My proposal to avoid recursion: alternate forest fire algorithm
I even implemented it using your class objects. Tested it with some ASCII-art and also with your actual code and it worked fine even on big zones:
def fill(self, x, y, t):
if self.cells[(x,y)] == None: # cannot use not: there are 0 values
to_fill = [(x,y)]
while to_fill:
# pick a point from the queue
x,y = to_fill.pop()
# change color if possible
self.cells[(x,y)] = t
# now the neighbours x,y +- 1
for delta_x in range(-1,2):
xdx = x+delta_x
if xdx > 0 and xdx < self.columns+1:
for delta_y in range(-1,2):
ydy = y+delta_y
# avoid diagonals
if (delta_x == 0) ^ (delta_y == 0):
if ydy > 0 and ydy < self.rows+1:
# valid x+delta_x,y+delta_y
# push in queue if no color
if self.cells[(xdx,ydy)] == None:
to_fill.append((xdx,ydy))
self.repaint()
When you pass a point, it checks if must be filled.
If must be filled, it inserts it in the queue and runs a loop.
The loop just pops an item from the queue, changes its color, and attempts to do the same for its neighbors: if still in the picture (x,y boundary checking) and not the diagonals, and no color defined on the neighbour, just insert the coord in the queue.
The loops stops when all items have been processed: after a while, either you reach the edges or you encounter only filled points, so no extra points are queued.
This approach only relies on available memory, not stack.
Proof that it works: successfully filled a huge blue zone without stack overflow.

Related

How to generate my maze instantly so I don't have to watch it Generate?

So I'm creating a game and I'm using Recursive backtracking algorithm to create the maze, however, I don't want it to show the maze generation and just to instantly generate the maze. I'm unsure of how to actually do this though so any help would be appreciated, I've already tried not drawing the generated white part but that then doesn't create the maze.
import pygame
import random
import time
class Cell(object):
def __init__(self, x, y, cell_size, screen, black, white, red, blue):
# position in matrix
self.x = x
self.y = y
# keeps track of which walls are still visible
self.walls = [True, True, True, True]
# checks if cell has been visited during generation
self.generated = False
# checks if cell is on path during solving
self.on_path = False
# checks if cell has been visited during solving
self.visited = False
self.cell_size = cell_size
self.screen = screen
self.black = black
self.white = white
self.red = red
self.blue = blue
def draw_cell(self):
# coordinates on screen
x = self.x * self.cell_size
y = self.y * self.cell_size
# draws a wall if it still exists
if self.walls[0]:
pygame.draw.line(self.screen, self.black, (x, y), (x + self.cell_size, y), 5)
if self.walls[1]:
pygame.draw.line(self.screen, self.black,
(x, y + self.cell_size), (x + self.cell_size, y + self.cell_size), 5)
if self.walls[2]:
pygame.draw.line(self.screen, self.black,
(x + self.cell_size, y), (x + self.cell_size, y + self.cell_size), 5)
if self.walls[3]:
pygame.draw.line(self.screen, self.black, (x, y), (x, y + self.cell_size), 5)
# marks out white if generated during generation
if self.generated:
pygame.draw.rect(self.screen, self.white, (x, y, self.cell_size, self.cell_size))
class Maze:
def __init__(self, screen, cell_size, rows, cols, white, black, red, blue):
self.screen = screen
self.cell_size = cell_size
self.rows = rows
self.cols = cols
self.state = None
self.maze = []
self.stack = []
self.current_x = 0
self.current_y = 0
self.row = []
self.neighbours = []
self.black = black
self.white = white
self.red = red
self.blue = blue
self.cell = None
def on_start(self):
# maintains the current state
# maze matrix of cell instances
self.maze = []
# stack of current cells on path
self.stack = []
self.current_x, self.current_y = 0, 0
self.maze.clear()
self.stack.clear()
for x in range(self.cols):
self.row = []
for y in range(self.rows):
self.cell = Cell(x, y, self.cell_size, self.screen, self.black, self.white, self.red, self.blue)
self.row.append(self.cell)
self.maze.append(self.row)
def in_bounds(self, x, y):
return 0 <= x < self.cols and 0 <= y < self.rows
def find_next_cell(self, x, y):
# keeps track of valid neighbors
self.neighbours = []
# loop through these two arrays to find all 4 neighbor cells
dx, dy = [1, -1, 0, 0], [0, 0, 1, -1]
for d in range(4):
# add cell to neighbor list if it is in bounds and not generated
if self.in_bounds(x + dx[d], y + dy[d]):
if not self.maze[x + dx[d]][y + dy[d]].generated:
self.neighbours.append((x + dx[d], y + dy[d]))
# returns a random cell in the neighbors list, or -1 -1 otherwise
if len(self.neighbours) > 0:
return self.neighbours[random.randint(0, len(self.neighbours) - 1)]
else:
return -1, -1
def remove_wall(self, x1, y1, x2, y2):
# x distance between original cell and neighbor cell
xd = self.maze[x1][y1].x - self.maze[x2][y2].x
# to the bottom
if xd == 1:
self.maze[x1][y1].walls[3] = False
self.maze[x2][y2].walls[1] = False
# to the top
elif xd == -1:
self.maze[x1][y1].walls[1] = False
self.maze[x2][y2].walls[3] = False
# y distance between original cell and neighbor cell
xy = self.maze[x1][y1].y - self.maze[x2][y2].y
# to the right
if xy == 1:
self.maze[x1][y1].walls[0] = False
self.maze[x2][y2].walls[2] = False
# to the left
elif xy == -1:
self.maze[x1][y1].walls[2] = False
self.maze[x2][y2].walls[0] = False
def create_maze(self):
self.maze[self.current_x][self.current_y].generated = True
# self.maze[self.current_x][self.current_y].draw_current()
next_cell = self.find_next_cell(self.current_x, self.current_y)
# checks if a neighbor was returned
if next_cell[0] >= 0 and next_cell[1] >= 0:
self.stack.append((self.current_x, self.current_y))
self.remove_wall(self.current_x, self.current_y, next_cell[0], next_cell[1])
self.current_x = next_cell[0]
self.current_y = next_cell[1]
# no neighbor, so go to the previous cell in the stack
elif len(self.stack) > 0:
previous = self.stack.pop()
self.current_x = previous[0]
self.current_y = previous[1]
def main():
WIDTH, HEIGHT = 800, 800
CELL_SIZE = 40
ROWS, COLUMNS = int(HEIGHT / CELL_SIZE), int(WIDTH / CELL_SIZE)
# color variables
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
# initialize pygame
pygame.init()
SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
SCREEN.fill(WHITE)
pygame.display.set_caption("Maze Gen")
CLOCK = pygame.time.Clock()
FPS = 60
m = Maze(SCREEN, CELL_SIZE, ROWS, COLUMNS, WHITE, BLACK, RED, BLUE)
m.on_start()
running = True
while running:
CLOCK.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for i in range(m.cols):
for j in range(m.rows):
m.maze[i][j].draw_cell()
m.create_maze()
pygame.display.flip()
if __name__ == "__main__":
main()
pygame.quit()
Call m.create_maze() in a loop before the application loop. Terminate the loop when len(m.stack) == 0:
def main():
# [...]
m = Maze(SCREEN, CELL_SIZE, ROWS, COLUMNS, WHITE, BLACK, RED, BLUE)
m.on_start()
while True:
m.create_maze()
if len(m.stack) == 0:
break
running = True
while running:
CLOCK.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
for i in range(m.cols):
for j in range(m.rows):
m.maze[i][j].draw_cell()
pygame.display.flip()

Pathfinding Visualizer in pygame [duplicate]

This question already has answers here:
tips on Adding/creating a drop down selection box in pygame
(1 answer)
trying creating dropdown menu pygame, but got stuck
(1 answer)
Closed 1 year ago.
I am still learning how to code and wanted to create a pathfinding visualizer by following this video:
https://www.youtube.com/watch?v=JtiK0DOeI4A&ab_channel=TechWithTim
I think i understand the algorithm and the whole code more or less, but i want to expand the program. I want to implement more algorithms, some buttons and a file dialog to import and export labyrinths and stop the time the algorithm takes and some stuff like that. I know how to implement another algorithm, but i am new to Pygame, so i have no idea how to display that. I tried to combined it with PyQt5 and searched for different solutions with Pygame itself, but nothing really worked.
How can i add some bar or something like that above where i can choose the algorithm i want to use with a dropdown menu or similar and import the labyrinth and all that stuff?
This is the corresponding code:
import pygame
import math
from queue import PriorityQueue
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
PURPLE = (128, 0, 128)
ORANGE = (255, 165, 0)
GREY = (128, 128, 128)
TURQUOISE = (64, 224, 208)
class Spot:
def __init__(self, row, col, width, total_rows):
self.row = row
self.col = col
self.x = row * width
self.y = col * width
self.color = WHITE
self.neighbors = []
self.width = width
self.total_rows = total_rows
def get_pos(self):
return (self.row, self.col)
def is_closed(self):
return (self.color == RED)
def is_open(self):
return (self.color == GREEN)
def is_barrier(self):
return (self.color == BLACK)
def is_start(self):
return (self.color == ORANGE)
def is_end(self):
return (self.color == TURQUOISE)
def reset(self):
self.color = WHITE
def make_closed(self):
self.color = RED
def make_open(self):
self.color = GREEN
def make_barrier(self):
self.color = BLACK
def make_start(self):
self.color = ORANGE
def make_end(self):
self.color = TURQUOISE
def make_path(self):
self.color = PURPLE
def draw(self, win):
pygame.draw.rect(win, self.color, (self.x, self.y, self.width, self.width))
def update_neighbors(self, grid):
self.neightbors = []
if self.row < self.total_rows -1 and not grid[self.row + 1][self.col].is_barrier(): # DOWN
self.neightbors.append(grid[self.row + 1][self.col])
if self.row > 0 and not grid[self.row - 1][self.col].is_barrier(): # UP
self.neightbors.append(grid[self.row - 1][self.col])
if self.col < self.total_rows -1 and not grid[self.row][self.col + 1].is_barrier(): # RIGHT
self.neightbors.append(grid[self.row][self.col + 1])
if self.col > 0 and not grid[self.row][self.col - 1].is_barrier(): # LEFT
self.neightbors.append(grid[self.row][self.col - 1])
def __lt__(self, other):
return (False)
def h(p1, p2):
x1, y1 = p1
x2, y2 = p2
return (abs(x1 - x2) + abs(y1 - y2))
def reconstruct_path(came_from, current, draw):
while current in came_from:
current = came_from[current]
current.make_path()
draw()
def algorithm(draw, grid, start, end):
count = 0
open_set = PriorityQueue()
open_set.put((0, count, start))
came_from = {}
g_score = {spot: float ("inf") for row in grid for spot in row}
g_score[start] = 0
f_score = {spot: float ("inf") for row in grid for spot in row}
f_score[start] = h(start.get_pos(), end.get_pos())
open_set_hash = {start}
while not open_set.empty():
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
current = open_set.get()[2]
open_set_hash.remove(current)
if current == end: # draws the path
reconstruct_path(came_from, end, draw)
end.make_end()
return True
for neighbor in current.neightbors:
temp_g_score = g_score[current] + 1
if temp_g_score < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = temp_g_score
f_score[neighbor] = temp_g_score + h(neighbor.get_pos(), end.get_pos())
if neighbor not in open_set_hash:
count += 1
open_set.put((f_score[neighbor], count, neighbor))
open_set_hash.add(neighbor)
neighbor.make_open()
draw()
if current != start:
current.make_closed()
return False
def make_grid(rows, width):
grid = []
gap = width // rows
for i in range(rows):
grid.append([])
for j in range(rows):
spot = Spot(i, j, gap, rows)
grid[i].append(spot)
return grid
def draw_grid(win, rows, width):
gap = width // rows
for i in range(rows):
pygame.draw.line(win, GREY, (0, i * gap), (width, i * gap))
for j in range(rows):
pygame.draw.line(win, GREY, (j * gap, 0), (j * gap, width))
def draw(win, grid, rows, width):
win.fill(WHITE)
for row in grid:
for spot in row:
spot.draw(win)
draw_grid(win, rows, width)
pygame.display.update()
def get_clicked_pos(pos, rows, width):
gap = width // rows
y, x = pos
row = y // gap
col = x // gap
return (row, col)
def main():
width = 800
win = pygame.display.set_mode((width, width))
pygame.display.set_caption("A* Path Finding Algorithm")
ROWS = 50
grid = make_grid(ROWS, width)
start = None
end = None
run = True
while run:
draw(win, grid, ROWS, width)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if pygame.mouse.get_pressed()[0]:
pos = pygame.mouse.get_pos()
row, col = get_clicked_pos(pos, ROWS, width)
spot = grid [row][col]
if not start and spot != end:
start = spot
start.make_start()
elif not end and spot != start:
end = spot
end.make_end()
elif spot != end and spot != start:
spot.make_barrier()
elif pygame.mouse.get_pressed()[2]:
pos = pygame.mouse.get_pos()
row, col = get_clicked_pos(pos, ROWS, width)
spot = grid [row][col]
spot.reset()
if spot == start:
start = None
elif spot == end:
end = None
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE and start and end:
for row in grid:
for spot in row:
spot.update_neighbors(grid)
algorithm(lambda: draw(win, grid, ROWS, width), grid, start, end)
if event.key == pygame.K_c:
start = None
end = None
grid = make_grid(ROWS, width)
pygame.quit()
if __name__ == "__main__":
main()

My A* pathfinding algorithm does not always get the shortest path

thanks in advance for any help.
I am making a pathfinder visualiser using python in pygame.
I have tried to make the A* algorithm, but sometimes it does not find the shortest path. I have been looking through several previous questions with the same issue, which has led me to believe it may be a problem with the heuristic. If I set the hueristic value to 0, then the algorithm becomes dijkstra's and always gets the shortest path.
A grid is used for the algorithm, with x being the number row and y being the number column (I believe might be the other way around but it doesnt matter)
Each square on the grid is an object, with x and y values, as well as a gScore, hScore and fScore. On initialisation these are all set to None.
I also have some functions at the bottom to do calculations, such as find the lowest fScore node from an array, find the gScore, find the hScore, fScore and get the distance between two grid squares.
I think the problem is in the hueristic function and have tried several different methods of fixing to no avail. From looking at the code below would anyone be able to see the problem, or point me in the right direction? Any help is much appreciated .
For simplicity I have only included the A* function without any of the pygame stuff, but I can add the entire program if need be, including the gridsquare object.
def a_star():
for row in grid:
for square in row:
if square.state == "start_pos":
start_pos = square
elif square.state == "end_pos":
end_pos = square
start_pos.gScore = find_g(start_pos, start_pos)
start_pos.hScore = find_h(start_pos, end_pos)
start_pos.fScore = find_f(start_pos.gScore, start_pos.hScore)
openList = [start_pos]
closedList = []
while len(openList) > 0:
current_node = get_lowest_f_node(openList)
if current_node.state == "end_pos":
print("found")
path = [end_pos]
node = current_node
while node.parent != None:
time.sleep(SHORTEST_PATH_DELAY)
node = node.parent
path.append(node)
return
openList.remove(current_node)
closedList.append(current_node)
x = current_node.x
y = current_node.y
# get nodes around current node
node1 = grid[x][y - 1]
node2 = grid[x][y + 1]
node3 = grid[x - 1][y]
node4 = grid[x + 1][y]
successor_nodes = [node1, node2, node3, node4]
for node in successor_nodes:
# check if walkable
if (node.state == "wall") or (node in closedList):
continue
if node.gScore == None:
node.gScore = current_node.gScore
tentative_g_score = current_node.gScore + get_distance(node, current_node)
if (node in closedList) and (tentative_g_score >= node.gScore):
continue
if (node not in openList) or (tentative_g_score < node.gScore):
node.parent = current_node
node.gScore = tentative_g_score
node.fScore = node.gScore + find_h(node, end_pos)
if node not in openList:
openList.append(node)
def get_lowest_f_node(array):
min_f = min(array, key = attrgetter("fScore"))
return min_f
# distance from current node and start node
def find_g(current, start_pos):
g = get_distance(current, start_pos)
return g
# distance from current node and target / destination / finish node
def find_h(current, end_pos):
h = get_distance(current, end_pos)
return h
# hscore and gscore added together
def find_f(score1, score2):
return score1 + score2
# distance from 2 points
def get_distance(start, end):
x1 = start.x
y1 = start.y
x2 = end.x
y2 = end.y
distancex = sqr(x2 - x1)
distancey = sqr(y2 - y1)
#distance = sqrt(distancex + distancey)
distance = distancex + distancey
return distance
def sqr(number):
return number * number
Below are some images of the result of the path finding, with different patters. The starting node is always the bottom red square.
^^^ This is where the A* algorithm finds the correct, shortest path. All good.
^^^This is where A* finds a path, but it is not the shortest path. This is what I am trying to fix, any help is much appreciated.
^^^ this is dijkstra's finding the correct path when presented with the same arrangement of walls.
I am very grateful for any help.
<==== Download This image
I'm no expert on A* but awhile I wrote a script for a YouTube video I was going to produce.
If you want it then it's here: https://pastebin.com/WycrpAfZ
You can also view it here:
import math, random, sys
import pygame
from pygame.locals import *
# exit the program
def events():
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
# define display surface
W, H = 1920, 1080
HW, HH = W / 2, H / 2
AREA = W * H
# initialise display
pygame.init()
pygame.font.init()
CLOCK = pygame.time.Clock()
FONT_SMALL = pygame.font.Font(None, 26)
FONT_LARGE = pygame.font.Font(None, 50)
DS = pygame.display.set_mode((W, H))
pygame.display.set_caption("code.Pylet - Template")
FPS = 1
# define some colors
BLACK = (0, 0, 0, 255)
WHITE = (255, 255, 255, 255)
RED = (255, 0, 0, 255)
GREEN = (0, 128, 0, 255)
BLUE = (0, 0, 255, 255)
PURPLE = (255, 255, 0, 255)
# define node class
class node:
def __init__(self, x, y, obstacle):
self.x = x
self.y = y
self.pos = (x, y)
self.h = 0
self.g = 0
self.f = 0
self.obstacle = obstacle
self.other = None
self.parent = None
def neighbourPos(self, offset):
return (self.x + offset[0], self.y + offset[1])
def draw(self, size, color = None, id = None, surface = None):
global text, FONT_SMALL, FONT_LARGE
if not surface: surface = pygame.display.get_surface()
pos = (self.x * size[0], self.y * size[1])
if not color:
if not self.obstacle:
if not self.other: pygame.draw.rect(surface, BLACK, pos + size, 0)
else: pygame.draw.rect(surface, BLUE, pos + size, 0)
else:
pygame.draw.rect(surface, WHITE, pos + size, 0)
else:
pygame.draw.rect(surface, color, pos + size, 0)
pygame.draw.rect(surface, WHITE, pos + size, 1)
if self.f:
text(FONT_SMALL, "G:{0}".format(self.g), pos[0] + 5, pos[1] + 5, 0, 0, surface)
text(FONT_SMALL, "H:{0}".format(self.h), pos[0] + size[0] - 5, pos[1] + 5, 1, 0, surface)
text(FONT_LARGE, "F:{0}".format(self.f), pos[0] + size[0] / 2, pos[1] + size[1] / 2 , 2, 2, surface)
if not id == None:
text(FONT_SMALL, "{0}".format(id), pos[0] + 5, pos[1] + size[1] - 5, 0, 1, surface)
def drawNodes(n, ms, cs):
for x in range(ms[0]):
for y in range(ms[1]):
n[x][y].draw(cs)
def drawNodeList(node_list, cs, color):
id = 0
for n in node_list:
n.draw(cs, color, id)
id += 1
def heuristics(pos1, pos2):
return int(math.hypot(pos1[0] - pos2[0], pos1[1] - pos2[1]) * 10)
def text(font, string, x, y, xJustify = None, yJustify = None, surface = None):
global WHITE
if not surface: surface = pygame.display.get_surface()
textSurface = font.render(string, 1, WHITE)
textRect = textSurface.get_rect()
if xJustify == 1:
x -= textRect.width
elif xJustify == 2:
x -= textRect.center[0]
if yJustify == 1:
y -= textRect.height
elif yJustify == 2:
y -= textRect.center[1]
surface.blit(textSurface, (x, y))
map = pygame.image.load("test.png").convert()
map_size = map_width, map_height = map.get_rect().size
cell_size = (W / map_width, H / map_height)
#create list of nodes
nodes = list([])
for x in range(map_width):
nodes.append(list([]))
for y in range(map_height):
color = map.get_at((x, y))
if color != WHITE:
nodes[x].append(node(x, y, False))
if color == BLUE:
start = nodes[x][y]
start.other = True
elif color == RED:
end = nodes[x][y]
end.other = True
else:
nodes[x].append(node(x, y, True))
# This list contains relative x & y positions to reference a node's neighbour
NEIGHBOURS = list([(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)])
# the closed list contains all the nodes that have been considered economical viable.
# By that I mean a node that has been closer to the end node than any other in the open list at one time
closed = list([])
# The open list contains all the closed list's neighbours that haven't been identified as being economically sound node yet
open = list([])
open.append(start) # add the start node so that we can then add it's neighbours
# if the algorithm finds the end node then pathFound will be true otherwise it's false.
# Once it becomes true there's no more calculations to do so the path finding script will be skipped over
pathFound = False
completedPath = list([]) #
# main loop
while True:
DS.fill(BLACK)
drawNodes(nodes, map_size, cell_size)
drawNodeList(open, cell_size, GREEN)
drawNodeList(closed, cell_size, RED)
if pathFound: drawNodeList(completedPath, cell_size, PURPLE)
pygame.display.update()
# wait for user to press mouse button
while not pygame.mouse.get_pressed()[0]:
events()
while pygame.mouse.get_pressed()[0]:
events()
# if we've found the quickest path from start node to end node then just draw, no need continue path finding
if pathFound: continue
if not open: continue
# get lowest f from the open list, the node with the lowest f is the most economical in terms of the path towards the end node
openNodeWithlowestF = open[0]
for o in open:
if o.f < openNodeWithlowestF.f: openNodeWithlowestF = o
mostEconomicalNodeSoFar = openNodeWithlowestF # let's make this more readable! Economical means the best path to the end given the choices but not definitive.
# remove the mostEconomicalNodeSoFar from the open list
open.remove(mostEconomicalNodeSoFar)
# add mostEconomicalNodeSoFar to the closed list
closed.append(mostEconomicalNodeSoFar)
# if the mostEconomicalNodeSoFar is equal to the end node then we've reach our target
if mostEconomicalNodeSoFar == end:
temp = end
while temp.parent:
completedPath.append(temp)
temp = temp.parent
completedPath.append(start)
pathFound = True
# get the path etc
# iterate through the list of neighbours belonging to the mostEconomicalNodeSoFar. Why?
for neighbourOffset in NEIGHBOURS:
nx, ny = mostEconomicalNodeSoFar.neighbourPos(neighbourOffset)
if nx < 0 or nx >= map_width or ny < 0 or ny >= map_height: continue
neighbour = nodes[nx][ny] # create a variable to represent the mostEconomicalNodeSoFar's neighbour
if neighbour.obstacle: continue # if the mostEconomicalNodeSoFar's neighbouring node is an obstacle then we can't ...?
if neighbour in closed: continue # if the mostEconomicalNodeSoFar's neighbouring node is in the closed list then we can't ...?
# now we need to see if the mostEconomicalNodeSoFar's neighbour is more economical ...?
hypotheticalFScore = mostEconomicalNodeSoFar.g + heuristics(neighbour.pos, mostEconomicalNodeSoFar.pos)
NeighbourIsBetterThanMostEconomicalNodeSoFar = False # Yes it's a long variable name but it describes what it is so all is good!
# is this neighbour already in open list? if it is then we don't want to be adding it again. to chec
if not neighbour in open:
NeighbourIsBetterThanMostEconomicalNodeSoFar = True
neighbour.h = heuristics(neighbour.pos, end.pos)
open.append(neighbour)
elif hypotheticalFScore < neighbour.g:
NeighbourIsBetterThanMostEconomicalNodeSoFar = True
if NeighbourIsBetterThanMostEconomicalNodeSoFar:
neighbour.parent = mostEconomicalNodeSoFar
neighbour.g = hypotheticalFScore
neighbour.f = neighbour.g + neighbour.h
#sys.exit()

how to redraw a line on a line which is already drawn in tkinter canvas smoothly?

I have some lines in tinter canvas, and also have their code. I want to make them red but not at a same time I want to draw another line(red line) go on them but it should take different time. for example fo one specific line it should take 3 seconds that line get red for another one it should take 7 seconds to make that red. it is like drawing another red line on the previous one.
def activator(self, hexagon, duration_time):
if not hexagon.is_end:
self.canvas.itemconfigure(hexagon.drawn, fill="tomato")
self.canvas.itemconfigure(hexagon.hex_aspects.outputs.drawn, fill="tomato")
for example I want my hexagon which created by createpolygon method of tinter get red but not immediately. It should do regarding to duration_time which is the a second variable. I mean it should be done within duration_time second(let say 3 second). is there any way for doing this? I have lots of object in my canvas which should get red during an specific time. line, circle, polygon..
A line on a tk.canvas is defined by a start and an end point; in order to access points on the line, we need to first create an affine line by first generating many points at an interval on the line, then join them with line segments.
This affine line is created upon clicking on an item on the canvas, but is hidden at first, and progressively revealed over a short time interval.
Once the redraw is completed, the affine line is hidden again, and the item being redrawn set to its new color.
This "simple" redraw requires quite a bit of machinery to implement. You can try it by clicking on a line to redraw it, and see the animation of the redraw.
Code:
import random
import tkinter as tk
WIDTH, HEIGHT = 500, 500
class AffinePoint:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return AffinePoint(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return AffinePoint(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return AffinePoint(self.x * scalar, self.y * scalar)
def __iter__(self):
yield self.x
yield self.y
def draw(self, canvas):
offset = AffinePoint(2, 2)
return canvas.create_oval(*(self + offset), *self - offset, fill='', outline='black')
def create_affine_points(canvas, num_points):
"""sanity check"""
for _ in range(num_points):
AffinePoint(random.randrange(0, WIDTH), random.randrange(0, HEIGHT)).draw(canvas)
class AffineLineSegment:
def __init__(self, start, end, num_t=100):
self.start = AffinePoint(*start)
self.end = AffinePoint(*end)
self.num_t = num_t
self.points = []
self._make_points()
self.segments = []
def _make_points(self):
for _t in range(self.num_t):
t = _t / self.num_t
self.points.append(self.start + (self.end - self.start) * t)
def __iter__(self):
for point in self.points:
yield point
def draw(self, canvas):
for p0, p1 in zip(self.points[:-1], self.points[1:]):
self.segments.append(canvas.create_line(*p0, *p1, width=5, state='hidden', fill='red'))
def hide(self, canvas):
for seg in self.segments:
canvas.itemconfigure(seg, state='hidden')
def create_affine_line(canvas, num_lines):
"""sanity check"""
for _ in range(num_lines):
start = random.randrange(0, WIDTH), random.randrange(0, HEIGHT)
end = random.randrange(0, WIDTH), random.randrange(0, HEIGHT)
AffineLineSegment(start, end).draw(canvas)
def select_and_redraw(event):
item = canvas.find_closest(event.x, event.y)[0]
x0, y0, x1, y1 = canvas.coords(item)
canvas.itemconfigure(item, fill='grey25')
canvas.itemconfigure(item, width=1)
a = AffineLineSegment((x0, y0), (x1, y1))
a.draw(canvas)
gen = (segment for segment in a.segments)
redraw(gen, a, item)
def redraw(gen, a, item):
try:
segment = next(gen)
canvas.itemconfigure(segment, state='normal')
root.after(10, redraw, gen, a, item)
except StopIteration:
a.hide(canvas)
canvas.itemconfigure(item, state='normal')
canvas.itemconfigure(item, fill='red')
canvas.itemconfigure(item, width=3)
finally:
root.after_cancel(redraw)
root = tk.Tk()
canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg="cyan")
canvas.pack()
canvas.bind('<ButtonPress-1>', select_and_redraw)
# sanity checks
# create_affine_points(canvas, 500)
# create_affine_line(canvas, 100)
for _ in range(10):
start = random.randrange(0, WIDTH), random.randrange(0, HEIGHT)
end = random.randrange(0, WIDTH), random.randrange(0, HEIGHT)
canvas.create_line(*start, * end, activefill='blue', fill='black', width=3)
root.mainloop()
Screen capture showing a line in the process of being redrawn
Try something like this
from tkinter import *
import numpy as np
root = Tk()
def lighter(color, percent):
color = np.array(color)
white = np.array([255, 255, 255])
vector = white-color
return tuple(color + vector * percent)
def Fade(line, start_rgb, percentage, times, delay):
'''assumes color is rgb between (0, 0, 0) and (255, 255, 255) adn percentage a value between 0.0 and 1.0'''
new_color = lighter(start_rgb, percentage)
red, blue, green = new_color
red = int(red)
blue = int(blue)
green = int(green)
new_hex = '#%02x%02x%02x' % (red, blue, green)
canvas.itemconfigure(line, fill=new_hex)
if times > 0:
root.after(delay, lambda: Fade(line, new_color, percentage, times - 1, delay))
canvas = Canvas(root, bg="black")
canvas.pack()
line = canvas.create_line(0, 0, 100, 100, width=10)
Fade(line, (0, 0, 50), 0.01, 1000, 10)
root.mainloop()

Diagonal lines of squares in grids

I have a python grid class in which I am trying to create a method to get the diagonal line of tiles that a certain tile belongs to. I have succeeded in doing this for going down from left to right in a diagonal line, and would like to know how to change it so that I can go from right to left by changing the "direction" parameter. Here is my method:
def getDiagonal(self, tile, direction = 1):
index = self.index(tile)
diagonal = []
currentIndex = [i - index[0] for i in index]
while currentIndex[1] != self.y:
diagonal.append(self[currentIndex[0]][currentIndex[1]])
currentIndex = [i + 1 for i in currentIndex]
return diagonal
And here is the entire module in which the Grid class is contained:
# Grid library for Pygame by Bobby Clarke
# GNU General Public License 3.0
# Version 1.1
import pygame
import math
from fractions import gcd
from functools import reduce
def _isEven(i):
return i % 2 == 0
def product(_list):
return reduce(lambda x, y: x * y, _list, 1)
def _range(start, stop, step=1):
"""Range function which can handle floats."""
while start < stop:
yield start
start += step
def _simplify(a, b):
hcf = gcd(a, b)
return (a / hcf, b / hcf)
class Tile(pygame.Rect):
def __init__(self, point, size, colour = None, imgs = [], tags = []):
self.size = [int(i) for i in size]
self.point = point
self.colour = colour
for img in imgs:
if isinstance(img, tuple):
imgs[imgs.index(img)] = pygame.image.fromstring(img[0],
img[1],
img[2])
self.imgs = imgs[:]
self.tags = tags[:]
pygame.Rect.__init__(self, self.point, self.size)
def __lt__(self, other):
return (self.point[0] < other.point[0] or
self.point[1] < other.point[1])
def __gt__(self, other):
return (self.point[0] > other.point[0] or
self.point[1] > other.point[1])
def __le__(self, other):
return (self.point[0] <= other.point[0] or
self.point[1] <= other.point[1])
def __ge__(self, other):
return (self.point[0] >= other.point[0] or
self.point[1] >= other.point[1])
def toData(self, imgFormat = "RGBA"):
return (self.point, self.size, self.colour,
[(pygame.image.tostring(img, imgFormat),
img.get_size(), imgFormat) for img in self.imgs], self.tags)
def fromData(data, baseTile = None):
tile = Tile(*data)
if baseTile and isinstance(baseTile, Tile):
baseTile = tile
else:
return tile
def getRect(self):
return self
def getColour(self):
return self.colour
def setColour(self, colour):
self.colour = colour
def getPoint(self):
return self.point
def addTag(self, *tags):
if isinstance(tags[0], list):
self.tags.extend(tags[0])
else:
self.tags.extend(tags)
def hasTag(self, tag):
return (tag in self.tags)
def delTag(self, tag):
self.tags.remove(tag)
def clearTags(self):
self.tags = []
def addImg(self, img, resize = False):
if isinstance(img, pygame.Surface):
if img.get_rect() != self and resize:
img = pygame.transform.scale(img, (self.size))
self.imgs.append(img)
elif img is not None:
raise TypeError("Images must be pygame.Surface object")
def delImg(self, img):
self.imgs.remove(img)
def clearImgs(self):
self.imgs = []
def isClicked(self):
return self.collidepoint(pygame.mouse.get_pos())
def draw(self, surface):
if self.colour is not None:
surface.fill(self.colour, self)
for img in self.imgs:
surface.blit(img, self)
class Grid():
def __init__(self, surface, num, colour = None, tiles = None,
force_square = False):
self.WIDTH = surface.get_width()
self.HEIGHT = surface.get_height()
self.surface = surface
aspect_ratio = _simplify(self.WIDTH, self.HEIGHT)
if isinstance(num, int):
if aspect_ratio == (1, 1) or force_square:
self.x = math.sqrt(num)
self.y = math.sqrt(num)
else:
self.x = aspect_ratio[0] * (num / product(aspect_ratio))
self.y = aspect_ratio[1] * (num / product(aspect_ratio))
else:
try:
self.x = num[0]
self.y = num[1]
except TypeError:
raise TypeError("2nd argument must be int or subscriptable")
self.tilesize = (self.WIDTH / self.x,
self.HEIGHT / self.y)
self.num = num
self.colour = colour
if tiles:
if hasattr(tiles, "__getitem__") and isinstance(tiles[0], Tile):
self.tiles = tiles
else:
self.tiles = [[Tile.fromData(tile) for tile in column]
for column in tiles]
else:
self.tiles = self.maketiles(colour)
def __getitem__(self, index):
return self.tiles[index]
def __setitem__(self, index, new):
self.tiles[index] = new
def __len__(self):
return len(self.tiles)
def index(self, tile):
for column in self.tiles:
if tile in column:
return self.tiles.index(column), column.index(tile)
def getTiles(self):
"""Get all tiles. Returns a generator"""
for column in self.tiles:
for tile in column:
yield tile
def tagSearch(self, tag):
"""Search for tiles by tag. Returns a generator"""
for tile in self.getTiles():
if tile.hasTag(tag):
yield tile
def pointSearch(self, point):
"""Search for tiles by point. Returns a tile"""
for tile in self.getTiles():
if tile.collidepoint(point):
return tile
def rectSearch(self, rect):
"""Search for tiles by rect. Returns a generator"""
for tile in self.getTiles():
if tile.colliderect(rect):
yield tile
def getColumn(self, i):
return self.tiles[i]
def getRow(self, i):
return [column[i] for column in self.tiles]
def checker(self, colour1, colour2 = None):
for column in self.tiles:
for tile in column:
if _isEven(self.tiles.index(column) + column.index(tile)):
tile.setColour(colour1)
else:
if colour2:
tile.setColour(colour2)
def getDiagonal(self, tile, direction = 1):
index = self.index(tile)
diagonal = []
currentIndex = [i - index[0] for i in index]
while currentIndex[1] != self.y:
diagonal.append(self[currentIndex[0]][currentIndex[1]])
currentIndex = [i + 1 for i in currentIndex]
return diagonal
def getBetweenTiles(self, tile1, tile2):
"""Inefficient and badly implemented"""
index1 = self.index(tile1)
index2 = self.index(tile2)
if index1[0] != index2[0] and index1[1] != index2[1]:
raise ValueError("Tiles must be in same row or column")
for column in self.tiles:
if tile1 in column and tile2 in column:
return column[column.index(tile1) : column.index(tile2)]
for i in range(self.y):
row = self.getRow(i)
if tile1 in row and tile2 in row:
return row[row.index(tile1) : row.index(tile2)]
def getSurroundingTiles(self, tile, adjacent = True, diagonal = True):
di = (0, 1, 0, -1, 1, 1, -1, -1)
dj = (1, 0, -1, 0, 1, -1, 1, -1)
# indices 0 - 3 are for horizontal, 4 - 7 are for vertical
index = list(self.getTiles()).index(tile)
max_x = self.x - 1 # Offset for 0 indexing
max_y = self.y - 1
i = int(math.floor(index / self.x))
j = int(index % self.y)
surroundingTiles = []
startat = 0 if adjacent else 4
stopat = 8 if diagonal else 4
for k in range(startat, stopat):
ni = i + di[k]
nj = j + dj[k]
if ni >= 0 and nj >= 0 and ni <= max_x and nj <= max_y:
surroundingTiles.append(self[ni][nj])
surroundingTiles.reverse()
return sorted(surroundingTiles)
def draw(self, drawGrid = False, gridColour = (0, 0, 0), gridSize = 1):
for tile in self.getTiles():
tile.draw(self.surface)
if drawGrid:
pygame.draw.rect(self.surface, gridColour, tile, gridSize)
def maketiles(self, colour):
"""Make the tiles for the grid"""
tiles = []
width = self.WIDTH / self.x
height = self.HEIGHT / self.y
for i in _range(0, self.WIDTH, width):
column = []
for j in _range(0, self.HEIGHT, height):
sq = Tile((i, j), (width, height), colour)
column.append(sq)
tiles.append(column)
return tiles
def toData(self):
return (self.num, self.colour,
[[tile.toData() for tile in column] for column in self.tiles])
def fromData(data, surface):
return Grid(*([surface] + list(data)))
Update:
I have pictorial examples of what I want to do here:
https://dl.dropboxusercontent.com/u/127476718/Images/this%20tile.png
https://dl.dropboxusercontent.com/u/127476718/Images/diags.png
To find the cells along the diagonal through another cell, you must go from this cell in the 4 diagonal directions. The following algorithm finds all cells along the diagonal in a grid of size rows x columns, starting with the cell (row, column):
cells_on_diagonals.append((row, column))
for dir in [(1, 1), (1, -1), (-1, 1), (-1, -1)]:
rn, cn = row + dir[1], column + dir[0]
while 0 <= cn < columns and 0 <= rn < rows:
cells_on_diagonals.append((rn, cn))
rn += dir[1]
cn += dir[0]
Minimal example
import pygame
pygame.init()
window = pygame.display.set_mode((350, 350))
clock = pygame.time.Clock()
grid_x, grid_y = 25, 25
tile_size, rows, columns = 20, 15, 15
run = True
while run:
clock.tick(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
mx, my = pygame.mouse.get_pos()
column, row = (mx - grid_x) // tile_size, (my - grid_y) // tile_size
cells_on_diagonals = []
if 0 <= column < columns and 0 <= row < rows:
cells_on_diagonals.append((row, column))
for dir in [(1, 1), (1, -1), (-1, 1), (-1, -1)]:
rn, cn = row + dir[1], column + dir[0]
while 0 <= cn < columns and 0 <= rn < rows:
cells_on_diagonals.append((rn, cn))
rn += dir[1]
cn += dir[0]
window.fill(0)
for row in range(rows):
for column in range(columns):
if (row, column) in cells_on_diagonals:
rect = (grid_x + column*tile_size, grid_y + row*tile_size, tile_size, tile_size)
pygame.draw.rect(window, "red", rect)
for column in range(columns+1):
x = grid_x + column*tile_size
pygame.draw.line(window, "white", (x, grid_y), (x, grid_x + rows*tile_size))
for row in range(rows+1):
y = grid_y + row*tile_size
pygame.draw.line(window, "white", (grid_x, y), (grid_x + columns*tile_size, y))
pygame.display.flip()
pygame.quit()
exit()

Categories