0% found this document useful (0 votes)
70 views

Import PySimpleGUI As SG

This document contains code for a pathfinding visualization tool that allows users to: 1) Generate and edit mazes by adding walls, paths, start and end nodes. 2) Run pathfinding algorithms like BFS, DFS, Dijkstra's and A* on the maze. 3) Adjust the speed of the algorithms and reset or clear the maze. The code defines classes for nodes and mazes, sets up a GUI, and contains functions for running the algorithms, handling input, and updating the display.

Uploaded by

21110267
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
70 views

Import PySimpleGUI As SG

This document contains code for a pathfinding visualization tool that allows users to: 1) Generate and edit mazes by adding walls, paths, start and end nodes. 2) Run pathfinding algorithms like BFS, DFS, Dijkstra's and A* on the maze. 3) Adjust the speed of the algorithms and reset or clear the maze. The code defines classes for nodes and mazes, sets up a GUI, and contains functions for running the algorithms, handling input, and updating the display.

Uploaded by

21110267
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 26

import PySimpleGUI as sg

from modules import priority_queue as pq


from collections import deque
from time import sleep
from random import choice as random_choice
from json import (load as jsonload, dump as jsondump)
from os import (path as path, name as operating_system)
OS = operating_system

MAZE_WIDTH = 51
MAZE_HEIGHT = 51
NODE_SIZE = 10

NODES = {} # Dictionary of nodes in the grid with (x,y) tuples as keys


START_NODE = None # Instance of Node. The node from which the algorithm starts
END_NODE = None # Instance of Node. The node at which the maze is 'solved'

ALGO = 'Breadth-First Search' # Pathfinding algorithm to use.


MODE = 'wall' # None, 'wall', 'path', 'start', 'end'
TEMP_DELAY = None # Temporary variable to store original DELAY
DELAY = 0 # Algorithm iteration delay (in milliseconds)
SPEED = None # Value of the speed slider
PAUSED = False # Flipped if the pause button is clicked

# Checking for user input on every loop iteration slows down the algorithms
# To avoid this, we will only check for user input every LOOP_CHECK iterations
# Because lower speeds require lower LOOP_CHECK values to avoid input delay,
# The speed slider will adjust LOOP_CHECK on a per-speed basis
LOOP_COUNT = 0
LOOP_CHECK = 0

COLORS = { # Dictionary of colors to use in Node.style()


'empty': '#CCCCCC', # Grey
'wall': '#003333', # Black
'start': '#00CC00', # Green
'start_border': '#006000', # Dark Green
'end': '#FF3366', # Red
'end_border': '#890F1F', # Dark Red
'active': '#EFC700', # Yellow
'visited': '#999966', # Olive
'neighbor': '#96E8FF', # Light Blue
'solution': '#009900', # Dark Green
'error': '#FF6D70', # Light red
'black': '#000000',
'white': '#FFFFFF',
}

DEFAULT_SETTINGS = {
"default_maze": "None",
"default_algorithm": "Breadth-First Search",
"default_speed": 4,
"maze_width": 51,
"maze_height": 51,
"node_size": 10
}
def set_algo(new_algo: str) -> None:
global ALGO
ALGO = new_algo
algo_ref = {
'Breadth-First Search': 'bfs',
'Depth-First Search': 'dfs',
'Dijkstra': 'dijkstra',
'A* (A Star)': 'astar',
}
# Select the appropriate radio
window[f'radio_algo_{algo_ref[new_algo]}'].update(value=True)
# Print event
print(f"Algorithm set to {algo_ref[new_algo].upper()}")

def set_draw_mode(draw_mode: str) -> None:


global MODE
MODE = draw_mode
# Depress all draw mode buttons
window['maze_tools_wall'].update(button_color=('#000', '#f0f0f0'))
window['maze_tools_path'].update(button_color=('#000', '#f0f0f0'))
window['maze_tools_start'].update(button_color=('#000', '#f0f0f0'))
window['maze_tools_end'].update(button_color=('#000', '#f0f0f0'))
# Windows only button relief styles:
if OS == 'nt':
window['maze_tools_wall'].Widget.configure(relief='raised')
window['maze_tools_path'].Widget.configure(relief='raised')
window['maze_tools_start'].Widget.configure(relief='raised')
window['maze_tools_end'].Widget.configure(relief='raised')
window['maze_tools_'+draw_mode].Widget.configure(relief='sunken')
# Press the selected draw mode button
window['maze_tools_'+draw_mode].update(button_color='white on grey')
# Print event
print(f"Draw mode set to '{draw_mode}'")
def reset() -> None:
global PAUSED
PAUSED = False
for node in NODES.values():
node.reset_node()
MAZE.clear_solution()
MAZE.bring_start_and_end_nodes_to_front()
enable_drawing_tools()
set_draw_mode('wall')
enable_menu(window)

