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