3
3
import tkinter .ttk as ttk
4
4
from tkinter import messagebox , simpledialog
5
5
6
- import backend
7
6
import main
8
7
8
+ ### Global Variables to store the solution analytics ###
9
9
algorithm = None
10
10
initialState = None
11
11
statepointer = cost = counter = depth = 0
15
15
16
16
class InterfaceApp :
17
17
18
+ # =============================================================================================================== #
19
+ ### Build the GUI ###
20
+
18
21
def __init__ (self , master = None ):
19
22
20
23
self ._job = None
@@ -151,36 +154,54 @@ def __init__(self, master=None):
151
154
152
155
self .gif = [tk .PhotoImage (file = 'loading.gif' , format = 'gif -index %i' % i ) for i in range (10 )]
153
156
154
-
155
157
def run (self ):
158
+ """
159
+ Run the program, display the GUI
160
+ """
156
161
app .displayStateOnGrid ('000000000' )
157
162
app .gif_loading .place_forget ()
158
- self .updateGrid ()
159
- self .mainwindow .after (0 , app .update , 0 )
163
+ self .refreshFrame ()
164
+ self .mainwindow .after (0 , app .refreshGIF , 0 )
160
165
self .mainwindow .mainloop ()
161
166
167
+ # =============================================================================================================== #
168
+ ### Widget Methods ###
169
+
162
170
@staticmethod
163
- def update (ind ):
171
+ def refreshGIF (ind ):
172
+ """
173
+ Refreshes the loading gif to show the next frame
174
+ """
164
175
frame = app .gif [ind ]
165
176
ind = (ind + 1 ) % 10
166
177
app .gif_loading .configure (image = frame )
167
- app .appFrame .after (50 , app .update , ind )
178
+ app .appFrame .after (50 , app .refreshGIF , ind )
168
179
169
180
def prevSequence (self , event = None ):
181
+ """
182
+ Displays the previous state on the grid
183
+ """
170
184
global statepointer
171
185
if statepointer > 0 :
172
186
self .stopFastForward ()
173
187
statepointer -= 1
174
- self .updateGrid ()
188
+ self .refreshFrame ()
175
189
176
190
def nextSequence (self , event = None ):
191
+ """
192
+ Displays the next state on the grid
193
+ """
177
194
global statepointer
178
195
if statepointer < len (path ) - 1 :
179
196
self .stopFastForward ()
180
197
statepointer += 1
181
- self .updateGrid ()
198
+ self .refreshFrame ()
182
199
183
200
def solve (self , event = None ):
201
+ """
202
+ Function is invoked at pressing the solve button. Solves the puzzle with the given initialState and algorithm
203
+ then gives a suitable response to the user
204
+ """
184
205
global algorithm , initialState
185
206
app .gif_loading .place (x = 600 , y = 125 , anchor = "s" )
186
207
if self .readyToSolve ():
@@ -189,68 +210,55 @@ def solve(self, event=None):
189
210
self .resetGrid ()
190
211
self .solveState ()
191
212
if len (path ) == 0 :
192
- print ('<!> Unsolvable State' )
193
213
messagebox .showinfo ('Unsolvable!' , 'The state you entered is unsolvable' )
194
214
self .displaySearchAnalysis (True )
195
215
else :
196
- print ('<!> Solved!' )
197
- self .updateGrid ()
216
+ self .refreshFrame ()
198
217
else :
199
218
solvingerror = 'Cannot solve.\n ' \
200
219
'Algorithm in use: ' + str (algorithm ) + '\n ' \
201
220
'Initial State : ' + str (initialState )
202
- print (solvingerror + '\n -' )
203
221
messagebox .showerror ('Cannot Solve' , solvingerror )
204
222
app .gif_loading .place_forget ()
205
223
206
224
def enterInitialState (self , event = None ):
225
+ """
226
+ Invoked at pressing enter initial state button. Displays a simple dialog box for the user to enter their
227
+ initial state. The state is validated and a suitable response it displayed to the user
228
+ """
207
229
global initialState , statepointer
208
230
inputState = simpledialog .askstring ('Initial State Entry' , 'Please enter your initial state' )
209
231
if inputState is not None :
210
- if backend .validateState (inputState ):
232
+ if self .validateState (inputState ):
211
233
initialState = inputState
212
234
self .reset ()
213
235
app .displayStateOnGrid (initialState )
214
236
else :
215
- print ('Invalid initial state' )
216
237
messagebox .showerror ('Input Error' , 'Invalid initial state' )
217
238
218
239
def selectAlgorithm (self , event = None ):
240
+ """
241
+ Invoked at activating the algorithms combobox. Associates the chosen value to the global variable 'algorithm'
242
+ """
219
243
global algorithm
220
244
try :
221
245
choice = self .algorithmbox .selection_get ()
222
246
self .reset ()
223
247
algorithm = choice
224
248
except :
225
- print ('Invalid algorithm selection' )
226
-
227
- def showContributors (self , event = None ):
228
- messagebox .showinfo ('Contributors' , "6744 - Adham Mohamed Aly\n "
229
- "6905 - Mohamed Farid Abdelaziz\n "
230
- "7140 - Yousef Ashraf Kotp\n " )
231
-
232
- def displaySearchAnalysis (self , force_display = False ):
233
- if self .solved () or force_display is True :
234
- analytics = 'Analysis of ' + str (algorithm ) + \
235
- '\n initial state = ' + str (initialState )
236
- if force_display :
237
- analytics += '\n < UNSOLVABLE >'
238
- analytics += '\n -------------------------------' \
239
- '\n ' + 'Nodes expanded: \n ' + str (counter ) + \
240
- '\n ' + 'Search depth: \n ' + str (depth ) + \
241
- '\n ' + 'Search cost: \n ' + str (cost ) + \
242
- '\n ' + 'Running Time: \n ' + str (runtime ) + ' s'
243
- else :
244
- analytics = ''
245
- app .analysisbox .configure (text = analytics )
249
+ pass
246
250
247
251
def fastForward (self , event = None ):
252
+ """
253
+ Invoked at pressing fast-forward button. Displays following states in rapid succession until it reaches the
254
+ goal state or until terminated by the stopFastForward() method
255
+ """
248
256
global statepointer
249
257
self .stopFastForward ()
250
258
if statepointer < cost :
251
259
app .stopbutton .configure (state = 'enabled' )
252
260
statepointer += 1
253
- self .updateGrid ()
261
+ self .refreshFrame ()
254
262
ms = 100
255
263
if 100 < cost <= 1000 :
256
264
ms = 20
@@ -261,6 +269,10 @@ def fastForward(self, event=None):
261
269
self .stopFastForward ()
262
270
263
271
def fastBackward (self , event = None ):
272
+ """
273
+ Invoked at pressing fast-backward button. Displays previous states in rapid succession until it reaches the
274
+ goal state or until terminated by the stopFastForward() method
275
+ """
264
276
global statepointer
265
277
self .stopFastForward ()
266
278
if statepointer > 0 :
@@ -272,44 +284,96 @@ def fastBackward(self, event=None):
272
284
app ._job = app .stepCount .after (ms , self .fastBackward )
273
285
else :
274
286
self .stopFastForward ()
275
- self .updateGrid ()
287
+ self .refreshFrame ()
276
288
277
- def stopFastForward (self , event = None ):
289
+ @staticmethod
290
+ def stopFastForward (event = None ):
291
+ """
292
+ Invoked at pressing stop fast-forward/backward button. Stops fast-forward/backward
293
+ """
278
294
if app ._job is not None :
279
295
app .stopbutton .configure (state = 'disabled' )
280
296
app .stepCount .after_cancel (app ._job )
281
297
app ._job = None
282
298
283
299
def resetStepCounter (self , event = None ):
300
+ """
301
+ Invoked at pressing reset button. Resets the grid to the initial state and the step counter to 0
302
+ """
284
303
global statepointer
285
304
if statepointer > 0 :
286
305
self .stopFastForward ()
287
306
statepointer = 0
288
- self .updateGrid ()
307
+ self .refreshFrame ()
308
+
309
+ def showContributors (self , event = None ):
310
+ """
311
+ Invoked at pressing the contributors button. Displays a message box Containing names and IDs of contributors
312
+ """
313
+ messagebox .showinfo ('Contributors' , "6744 - Adham Mohamed Aly\n "
314
+ "6905 - Mohamed Farid Abdelaziz\n "
315
+ "7140 - Yousef Ashraf Kotp\n " )
316
+
317
+ # =============================================================================================================== #
318
+ ### Helper Functions ###
319
+
320
+ def displaySearchAnalysis (self , force_display = False ):
321
+ """
322
+ Displays the analysis of the search algorithm after execution.
323
+ """
324
+ if self .solved () or force_display is True :
325
+ analytics = 'Analysis of ' + str (algorithm ) + \
326
+ '\n initial state = ' + str (initialState )
327
+ if force_display :
328
+ analytics += '\n < UNSOLVABLE >'
329
+ analytics += '\n -------------------------------' \
330
+ '\n ' + 'Nodes expanded: \n ' + str (counter ) + \
331
+ '\n ' + 'Search depth: \n ' + str (depth ) + \
332
+ '\n ' + 'Search cost: \n ' + str (cost ) + \
333
+ '\n ' + 'Running Time: \n ' + str (runtime ) + ' s'
334
+ else :
335
+ analytics = ''
336
+ app .analysisbox .configure (text = analytics )
289
337
290
338
def displayStateOnGrid (self , state ):
291
- if not backend .validateState (state ):
339
+ """
340
+ Display input state to the grid
341
+ :param state: String representation of the required state
342
+ """
343
+ if not self .validateState (state ):
292
344
state = '000000000'
293
- self .cell0 .configure (text = backend .adjustDigit (state [0 ]))
294
- self .cell1 .configure (text = backend .adjustDigit (state [1 ]))
295
- self .cell2 .configure (text = backend .adjustDigit (state [2 ]))
296
- self .cell3 .configure (text = backend .adjustDigit (state [3 ]))
297
- self .cell4 .configure (text = backend .adjustDigit (state [4 ]))
298
- self .cell5 .configure (text = backend .adjustDigit (state [5 ]))
299
- self .cell6 .configure (text = backend .adjustDigit (state [6 ]))
300
- self .cell7 .configure (text = backend .adjustDigit (state [7 ]))
301
- self .cell8 .configure (text = backend .adjustDigit (state [8 ]))
345
+ self .cell0 .configure (text = self .adjustDigit (state [0 ]))
346
+ self .cell1 .configure (text = self .adjustDigit (state [1 ]))
347
+ self .cell2 .configure (text = self .adjustDigit (state [2 ]))
348
+ self .cell3 .configure (text = self .adjustDigit (state [3 ]))
349
+ self .cell4 .configure (text = self .adjustDigit (state [4 ]))
350
+ self .cell5 .configure (text = self .adjustDigit (state [5 ]))
351
+ self .cell6 .configure (text = self .adjustDigit (state [6 ]))
352
+ self .cell7 .configure (text = self .adjustDigit (state [7 ]))
353
+ self .cell8 .configure (text = self .adjustDigit (state [8 ]))
302
354
303
355
@staticmethod
304
356
def readyToSolve ():
357
+ """
358
+ Checks if current state is ready to be solved by checking if the global variables 'initialState' and
359
+ 'algorithm' are not None
360
+ :return: boolean
361
+ """
305
362
return initialState is not None and algorithm is not None
306
363
307
364
@staticmethod
308
365
def solved ():
366
+ """
367
+ Checks if there is a solution registered in the global variables
368
+ :return: boolean
369
+ """
309
370
return len (path ) > 0
310
371
311
372
@staticmethod
312
373
def solveState ():
374
+ """
375
+ Solves the puzzle with 'initialState' and the chosen 'algorithm'. Assumes the current state is ready to solve.
376
+ """
313
377
global path , cost , counter , depth , runtime
314
378
if str (algorithm ) == 'BFS' :
315
379
main .BFS (initialState )
@@ -327,16 +391,20 @@ def solveState():
327
391
main .AStarSearch_euclid (initialState )
328
392
path , cost , counter , depth , runtime = \
329
393
main .euclid_path , main .euclid_cost , main .euclid_counter , round (main .euclid_depth ), main .time_euclid
330
- else :
331
- print ('Error occurred' )
332
394
333
395
def resetGrid (self ):
396
+ """
397
+ Resets the grid and step counter to the initial state
398
+ """
334
399
global statepointer
335
400
statepointer = 0
336
- self .updateGrid ()
337
- app .stepCount .configure (text = self .getStepCount ())
401
+ self .refreshFrame ()
402
+ app .stepCount .configure (text = self .getStepCountString ())
338
403
339
404
def reset (self ):
405
+ """
406
+ Resets global variables and the GUI frame. Removes currently registered solution
407
+ """
340
408
global path , cost , counter , runtime
341
409
cost = counter = 0
342
410
runtime = 0.0
@@ -345,15 +413,22 @@ def reset(self):
345
413
app .analysisbox .configure (text = '' )
346
414
347
415
@staticmethod
348
- def getStepCount ():
416
+ def getStepCountString ():
417
+ """
418
+ Returns string representation of the step count to be displayed on the step-counter
419
+ :return: String
420
+ """
349
421
return str (statepointer ) + ' / ' + str (cost )
350
422
351
423
@staticmethod
352
- def updateGrid ():
424
+ def refreshFrame ():
425
+ """
426
+ Refreshes the frame with all its components: grid, counter, button, etc.
427
+ """
353
428
if cost > 0 :
354
429
state = main .getStringRepresentation (path [statepointer ])
355
430
app .displayStateOnGrid (state )
356
- app .stepCount .configure (text = app .getStepCount ())
431
+ app .stepCount .configure (text = app .getStepCountString ())
357
432
app .displaySearchAnalysis ()
358
433
if statepointer == 0 :
359
434
app .resetbutton .configure (state = 'disabled' )
@@ -371,6 +446,33 @@ def updateGrid():
371
446
app .fastforwardbutton .configure (state = 'enabled' )
372
447
app .nextbutton .configure (state = 'enabled' )
373
448
449
+ @staticmethod
450
+ def validateState (inputState ):
451
+ """
452
+ Validates given state
453
+ :param inputState: String representation of state to be validated
454
+ :return: boolean
455
+ """
456
+ seen = []
457
+ if inputState is None or len (inputState ) != 9 or not inputState .isnumeric ():
458
+ return False
459
+ for dig in inputState :
460
+ if dig in seen or dig == '9' :
461
+ return False
462
+ seen .append (dig )
463
+ return True
464
+
465
+ @staticmethod
466
+ def adjustDigit (dig ):
467
+ """
468
+ Converts the zero to an empty cell. Otherwise, returns the digit as it is.
469
+ :param dig: Character of the digit to be adjusted
470
+ :return: string
471
+ """
472
+ if dig == '0' :
473
+ return ' '
474
+ return dig
475
+
374
476
375
477
if __name__ == "__main__" :
376
478
global app
0 commit comments