def enable_drawing_tools() -> None:


"""Enables the `wall`, `path`, `start, and `end` buttons in the UI and sets
the draw mode to 'wall'."""
for button in ['maze_tools_wall', 'maze_tools_path',
'maze_tools_start', 'maze_tools_end']:
window[button].update(disabled=False)

def clear() -> None:


for node in NODES.values():
node.make_empty_node()
MAZE.clear_solution()
global PAUSED
PAUSED = False
set_draw_mode('wall')
enable_menu(window)
enable_drawing_tools()

def set_speed(speed: float) -> None:


global DELAY
global TEMP_DELAY
global LOOP_CHECK
global SPEED
SPEED = int(speed)
window['controls_speed_label'].update(value=f'Speed: {SPEED}')
if SPEED == 1:
DELAY = 1500
TEMP_DELAY = 1500
LOOP_CHECK = -1
elif SPEED == 2:
DELAY = 500
TEMP_DELAY = 500
LOOP_CHECK = -1
elif SPEED == 3:
DELAY = 100
TEMP_DELAY = 100
LOOP_CHECK = -1
elif SPEED == 4:
DELAY = 25
TEMP_DELAY = 25
LOOP_CHECK = -1
elif SPEED == 5:
DELAY = 0
TEMP_DELAY = 0
LOOP_CHECK = 10
print(f'Delay set to: {DELAY}ms.')

def disable_menu(window) -> None:


pass

def enable_menu(window) -> None:


pass

def disable_drawing_tools() -> None:


for button in ['maze_tools_wall', 'maze_tools_path',
'maze_tools_start', 'maze_tools_end']:
window[button].update(disabled=True, button_color=('#000', '#f0f0f0'))

def read_algo_controls(timeout=None) -> tuple:


event, values = window.read(timeout)
# Break out of the function if it's just a timeout event
if event == '__TIMEOUT__':
return (False, event)

global TEMP_DELAY
global DELAY
DELAY = TEMP_DELAY

if event == sg.WIN_CLOSED:
return (True, event)

# Next Button
elif event == 'controls_next':
TEMP_DELAY = DELAY
DELAY = None
return (False, event)

# Speed Slider
elif event == 'controls_speed_slider':
set_speed(values['controls_speed_slider'])
if PAUSED:
return read_algo_controls(timeout=None)
return (False, event)

# Reset/Clear Buttons
elif event == 'maze_tools_clear':
clear()
return (True, event)
elif event == 'maze_tools_reset':
reset()
return (True, event)

# Menu items will do nothing


elif event in ('File', 'Tools',
'Open Maze', 'Save Maze',
'Generate Maze', 'Fill Maze',
'Runtime Info', 'Maze Dimension', 'Defaults'):
return (False, '__TIMEOUT__')

# Log window event and values


print("Event: \t", event)
print("Values: ", values)
return (False, event)

def check_for_input() -> bool:


global LOOP_COUNT
global LOOP_CHECK
if LOOP_COUNT > LOOP_CHECK:
interrupted, event = read_algo_controls(timeout=DELAY)
if interrupted:
LOOP_COUNT = 0
return (True, event)
if event in ('controls_next' or 'controls_speed_slider'):
# Make sure controls are read on the next loop iteration
LOOP_COUNT = LOOP_CHECK+1
else:
# Algorithm controls have been checked, reset LOOP_COUNT
LOOP_COUNT = 0
else:
LOOP_COUNT += 1
# Slow algorithm down according to delay
sleep(DELAY/1000)
window.refresh()
return (False, None)

def bfs_dfs() -> None:


global LOOP_COUNT
global LOOP_CHECK
interrupted = False
# use a stack suitable for both bfs and dfs,
# allowing for both lifo and fifo operations
stack = deque([])
# add the starting node to the stack
stack.append(START_NODE)
# as long as the stack has a node
step=0
while stack:
# set the top node as the currently active node
current_node = stack.pop()
current_node.make_active_node()
step=step+1
# Checks for and processes user input
# every LOOP_CHECK iterations of this loop
interrupted, event = check_for_input()
if interrupted:
break

# flag the current node as active


# check if it's the end node
if current_node.is_end_node:
break
# for all valid neighbor nodes:
# (in-bound nodes that are not walls, and have not been visited)
neighbors = current_node.get_neighbors()
if not neighbors:
print(f'No neighbors at {current_node.loc}')
current_node.make_visited_node()
else:
for neighbor in neighbors:
# mark the neighbor as visited and style as neighbor
neighbor.make_neighbor_node()
# mark the current node as visited
current_node.make_visited_node()
# print(f'{current_node.loc} visited.')
neighbor.parent = current_node

