1
2
3
4 '''
5 Main part of kuimaze - framework for working with mazes. Contains class Maze (capable of displaying it) and couple helper classes
6 @author: Otakar Jašek, Tomas Svoboda
7 @contact: jasekota(at)fel.cvut.cz, svobodat@fel.cvut.cz
8 @copyright: (c) 2017, 2018
9 '''
10
11 import collections
12 import enum
13 import numpy as np
14 import os
15 import random
16 import warnings
17 from PIL import Image, ImageTk
18 import sys
19 import inspect
20
21 import tkinter
22
23 import kuimaze
24
25
26 fw_orig = warnings.formatwarning
27 warnings.formatwarning = lambda msg, categ, fname, lineno, line=None: fw_orig(msg, categ, fname, lineno, '')
28
29
30
31 weighted_state = collections.namedtuple('State', ['x', 'y', 'reward'])
32
33 state = collections.namedtuple('State', ['x', 'y'])
34
35 path_section = collections.namedtuple('Path', ['state_from', 'state_to', 'cost', 'action'])
36
37
38
39 MAX_CELL_SIZE = 200
40
41 MAX_WINDOW_PERCENTAGE = 0.85
42
43 BORDER_SIZE = 0
44
45 LINE_SIZE_PERCENTAGE = 0.02
46
47
48 DRAW_LABELS = True
49
50 LINE_COLOR = "#FFF555333"
51 WALL_COLOR = "#000000000"
52 EMPTY_COLOR = "#FFFFFFFFF"
53 EXPLORED_COLOR = "#000BBB000"
54 SEEN_COLOR = "#BBBFFFBBB"
55 START_COLOR = "#000000FFF"
56
57 FINISH_COLOR = "#000FFFFFF"
58 DANGER_COLOR = "#FFF000000"
59
60
61 FONT_FAMILY = "Helvetica"
62
63
64 FONT_SIZE = round(12*MAX_CELL_SIZE/50)
65
66 REWARD_NORMAL = -0.04
67 REWARD_DANGER = -1
68 REWARD_GOAL = 1
69
70 -class SHOW(enum.Enum):
71 '''
72 Enum class used for storing what is displayed in GUI - everything higher includes everything lower (except NONE, of course).
73 So if SHOW is NODE_REWARDS, it automatically means, that it will display FULL_MAZE (and EXPLORED), however it won't display ACTION_COSTS
74 '''
75 NONE = 0
76 EXPLORED = 1
77 FULL_MAZE = 2
78
79
81 '''
82 Enum class to represent actions in a grid-world.
83 '''
84 UP = 0
85 RIGHT = 1
86 DOWN = 2
87 LEFT = 3
88
98
100 '''
101 Class for probabilistic maze - implements roulette wheel with intervals
102 '''
103
104 - def __init__(self, obey=0.8, confusionL=0.1, confusionR=0.1, confusion180=0):
105
106 self._obey = None
107 self._confusionLeft = None
108 self._confusionRight = None
109 self.set_probs(obey, confusionL, confusionR, confusion180)
110
111 - def set_probs(self, obey, confusionL, confusionR, confusion180):
112 assert obey + confusionL + confusionR + confusion180 == 1
113 assert 0 <= obey <= 1
114 assert 0 <= confusionL <= 1
115 assert 0 <= confusionR <= 1
116 assert 0 <= confusion180 <= 1
117 self._obey = obey
118 self._confusionLeft = self._obey + confusionL
119 self._confusionRight = self._confusionLeft + confusionR
120
122 roulette = random.uniform(0.0, 1.0)
123 if 0 <= roulette < self._obey:
124 return action
125 else:
126
127 if self._obey <= roulette < self._confusionLeft:
128 return (action - 1) % 4
129 else:
130
131 if self._confusionLeft <= roulette < self._confusionRight:
132 return (action + 1) % 4
133 else:
134
135 return (action + 2) % 4
136
138 return str(self.probtable)
139
140
142 - def __init__(self, obey=0.8, confusionL=0.1, confusionR=0.1, confusion180=0):
143 assert abs(1-(obey+confusionR+confusionL+confusion180)) < 0.00001
144
145
146
147 self.probtable = dict()
148 self.probtable[ACTION.UP, ACTION.LEFT] = confusionL
149 self.probtable[ACTION.UP, ACTION.UP] = obey
150 self.probtable[ACTION.UP, ACTION.RIGHT] = confusionR
151 self.probtable[ACTION.UP, ACTION.DOWN] = confusion180
152
153 self.probtable[ACTION.RIGHT, ACTION.LEFT] = confusion180
154 self.probtable[ACTION.RIGHT, ACTION.UP] = confusionL
155 self.probtable[ACTION.RIGHT, ACTION.RIGHT] = obey
156 self.probtable[ACTION.RIGHT, ACTION.DOWN] = confusionR
157
158 self.probtable[ACTION.DOWN, ACTION.LEFT] = confusionR
159 self.probtable[ACTION.DOWN, ACTION.UP] = confusion180
160 self.probtable[ACTION.DOWN, ACTION.RIGHT] = confusionL
161 self.probtable[ACTION.DOWN, ACTION.DOWN] = obey
162
163 self.probtable[ACTION.LEFT, ACTION.LEFT] = obey
164 self.probtable[ACTION.LEFT, ACTION.UP] = confusionR
165 self.probtable[ACTION.LEFT, ACTION.RIGHT] = confusion180
166 self.probtable[ACTION.LEFT, ACTION.DOWN] = confusionL
167
169 return self.probtable[item]
170
172 return str(self.probtable)
173
174
175
176
178 '''
179 Maze class takes care of GUI and interaction functions.
180 '''
181 __deltas = [[0, -1], [1, 0], [0, 1], [-1, 0], [1, -1], [1, 1], [-1, -1], [-1, 1]]
182
183
184 - def __init__(self, image, grad, node_rewards=None, path_costs=None, trans_probs=None, show_level=SHOW.FULL_MAZE,
185 start_node=None, goal_nodes=None, ):
186 '''
187 Parameters node_rewards, path_costs and trans_probs are meant for defining more complicated mazes. Parameter start_node redefines start state completely, parameter goal_nodes will add nodes to a list of goal nodes.
188
189 @param image: path_section to an image file describing problem. Expects to find RGB image in given path_section
190
191 white color - empty space
192
193 black color - wall space
194
195 red color - goal state
196
197 blue color - start state
198 @type image: string
199 @keyword node_rewards: optional setting of state rewards. If not set, or incorrect input, it will be set to default value - all nodes have reward of zero.
200 @type node_rewards: either string pointing to stored numpy.ndarray or numpy.ndarray itself or None for default value. Shape of numpy.ndarray must be (x, y) where (x, y) is shape of problem.
201 @keyword path_costs: optional setting of path_section costs. If not set, or incorrect input, it will be set to default value - all paths have cost of one.
202 @type path_costs: either string pointing to stored numpy.ndarray or numpy.ndarray itself or None for default value. Shape of numpy.ndarray must be (x, y, 2) where (x, y) is shape of problem.
203 @keyword trans_probs: optional setting of transition probabilities for modelling MDP. If not set, or incorrect input, it will be set to default value - actions have probability of 1 for itself and 0 for any other.
204 @type trans_probs: either string pointing to stored numpy.ndarray or numpy.ndarray itself or None for default value. Shape of numpy.ndarray must be (x, y, 4, 4) where (x, y) is shape of problem.
205 @keyword show_level: Controlling level of displaying in GUI.
206 @type show_level: L{kuimaze.SHOW}
207 @keyword start_node: Redefining start state. Must be a valid state inside a problem without a wall.
208 @type start_node: L{namedtuple state<state>} or None for default start state loaded from image.
209 @keyword goal_nodes: Appending to a list of goal nodes. Must be valid nodes inside a problem without a wall.
210 @type goal_nodes: iterable of L{namedtuples state<state>} or None for default set of goal nodes loaded from image.
211
212 @raise AssertionError: When image is not RGB image or if show is not of type L{kuimaze.SHOW} or if initialization didn't finish correctly.
213 '''
214 try:
215 im_data = Image.open(image)
216 self.__filename = image
217 except:
218 im_data = image
219 self.__filename = 'given'
220 maze = np.array(im_data, dtype=int)
221 assert (len(maze.shape) == 3 and maze.shape[2] == 3)
222 self.__maze = maze.sum(axis=2, dtype=bool).T
223 self.__start = None
224 self.__finish = None
225 self.hard_places = []
226 self.__node_rewards = None
227 self.__node_utils = None
228 self.__path_costs = None
229 self.__trans_probs = None
230 self.__i = 0
231 self.__till_end = False
232 self.__gui_root = None
233 self.__gui_lock = False
234 self.__player = None
235 self.__gui_setup = False
236 self.__running_find = False
237 self.__eps_folder = os.getcwd()
238 self.__eps_prefix = ""
239
240 assert type(grad) == tuple or type(grad) == list
241 assert len(grad) == 2 and -1 < grad[0] < 1 and -1 < grad[1] < 1
242 self.__grad = grad
243 self.__set_grad_data()
244
245 self.__has_triangles = False
246
247 maze = maze.tolist()
248 finish = []
249 if start_node is None or goal_nodes is None:
250 for y, col in enumerate(maze):
251 for x, cell in enumerate(col):
252 if cell == [255, 0, 0]:
253 finish.append(state(x, y))
254 if cell == [0, 0, 255]:
255 self.__start = state(x, y)
256 if cell == [0, 255, 0]:
257 self.hard_places.append(state(x, y))
258
259
260
261 if not('InfEasyMaze' in str(inspect.stack()[1:])):
262 finish.append(state(x,y))
263 else:
264 print('InfEasyMaze was the caller, hard_places are not added to the goals')
265 self.__finish = frozenset(finish)
266
267 if start_node is not None:
268 if self.__is_inside_valid(start_node):
269 if self.__start is not None:
270 warnings.warn('Replacing start state as there could be only one!')
271 self.__start = state(start_node.x, start_node.y)
272
273 if goal_nodes is not None:
274 finish = list(self.__finish)
275 warnings.warn('Adding to list of goal nodes!')
276 for point in goal_nodes:
277 if self.__is_inside_valid(point):
278 finish.append(point)
279 self.__finish = frozenset(finish)
280
281 if node_rewards is not None:
282 if isinstance(node_rewards, str):
283 node_rewards = np.load(node_rewards)
284 else:
285 node_rewards = np.array(node_rewards)
286 node_rewards = np.transpose(node_rewards)
287 print(node_rewards.shape, self.__maze.shape)
288 if node_rewards.shape == self.__maze.shape:
289 self.__node_rewards = node_rewards
290 print(self.__node_rewards)
291
292 if self.__node_rewards is None:
293 self.__node_rewards = np.zeros(self.__maze.shape, dtype=float)
294 for y, col in enumerate(maze):
295 for x, cell in enumerate(col):
296 pos = state(x,y)
297 self.__node_rewards[x, y] = REWARD_NORMAL
298 if pos in self.__finish:
299 self.__node_rewards[x,y] = REWARD_GOAL
300 if pos in self.hard_places:
301 self.__node_rewards[x,y] = REWARD_DANGER
302 print(self.__node_rewards)
303
304 if self.__node_utils is None:
305 self.__node_utils = np.zeros(self.__maze.shape, dtype=float)
306
307 if path_costs is not None:
308 if isinstance(path_costs, str):
309 path_costs = np.load(path_costs)
310 if path_costs.shape == (self.__maze.shape[0], self.__maze.shape[1], 2):
311 self.__path_costs = path_costs
312 if self.__path_costs is None:
313 self.__path_costs = np.ones((self.__maze.shape[0], self.__maze.shape[1], 2), dtype=int)
314
315 if trans_probs is not None:
316 self.__trans_probs = ProbsRoulette()
317 if self.__trans_probs is None:
318 self.__trans_probs = ProbsRoulette(0.8, 0.1, 0.1, 0)
319
320 assert (isinstance(show_level, SHOW))
321 self.show_level = show_level
322 self.__backup_show = show_level
323 self.__clear_player_data()
324
325 assert (self.__start is not None)
326 assert (self.__finish is not None)
327 assert (self.__node_rewards is not None)
328 assert (self.__path_costs is not None)
329 assert (self.__trans_probs is not None)
330 print('maze init done')
331
334
336 '''
337 Returns a start state
338 @return: start state
339 @rtype: L{namedtuple state<state>}
340 '''
341 return self.__start
342
345
347 '''
348 a visualisation method - sets an interal variable for displaying utilities
349 @param utils: dictionary of utilities, indexed by tuple - state coordinates
350 @return: None
351 '''
352 for position in utils.keys():
353 self.__node_utils[position] = utils[position]
354
356 '''
357 Check whether a C{current_node} is goal state or not
358 @param current_state: state to check.
359 @type current_state: L{namedtuple state<state>}
360 @return: True if state is a goal state, False otherwise
361 @rtype: boolean
362 '''
363 return state(current_state.x, current_state.y) in self.__finish
364
366 return state(current_state.x, current_state.y) in self.hard_places
367
369 '''
370 Returns a list of goal nodes
371 @return: list of goal nodes
372 @rtype: list
373 '''
374 return list(self.__finish)
375
377 '''
378 Returns a list of all the problem states
379 @return: list of all states
380 @rtype: list of L{namedtuple weighted_state<weighted_state>}
381 '''
382 dims = self.get_dimensions()
383 states = []
384 for x in range(dims[0]):
385 for y in range(dims[1]):
386 if self.__maze[x, y]:
387 states.append(weighted_state(x, y, self.__node_rewards[x, y]))
388 return states
389
391 '''
392 Returns dimensions of problem
393 @return: x and y dimensions of problem. Note that state indices are zero-based so if returned dimensions are (5, 5), state (5, 5) is B{not} inside problem.
394 @rtype: tuple
395 '''
396 return self.__maze.shape
397
399 '''
400 Generate (yield) actions possible for the current_state
401 It does not check the outcome this is left to the result method
402 @param current_state:
403 @return: action (relevant for the problem - problem in this case)
404 @rtype: L{action from ACTION<ACTION>}
405 '''
406 for action in ACTION:
407 yield action
408
409 - def result(self, current_state, action):
410 '''
411 Apply the action and get the state; deterministic version
412 @param current_state: state L{namedtuple state<state>}
413 @param action: L{action from ACTION<ACTION>}
414 @return: state (result of the action applied at the current_state)
415 @rtype: L{namedtuple state<state>}
416 '''
417 x, y = self.__deltas[action]
418 nx = current_state.x + x
419 ny = current_state.y + y
420 if self.__is_inside(state(nx, ny)) and self.__maze[nx, ny]:
421 nstate = weighted_state(nx, ny, self.__node_rewards[nx, ny])
422 else:
423 nstate = weighted_state(current_state.x, current_state.y,
424 self.__node_rewards[current_state.x, current_state.y])
425
426 return state(nstate.x, nstate.y)
427
429 '''
430 For the commanded action it generates all posiible outcomes with associated probabilities
431 @param state: state L{namedtuple state<state>}
432 @param action: L{action from ACTION<ACTION>}
433 @return: list of tuples (next_state, probability_of_ending_in_the_next_state)
434 @rtype: list of tuples
435 '''
436 states_dict = {}
437 for out_action in ACTION:
438 next_state = self.result(curr, out_action.value)
439 if not(next_state in states_dict):
440 states_dict[next_state] = 0
441 states_dict[next_state] += self.__trans_probs[action, out_action]
442
443
444 states_probs = []
445 for key in states_dict:
446 states_probs.append((key, states_dict[key]))
447 return states_probs
448
450 '''
451 sets explored states list, preparation for visualisation
452 @param states: iterable of L{state<state>}
453 '''
454 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
455 for state in states:
456 self.__explored[state.x, state.y] = True
457 if self.__changed_cells is not None:
458 self.__changed_cells.append(state)
459
460 - def set_probs(self, obey, confusionL, confusionR, confusion180):
461 self.__trans_probs.set_probs(obey, confusionL, confusionR, confusion180)
462
464 self.__trans_probs = ActionProbsTable(obey, confusionL, confusionR, confusion180)
465
467 '''
468 sets seen states list, preparation for visualisation
469 @param states: iterable of L{state<state>}
470 '''
471 for state in states:
472 self.__seen[state.x, state.y] = True
473 if self.__changed_cells is not None:
474 self.__changed_cells.append(state)
475
477 real_action = self.__trans_probs.confuse_action(action)
478 return real_action
479
481 '''
482 Check whether a state is inside a problem
483 @param current_state: state to check
484 @type current_state: L{namedtuple state<state>}
485 @return: True if state is inside problem, False otherwise
486 @rtype: boolean
487 '''
488 dims = self.get_dimensions()
489 return current_state.x >= 0 and current_state.y >= 0 and current_state.x < dims[0] and current_state.y < dims[1]
490
492 '''
493 Check whether a state is inside a problem and is not a wall
494 @param current_state: state to check
495 @type current_state: L{namedtuple state<state>}
496 @return: True if state is inside problem and is not a wall, False otherwise
497 @rtype: boolean
498 '''
499 return self.__is_inside(current_state) and self.__maze[current_state.x, current_state.y]
500
502 '''
503 Clear player data for using with different player or running another find_path
504 '''
505 self.__seen = np.zeros(self.__maze.shape, dtype=bool)
506 self.__seen[self.__start.x, self.__start.y] = True
507 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
508 self.__explored[self.__start.x, self.__start.y] = True
509 self.__i = 0
510 self.__running_find = False
511 self.__renew_gui()
512 self.__changed_cells = None
513
514 self.__clear_lines()
515
517 '''
518 Clear player data for using with different player or running another find_path
519 '''
520 self.__seen = np.zeros(self.__maze.shape, dtype=bool)
521 self.__seen[self.__start.x, self.__start.y] = True
522 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
523 self.__explored[self.__start.x, self.__start.y] = True
524 self.__i = 0
525 self.__running_find = False
526
528 '''
529 Set player associated with this problem.
530 @param player: player to be used for association
531 @type player: L{BaseAgent<kuimaze.BaseAgent>} or its descendant
532 @raise AssertionError: if player is not instance of L{BaseAgent<kuimaze.BaseAgent>} or its descendant
533 '''
534 assert (isinstance(player, kuimaze.baseagent.BaseAgent))
535 self.__player = player
536 self.__clear_player_data()
537
538
539 '''
540 if self.__gui_root is not None:
541 self.__gui_root.mainloop()
542 '''
543
545 '''
546 Main GUI function - call this from L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>} to update GUI and
547 break at this point to be able to step your actions.
548 Example of its usage can be found at L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>}
549
550 Don't use it too often as it is quite expensive and rendering after single exploration might be slowing your
551 code down a lot.
552
553 You can optionally set parameter C{drawed_nodes} to a list of lists of dimensions corresponding to dimensions of
554 problem and if show_level is higher or equal to L{SHOW.NODE_REWARDS}, it will plot those in state centers
555 instead of state rewards.
556 If this parameter is left unset, no redrawing of texts in center of nodes is issued, however, it can be set to
557 True which will draw node_rewards saved in the problem.
558
559 If show_level is L{SHOW.NONE}, thisets function has no effect
560
561 @param drawed_nodes: custom objects convertible to string to draw to center of nodes or True or None
562 @type drawed_nodes: list of lists of the same dimensions as problem or boolean or None
563 '''
564 assert (self.__player is not None)
565 if self.show_level is not SHOW.NONE:
566 first_run = False
567 if not self.__gui_setup:
568 self.__setup_gui()
569 first_run = True
570 if self.show_level.value >= SHOW.FULL_MAZE.value:
571 self.__gui_update_map(explored_only=False)
572 else:
573 if self.show_level.value == SHOW.EXPLORED.value:
574 self.__gui_update_map(explored_only=True)
575 if first_run:
576
577
578 first_run = False
579 if not self.__till_end and self.__running_find:
580 self.__gui_lock = True
581 self.__changed_cells = []
582 self.__gui_canvas.update()
583 '''
584 while self.__gui_lock:
585 time.sleep(0.01)
586 self.__gui_root.update()
587 '''
588
590 '''
591 Show resulting path_section given as a list of consecutive L{namedtuples path_section<path_section>} to show in GUI.
592 Example of such usage can be found in L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>}
593
594 @param full_path: path_section in a form of list of consecutive L{namedtuples path_section<path_section>}
595 @type full_path: list of consecutive L{namedtuples path_section<path_section>}
596 '''
597 if self.show_level is not SHOW.NONE and len(full_path) is not 0:
598 def coord_gen(paths):
599 paths.append(path_section(paths[-1].state_to, None, None, None))
600 for item in paths:
601 for j in range(2):
602 num = item.state_from.x if j == 0 else item.state_from.y
603 yield (num + 1.5) * self.__cell_size + BORDER_SIZE
604 size = int(self.__cell_size/3)
605 coords = list(coord_gen(full_path))
606 full_path = full_path[:-1]
607 self.__drawn_lines.append((self.__gui_canvas.create_line(
608 *coords, width=self.__line_size, capstyle='round', fill=LINE_COLOR,
609 arrow=tkinter.LAST, arrowshape=(size, size, int(size/2.5))), coords))
610 self.__text_to_top()
611
613 '''
614 Set new show level. It will redraw whole GUI, so it takes a while.
615 @param show_level: new show_level to set
616 @type show_level: L{SHOW}
617 @raise AssertionError: if show_level is not an instance of L{SHOW}
618 '''
619 assert (isinstance(show_level, SHOW))
620 self.__backup_show = show_level
621 self.__changed_cells = None
622 if self.show_level is not show_level:
623 self.__destroy_gui(unblock=False)
624 self.show_level = show_level
625 if self.show_level is SHOW.NONE:
626 self.__gui_lock = False
627 self.__show_tkinter.set(show_level.value)
628 coords = [c for i, c in self.__drawn_lines]
629 self.show_and_break()
630 if self.show_level is not SHOW.NONE:
631 self.__drawn_lines = []
632 for coord in coords:
633 self.__drawn_lines.append((self.__gui_canvas.create_line(
634 *coord, width=self.__line_size, capstyle='round', fill=LINE_COLOR), coord))
635
637 '''
638 Set folder where the EPS files will be saved.
639 @param folder: folder to save EPS files
640 @type folder: string with a valid path_section
641 '''
642 folder = os.path.join(os.path.dirname(os.path.dirname(sys.argv[0])))
643 self.__save_name = os.path.join(folder, "%04d.eps" % (self.__i,))
644
646 '''
647 Setup and draw basic GUI. Imports tkinter.
648 '''
649 self.__gui_root = tkinter.Tk()
650 self.__gui_root.title('KUI - Maze')
651 self.__gui_root.protocol('WM_DELETE_WINDOW', self.__destroy_gui)
652 self.__gui_root.resizable(0, 0)
653 w = (self.__gui_root.winfo_screenwidth() / (self.get_dimensions()[0] + 2)) * MAX_WINDOW_PERCENTAGE
654 h = (self.__gui_root.winfo_screenheight() / (self.get_dimensions()[1] + 2)) * MAX_WINDOW_PERCENTAGE
655 use_font = FONT_FAMILY + str(FONT_SIZE)
656 self.__cell_size = min(w, h, MAX_CELL_SIZE)
657 self.__show_tkinter = tkinter.IntVar()
658 self.__show_tkinter.set(self.show_level)
659 top_frame = tkinter.Frame(self.__gui_root)
660 top_frame.pack(expand=False, side=tkinter.TOP)
661 width_pixels = (self.__cell_size * (self.get_dimensions()[0] + 2) + 2 * BORDER_SIZE)
662 height_pixels = (self.__cell_size * (self.get_dimensions()[1] + 2) + 2 * BORDER_SIZE)
663 self.__gui_canvas = tkinter.Canvas(top_frame, width=width_pixels, height=height_pixels)
664 self.__gui_canvas.pack(expand=False, side=tkinter.LEFT)
665 self.__color_handles = (-np.ones(self.get_dimensions(), dtype=int)).tolist()
666 self.__text_handles = (-np.ones(self.get_dimensions(), dtype=int)).tolist()
667 self.__text_handles_four = (-np.ones([self.get_dimensions()[0], self.get_dimensions()[1], 4], dtype=int)).tolist()
668 font_size = max(2, int(0.2 * self.__cell_size))
669 font_size_small = max(1, int(0.14 * self.__cell_size))
670 self.__font = FONT_FAMILY + " " + str(font_size)
671 self.__font_small = FONT_FAMILY + " " + str(font_size_small)
672 self.__line_size = max(1, int(self.__cell_size * LINE_SIZE_PERCENTAGE))
673 self.__drawn_lines = []
674 self.__changed_cells = None
675 for x in range(self.get_dimensions()[0]):
676 draw_num = DRAW_LABELS
677 if font_size == 1 and ((x % int(self.get_dimensions()[0] / 5)) != 0 and x != self.get_dimensions()[0] - 1):
678 draw_num = False
679 if draw_num:
680 self.__gui_canvas.create_text(self.__get_cell_center(x), (BORDER_SIZE + self.__cell_size) / 2,
681 text=str(x), font=self.__font)
682 self.__gui_canvas.create_text(self.__get_cell_center(x),
683 BORDER_SIZE + self.__cell_size * (self.get_dimensions()[1] + 1) + (
684 BORDER_SIZE + self.__cell_size) / 2, text=str(x), font=self.__font)
685 for y in range(self.get_dimensions()[1]):
686 draw_num = DRAW_LABELS
687 if font_size == 1 and ((y % int(self.get_dimensions()[1] / 5)) != 0 and y != self.get_dimensions()[1] - 1):
688 draw_num = False
689 if draw_num:
690 self.__gui_canvas.create_text((BORDER_SIZE + self.__cell_size) / 2, self.__get_cell_center(y),
691 text=str(y), font=self.__font)
692 self.__gui_canvas.create_text(BORDER_SIZE + self.__cell_size * (self.get_dimensions()[0] + 1) + (
693 BORDER_SIZE + self.__cell_size) / 2, self.__get_cell_center(y), text=str(y), font=self.__font)
694 box_size = (
695 int(self.__cell_size * self.get_dimensions()[0] + 2), int(self.__cell_size * self.get_dimensions()[1] + 2))
696 self.__gui_setup = True
697
699 '''
700 Safely destroy GUI. It is possible to pass an argument whether to unblock
701 L{find_path()<kuimaze.BaseAgent.find_path()>}
702 method, by default it is unblocking.
703
704 @param unblock: Whether to unblock L{find_path()<kuimaze.BaseAgent.find_path()>} method by calling this method
705 @type unblock: boolean
706 '''
707 if unblock:
708 self.__gui_lock = False
709 if self.__gui_root is not None:
710 self.__gui_root.update()
711 self.__gui_root.destroy()
712 self.__gui_root = None
713 self.show_level = SHOW.NONE
714 self.__gui_setup = False
715
717 '''
718 Renew GUI if a new player connects to a problem object.
719 '''
720
721 self.__has_triangles = False
722 self.show_level = self.__backup_show
723
725 '''
726 Just a simple callback for tkinter radiobuttons for selecting show level
727 '''
728 self.set_show_level(SHOW(self.__show_tkinter.get()))
729
731 '''
732 Clear path_section lines if running same player twice.
733 '''
734 if self.__gui_setup:
735 for line, _ in self.__drawn_lines:
736 self.__gui_canvas.delete(line)
737 self.__drawn_lines = []
738
740 '''
741 Set collor at position given by current position. Code inspired by old implementation of RPH Maze (predecessor of kuimaze)
742 @param current_node: state at which to set a color
743 @type current_node: L{namedtuple state<state>}
744 @param color: color string recognized by tkinter (see U{http://wiki.tcl.tk/37701})
745 @type color: string
746 '''
747 assert (self.__gui_setup)
748 x, y = current_node.x, current_node.y
749 if self.__color_handles[x][y] > 0:
750 if self.__gui_canvas.itemcget(self.__color_handles[x][y], "fill") is not color:
751 self.__gui_canvas.itemconfigure(self.__color_handles[x][y], fill=color)
752 else:
753 left = self.__get_cell_center(x) - self.__cell_size / 2
754 right = left + self.__cell_size
755 up = self.__get_cell_center(y) - self.__cell_size / 2
756 down = up + self.__cell_size
757 self.__color_handles[x][y] = self.__gui_canvas.create_rectangle(left, up, right, down, fill=color)
758
760 '''
761 Save canvas as color EPS - response for third button.
762 '''
763 self.set_eps_folder()
764 if not disabled:
765 self.__gui_canvas.postscript(file=self.__save_name, colormode="color")
766 self.__i += 1
767 else:
768 raise EnvironmentError('Maze must be rendered before saving to eps!')
769
771 '''
772 Mapping from problem coordinates to GUI coordinates.
773 @param x: x coord in problem
774 @param y: y coord in problem
775 @return: (x, y) coordinates in GUI (centers of cells)
776 '''
777 return self.__get_cell_center(x), self.__get_cell_center(y)
778
780 '''
781 Mapping from problem coordinate to GUI coordinate, only one coord.
782 @param x: coord in problem (could be either x or y)
783 @return: center of cell corresponding to such coordinate in GUI
784 '''
785 return BORDER_SIZE + self.__cell_size * (x + 1.5)
786
788 '''
789 Updating cell colors depending on what has been already explored.
790
791 @param explored_only: if True, update only explored position and leave unexplored black. if False, draw everything
792 @type explored_only: boolean
793 '''
794 assert (self.__gui_setup)
795
796 def get_cells():
797 dims = self.get_dimensions()
798 if self.__changed_cells is None:
799 for x in range(dims[0]):
800 for y in range(dims[1]):
801 yield x, y
802 else:
803 for item in self.__changed_cells:
804 yield item.x, item.y
805
806 for x, y in get_cells():
807 n = state(x, y)
808 if not self.__maze[x, y]:
809 self.__set_cell_color(n, self.__color_string_depth(WALL_COLOR, x, y))
810 else:
811 if self.is_goal_state(n) and not self.is_danger_state(n):
812 self.__set_cell_color(n, self.__color_string_depth(FINISH_COLOR, x, y))
813 if self.__explored[x, y]:
814 self.__set_cell_color(n, self.__color_string_depth(EXPLORED_COLOR, x, y))
815 else:
816 if self.__explored[x, y]:
817 self.__set_cell_color(n, self.__color_string_depth(EXPLORED_COLOR, x, y))
818 else:
819 if self.__seen[x, y]:
820 self.__set_cell_color(n, self.__color_string_depth(SEEN_COLOR, x, y))
821 else:
822 if explored_only:
823 self.__set_cell_color(n, self.__color_string_depth(WALL_COLOR, x, y))
824 else:
825 self.__set_cell_color(n, self.__color_string_depth(EMPTY_COLOR, x, y))
826 if n == self.__start:
827 self.__set_cell_color(n, self.__color_string_depth(START_COLOR, x, y))
828 if self.is_danger_state(n):
829 self.__set_cell_color(n, self.__color_string_depth(DANGER_COLOR, x, y))
830
832 '''
833 Update state rewards in GUI. If drawed_nodes is passed and is not None, it is expected to be list of lists of objects with string representation of same dimensions as the problem. Might fail on IndexError if passed list is smaller.
834 if one of these objects in list is None, then no text is printed.
835
836 If drawed_nodes is None, then node_rewards saved in Maze objects are printed instead
837
838 @param drawed_nodes: list of lists of objects to be printed in GUI instead of state rewards
839 @type drawed_nodes: list of lists of appropriate dimensions or None
840 @raise IndexError: if drawed_nodes parameter doesn't match dimensions of problem
841 '''
842 dims = self.get_dimensions()
843
844 def get_cells():
845 for x in range(dims[0]):
846 for y in range(dims[1]):
847 yield x, y
848
849 if dictionary is None:
850 for x, y in get_cells():
851 if self.__maze[x, y]:
852 n = state(x, y)
853 vector = (n.x - self.__start.x, n.y - self.__start.y)
854 ret = self.__grad[0] * vector[0] + self.__grad[1] * vector[1]
855 self.__draw_text(n, format(ret, '.2f'))
856 return
857
858 assert type(dictionary[0]) == dict, "ERROR: Visualisation input must be dictionary"
859
860 if type(dictionary[0]['value']) == tuple or type(dictionary[0]['value']) == list:
861 assert len(dictionary[0]['value']) == 4, "ERROR: When visualising list or tuple, length must be 4!"
862 if not self.__has_triangles:
863
864 for x, y in get_cells():
865 if self.__maze[x, y]:
866 center = self.__get_cell_center_coords(x, y)
867 size = int(self.__cell_size/2)
868 point1 = [center[0] - size, center[1] - size]
869 point2 = [center[0] + size, center[1] + size]
870 point3 = [center[0] + size, center[1] - size]
871 point4 = [center[0] - size, center[1] + size]
872 self.__gui_canvas.create_line(point1[0], point1[1], point2[0], point2[1], width=1.4)
873 self.__gui_canvas.create_line(point3[0], point3[1], point4[0], point4[1], width=1.4)
874 self.__has_triangles = True
875 for element in dictionary:
876 x = element['x']
877 y = element['y']
878 if self.__maze[x, y]:
879 n = state(x, y)
880 index = y * dims[0] + x
881
882 self.__draw_text_four(n, element['value'])
883 return
884
885
886 if True:
887 for element in dictionary:
888 x = element['x']
889 y = element['y']
890 if self.__maze[x, y]:
891 n = state(x, y)
892 index = y * dims[0] + x
893
894 try:
895 string_to_print = format(element['value'], '.2f')
896 except:
897 string_to_print = str(element['value'])
898 self.__draw_text(n, string_to_print)
899
900
901 - def __draw_text(self, current_node, string):
902 '''
903 Draw text in the center of cells in the same manner as draw colors is done.
904
905 @param current_node: position on which the text is to be printed in Maze coordinates
906 @type current_node: L{namedtuple state<state>}
907 @param string: string to be drawn
908 @type string: string
909 '''
910
911 x, y = current_node.x, current_node.y
912 assert self.__gui_setup
913 if self.__text_handles[x][y] > 0:
914 if self.__gui_canvas.itemcget(self.__text_handles[x][y], "text") != string:
915 self.__gui_canvas.itemconfigure(self.__text_handles[x][y], text=string)
916 else:
917 self.__text_handles[x][y] = self.__gui_canvas.create_text(*self.__get_cell_center_coords(x, y), text=string,
918 font=self.__font)
919
920 - def __text_to_top(self):
921 '''
922 Move text fields to the top layer of the canvas - to cover arrow
923 :return:
924 '''
925 if self.__has_triangles:
926 for x in range(self.get_dimensions()[0]):
927 for y in range(self.get_dimensions()[1]):
928 for i in range(4):
929 if self.__text_handles_four[x][y][i] > 0:
930 self.__gui_canvas.tag_raise(self.__text_handles_four[x][y][i])
931 else:
932 for x in range(self.get_dimensions()[0]):
933 for y in range(self.get_dimensions()[1]):
934 if self.__text_handles[x][y] > 0:
935 self.__gui_canvas.tag_raise(self.__text_handles[x][y])
936
937 - def __draw_text_four(self, current_node, my_list):
938 '''
939 Draw four text cells into one square
940
941 @param current_node: position on which the text is to be printed in Maze coordinates
942 @param my_list: list to be drawn
943 @type my_list: list of floats or ints
944 '''
945
946 x, y = current_node.x, current_node.y
947 format_string = '.2f'
948 assert self.__gui_setup
949 for i in range(4):
950 if self.__text_handles_four[x][y][i] > 0:
951 if self.__gui_canvas.itemcget(self.__text_handles_four[x][y][i], "text") != format(my_list[i], format_string):
952 self.__gui_canvas.itemconfigure(self.__text_handles_four[x][y][i], text=format(my_list[i], format_string))
953 else:
954 center = self.__get_cell_center_coords(x, y)
955 size = self.__cell_size/2
956 if i == 0:
957 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0], center[1] - int(0.7*size)],
958 text=format(my_list[i], format_string), font=self.__font_small)
959 elif i == 1:
960 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0] + int(0.565*size), center[1]],
961 text=format(my_list[i], format_string), font=self.__font_small)
962 elif i == 2:
963 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0], center[1] + int(0.7*size)],
964 text=format(my_list[i], format_string), font=self.__font_small)
965 elif i == 3:
966 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0] - int(0.565*size), center[1]],
967 text=format(my_list[i], format_string), font=self.__font_small)
968
970 '''
971 Method adjust color due to depth of square in maze
972 :param color: color string in hexadecimal ... for example "#FFF000000" for red
973 :param x: index of square
974 :param y: index of square
975 :return: new color string
976 '''
977 assert len(color) == 10
978 rgb = [int(color[1:4], 16), int(color[4:7], 16), int(color[7:10], 16)]
979 tmp = self.__koef * (x * self.__grad[0] + y * self.__grad[1] + self.__offset)
980 strings = []
981 for i in range(3):
982 rgb[i] = rgb[i] - abs(int(tmp) - self.__max_minus)
983 if rgb[i] < 0:
984 rgb[i] = 0
985 strings.append(hex(rgb[i])[2:])
986 for i in range(3):
987 while len(strings[i]) < 3:
988 strings[i] = "0" + strings[i]
989 ret = "#" + strings[0] + strings[1] + strings[2]
990 return ret
991
993 '''
994 Sets data needed for rendering 3D ilusion
995 :return: None
996 '''
997 self.__max_minus = 2048
998 lt = 0
999 lb = self.get_dimensions()[1] * self.__grad[1]
1000 rt = self.get_dimensions()[0] * self.__grad[0]
1001 rb = self.get_dimensions()[0] * self.__grad[0] + self.get_dimensions()[1] * self.__grad[1]
1002 tmp = [lt, lb, rt, rb]
1003 maxi = max(tmp)
1004 mini = min(tmp)
1005 self.__offset = 0 - mini
1006 if self.__grad[0] != 0 or self.__grad[1] != 0:
1007 self.__koef = self.__max_minus / (maxi - mini)
1008 else:
1009 self.__koef = 0
1010 self.__max_minus = 0
1011