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