# add the neighbor to a queue


if ALGO[0] == 'B': # BFS, use queue: first in, first out
stack.appendleft(neighbor)
elif ALGO[0] == 'D': # DFS, use stack: last in, first out
stack.append(neighbor)
# Mark the solution path
print("Số nút đã duyệt qua: ",step)
if not interrupted: MAZE.highlight_solution(current_node)
# Return false if the window is closing
if event in ('Exit', sg.WIN_CLOSED):
return False
# Continue as normal
return True
def dijkstra() -> None:
interrupted = False

# Initialize an updateable priority queue with the start node, at priority 0


# The 'keys' for the queue will be the coordinates for the nodes
queue = pq.UpdateableQueue()
queue.push(START_NODE.loc, 0)
step=0
while queue.__len__() > 0:
step=step+1
# Get the highest priority node
current_node = NODES[queue.pop()[0]]
# Check to see if it's the end node
if current_node.is_end_node:
break
# Mark it as visited
current_node.make_active_node()
window.refresh()

# Checks for and processes user input


# every LOOP_CHECK iterations of this loop
interrupted, event = check_for_input()
if interrupted:
break

# Get all valid neighbor nodes of that node


neighbors = current_node.get_neighbors()
# If there are no neighbors, mark that node as visited
if not neighbors:
print(f'No neighbors at {current_node.loc}')
current_node.make_visited_node()
# If there are neighbors,
else:
# For each neighbor:
for neighbor in neighbors:
# Mark that neighbor as visited, and color it blue
neighbor.make_neighbor_node()
# Calculate the distance of that node to the start node
min_distance = min(neighbor.distance, current_node.distance + 1)
if min_distance != neighbor.distance:
neighbor.distance = min_distance
# Change queue priority for nieghbor since it's now closer
queue.push(neighbor.loc, neighbor.distance)
# Set the current node as the parent node for each neighbor
neighbor.parent = current_node
# Mark the current node as visited
current_node.make_visited_node()
# Mark the solution path
print("Số nút đã duyệt qua: ",step)
if not interrupted: MAZE.highlight_solution(current_node)
return True
def astar() -> None:
interrupted = False
queue = pq.UpdateableQueue()
queue.push(START_NODE.loc, 0)
step=0
while queue.__len__() > 0:
step=step+1
current_node = NODES[queue.pop()[0]]
if current_node.is_end_node:
break
current_node.make_active_node()
window.refresh()
interrupted, event = check_for_input()
if interrupted:
break
neighbors = current_node.get_neighbors()
if not neighbors:
print(f'No neighbors at {current_node.loc}')
current_node.make_visited_node()
else:
for neighbor in neighbors:
neighbor.make_neighbor_node()
# Set distance to be distance from the neighbor to end node
neighbor.distance = (abs(END_NODE.x - neighbor.x) +
abs(END_NODE.y - neighbor.y))
queue.push(neighbor.loc, neighbor.distance)
neighbor.parent = current_node
current_node.make_visited_node()
print("Số nút đã duyệt qua: ",step)
if not interrupted: MAZE.highlight_solution(current_node)
return True

def solve_maze() -> None:


if START_NODE and END_NODE:
# Disable UI elements that can't be used while solving
disable_menu(window)
disable_drawing_tools()
# Enable UI elements that can only be used while solving

print('*'*40)
print(f'Solve started: {ALGO.upper()} algorithm.')
print('*'*40)
if ALGO in ('Breadth-First Search', 'Depth-First Search'):
if not bfs_dfs():
return False
elif ALGO == 'Dijkstra':
if not dijkstra():
return False
elif ALGO == 'A* (A Star)':
if not astar():
return False
enable_menu(window)
else:
sg.popup('The maze needs a start and and end node for a solvable maze.',
'Set these nodes with the "Start Node" and "End Node" buttons')

def open_maze_file(filename: str) -> bool:


def valid_maze_file(filename: str):
if filename and filename != 'None':
return True
else:
return False

if valid_maze_file(filename):
try:
print(f'Open maze file: {filename}')
clear()

# get the width and height of the new maze


with open(f'{filename}', 'r', encoding='utf8') as new_maze:
width = len(new_maze.readline())-1
height = len(new_maze.readlines())+1
MAZE.resize_maze(width, height)
with open(f'{filename}', 'r', encoding='utf8') as new_maze:
x = 0 # x coordinate
y = 0 # y coordinate
for line in new_maze.readlines():
for char in line:
if char == ' ':
NODES[(x,y)].make_empty_node()
elif char == '█':
NODES[(x,y)].make_wall_node()
elif char == 'S':
NODES[(x,y)].make_start_node()
elif char == 'E':
NODES[(x,y)].make_end_node()
# reset the x coordinate at the end of each line
x = x+1 if x < width else 0
y += 1
MAZE.bring_start_and_end_nodes_to_front()
except:
# If there's no nodes, generate them
if not NODES:
MAZE.resize_maze(MAZE_WIDTH,MAZE_HEIGHT,NODE_SIZE)
clear()
sg.popup('Error loading maze.')
def save_maze_file(filename: str) -> bool:
if not filename:
return False

