Import PySimpleGUI As SG
Import PySimpleGUI As SG
MAZE_WIDTH = 51
MAZE_HEIGHT = 51
NODE_SIZE = 10
# 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
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()}")
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)
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')
if valid_maze_file(filename):
try:
print(f'Open maze file: {filename}')
clear()
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
# 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 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 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()
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 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()
# 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()