#!/usr/bin/env python3 """ pynpuzzle - Solve n-puzzle with Python Version : 1.0.0 Author : Hamidreza Mahdavipanah Repository: http://github.com/mahdavipanah/pynpuzzle License : MIT License """ import math import random import re import webbrowser import sys import traceback from os import listdir from os.path import isfile, join import datetime from importlib import import_module import multiprocessing import threading import time from copy import deepcopy import tkinter from tkinter import ttk from tkinter import messagebox from tkinter import simpledialog from tkinter import filedialog, scrolledtext import psutil # Global variables # # Stores app logs LOGS = [] # Loaded algorithms modules from ./algorithm/ folder algorithms_modules = [] # Process that runs the algorithm # If it's None it means app is not calculating # If it's not None it contains multiprocessing.Process object and app is calculating search_process = None # An event object that tells the timer thread to stop timer_event = multiprocessing.Event() # The thread that updates execution time, max ram usage and ram usage information timer_thread = None # The thread that is waiting for the algorithm to send it's result pipe_thread = None # A pipe which algorithm can send it's result to app through it output_pipe = None # A list containing current output steps's statuses OUTPUT_LST = [] # Number of current output's step OUTPUT_STEP = 0 # An event object that tells the play thread to stop play_event = None # The thread that plays output steps one by one play_timer = None # A ScrolledText widget that contains application logs and is inside show logs window logs_text = None # Goal state GOAL_STATE = [i for i in range(9)] # Show logs window logs_window = None # About window about_window = None # Controls whether the output text validation is enable or not # For the sake of user experience, output entries are not disabled and instead are bound to a validation method # That always returns false, but sometimes the app itself wants to change the entries values, so temporarily # changes this variable's value to true OUTPUT_EDITABLE = False # Indicates whether timer thread should clear status bar or not (It's useful when some problems happened) timer_clear_status_bar = False # Main window main_window = tkinter.Tk() main_window.title("pynpuzzle - Solve n-puzzle with Python") main_window.grid_rowconfigure(2, weight=1) main_window.grid_columnconfigure(0, weight=1, uniform=1) main_window.grid_columnconfigure(1, weight=1, uniform=1) # Main window size configurations main_window.minsize(width=840, height=360) main_window.geometry("840x360") # Status bar variables that are bound to status bar labes max_ram_var = tkinter.StringVar() cpu_var = tkinter.StringVar() ram_var = tkinter.StringVar() available_ram_var = tkinter.StringVar() def return_false_validate(): """ Validation function for output entry widgets. Returns OUTPUT_EDITABLE global variable. """ return OUTPUT_EDITABLE def draw_puzzle(puzzle_frame, n, read_only=False): """ Fills a frame widget with n + 1 entry widget. puzzle_frame : The puzzle frame to get filled by entry widgets. n : Puzzle type (n-puzzle). read_only : Should widgets be read-only or not """ n = int(math.sqrt(n + 1)) for i in range(n): for j in range(n): entry = tkinter.Entry(puzzle_frame, width=4, justify='center') entry.grid(row=i, column=j, sticky='WENS') if read_only: # Bind the widget validation command to return_false_validate command and therefore # OUTPUT_EDITABLE global variable. entry.config(validatecommand=return_false_validate, validate=tkinter.ALL) puzzle_frame.grid_columnconfigure(i, weight=1) puzzle_frame.grid_rowconfigure(i, weight=1) def config_frame_state(frame, state): """ Changes the status property of a frame children. """ for child in frame.winfo_children(): # Only output_play_button can change output_stop_button's state if child is output_stop_button: # If output is playing right now if play_event: # Someone wants to disable the output frame, so output play must stop play_event.set() continue child['state'] = state # output_0_label's and output_to_label's cursor property are depending on their frame status if frame is output_action_frame: cursor = 'arrow' if state == tkinter.NORMAL: cursor = 'fleur' output_0_label['cursor'] = cursor output_to_label['cursor'] = cursor def config_io_frame_state(frame, state): """ A special function only for changing the state of output or input label """ if frame is output_labelframe: # For the sake of user experience output entry widgets are not getting disabled and instead their values # Are not editable config_frame_state(output_action_frame, state) else: config_frame_state(input_puzzle_frame, state) config_frame_state(input_action_frame, state) def create_puzzle_frame(parent_frame, n, current_puzzle_frame=None, read_only=False): """ Creates a new puzzle frame inside a parent frame and if the puzzle frame already exists, first destroys it. This is done because when the n changes we have to change the puzzle frame's grid row and column configurations and it turned out in tkinter it can be done by recreating the frame widget! Returns the newly created puzzle frame. """ if current_puzzle_frame: current_puzzle_frame.destroy() puzzle_frame = tkinter.Frame(parent_frame) puzzle_frame.grid(row=0, column=0, sticky='WENS') draw_puzzle(puzzle_frame, n, read_only) return puzzle_frame def fill_puzzle_frame(puzzle_frame, lst): """ Fills a puzzle frame with a puzzle list. """ global OUTPUT_EDITABLE lst = lst[:] lst = ['' if x == 0 else x for x in lst] # Enable editing the output puzzle temporarily OUTPUT_EDITABLE = True i = 0 for child in puzzle_frame.winfo_children(): child.delete(0, tkinter.END) child.insert(0, lst[i]) if puzzle_frame is output_puzzle_frame and lst[i] == '': child['highlightbackground'] = 'Orange' elif puzzle_frame is output_puzzle_frame: # Change the child's highlightbackground color to entry widget's default property using another # entry widget which we are sure has default property's value child['highlightbackground'] = output_step_text['highlightbackground'] i += 1 # Disable editing the output puzzle OUTPUT_EDITABLE = False def list_to_puzzle(lst): """ Converts a one dimensional puzzle list and returns it's two dimensional representation. [1, 2, 3, 4, 5, 6, 7, 8, 0] --> [[1, 2, 3], [4, 5, 6], [7, 8, 9]] """ n_sqrt = int(math.sqrt(len(lst))) puzzle = [] for i in range(0, len(lst), n_sqrt): line = [] for j in range(0, n_sqrt): line.append(lst[i + j]) puzzle.append(line) return puzzle def puzzle_to_list(puzzle): """ Converts a two dimensional puzzle to a one dimensional puzzle. [[1, 2, 3], [4, 5, 6], [7, 8, 9]] --> [1, 2, 3, 4, 5, 6, 7, 8, 0] """ lst = [] for row in puzzle: lst.extend(row) return lst def check_puzzle_list(lst, n): """ Checks a puzzle one dimensional list and validates it. lst : The list to be validated. n : Puzzle type (n-puzzle). Returns True of it's fine and False if it's not valid. """ # Check list's length if len(lst) != n + 1: return False lst = lst[:] lst = [0 if x == '' else x for x in lst] # Generate a new list containing numbers from 0 to n # and then check if the list has all of those numbers in it new_lst = [i for i in range(0, n + 1)] for lst_item in new_lst: try: lst.remove(lst_item) except ValueError: return False if len(lst) != 0: return False return True def get_puzzle_frame_list(puzzle_frame): """ Returns a one dimensional puzzle list that is inside a frame widget. """ lst = [] for child in puzzle_frame.winfo_children(): txt = child.get().strip() if txt == '': txt = 0 lst.append(int(txt)) return lst def start_timer(): """ Starts the timer for updating status bar. """ global timer_thread global timer_event global timer_clear_status_bar max_ram_var.set('0') cpu_var.set('0') search_process_psutil = psutil.Process(search_process.pid) def timing(): while not timer_event.is_set(): prev_val = float(max_ram_var.get()) new_val = round(search_process_psutil.memory_full_info().uss / (2 ** 20), 3) ram_var.set(new_val) if new_val > prev_val: max_ram_var.set(new_val) cpu_times = search_process_psutil.cpu_times() cpu_var.set(round(cpu_times.user + cpu_times.system, 3)) timer_event.wait(0.001) if timer_clear_status_bar: cpu_var.set('') max_ram_var.set('') ram_var.set('') timer_event.clear() timer_clear_status_bar = False timer_thread = threading.Thread(target=timing, daemon=True) timer_thread.start() def load_output_step(n): """ Fills the output puzzle with nth output step. """ global OUTPUT_STEP OUTPUT_STEP = n step_n_lst = OUTPUT_LST[n] fill_puzzle_frame(output_puzzle_frame, step_n_lst) output_step_text.delete(0, tkinter.END) output_step_text.insert(0, n) def piper(): """ A thread target that listens for algorithm's output through a pipe :return: """ global OUTPUT_LST global OUTPUT_STEP global timer_event global timer_clear_status_bar try: # Waits for algorithm to send the result OUTPUT_LST = output_pipe.recv() output_error = False output_exception = False # If the returned value is a string, Some exception has have happened if type(OUTPUT_LST) is str: output_exception = True else: # Validate algorithm's output if not OUTPUT_LST: output_error = True elif type(OUTPUT_LST) is not list: output_error = True else: try: n = int(n_spinbox.get()) sqrt_n = math.sqrt(n + 1) for output_step in OUTPUT_LST: if type(output_step) is not list: raise BaseException() if len(output_step) != sqrt_n: raise BaseException() for step_row in output_step: if type(step_row) is not list: raise BaseException() if len(step_row) != sqrt_n: raise BaseException() if not check_puzzle_list([int(output_step[i][j]) for i in range(len(output_step)) for j in range(len(output_step))], n): raise BaseException() except: output_error = True if not output_error: # Converts output's puzzles to one dimensional representation of them tmp_lst = [] for result in OUTPUT_LST: tmp_lst.append(puzzle_to_list(result)) OUTPUT_LST = tmp_lst except EOFError: # Stop button pressed pass else: # Calculation successfully done! # # Stop status thread if output_exception or output_error: timer_clear_status_bar = True timer_event.set() calculation_stop() # If some exception has have happened inside algorithm's function if output_exception: messagebox.showerror("Algorithm exception", "Some exception happened in algorithm's source code:\n\n" + OUTPUT_LST, parent=main_window) OUTPUT_LST = [] OUTPUT_STEP = 0 return if output_error: messagebox.showerror("Algorithm output error", "Algorithm's output is not valid.", parent=main_window) OUTPUT_LST = [] OUTPUT_STEP = 0 return # Enable output's action frame config_frame_state(output_action_frame, tkinter.NORMAL) output_to_label['text'] = len(OUTPUT_LST) - 1 output_0_label['text'] = '0' # Load the first step to output puzzle load_output_step(0) def start_piping(): """ Starts the piper thread to listen to algorithm's output. """ global pipe_thread global output_pipe output_pipe, process_pipe = multiprocessing.Pipe() pipe_thread = threading.Thread(target=piper, daemon=True) pipe_thread.start() # Return the sender pipe return process_pipe def log_datetime(): """ Returns the datetime for logging. """ now = datetime.datetime.now() return now.strftime("%Y-%m-%d %H:%M") def update_logs_text_if_visible(): """ If show logs window is open, then update the text widget's content. If someone updates app logs, then should invoke this function. """ if logs_text: logs_text['state'] = tkinter.NORMAL logs_text.delete(0.0, tkinter.END) logs_text.insert(0.0, ''.join(LOGS)) logs_text['state'] = tkinter.DISABLED # Scroll to end of text logs_text.see(tkinter.END) def load_algorithms(): """ Load algorithm's modules from ./algorithm/ folder. It assumes all python files as algorithms and tries to load them. """ global algorithms_modules global LOGS # Get list of all files' names algorithms_files = [f for f in listdir('./algorithms/') if isfile(join('./algorithms/', f))] # Keep all python files's names algorithms_files = [f for f in algorithms_files if f.endswith('.py')] # Remove .py extension from their file's names algorithms_files = [f.rstrip('.py') for f in algorithms_files] for module in algorithms_modules: try: # If the module is already loaded remove it, so it can be reloaded. # This happens in algorithm's reloading process. del sys.modules[module.__name__] except: pass algorithms_modules = [] for file in algorithms_files: try: # Try to import the module and add it to algorithms modules list algorithms_modules.append(import_module('algorithms.' + file)) # If some problem happened when importing the module (For example if the module has some syntax errors). except: LOGS.append(log_datetime() + " : Error : Exception raised : " + file + ".py\n") for module in algorithms_modules: LOGS.append(log_datetime() + " : OK : Loaded : " + module.__name__[11:] + ".py\n") def check_search_function(module): """ Checks if the module has a search function. """ global LOGS if not getattr(module, 'search', None): LOGS.append(log_datetime() + " : Error : Algorithm's search not defined : " + module.__name__[11:] + '.py\n') return False return True algorithms_modules = list(filter(check_search_function, algorithms_modules)) def check_search_function_arguments(module): """ Checks if the module's search function's arguments are proper. """ global LOGS if getattr(module, 'search').__code__.co_argcount != 2: LOGS.append(log_datetime() + " : Error : Search function should only accept 2 positional arguments : " + module.__name__[11:] + '.py\n') return False return True algorithms_modules = list(filter(check_search_function_arguments, algorithms_modules)) algorithms_names = [] for module in algorithms_modules: search_name = module.search.__doc__ # If algorithm's name is not defined in search function's docstring if not search_name: LOGS.append( log_datetime() + " : Warning : Algorithm's name not defined : " + module.__name__[11:] + '.py\n') search_name = module.__name__[11:] module.search.__doc__ = search_name.strip() algorithms_names.append(module.search.__doc__) update_logs_text_if_visible() prev_algorithm_name = algorithm_name.get() # Update algorithms combobox with loaded algorithm's names algorithm_combobox['values'] = algorithms_names # If there is any loaded algorithms if len(algorithms_names): if algorithms_names.count(prev_algorithm_name): # Select the previously selected algorithm algorithm_combobox.set(prev_algorithm_name) else: # Select the first algorithm from combobox algorithm_combobox.set(algorithms_names[0]) def menu_reload_algorithms_command(): """ Reload algorithms menu button click handler """ global LOGS load_algorithms() LOGS.append(log_datetime() + ' : Reloading algorithms...\n') update_logs_text_if_visible() def menu_bar_show_logs_command(): """ Show logs menu button click handler """ global logs_window global logs_text # If there is another show logs window open if logs_window: # Bring the window to front logs_window.lift() return # Logs window logs_window = tkinter.Toplevel(main_window) logs_window.title("Logs") logs_window.geometry('680x252') logs_window.lift() # ScrolledText widget logs_text = scrolledtext.ScrolledText(logs_window, state=tkinter.DISABLED) logs_text.pack(fill=tkinter.BOTH, expand=True) # Load logs to text widget update_logs_text_if_visible() def on_close(): """ Logs window 'on close' handler """ global logs_text global logs_window logs_text = None logs_window.destroy() logs_window = None logs_window.protocol('WM_DELETE_WINDOW', on_close) # Show the window logs_window.mainloop() def menu_bar_about_command(): """ About menu button click handler """ global about_window # If there is another about window open if about_window: # Bring the window to front about_window.lift() return # Logs window about_window = tkinter.Toplevel(main_window) about_window.title("About pynpuzzle") about_window.minsize(width=400, height=270) about_window.maxsize(width=400, height=270) about_window.geometry('400x270') about_window.resizable(0, 0) about_window.lift() tkinter.Label(about_window, text="pynpuzzle", font="TkDefaultFont 15 bold").pack(pady=(15, 10)) tkinter.Label(about_window, text="1.0.0", font="TkDefaultFont 10").pack() tkinter.Label(about_window, text="Solve n-puzzle with Python", font="TkDefaultFont 10").pack(pady=10) tkinter.Label(about_window, text="Github repository:", font="TkDefaultFont 10").pack(pady=(15, 0)) github_link = tkinter.Label(about_window, text="http://github.com/mahdavipanah/pynpuzzle", font="TkDefaultFont 10", fg="Blue", cursor="fleur") github_link.pack(pady=(0, 10)) github_link.bind('<Button-1>', lambda x: webbrowser.open('http://github.com/mahdavipanah/pynpuzzle')) tkinter.Label(about_window, text="Created by:", font="TkDefaultFont 10").pack() tkinter.Label(about_window, text="Hamidreza Mahdavipanah", font="TkDefaultFont 10").pack() tkinter.Label(about_window, text="Licensed under MIT license", font="TkDefaultFont 8").pack(pady=(20, 0)) def on_close(): """ About window on close handler """ global about_window about_window.destroy() about_window = None about_window.protocol('WM_DELETE_WINDOW', on_close) # Show the window about_window.mainloop() def menu_change_goal_state_command(): """ Change goal state menu button click handler """ # Current goal state prev_goal_state = GOAL_STATE[:] # Change goal window change_goal_window = tkinter.Toplevel(main_window) change_goal_window.title("Change n-puzzle's goal state") change_goal_window.minsize(width=590, height=252) change_goal_window.geometry('590x252') goal_puzzle_frame = create_puzzle_frame(change_goal_window, int(n_spinbox.get())) goal_action_frame = tkinter.Frame(change_goal_window) goal_action_frame.grid(row=1, column=0, sticky='WENS') def change(): """ Changes the goal state. """ global GOAL_STATE lst = is_input_puzzle_valid(goal_puzzle_frame) if not lst: messagebox.showerror("Input error", "Inputs are not valid!", parent=change_goal_window) return GOAL_STATE = lst change_goal_window.destroy() def close_window(): """ Closes change goal state window. """ # Check if windows' input goal state is equal to current goal state equal = True new_goal_state = get_puzzle_frame_list(goal_puzzle_frame) for i in range(len(GOAL_STATE)): if new_goal_state[i] != prev_goal_state[i]: equal = False break # If there is a new goal state in input, ask user if wants to save the goal state before closing the window if not equal: if messagebox.askyesno("Goal state has been changed", "Do you want to save goal state?", parent=change_goal_window): change() else: change_goal_window.destroy() else: change_goal_window.destroy() def change_goal_window_random(): """ Generates a shuffled list and fills goal state window's input with it. """ n = int(n_spinbox.get()) lst = [i for i in range(0, n + 1)] random.shuffle(lst) fill_puzzle_frame(goal_puzzle_frame, lst) # Window's widgets tkinter.Button(goal_action_frame, text='Save to file', command=lambda: save_file_cmd(goal_puzzle_frame, change_goal_window)).grid(row=0, column=0, sticky='WENS') tkinter.Button(goal_action_frame, text='Load from file', command=lambda: read_file_cmd(goal_puzzle_frame, change_goal_window)).grid(row=0, column=1, sticky='WENS') tkinter.Button(goal_action_frame, text='Random', command=change_goal_window_random).grid( row=0, column=2, sticky='WENS') tkinter.Button(goal_action_frame, text='Default', command=lambda: fill_puzzle_frame(goal_puzzle_frame, [i for i in range(len(GOAL_STATE))])).grid( row=0, column=3, sticky='WENS') goal_cancel_border_frame = tkinter.Frame(goal_action_frame, bg='Red') goal_cancel_border_frame.grid(row=0, column=4, sticky='WENS') goal_cancel_border_frame.grid_columnconfigure(0, weight=1) tkinter.Button(goal_cancel_border_frame, text='Cancel', command=close_window).grid(row=0, column=0, sticky='WENS', padx=1, pady=1) goal_change_border_frame = tkinter.Frame(goal_action_frame, bg='Green') goal_change_border_frame.grid(row=0, column=5, sticky='WENS') goal_change_border_frame.grid_columnconfigure(0, weight=1) tkinter.Button(goal_change_border_frame, text='Change', command=change).grid(row=0, column=0, sticky='WENS', padx=1, pady=1) goal_action_frame.grid_columnconfigure(0, weight=1, uniform=1) goal_action_frame.grid_columnconfigure(1, weight=1, uniform=1) goal_action_frame.grid_columnconfigure(2, weight=1, uniform=1) goal_action_frame.grid_columnconfigure(3, weight=1, uniform=1) goal_action_frame.grid_columnconfigure(4, weight=1, uniform=1) goal_action_frame.grid_columnconfigure(5, weight=1, uniform=1) change_goal_window.grid_rowconfigure(0, weight=1) change_goal_window.grid_columnconfigure(0, weight=1) change_goal_window.lift() change_goal_window.grab_set() change_goal_window.protocol('WM_DELETE_WINDOW', close_window) # Initialized window's input with current goal state fill_puzzle_frame(goal_puzzle_frame, GOAL_STATE) # Show the window change_goal_window.mainloop() # Menu bar menu_bar = tkinter.Menu(main_window) menu_bar.add_command(label="Change goal state", command=menu_change_goal_state_command) menu_bar.add_command(label="Reload algorithms", command=menu_reload_algorithms_command) menu_bar.add_command(label="Show logs", command=menu_bar_show_logs_command) menu_bar.add_command(label="About", command=menu_bar_about_command) # Add menu bar to main window main_window['menu'] = menu_bar # n frame n_frame = tkinter.Frame(main_window) n_frame.grid(row=0, column=0, sticky='EWN', padx=5, pady=5) n_frame.grid_rowconfigure(0, weight=1) n_frame.grid_columnconfigure(1, weight=1) # n label tkinter.Label(n_frame, text="n: ").grid(row=0, column=0) def change_app_n(n): """ Refreshes app based on new n. """ global input_puzzle_frame global output_puzzle_frame global GOAL_STATE # Recreate input puzzle input_puzzle_frame = create_puzzle_frame(input_labelframe, n, input_puzzle_frame) # Recreate output puzzle output_puzzle_frame = create_puzzle_frame(output_labelframe, n, output_puzzle_frame, True) config_io_frame_state(output_labelframe, tkinter.DISABLED) # Regenerate goal state GOAL_STATE = [i for i in range(n + 1)] # Clear status bar ram_var.set('') max_ram_var.set('') cpu_var.set('') # n spinbox def spinbox_command(action): """ n input spinbox up and down handler. """ value = int(math.sqrt(int(n_spinbox.get()) + 1)) # If up button clicked if action == 'up': value += 1 # If down button clicked else: if value == 3: return value -= 1 value = value * value - 1 n_spinbox.delete(0, tkinter.END) n_spinbox.insert(0, value) change_app_n(value) # n spinbox n_spinbox = tkinter.Spinbox(n_frame, command=(main_window.register(spinbox_command), '%d')) n_spinbox.insert(tkinter.INSERT, 8) n_spinbox.grid(row=0, column=1, sticky='EWN') # Algorithm frame algorithm_frame = tkinter.Frame(main_window) algorithm_frame.grid(row=0, column=1, sticky='EWN', padx=5, pady=5) algorithm_frame.grid_rowconfigure(0, weight=1) algorithm_frame.grid_columnconfigure(1, weight=1) # Algorithm label algorithm_combobox_label = tkinter.Label(algorithm_frame, text="algorithm: ") algorithm_combobox_label.grid(row=0, column=0) # Algorithm combobox algorithm_name = tkinter.StringVar() algorithm_combobox = ttk.Combobox(algorithm_frame, textvariable=algorithm_name, validate=tkinter.ALL, validatecommand=lambda: False) algorithm_combobox.grid(row=0, column=1, sticky='EWN') def calculation_stop(): """ Does some routine works that has to be done when to stop calculation. """ # Show start button start_button.grid() start_button_border_frame.grid() # Hide progress bar progress_bar.grid_remove() progress_bar.stop() stop_button['state'] = tkinter.DISABLED # Re-enable menu bar buttons menu_bar.entryconfig('Reload algorithms', state=tkinter.NORMAL) menu_bar.entryconfig('Change goal state', state=tkinter.NORMAL) n_spinbox['state'] = tkinter.NORMAL # Enable input data entry config_io_frame_state(input_labelframe, tkinter.NORMAL) def stop_button_cmd(): """ Stop button click handler """ # Do some routines for stopping calculation calculation_stop() # Stop algorithm's process search_process.terminate() output_pipe.close() # Stop timer thread and stop refreshing status bar timer_event.set() # Clear status labels threading.Timer(0.1, max_ram_var.set, args=('',)).start() threading.Timer(0.1, cpu_var.set, args=('',)).start() threading.Timer(0.1, ram_var.set, args=('',)).start() # Action buttons # # Output stop widget and it's border frame stop_button_border_frame = tkinter.Frame(main_window, bg='Red') stop_button_border_frame.grid(row=1, column=0, sticky='EWN', padx=5, pady=5) stop_button_border_frame.grid_columnconfigure(0, weight=1) stop_button = tkinter.Button(stop_button_border_frame, text="Stop", state=tkinter.DISABLED, command=lambda: stop_button_cmd()) stop_button.grid(row=0, column=0, sticky='EWN', padx=1, pady=1) def is_input_puzzle_valid(puzzle_frame): """ Checks if given puzzle frame has a valid puzzle in it. If puzzle frame has a valid input return's it's one dimensional list and returns None otherwise. """ try: lst = get_puzzle_frame_list(puzzle_frame) if not check_puzzle_list(lst, int(n_spinbox.get())): raise Exception return lst except: return None def search_runner(func, pipe, lst, goal_state): """ This function invokes the given func with lst and goal_state arguments and sends func's returned value to pipe. If some exception happened in func, sends print ready exception's string to show to user. """ try: ret_val = func(lst, goal_state) pipe.send(ret_val) except BaseException as e: exception_message = traceback.format_exception(type(e), e, e.__traceback__) del exception_message[1] pipe.send(''.join(exception_message)) def start_button_cmd(): """ Start button click handler """ global output_puzzle_frame global search_process global OUTPUT_EDITABLE if not len(algorithms_modules): return # Check if input puzzle has a valid input lst = is_input_puzzle_valid(input_puzzle_frame) if not lst: messagebox.showerror("Input error", "Inputs are not valid!", parent=main_window) return # Change widgets's looks start_button.grid_remove() start_button_border_frame.grid_remove() progress_bar.grid() progress_bar.start() stop_button['state'] = tkinter.NORMAL menu_bar.entryconfig('Reload algorithms', state=tkinter.DISABLED) menu_bar.entryconfig('Change goal state', state=tkinter.DISABLED) n_spinbox['state'] = tkinter.DISABLED config_io_frame_state(input_labelframe, tkinter.DISABLED) output_to_label['text'] = '' output_0_label['text'] = '' output_step_text.delete(0, tkinter.END) config_io_frame_state(output_labelframe, tkinter.DISABLED) # Clear output puzzle OUTPUT_EDITABLE = True for child in output_puzzle_frame.winfo_children(): if child.get().strip() == '': child['highlightbackground'] = output_step_text['highlightbackground'] child.delete(0, tkinter.END) OUTPUT_EDITABLE = False # Find the search function of the selected algorithm for module in algorithms_modules: if module.search.__doc__ == algorithm_name.get(): search_function = module.search # Algorithm's search process search_process = multiprocessing.Process(target=search_runner, args=(search_function, start_piping(), list_to_puzzle(lst), list_to_puzzle(GOAL_STATE))) search_process.daemon = True search_process.start() start_timer() # Start button widget and it's border frame start_button_border_frame = tkinter.Frame(main_window, bg='Green') start_button_border_frame.grid(row=1, column=1, sticky='EWN', padx=5, pady=5) start_button_border_frame.grid_columnconfigure(0, weight=1) start_button = tkinter.Button(start_button_border_frame, text="Start", command=start_button_cmd) start_button.grid(row=0, column=0, sticky='WENS', padx=1, pady=1) # Progress bar widget progress_bar = ttk.Progressbar(main_window, mode='indeterminate', maximum=20) progress_bar.grid(row=1, column=1, sticky='EW', padx=5, pady=5) progress_bar.grid_remove() # Output labelframe output_labelframe = tkinter.LabelFrame(main_window, text="Output") output_labelframe.grid(row=2, column=0, sticky='WENS', padx=5, pady=5) output_labelframe.grid_rowconfigure(0, weight=1) output_labelframe.grid_columnconfigure(0, weight=1) # Output puzzle frame output_puzzle_frame = create_puzzle_frame(output_labelframe, 8, None, True) # Output action frame output_action_frame = tkinter.Frame(output_labelframe, bd=1, relief=tkinter.SUNKEN) def output_0_to_label_click(n): """ output_0_label click handler Goes to first step of the output. """ if output_0_label['cursor'] == 'fleur': load_output_step(n) # output_0_label widget output_0_label = tkinter.Label(output_action_frame, cursor="fleur") output_0_label.pack(side=tkinter.LEFT) output_0_label.bind('<Button-1>', lambda x: output_0_to_label_click(0)) def prev_step_button(): """ Output's previous step button click handler Goes to previous step of the output. """ if OUTPUT_STEP == 0: return False load_output_step(OUTPUT_STEP - 1) return True tkinter.Button(output_action_frame, text="<<", width=0, command=prev_step_button).pack(side=tkinter.LEFT) def output_stop_button_cmd(): """ Output's stop button click handler Stops playing the output. """ play_event.set() output_play_button['state'] = tkinter.NORMAL output_stop_button['state'] = tkinter.DISABLED # Output's stop button widget output_stop_button = tkinter.Button(output_action_frame, text="Stop", width=0, fg='Red', state=tkinter.DISABLED, command=output_stop_button_cmd) output_stop_button.pack(side=tkinter.LEFT) output_step_text = tkinter.Entry(output_action_frame, width=10, justify=tkinter.CENTER) output_step_text.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=True) def step_text_enter(*_): """ Output's step text widget 'Enter' and 'Return' key handler Goes to entered step of the output. """ # Check if the entered text is a number try: step_num = int(output_step_text.get()) except ValueError: output_step_text.delete(0, tkinter.END) output_step_text.insert(0, OUTPUT_STEP) return # Check if the entered number is in the range of the steps if not (0 <= step_num <= int(output_to_label['text'])): output_step_text.delete(0, tkinter.END) output_step_text.insert(0, OUTPUT_STEP) return load_output_step(step_num) output_step_text.bind('<KP_Enter>', step_text_enter) output_step_text.bind('<Return>', step_text_enter) def play_button_command(): """ Output's play button click handler Start playing the output steps one by one. """ global play_event global play_timer def playing(): """ Target function for play_thread Plays output step's one by one with one second delay between them. """ time.sleep(1) while not play_event.is_set(): next_step_button() if OUTPUT_STEP == int(output_to_label['text']): output_stop_button_cmd() return play_event.wait(1) # Disable play button output_play_button['state'] = tkinter.DISABLED # Enable stop button output_stop_button['state'] = tkinter.NORMAL # If output's step is already the last one, change the step to the first step so it can be played from beginning if OUTPUT_STEP == int(output_to_label['text']): load_output_step(0) play_event = threading.Event() play_timer = threading.Thread(target=playing) play_timer.start() # Output's play button widget output_play_button = tkinter.Button(output_action_frame, text="Play", width=0, fg='Green', command=play_button_command) output_play_button.pack(side=tkinter.LEFT) def next_step_button(): """ Output's next step button click handler Goes to next step of the output. """ if OUTPUT_STEP == int(output_to_label['text']): return False load_output_step(OUTPUT_STEP + 1) return True tkinter.Button(output_action_frame, text=">>", width=0, command=next_step_button).pack(side=tkinter.LEFT) output_to_label = tkinter.Label(output_action_frame, text="", cursor='fleur') output_to_label.pack(side=tkinter.LEFT) output_to_label.bind('<Button-1>', lambda x: output_0_to_label_click(int(output_to_label['text']))) output_action_frame.grid(row=1, column=0, sticky='WENS') # Config output frame config_io_frame_state(output_labelframe, tkinter.DISABLED) # Input labelframe input_labelframe = tkinter.LabelFrame(main_window, text="Input") input_labelframe.grid(row=2, column=1, sticky='WENS', padx=5, pady=5) input_labelframe.grid_rowconfigure(0, weight=1) input_labelframe.grid_columnconfigure(0, weight=1) # Input puzzle frame input_puzzle_frame = create_puzzle_frame(input_labelframe, 8) # Input action frame input_action_frame = tkinter.Frame(input_labelframe) input_action_frame.grid(row=1, column=0, sticky='WENS') input_action_frame.grid_columnconfigure(0, weight=1, uniform=1) input_action_frame.grid_columnconfigure(1, weight=1, uniform=1) input_action_frame.grid_columnconfigure(2, weight=1, uniform=1) input_action_frame.grid_columnconfigure(3, weight=1, uniform=1) def save_file_cmd(puzzle_frame, parent): """ Input's save to file button click handler puzzle_frame : The puzzle frame which it's puzzle will be saved to file parent : The parent window of the puzzle_frame This is used for showing the 'save as file' dialog so it can be showed on top of the window. """ # Check if puzzle frame has a valid input, and if not, ask the user if he's sure he wants to save the puzzle lst = is_input_puzzle_valid(puzzle_frame) if not lst: if not messagebox.askokcancel("Input not valid", "Input puzzle is not valid, are you sure to save it as a file?", parent=parent): return # Open the 'save as file' dialog file_name = filedialog.asksaveasfilename(title="Choose a file to save puzzle", parent=parent) # Check if user has selected a file if not file_name: return # Generate file's content len_sqrt = int(math.sqrt(len(lst))) file_lines = [] for i in range(0, len(lst), 3): line_nums = [] for j in range(0, len_sqrt): line_nums.append(str(lst[i + j])) file_lines.append(' '.join(line_nums)) try: with open(file_name, 'w') as file: file.write('\n'.join(file_lines)) except: messagebox.showerror("Error saving to file", "Some problem happened while saving puzzle to the file.", parent=parent) # Save to file button widgget tkinter.Button(input_action_frame, text="Save to file", command=lambda: save_file_cmd(input_puzzle_frame, main_window)).grid(row=0, column=0, sticky='WENS') def read_file_cmd(puzzle_frame, parent): """ Input's read from file button click handler puzzle_frame : The puzzle frame which it's puzzle will be saved to file parent : The parent window of the puzzle_frame This is used for showing the 'save as file' dialog so it can be showed on top of the window. """ # Show 'open file' dialog file_name = filedialog.askopenfilename(title="Choose a file as input", parent=parent) # Check if user has selected a file if not file_name: return # Try to open the input file try: with open(file_name) as file: lines = [] pattern = re.compile(r'\s+') for line in file: # Check if line is empty if re.sub(pattern, '', line) == '': # Stop reading from input file break lines.append(line.rstrip()) lst = [] try: for line in lines: line_split = line.split(' ') lst.extend([int(i) for i in line_split]) if len(line_split) != len(lines): messagebox.showerror("Input error", "Puzzle dimension is not valid.", parent=parent) return except ValueError: messagebox.showerror("Input error", "Input must non-number values.", parent=parent) input_puzzle_n = len(lines) ** 2 - 1 if not check_puzzle_list(lst, input_puzzle_n): messagebox.showerror("Input error", "Puzzle numbers are not valid.", parent=parent) return if input_puzzle_n != int(n_spinbox.get()): n_spinbox.delete(0, tkinter.END) n_spinbox.insert(0, input_puzzle_n) change_app_n(input_puzzle_n) fill_puzzle_frame(puzzle_frame, lst) except: messagebox.showerror("Error opening input file", "Some problem happened while opening input file.", parent=parent) tkinter.Button(input_action_frame, text="Read from file", command=lambda: read_file_cmd(input_puzzle_frame, main_window)).grid(row=0, column=1, sticky='WENS') def random_button_command(puzzle_frame): """ Generates a random solvable puzzle and fills the puzzle_frame with it. See https://www.sitepoint.com/randomizing-sliding-puzzle-tiles/ for more information. """ n = int(n_spinbox.get()) lst = [i for i in range(0, n + 1)] random.shuffle(lst) sum_inversions = 0 for tile in [x for x in lst if x != 0]: before_tiles = GOAL_STATE[:GOAL_STATE.index(tile)] for after_tile in [x for x in lst[lst.index(tile):] if x != 0]: if before_tiles.count(after_tile): sum_inversions += 1 sqrt_n = math.sqrt(n + 1) def row_number(i): return math.ceil((i + 1) / sqrt_n) if sqrt_n % 2 == 1: solvable = sum_inversions % 2 == 0 else: solvable = (sum_inversions + abs(row_number(lst.index(0)) - row_number(GOAL_STATE.index(0)))) % 2 == 0 if not solvable: if lst[0] != 0 and lst[1] != 0: lst[0], lst[1] = lst[1], lst[0] else: lst[len(lst) - 1], lst[len(lst) - 2] = lst[len(lst) - 2], lst[len(lst) - 1] fill_puzzle_frame(puzzle_frame, lst) # Input's random button widget tkinter.Button(input_action_frame, text="Random", command=lambda: random_button_command(input_puzzle_frame)).grid(row=0, column=3, sticky='WENS', columnspan=1) def operator(puzzle): """ Returns all possible puzzle's states that are reachable from current puzzle's state """ states = [] zero_i = None zero_j = None for i in range(len(puzzle)): for j in range(len(puzzle)): if puzzle[i][j] == 0: zero_i = i zero_j = j break def add_swap(i, j): new_state = deepcopy(puzzle) new_state[i][j], new_state[zero_i][zero_j] = new_state[zero_i][zero_j], new_state[i][j] states.append(new_state) if zero_i != 0: add_swap(zero_i - 1, zero_j) if zero_j != 0: add_swap(zero_i, zero_j - 1) if zero_i != len(puzzle) - 1: add_swap(zero_i + 1, zero_j) if zero_j != len(puzzle) - 1: add_swap(zero_i, zero_j + 1) return states def puzzles_equal(first, second): for i in range(len(first)): for j in range(len(first)): if first[i][j] != second[i][j]: return False return True def n_step_random_command(): """ Generates a random puzzle that can be solved in n-step. """ n_step = simpledialog.askinteger("n-step random", "Enter number of steps:", parent=main_window) if not n_step: return puzzle = list_to_puzzle(GOAL_STATE) prev_puzzle = puzzle for i in range(n_step): new_puzzles = operator(puzzle) for i in range(len(new_puzzles)): if puzzles_equal(new_puzzles[i], prev_puzzle): del new_puzzles[i] break prev_puzzle = puzzle puzzle = new_puzzles[random.randrange(0, len(new_puzzles))] fill_puzzle_frame(input_puzzle_frame, puzzle_to_list(puzzle)) # Input's n-step random button widget tkinter.Button(input_action_frame, text="n-step random", command=n_step_random_command).grid(row=0, column=2, sticky='WENS', columnspan=1) # Status bar status_frame = tkinter.Frame(main_window, bd=1, relief=tkinter.SUNKEN) status_frame_1 = tkinter.Frame(status_frame, bd=1, relief=tkinter.GROOVE) tkinter.Label(status_frame_1, text="Execution time(s): ").grid(row=0, column=0, sticky='WENS', padx=2) tkinter.Label(status_frame_1, textvariable=cpu_var).grid(row=0, column=1, sticky='W') status_frame_1.grid_columnconfigure(1, weight=1) status_frame_1.grid(row=0, column=0, sticky='WENS') status_frame_2 = tkinter.Frame(status_frame, bd=1, relief=tkinter.GROOVE) tkinter.Label(status_frame_2, text="Max RAM usage(MB): ").grid(row=0, column=0, sticky='WENS', padx=2) tkinter.Label(status_frame_2, textvariable=max_ram_var).grid(row=0, column=1, sticky='W') status_frame_2.grid_columnconfigure(1, weight=1) status_frame_2.grid(row=0, column=1, sticky='WENS') status_frame_3 = tkinter.Frame(status_frame, bd=1, relief=tkinter.GROOVE) tkinter.Label(status_frame_3, text="RAM usage(MB): ").grid(row=0, column=0, sticky='WENS', padx=2) tkinter.Label(status_frame_3, textvariable=ram_var).grid(row=0, column=1, sticky='W') status_frame_3.grid_columnconfigure(1, weight=1) status_frame_3.grid(row=0, column=2, sticky='WENS') status_frame_4 = tkinter.Frame(status_frame, bd=1, relief=tkinter.SUNKEN) tkinter.Label(status_frame_4, text="Available RAM(MB): ").grid(row=0, column=0, sticky='WENS', padx=2) tkinter.Label(status_frame_4, textvariable=available_ram_var).grid(row=0, column=1, sticky='W') status_frame_4.grid_columnconfigure(1, weight=1) status_frame_4.grid(row=0, column=3, sticky='WENS') status_frame.grid(row=3, column=0, sticky='WENS', columnspan=2) status_frame.columnconfigure(0, weight=1, uniform=1) status_frame.columnconfigure(1, weight=1, uniform=1) status_frame.columnconfigure(2, weight=1, uniform=1) status_frame.columnconfigure(3, weight=1, uniform=1) load_algorithms() def available_ram_display(): """ A thread target that updates available ram status label every 1 millisecond """ while True: available_ram_var.set(round(psutil.virtual_memory().available / (2 ** 20), 3)) time.sleep(0.001) threading.Thread(target=available_ram_display, daemon=True).start() if __name__ == '__main__': # Support windows binary freezing multiprocessing.freeze_support() # Show the main window main_window.mainloop()