# list that stores the maze


maze_to_write = []
for col in range(MAZE_HEIGHT):
row_list = []
for row in range(MAZE_WIDTH):
if NODES[(row, col)].is_start_node:
row_list.append('S')
elif NODES[(row, col)].is_end_node:
row_list.append('E')
elif NODES[(row, col)].is_empty:
row_list.append(' ')
elif NODES[(row, col)].is_wall:
row_list.append('█')
maze_to_write.append(row_list)
for row in maze_to_write:
row[MAZE_WIDTH-1] = row[MAZE_WIDTH-1][0]
with open(f'{filename.name}', 'w', encoding="utf-8") as file_to_write:
for row in range(MAZE_HEIGHT):
file_to_write.writelines(maze_to_write[row])
if row != MAZE_HEIGHT-1:
file_to_write.write('\n')

print(f'Save maze to: {filename}')


def generate_maze() -> None:
print('Generate Maze')
clear()

def pick_maze_generator_starting_point() -> tuple:


# List of permissible starting points for the x and y coordinates
coords_x = [x for x in range(1, MAZE_WIDTH-1, 2)]
coords_y = [x for x in range(1, MAZE_HEIGHT-1, 2)]
return (random_choice(coords_x), random_choice(coords_y))

def connect_nodes(current_node, old_node) -> None:


x_diff = current_node.x - old_node.x
y_diff = current_node.y - old_node.y
if x_diff == -2:
NODES[(current_node.x+1, current_node.y)].make_empty_node()
elif x_diff == 2:
NODES[(current_node.x-1, current_node.y)].make_empty_node()
elif y_diff == -2:
NODES[(current_node.x, current_node.y+1)].make_empty_node()
elif y_diff == 2:
NODES[(current_node.x, current_node.y-1)].make_empty_node()

# Populates existing maze with wall nodes


MAZE.clear_solution()
MAZE.fill_maze()
# Set a start node
NODES[(1,0)].make_start_node()
# Set an end node
if MAZE_WIDTH % 2 == 0:
NODES[(MAZE_WIDTH-3, MAZE_HEIGHT-1)].make_end_node()
else:
NODES[(MAZE_WIDTH-2, MAZE_HEIGHT-1)].make_end_node()
if MAZE_HEIGHT % 2 == 0:
NODES[(END_NODE.x, END_NODE.y-1)].make_empty_node()
stack = []
stack.append(NODES[pick_maze_generator_starting_point()])
old_node = None
while stack:
current_node = stack[len(stack)-1]
if old_node:
connect_nodes(old_node, current_node)
current_node.make_empty_node()
window.refresh()
old_node = current_node
directions = current_node.get_directions_to_dig()
if not directions:
stack.pop()
continue
direction = random_choice(directions)
x_diff = current_node.x - direction.x
y_diff = current_node.y - direction.y
if x_diff == -1:
stack.append(NODES[(current_node.x+2, current_node.y)])
elif x_diff == 1:
stack.append(NODES[(current_node.x-2, current_node.y)])
elif y_diff == -1:
stack.append(NODES[(current_node.x, current_node.y+2)])
elif y_diff == 1:
stack.append(NODES[(current_node.x, current_node.y-2)])

MAZE.bring_start_and_end_nodes_to_front()
def save_settings(settings):
settings_dict = {
"default_settings_default_maze": "default_maze",
"default_settings_default_algorithm": "default_algorithm",
"default_settings_default_speed": "default_speed",
"default_settings_maze_width": "maze_width",
"default_settings_maze_height": "maze_height",
"default_settings_maze_node_size": "node_size"
}
# Populate a new settings dictionary
parsed_settings = {}
for setting in settings:
if setting in settings_dict:
if settings[setting]:
parsed_settings[settings_dict[setting]] = settings[setting]
else:
parsed_settings[settings_dict[setting]] =
DEFAULT_SETTINGS[settings_dict[setting]]
with open(path.join(path.dirname(__file__), 'settings.cfg'), 'w') as
settings_file:
jsondump(parsed_settings, settings_file, indent=4)

def apply_settings():
settings = read_settings()
window['controls_speed_slider'].update(value=settings["default_speed"])
set_speed(settings["default_speed"])
# Set algorithm
set_algo(settings["default_algorithm"])
# Open maze
if settings["default_maze"] != "None":
open_maze_file(settings["default_maze"])
else:
MAZE.resize_maze(settings["maze_width"],
settings["maze_height"],
settings["node_size"])

