# main.py import tkinter as tk from render import Render import time import threading from maze_generator import generate_maze from tkinter import messagebox from pathfinding_algorithms import bfs_generator, dfs_generator, a_star_generator class MazeSolverApp: def __init__(self, root): self.root = root self.render = Render(root, self) self.timer_running = False self.start_time = None self.elapsed_time = 0 self.timer_id = None self.maze = None self.solving = False self.autogenerated_seed = None # Track the last autogenerated seed self.stop_requested = False # Add this flag for stopping the solver # Bind keys to quit the application self.root.bind('', self.quit_application) self.root.bind('q', self.quit_application) def quit_application(self, _=None): """Cleanly quit the application.""" print("Quitting the application...") self.root.quit() # This will close the Tkinter loop self.root.destroy() # This will clean up and destroy the window print("Application exited.") def start_timer(self): if not self.timer_running: self.timer_running = True self.start_time = time.time() - self.elapsed_time self.update_timer() def stop_timer(self): if self.timer_running: self.timer_running = False if self.timer_id: self.root.after_cancel(self.timer_id) def reset_timer(self): self.stop_timer() self.elapsed_time = 0 self.render.update_timer_label("00:00") def update_timer(self): if self.timer_running: self.elapsed_time = time.time() - self.start_time minutes = int(self.elapsed_time // 60) seconds = int(self.elapsed_time % 60) self.render.update_timer_label(f"{minutes:02d}:{seconds:02d}") self.timer_id = self.root.after(1000, self.update_timer) def generate_maze(self): params = self.render.get_maze_parameters() if params is None: return # Invalid parameters, abort self.reset_timer() # Get additional parameters from the new sliders dead_ends = int(self.render.dead_ends_scale.get()) branching_factor = int(self.render.branching_factor_scale.get()) connectedness = int(self.render.connectedness_scale.get()) * 10 # Generate maze in a separate thread to avoid freezing the UI threading.Thread(target=self._generate_maze_thread, args=(params, dead_ends, branching_factor, connectedness)).start() def _generate_maze_thread(self, params, dead_ends, branching_factor, connectedness): try: # Check if the seed input is empty or if it's the autogenerated seed seed_input = params['seed'] if seed_input is None or str(seed_input) == self.autogenerated_seed: seed = None # Force the generator to create a new random seed else: seed = seed_input # Use the user-provided seed # Get the values from the new sliders dead_ends = int(self.render.dead_ends_scale.get()) branching_factor = int(self.render.branching_factor_scale.get()) connectedness = int(self.render.connectedness_scale.get()) * 10 # Convert to percentage # Call generate_maze with the seed maze, used_seed = generate_maze( rows=params['rows'], cols=params['cols'], generation_algorithm=params['generation_algorithm'], seed=seed, wall_density=params['wall_density'], dead_ends=dead_ends, branching_factor=branching_factor, connectedness=connectedness ) self.maze = maze # If the seed was autogenerated (seed=None), update the seed field if seed is None: self.autogenerated_seed = str(used_seed) # Track the new autogenerated seed self.render.seed_entry.delete(0, tk.END) self.render.seed_entry.insert(0, str(used_seed)) # Insert the generated seed else: self.autogenerated_seed = None # Reset if a user-provided seed is used # Update the UI in the main thread self.root.after(0, self.render.draw_maze, self.maze) # self.root.after(0, self.start_timer) print(f"Maze generated with seed: {used_seed}") except Exception as e: print(f"Error generating maze: {e}") messagebox.showerror("Error", f"An error occurred while generating the maze:\n{e}") def solve_maze(self): if self.maze is None or self.maze.size == 0: # Adjust the maze check to avoid ambiguity messagebox.showwarning("No Maze", "Please generate a maze first.") return if self.solving: messagebox.showinfo("Solving", "Maze is already being solved.") return self.solving = True self.stop_requested = False # Reset the flag when starting self.reset_timer() self.start_time = time.time() self.start_timer() self.steps = 0 # Get selected algorithm algorithm = self.render.update_algorithm_selection() # Initialize the solver generator if algorithm == "BFS": self.solver_generator = bfs_generator(self.maze, (1, 1), (self.maze.shape[0]-2, self.maze.shape[1]-2)) elif algorithm == "DFS": self.solver_generator = dfs_generator(self.maze, (1, 1), (self.maze.shape[0]-2, self.maze.shape[1]-2)) elif algorithm == "A*": self.solver_generator = a_star_generator(self.maze, (1, 1), (self.maze.shape[0]-2, self.maze.shape[1]-2)) else: messagebox.showerror("Error", f"Unknown algorithm: {algorithm}") self.solving = False self.stop_timer() return # Start the solving process self.root.after(0, self._process_solver_step) def _process_solver_step(self): if self.stop_requested: self.solving = False self.stop_timer() print("Solver stopped by user.") return try: action, cell, steps = next(self.solver_generator) self.steps = steps if action == 'visit': self.render.mark_visited(cell) elif action == 'enqueue': self.render.mark_frontier(cell) elif action == 'path': self.render.mark_path(cell) elif action == 'done': self.solving = False self.stop_timer() self.show_summary() return # Schedule the next step self.root.after(10, self._process_solver_step) # Adjust delay as needed for visualization speed except StopIteration: self.solving = False self.stop_timer() self.show_summary() def show_summary(self): elapsed_time = time.time() - self.start_time if self.start_time else 0 params = self.render.get_maze_parameters() or {} algorithm = self.render.update_algorithm_selection() or "Unknown" summary = ( f"Algorithm: {algorithm}\n" f"Time Taken: {elapsed_time:.2f} seconds\n" f"Steps: {self.steps or 0}\n" f"Maze Size: {params.get('rows', '?')}x{params.get('cols', '?')}\n" f"Generation Algorithm: {params.get('generation_algorithm', 'Unknown')}\n" f"Wall Density: {params.get('wall_density', 'Unknown')}\n" f"Seed: {params.get('seed', 'None')}" ) messagebox.showinfo("Maze Solved!", summary) def stop_solving(self): if self.solving: self.stop_requested = True # Set the flag to stop the solver messagebox.showinfo("Stop Solving", "Maze Solver Should be stopped") else: messagebox.showinfo("Not Solving", "No solving process is currently running.") if __name__ == "__main__": root = tk.Tk() root.title("Maze Solver") app = MazeSolverApp(root) try: root.mainloop() # Start the Tkinter event loop except KeyboardInterrupt: print("\nKeyboardInterrupt detected. Exiting the application cleanly...") app.quit_application() # Call the quit method to clean up