class Node(object):
def __init__(self, maze: str, location: tuple) -> None:
self.maze = maze # window graph object
self.x = location[0] # x coordinate
self.y = location[1] # y coordinate
self.loc = location # tuple of (x,y)

# Status attributes
self.is_empty = True
self.is_wall = False
self.is_start_node = False
self.is_end_node = False
self.is_visited = False
self.is_active = False

# List of all surrounding node locations


self.surrounding_locations = (
(self.x, self.y+1), # top
(self.x+1, self.y+1), # top-right
(self.x+1, self.y), # right
(self.x+1, self.y-1), # bottom-right
(self.x, self.y-1), # bottom
(self.x-1, self.y-1), # bottom-left
(self.x-1, self.y), # left
(self.x-1, self.y+1), # top-left
)
self.parent = None
self.distance = float('inf')

# Draw the node on the graph and store the drawn figure in the self.id
self.id = maze.draw_rectangle(top_left=(self.x*NODE_SIZE,
self.y*NODE_SIZE),
bottom_right=(self.x*NODE_SIZE+NODE_SIZE,
self.y*NODE_SIZE+NODE_SIZE),
fill_color=COLORS['empty'],
line_color='#fff',
line_width=1)
NODES[(self.x, self.y)] = self

def get_center(self) -> tuple:


return (self.x * NODE_SIZE + (NODE_SIZE/2),
self.y * NODE_SIZE + (NODE_SIZE/2))

def is_next_to(self, node) -> bool:


if node:
if self.loc in node.surrounding_locations:
return True
return False

def style(self, color, border_color='#fff', border_width=1,


send_to_back=False):
self.maze.delete_figure(self.id)
self.id = self.maze.draw_rectangle(
top_left=(self.x*NODE_SIZE,
self.y*NODE_SIZE),
bottom_right=(self.x*NODE_SIZE+NODE_SIZE,
self.y*NODE_SIZE+NODE_SIZE),
fill_color=color,
line_color=border_color,
line_width=border_width)
if send_to_back:
MAZE.send_figure_to_back(self.id)

def get_neighbors(self) -> list:


neighbors = []
if self.y != 0:
neighbors.append(NODES[(self.x, self.y-1)]) # top
# subtract 1 from MAZE_WIDTH because location indexes start at 0
if self.x != MAZE_WIDTH-1:
neighbors.append(NODES[(self.x+1, self.y)]) # right
# subtract 1 from MAZE_HEIGHT because location indexes start at 0
if self.y != MAZE_HEIGHT-1:
neighbors.append(NODES[(self.x, self.y+1)]) # bottom
if self.x != 0:
neighbors.append(NODES[(self.x-1, self.y)]) # left
# Prune neighbors list to remove visited nodes and wall nodes
return [node for node in neighbors if not node.is_wall and not
node.is_visited]

def get_directions_to_dig(self) -> list:


# Immediate neighbor nodes
neighbors = [
NODES[(self.x, self.y+1)], # top
NODES[(self.x+1, self.y)], # right
NODES[(self.x, self.y-1)], # bottom
NODES[(self.x-1, self.y)], # left
]
if self.y+2 > MAZE_HEIGHT-2 or NODES[(self.x, self.y+2)].is_empty:
neighbors[0] = False
if self.x+2 > MAZE_WIDTH-2 or NODES[(self.x+2, self.y)].is_empty:
neighbors[1] = False
if self.y-2 < 1 or NODES[(self.x, self.y-2)].is_empty:
neighbors[2] = False
if self.x-2 < 1 or NODES[(self.x-2, self.y)].is_empty:
neighbors[3] = False
return [neighbor for neighbor in neighbors if neighbor]

def make_start_node(self) -> None:


global START_NODE
# Remove existing start node
if START_NODE:
START_NODE.make_empty_node()
START_NODE = self
self.style(COLORS['start'],
border_color=COLORS['start_border'],
border_width=4)
self.is_empty = True
self.is_wall = False
self.is_start_node = True
self.is_end_node = False
self.distance = 0
self.start_distance = 0
self.end_distance = float('inf')

def make_end_node(self) -> None:


global END_NODE
# Remove existing end node
if END_NODE:
END_NODE.make_empty_node()
END_NODE = self
self.style(COLORS['end'],
border_color=COLORS['end_border'],
border_width=4)
self.is_empty = True
self.is_wall = False
self.is_start_node = False
self.is_end_node = True
self.distance = float('inf')
self.start_distance = float('inf')
self.end_distance = float('inf')

def make_wall_node(self) -> None:


self.style(color=COLORS['wall'],
border_color=COLORS['wall'])
self.maze.send_figure_to_back(self.id)
self.is_empty = False
self.is_wall = True
self.is_visited = False
self.is_start_node = False
self.is_end_node = False
self.distance = float('inf')
self.start_distance = float('inf')
self.end_distance = float('inf')

def make_empty_node(self) -> None:


self.style(COLORS['empty'])
self.is_empty = True
self.is_wall = False
self.is_visited = False
self.is_active = False
self.distance = float('inf')
self.start_distance = float('inf')
self.end_distance = float('inf')
if self.is_start_node:
global START_NODE
self.is_start_node = False
START_NODE = None
elif self.is_end_node:
global END_NODE
self.is_end_node = False
END_NODE = None
# If drawn next to a start or end node,
# Make sure it's behind that node.
if self.is_next_to(START_NODE):
MAZE.bring_figure_to_front(START_NODE.id)
if self.is_next_to(END_NODE):
MAZE.bring_figure_to_front(END_NODE.id)

def make_visited_node(self) -> None:


self.style(COLORS['visited'])
self.is_visited = True

def make_neighbor_node(self) -> None:


self.style(COLORS['neighbor'])
self.is_visited = True

def make_active_node(self) -> None:


self.style(COLORS['active'],
COLORS['black'],
border_width=3)
self.is_active = True

def make_solution_node(self) -> None:


self.style(COLORS['white'], COLORS['white'], send_to_back=True)

def make_error_node(self) -> None:


self.style(COLORS['error'])

def reset_node(self):
# reset flags
self.is_visited = False
self.is_active = False
# reset colors
if self.is_start_node:
self.make_start_node()
elif self.is_end_node:
self.make_end_node()
elif self.is_wall:
self.make_wall_node()
elif self.is_empty:
self.make_empty_node()
class Maze(sg.Graph): # Extend PySimpleGUI Graph Class
def __init__(self, key, canvas_size, graph_bottom_left, graph_top_right,
background_color, drag_submits, enable_events):
super().__init__(key=key,
canvas_size=canvas_size,
graph_bottom_left=graph_bottom_left,
graph_top_right=graph_top_right,
background_color=background_color,
drag_submits=drag_submits,
enable_events=enable_events)
self.solution_figures = []

def resize_maze(self, nodes_across, nodes_down, node_size=NODE_SIZE) ->


None:
global MAZE
global MAZE_WIDTH
global MAZE_HEIGHT
global NODE_SIZE
global NODES
MAZE_WIDTH = nodes_across
MAZE_HEIGHT = nodes_down
NODE_SIZE = node_size
print(f"Resize maze:\n",
f"\t{nodes_across} nodes wide,\n",
f"\t{nodes_down} nodes down,\n",
f"\twith a node size of {node_size}"
)

for node in NODES.values():


window['maze'].delete_figure(node.id)
NODES.clear()

# Create a new graph


MAZE.clear_solution()
MAZE.change_coordinates(graph_bottom_left=(0, MAZE_HEIGHT*NODE_SIZE),
graph_top_right=(MAZE_WIDTH*NODE_SIZE, 0))
MAZE.set_size(size=(MAZE_WIDTH*NODE_SIZE,
MAZE_HEIGHT*NODE_SIZE))
for x in range(int(MAZE_WIDTH)):
for y in range(int(MAZE_HEIGHT)):
init_node = Node(window['maze'], (x,y))

def fill_maze(self) -> None:


clear()
for node in NODES.values():
node.make_wall_node()

def highlight_solution(self, current_node):


maze_is_solvable = True
# If the current node is not the solution node, the maze is unsolvable
if not current_node.is_end_node:
maze_is_solvable = False
for node in [node for node in NODES.values() if node.is_visited]:
node.make_error_node()
# If the maze has been solved
if maze_is_solvable:
# Draw a path from the end node to the start node using node.parent
self.solution_figures = []
while current_node.parent is not None:
if current_node.is_start_node == True:
break
#current_node.make_solution_node()
fig = self.draw_line(point_from=current_node.get_center(),
point_to=current_node.parent.get_center(),
color=COLORS['end'],
width=3)
window.refresh()
self.solution_figures.append(fig)
current_node = current_node.parent
# Re-establish the maze end points
START_NODE.make_start_node()
END_NODE.make_end_node()
# Show popup if maze is unsolvable
if not maze_is_solvable:
sg.popup('Maze could not be solved.')

def clear_solution(self) -> list:


if self.solution_figures:
for figure_id in self.solution_figures:
self.delete_figure(figure_id)
self.solution_figures = []

def bring_start_and_end_nodes_to_front(self):
if START_NODE:
self.bring_figure_to_front(START_NODE.id)
if END_NODE:
self.bring_figure_to_front(END_NODE.id)
def read_settings():
settings_file_path = path.join(path.dirname(__file__), 'settings.cfg')
try:
with open(settings_file_path, 'r') as settings_file:
current_saved_settings = jsonload(settings_file)
# If there's a problem, show a popup saying there was no setting file found
# and write settings.cfg file to that directory with the default settings
except Exception as e:
settings_file_path = path.join(path.dirname(__file__), 'settings.cfg')
sg.popup('No settings file found.',
'New file automatically created at:',
f'{settings_file_path}', keep_on_top=True)
with open(settings_file_path, 'w') as settings_file:
jsondump(DEFAULT_SETTINGS, settings_file, indent=4)
current_saved_settings = DEFAULT_SETTINGS
return current_saved_settings

def create_settings_window(root_dir):
sg.theme('SystemDefaultForReal')
settings = read_settings()

# Valid values for maze and node dimensions


valid_maze_dims = tuple(range(2,201))
valid_node_dims = tuple(range(5,31,5))

col_1 = [
[sg.Text('Default Maze:')],
[sg.Text('Algorithm:')],
[sg.Text('Speed:')],
[sg.Text('Maze Width:')]
]
col_2 = [
# Default Maze
[sg.Input(key='default_settings_default_maze',
default_text=settings['default_maze']),
sg.FileBrowse(file_types=[('Text Document', '*.txt')],
initial_folder=root_dir)],
# Default Algorithm
[sg.Combo(key='default_settings_default_algorithm',
default_value=settings['default_algorithm'],
values=['Breadth-First Search',
'Depth-First Search','Dijkstra',
'A* (A Star)'],
size=20,
readonly=True)],
# Default Speed
[sg.Combo(key='default_settings_default_speed',
default_value=settings['default_speed'],
values=[1,2,3,4,5],
size=4,
readonly=True)],
# Maze Dimensions
[sg.Spin(key='default_settings_maze_width',
initial_value=settings['maze_width'],
values=(valid_maze_dims),
size=(5,1),
expand_x=True),
sg.Text('Maze Height:'),
sg.Spin(key='default_settings_maze_height',
initial_value=settings['maze_height'],
values=(valid_maze_dims),
size=(5,1),
expand_x=True),
sg.Text('Node Size:'),
sg.Spin(key='default_settings_maze_node_size',
initial_value=settings['node_size'],
values=(valid_node_dims),
size=(5,1),
expand_x=True,
readonly=True)],
]
settings_layout = [
[sg.Column(col_1), sg.Column(col_2)],
[sg.Button('Save'), sg.Button('Close')]
]
settings_window = sg.Window('Set Defaults',
layout=settings_layout,
keep_on_top=True,
finalize=True)
return settings_window
def create_resize_window():
sg.theme('SystemDefaultForReal')
col_1 = [
[sg.Text('Maze Width:')],
[sg.Text('Maze Height:')],
[sg.Text('Node Size:')],
]
col_2 = [
[sg.Spin(key="resize_window_maze_width",
initial_value =MAZE_WIDTH,
values=(list(range(500))),
size=(5,1))],
[sg.Spin(key="resize_window_maze_height",
initial_value =MAZE_HEIGHT,
values=(list(range(500))),
size=(5,1))],
[sg.Spin(key="resize_window_node_size",
initial_value =NODE_SIZE,
values=(list(range(500))),
size=(5,1))],
]
resize_layout = [
[sg.Column(col_1), sg.Column(col_2)],
[sg.Button('Resize'), sg.Button('Close')]
]
resize_window = sg.Window('Set Defaults',
layout=resize_layout,
keep_on_top=True,
finalize=True)
return resize_window
# Create the Window
def create_main_window() -> object:
# Establish color theme
sg.theme('SystemDefaultForReal')

# Maze graph
global MAZE
MAZE = Maze(key="maze",
canvas_size=(MAZE_WIDTH*NODE_SIZE, MAZE_HEIGHT*NODE_SIZE),
graph_bottom_left=(0, MAZE_HEIGHT*NODE_SIZE),
graph_top_right=(MAZE_WIDTH*NODE_SIZE, 0),
background_color="#ffffff",
drag_submits=True,
enable_events=True)
# Main menu
menu = [['File', ['Open Maze', 'Save Maze', 'Exit']],
['Tools', ['Generate Maze', 'Fill Maze']]]

# Algorithm selection radios


layout_algo_radios = [
[sg.Radio(group_id='algo', key='radio_algo_bfs', enable_events=True,
text='Breadth First Search', default=True)],
[sg.Radio(group_id='algo', key='radio_algo_dfs', enable_events=True,
text='Depth First Search')],
[sg.Radio(group_id='algo',
key='radio_algo_dijkstra',
enable_events=True,
text='Dijkstra')],
[sg.Radio(group_id='algo', key='radio_algo_astar', enable_events=True,
text='A* (A Star)')],
]

# Maze draw mode buttons


layout_maze_tools = [
[sg.Button(button_text='Wall', key='maze_tools_wall', expand_x=True,
tooltip="Draw walls on the grid."),
sg.Button(button_text='Path', key='maze_tools_path', expand_x=True,
tooltip="Erase walls and make paths.")],
[sg.Button(button_text='Start Node', key='maze_tools_start',
expand_x=True, tooltip="Designate a starting square.")],
[sg.Button(button_text='End Node', key='maze_tools_end',
expand_x=True, tooltip="Designate an end square.")]
]

# Algorithm controls
layout_controls = [
[sg.Button('Solve', key='controls_solve', expand_x=True,
tooltip="Solves the maze using the selected algorithm.")],
[sg.Text(f'Speed:', key='controls_speed_label')],
[sg.Slider(range=(1,5), default_value=5, key='controls_speed_slider',
orientation='h', size=(10, 15), expand_x=True,
enable_events=True, disable_number_display=True,
tooltip="Speed of the algorithm. Higher is faster.")]
]

# Consolidated layout
layout = [
# Menu Row
[sg.Menu(menu_definition=menu, key="main_menu",
background_color='#f0f0f0', tearoff=False, pad=(200, 2))],
# Maze Row
[sg.Column(layout=[[MAZE]],
element_justification='center', expand_x=True)],
# Three frames in one row
[sg.Frame(title='Algorithm', layout=layout_algo_radios,
expand_y=True, expand_x=True),
sg.Frame(title='Draw', layout=layout_maze_tools,
expand_y=True, expand_x=True),
sg.Frame(title='Controls', layout=layout_controls,
expand_y=True, expand_x=True)
],
# Reset & Clear Buttons
[sg.Button('Clear Maze', key='maze_tools_clear', expand_x=True,
tooltip="Erases the entire maze, leaving an empty grid."),
sg.Button('Reset Current Maze', key='maze_tools_reset',
expand_x=True,
tooltip="Resets the current maze to its initial state.")]
]
return sg.Window('Maze_Solver', layout=layout,
icon='../assets/icon.ico', finalize=True)
window = create_main_window()
apply_settings()
set_draw_mode('wall')
# Continuously read the main window for user input
while True:
if window is None:
window = create_main_window()
event, values = window.read()
# Break the loop if the window is closed
if event == sg.WIN_CLOSED or event == 'Exit':
break

# Maze interactions
if event == 'maze':
if not MODE:
pass
else:
# get (x,y) coordinates of the node that was clicked
loc = (values['maze'][0] // NODE_SIZE,
values['maze'][1] // NODE_SIZE)
# make sure node location is in-bounds
if -1 < loc[0] < MAZE_WIDTH and -1 < loc[1] < MAZE_HEIGHT:
# set the current working node
clicked_node = NODES[loc]
# draw a node based on the draw mode
if MODE == 'wall':
clicked_node.make_wall_node()
elif MODE == 'path':
clicked_node.make_empty_node()
elif MODE == 'start':
clicked_node.make_start_node()
elif MODE == 'end':
clicked_node.make_end_node()

# Algorithm radio switches


elif event == 'radio_algo_bfs':
set_algo('Breadth-First Search')
elif event == 'radio_algo_dfs':
set_algo('Depth-First Search')
elif event == 'radio_algo_dijkstra':
set_algo('Dijkstra')
elif event == 'radio_algo_astar':
set_algo('A* (A Star)')

# Draw tools
elif event == 'maze_tools_wall':
set_draw_mode('wall')
elif event == 'maze_tools_path':
set_draw_mode('path')
elif event == 'maze_tools_start':
set_draw_mode('start')
elif event == 'maze_tools_end':
set_draw_mode('end')

# Reset buttons
elif event == 'maze_tools_clear':
clear()
elif event == 'maze_tools_reset':
reset()

# Algorithm controls
elif event == 'controls_solve':
solve_maze()
elif event == 'controls_speed_slider':
set_speed(values['controls_speed_slider'])

# Menu
elif event == 'Open Maze':
open_maze_file(sg.filedialog.askopenfilename(
filetypes=[('Text Document', '*.txt')],
defaultextension=[('Text Document', '*.txt')]))
elif event == 'Save Maze':
save_maze_file(sg.filedialog.asksaveasfile(
filetypes=[('Text Document', '*.txt')],
defaultextension=[('Text Document', '*.txt')]))
elif event == 'Generate Maze':
generate_maze()
elif event == 'Fill Maze':
MAZE.fill_maze()
window.close()

You might also like