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_probs = []
437 for out_action in ACTION:
438 next_state = self.result(curr, out_action.value)
439 states_probs.append((next_state, self.__trans_probs[action, out_action]))
440 return states_probs
441
443 '''
444 sets explored states list, preparation for visualisation
445 @param states: iterable of L{state<state>}
446 '''
447 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
448 for state in states:
449 self.__explored[state.x, state.y] = True
450 if self.__changed_cells is not None:
451 self.__changed_cells.append(state)
452
453 - def set_probs(self, obey, confusionL, confusionR, confusion180):
454 self.__trans_probs.set_probs(obey, confusionL, confusionR, confusion180)
455
457 self.__trans_probs = ActionProbsTable(obey, confusionL, confusionR, confusion180)
458
460 '''
461 sets seen states list, preparation for visualisation
462 @param states: iterable of L{state<state>}
463 '''
464 for state in states:
465 self.__seen[state.x, state.y] = True
466 if self.__changed_cells is not None:
467 self.__changed_cells.append(state)
468
470 real_action = self.__trans_probs.confuse_action(action)
471 return real_action
472
474 '''
475 Check whether a state is inside a problem
476 @param current_state: state to check
477 @type current_state: L{namedtuple state<state>}
478 @return: True if state is inside problem, False otherwise
479 @rtype: boolean
480 '''
481 dims = self.get_dimensions()
482 return current_state.x >= 0 and current_state.y >= 0 and current_state.x < dims[0] and current_state.y < dims[1]
483
485 '''
486 Check whether a state is inside a problem and is not a wall
487 @param current_state: state to check
488 @type current_state: L{namedtuple state<state>}
489 @return: True if state is inside problem and is not a wall, False otherwise
490 @rtype: boolean
491 '''
492 return self.__is_inside(current_state) and self.__maze[current_state.x, current_state.y]
493
495 '''
496 Clear player data for using with different player or running another find_path
497 '''
498 self.__seen = np.zeros(self.__maze.shape, dtype=bool)
499 self.__seen[self.__start.x, self.__start.y] = True
500 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
501 self.__explored[self.__start.x, self.__start.y] = True
502 self.__i = 0
503 self.__running_find = False
504 self.__renew_gui()
505 self.__changed_cells = None
506
507 self.__clear_lines()
508
510 '''
511 Clear player data for using with different player or running another find_path
512 '''
513 self.__seen = np.zeros(self.__maze.shape, dtype=bool)
514 self.__seen[self.__start.x, self.__start.y] = True
515 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
516 self.__explored[self.__start.x, self.__start.y] = True
517 self.__i = 0
518 self.__running_find = False
519
521 '''
522 Set player associated with this problem.
523 @param player: player to be used for association
524 @type player: L{BaseAgent<kuimaze.BaseAgent>} or its descendant
525 @raise AssertionError: if player is not instance of L{BaseAgent<kuimaze.BaseAgent>} or its descendant
526 '''
527 assert (isinstance(player, kuimaze.baseagent.BaseAgent))
528 self.__player = player
529 self.__clear_player_data()
530
531
532 '''
533 if self.__gui_root is not None:
534 self.__gui_root.mainloop()
535 '''
536
538 '''
539 Main GUI function - call this from L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>} to update GUI and
540 break at this point to be able to step your actions.
541 Example of its usage can be found at L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>}
542
543 Don't use it too often as it is quite expensive and rendering after single exploration might be slowing your
544 code down a lot.
545
546 You can optionally set parameter C{drawed_nodes} to a list of lists of dimensions corresponding to dimensions of
547 problem and if show_level is higher or equal to L{SHOW.NODE_REWARDS}, it will plot those in state centers
548 instead of state rewards.
549 If this parameter is left unset, no redrawing of texts in center of nodes is issued, however, it can be set to
550 True which will draw node_rewards saved in the problem.
551
552 If show_level is L{SHOW.NONE}, thisets function has no effect
553
554 @param drawed_nodes: custom objects convertible to string to draw to center of nodes or True or None
555 @type drawed_nodes: list of lists of the same dimensions as problem or boolean or None
556 '''
557 assert (self.__player is not None)
558 if self.show_level is not SHOW.NONE:
559 first_run = False
560 if not self.__gui_setup:
561 self.__setup_gui()
562 first_run = True
563 if self.show_level.value >= SHOW.FULL_MAZE.value:
564 self.__gui_update_map(explored_only=False)
565 else:
566 if self.show_level.value == SHOW.EXPLORED.value:
567 self.__gui_update_map(explored_only=True)
568 if first_run:
569
570
571 first_run = False
572 if not self.__till_end and self.__running_find:
573 self.__gui_lock = True
574 self.__changed_cells = []
575 self.__gui_canvas.update()
576 '''
577 while self.__gui_lock:
578 time.sleep(0.01)
579 self.__gui_root.update()
580 '''
581
583 '''
584 Show resulting path_section given as a list of consecutive L{namedtuples path_section<path_section>} to show in GUI.
585 Example of such usage can be found in L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>}
586
587 @param full_path: path_section in a form of list of consecutive L{namedtuples path_section<path_section>}
588 @type full_path: list of consecutive L{namedtuples path_section<path_section>}
589 '''
590 if self.show_level is not SHOW.NONE and len(full_path) is not 0:
591 def coord_gen(paths):
592 paths.append(path_section(paths[-1].state_to, None, None, None))
593 for item in paths:
594 for j in range(2):
595 num = item.state_from.x if j == 0 else item.state_from.y
596 yield (num + 1.5) * self.__cell_size + BORDER_SIZE
597 size = int(self.__cell_size/3)
598 coords = list(coord_gen(full_path))
599 full_path = full_path[:-1]
600 self.__drawn_lines.append((self.__gui_canvas.create_line(
601 *coords, width=self.__line_size, capstyle='round', fill=LINE_COLOR,
602 arrow=tkinter.LAST, arrowshape=(size, size, int(size/2.5))), coords))
603 self.__text_to_top()
604
606 '''
607 Set new show level. It will redraw whole GUI, so it takes a while.
608 @param show_level: new show_level to set
609 @type show_level: L{SHOW}
610 @raise AssertionError: if show_level is not an instance of L{SHOW}
611 '''
612 assert (isinstance(show_level, SHOW))
613 self.__backup_show = show_level
614 self.__changed_cells = None
615 if self.show_level is not show_level:
616 self.__destroy_gui(unblock=False)
617 self.show_level = show_level
618 if self.show_level is SHOW.NONE:
619 self.__gui_lock = False
620 self.__show_tkinter.set(show_level.value)
621 coords = [c for i, c in self.__drawn_lines]
622 self.show_and_break()
623 if self.show_level is not SHOW.NONE:
624 self.__drawn_lines = []
625 for coord in coords:
626 self.__drawn_lines.append((self.__gui_canvas.create_line(
627 *coord, width=self.__line_size, capstyle='round', fill=LINE_COLOR), coord))
628
630 '''
631 Set folder where the EPS files will be saved.
632 @param folder: folder to save EPS files
633 @type folder: string with a valid path_section
634 '''
635 folder = os.path.join(os.path.dirname(os.path.dirname(sys.argv[0])))
636 self.__save_name = os.path.join(folder, "%04d.eps" % (self.__i,))
637
639 '''
640 Setup and draw basic GUI. Imports tkinter.
641 '''
642 self.__gui_root = tkinter.Tk()
643 self.__gui_root.title('KUI - Maze')
644 self.__gui_root.protocol('WM_DELETE_WINDOW', self.__destroy_gui)
645 self.__gui_root.resizable(0, 0)
646 w = (self.__gui_root.winfo_screenwidth() / (self.get_dimensions()[0] + 2)) * MAX_WINDOW_PERCENTAGE
647 h = (self.__gui_root.winfo_screenheight() / (self.get_dimensions()[1] + 2)) * MAX_WINDOW_PERCENTAGE
648 use_font = FONT_FAMILY + str(FONT_SIZE)
649 self.__cell_size = min(w, h, MAX_CELL_SIZE)
650 self.__show_tkinter = tkinter.IntVar()
651 self.__show_tkinter.set(self.show_level)
652 top_frame = tkinter.Frame(self.__gui_root)
653 top_frame.pack(expand=False, side=tkinter.TOP)
654 width_pixels = (self.__cell_size * (self.get_dimensions()[0] + 2) + 2 * BORDER_SIZE)
655 height_pixels = (self.__cell_size * (self.get_dimensions()[1] + 2) + 2 * BORDER_SIZE)
656 self.__gui_canvas = tkinter.Canvas(top_frame, width=width_pixels, height=height_pixels)
657 self.__gui_canvas.pack(expand=False, side=tkinter.LEFT)
658 self.__color_handles = (-np.ones(self.get_dimensions(), dtype=int)).tolist()
659 self.__text_handles = (-np.ones(self.get_dimensions(), dtype=int)).tolist()
660 self.__text_handles_four = (-np.ones([self.get_dimensions()[0], self.get_dimensions()[1], 4], dtype=int)).tolist()
661 font_size = max(2, int(0.2 * self.__cell_size))
662 font_size_small = max(1, int(0.14 * self.__cell_size))
663 self.__font = FONT_FAMILY + " " + str(font_size)
664 self.__font_small = FONT_FAMILY + " " + str(font_size_small)
665 self.__line_size = max(1, int(self.__cell_size * LINE_SIZE_PERCENTAGE))
666 self.__drawn_lines = []
667 self.__changed_cells = None
668 for x in range(self.get_dimensions()[0]):
669 draw_num = DRAW_LABELS
670 if font_size == 1 and ((x % int(self.get_dimensions()[0] / 5)) != 0 and x != self.get_dimensions()[0] - 1):
671 draw_num = False
672 if draw_num:
673 self.__gui_canvas.create_text(self.__get_cell_center(x), (BORDER_SIZE + self.__cell_size) / 2,
674 text=str(x), font=self.__font)
675 self.__gui_canvas.create_text(self.__get_cell_center(x),
676 BORDER_SIZE + self.__cell_size * (self.get_dimensions()[1] + 1) + (
677 BORDER_SIZE + self.__cell_size) / 2, text=str(x), font=self.__font)
678 for y in range(self.get_dimensions()[1]):
679 draw_num = DRAW_LABELS
680 if font_size == 1 and ((y % int(self.get_dimensions()[1] / 5)) != 0 and y != self.get_dimensions()[1] - 1):
681 draw_num = False
682 if draw_num:
683 self.__gui_canvas.create_text((BORDER_SIZE + self.__cell_size) / 2, self.__get_cell_center(y),
684 text=str(y), font=self.__font)
685 self.__gui_canvas.create_text(BORDER_SIZE + self.__cell_size * (self.get_dimensions()[0] + 1) + (
686 BORDER_SIZE + self.__cell_size) / 2, self.__get_cell_center(y), text=str(y), font=self.__font)
687 box_size = (
688 int(self.__cell_size * self.get_dimensions()[0] + 2), int(self.__cell_size * self.get_dimensions()[1] + 2))
689 self.__gui_setup = True
690
692 '''
693 Safely destroy GUI. It is possible to pass an argument whether to unblock
694 L{find_path()<kuimaze.BaseAgent.find_path()>}
695 method, by default it is unblocking.
696
697 @param unblock: Whether to unblock L{find_path()<kuimaze.BaseAgent.find_path()>} method by calling this method
698 @type unblock: boolean
699 '''
700 if unblock:
701 self.__gui_lock = False
702 if self.__gui_root is not None:
703 self.__gui_root.update()
704 self.__gui_root.destroy()
705 self.__gui_root = None
706 self.show_level = SHOW.NONE
707 self.__gui_setup = False
708
710 '''
711 Renew GUI if a new player connects to a problem object.
712 '''
713
714 self.__has_triangles = False
715 self.show_level = self.__backup_show
716
718 '''
719 Just a simple callback for tkinter radiobuttons for selecting show level
720 '''
721 self.set_show_level(SHOW(self.__show_tkinter.get()))
722
724 '''
725 Clear path_section lines if running same player twice.
726 '''
727 if self.__gui_setup:
728 for line, _ in self.__drawn_lines:
729 self.__gui_canvas.delete(line)
730 self.__drawn_lines = []
731
733 '''
734 Set collor at position given by current position. Code inspired by old implementation of RPH Maze (predecessor of kuimaze)
735 @param current_node: state at which to set a color
736 @type current_node: L{namedtuple state<state>}
737 @param color: color string recognized by tkinter (see U{http://wiki.tcl.tk/37701})
738 @type color: string
739 '''
740 assert (self.__gui_setup)
741 x, y = current_node.x, current_node.y
742 if self.__color_handles[x][y] > 0:
743 if self.__gui_canvas.itemcget(self.__color_handles[x][y], "fill") is not color:
744 self.__gui_canvas.itemconfigure(self.__color_handles[x][y], fill=color)
745 else:
746 left = self.__get_cell_center(x) - self.__cell_size / 2
747 right = left + self.__cell_size
748 up = self.__get_cell_center(y) - self.__cell_size / 2
749 down = up + self.__cell_size
750 self.__color_handles[x][y] = self.__gui_canvas.create_rectangle(left, up, right, down, fill=color)
751
753 '''
754 Save canvas as color EPS - response for third button.
755 '''
756 self.set_eps_folder()
757 if not disabled:
758 self.__gui_canvas.postscript(file=self.__save_name, colormode="color")
759 self.__i += 1
760 else:
761 raise EnvironmentError('Maze must be rendered before saving to eps!')
762
764 '''
765 Mapping from problem coordinates to GUI coordinates.
766 @param x: x coord in problem
767 @param y: y coord in problem
768 @return: (x, y) coordinates in GUI (centers of cells)
769 '''
770 return self.__get_cell_center(x), self.__get_cell_center(y)
771
773 '''
774 Mapping from problem coordinate to GUI coordinate, only one coord.
775 @param x: coord in problem (could be either x or y)
776 @return: center of cell corresponding to such coordinate in GUI
777 '''
778 return BORDER_SIZE + self.__cell_size * (x + 1.5)
779
781 '''
782 Updating cell colors depending on what has been already explored.
783
784 @param explored_only: if True, update only explored position and leave unexplored black. if False, draw everything
785 @type explored_only: boolean
786 '''
787 assert (self.__gui_setup)
788
789 def get_cells():
790 dims = self.get_dimensions()
791 if self.__changed_cells is None:
792 for x in range(dims[0]):
793 for y in range(dims[1]):
794 yield x, y
795 else:
796 for item in self.__changed_cells:
797 yield item.x, item.y
798
799 for x, y in get_cells():
800 n = state(x, y)
801 if not self.__maze[x, y]:
802 self.__set_cell_color(n, self.__color_string_depth(WALL_COLOR, x, y))
803 else:
804 if self.is_goal_state(n) and not self.is_danger_state(n):
805 self.__set_cell_color(n, self.__color_string_depth(FINISH_COLOR, x, y))
806 if self.__explored[x, y]:
807 self.__set_cell_color(n, self.__color_string_depth(EXPLORED_COLOR, x, y))
808 else:
809 if self.__explored[x, y]:
810 self.__set_cell_color(n, self.__color_string_depth(EXPLORED_COLOR, x, y))
811 else:
812 if self.__seen[x, y]:
813 self.__set_cell_color(n, self.__color_string_depth(SEEN_COLOR, x, y))
814 else:
815 if explored_only:
816 self.__set_cell_color(n, self.__color_string_depth(WALL_COLOR, x, y))
817 else:
818 self.__set_cell_color(n, self.__color_string_depth(EMPTY_COLOR, x, y))
819 if n == self.__start:
820 self.__set_cell_color(n, self.__color_string_depth(START_COLOR, x, y))
821 if self.is_danger_state(n):
822 self.__set_cell_color(n, self.__color_string_depth(DANGER_COLOR, x, y))
823
825 '''
826 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.
827 if one of these objects in list is None, then no text is printed.
828
829 If drawed_nodes is None, then node_rewards saved in Maze objects are printed instead
830
831 @param drawed_nodes: list of lists of objects to be printed in GUI instead of state rewards
832 @type drawed_nodes: list of lists of appropriate dimensions or None
833 @raise IndexError: if drawed_nodes parameter doesn't match dimensions of problem
834 '''
835 dims = self.get_dimensions()
836
837 def get_cells():
838 for x in range(dims[0]):
839 for y in range(dims[1]):
840 yield x, y
841
842 if dictionary is None:
843 for x, y in get_cells():
844 if self.__maze[x, y]:
845 n = state(x, y)
846 vector = (n.x - self.__start.x, n.y - self.__start.y)
847 ret = self.__grad[0] * vector[0] + self.__grad[1] * vector[1]
848 self.__draw_text(n, format(ret, '.2f'))
849 return
850
851 assert type(dictionary[0]) == dict, "ERROR: Visualisation input must be dictionary"
852
853 if type(dictionary[0]['value']) == tuple or type(dictionary[0]['value']) == list:
854 assert len(dictionary[0]['value']) == 4, "ERROR: When visualising list or tuple, length must be 4!"
855 if not self.__has_triangles:
856
857 for x, y in get_cells():
858 if self.__maze[x, y]:
859 center = self.__get_cell_center_coords(x, y)
860 size = int(self.__cell_size/2)
861 point1 = [center[0] - size, center[1] - size]
862 point2 = [center[0] + size, center[1] + size]
863 point3 = [center[0] + size, center[1] - size]
864 point4 = [center[0] - size, center[1] + size]
865 self.__gui_canvas.create_line(point1[0], point1[1], point2[0], point2[1], width=1.4)
866 self.__gui_canvas.create_line(point3[0], point3[1], point4[0], point4[1], width=1.4)
867 self.__has_triangles = True
868 for element in dictionary:
869 x = element['x']
870 y = element['y']
871 if self.__maze[x, y]:
872 n = state(x, y)
873 index = y * dims[0] + x
874
875 self.__draw_text_four(n, element['value'])
876 return
877
878
879 if True:
880 for element in dictionary:
881 x = element['x']
882 y = element['y']
883 if self.__maze[x, y]:
884 n = state(x, y)
885 index = y * dims[0] + x
886
887 try:
888 string_to_print = format(element['value'], '.2f')
889 except:
890 string_to_print = str(element['value'])
891 self.__draw_text(n, string_to_print)
892
893
894 - def __draw_text(self, current_node, string):
895 '''
896 Draw text in the center of cells in the same manner as draw colors is done.
897
898 @param current_node: position on which the text is to be printed in Maze coordinates
899 @type current_node: L{namedtuple state<state>}
900 @param string: string to be drawn
901 @type string: string
902 '''
903
904 x, y = current_node.x, current_node.y
905 assert self.__gui_setup
906 if self.__text_handles[x][y] > 0:
907 if self.__gui_canvas.itemcget(self.__text_handles[x][y], "text") != string:
908 self.__gui_canvas.itemconfigure(self.__text_handles[x][y], text=string)
909 else:
910 self.__text_handles[x][y] = self.__gui_canvas.create_text(*self.__get_cell_center_coords(x, y), text=string,
911 font=self.__font)
912
913 - def __text_to_top(self):
914 '''
915 Move text fields to the top layer of the canvas - to cover arrow
916 :return:
917 '''
918 if self.__has_triangles:
919 for x in range(self.get_dimensions()[0]):
920 for y in range(self.get_dimensions()[1]):
921 for i in range(4):
922 if self.__text_handles_four[x][y][i] > 0:
923 self.__gui_canvas.tag_raise(self.__text_handles_four[x][y][i])
924 else:
925 for x in range(self.get_dimensions()[0]):
926 for y in range(self.get_dimensions()[1]):
927 if self.__text_handles[x][y] > 0:
928 self.__gui_canvas.tag_raise(self.__text_handles[x][y])
929
930 - def __draw_text_four(self, current_node, my_list):
931 '''
932 Draw four text cells into one square
933
934 @param current_node: position on which the text is to be printed in Maze coordinates
935 @param my_list: list to be drawn
936 @type my_list: list of floats or ints
937 '''
938
939 x, y = current_node.x, current_node.y
940 format_string = '.2f'
941 assert self.__gui_setup
942 for i in range(4):
943 if self.__text_handles_four[x][y][i] > 0:
944 if self.__gui_canvas.itemcget(self.__text_handles_four[x][y][i], "text") != format(my_list[i], format_string):
945 self.__gui_canvas.itemconfigure(self.__text_handles_four[x][y][i], text=format(my_list[i], format_string))
946 else:
947 center = self.__get_cell_center_coords(x, y)
948 size = self.__cell_size/2
949 if i == 0:
950 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0], center[1] - int(0.7*size)],
951 text=format(my_list[i], format_string), font=self.__font_small)
952 elif i == 1:
953 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0] + int(0.565*size), center[1]],
954 text=format(my_list[i], format_string), font=self.__font_small)
955 elif i == 2:
956 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0], center[1] + int(0.7*size)],
957 text=format(my_list[i], format_string), font=self.__font_small)
958 elif i == 3:
959 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0] - int(0.565*size), center[1]],
960 text=format(my_list[i], format_string), font=self.__font_small)
961
963 '''
964 Method adjust color due to depth of square in maze
965 :param color: color string in hexadecimal ... for example "#FFF000000" for red
966 :param x: index of square
967 :param y: index of square
968 :return: new color string
969 '''
970 assert len(color) == 10
971 rgb = [int(color[1:4], 16), int(color[4:7], 16), int(color[7:10], 16)]
972 tmp = self.__koef * (x * self.__grad[0] + y * self.__grad[1] + self.__offset)
973 strings = []
974 for i in range(3):
975 rgb[i] = rgb[i] - abs(int(tmp) - self.__max_minus)
976 if rgb[i] < 0:
977 rgb[i] = 0
978 strings.append(hex(rgb[i])[2:])
979 for i in range(3):
980 while len(strings[i]) < 3:
981 strings[i] = "0" + strings[i]
982 ret = "#" + strings[0] + strings[1] + strings[2]
983 return ret
984
986 '''
987 Sets data needed for rendering 3D ilusion
988 :return: None
989 '''
990 self.__max_minus = 2048
991 lt = 0
992 lb = self.get_dimensions()[1] * self.__grad[1]
993 rt = self.get_dimensions()[0] * self.__grad[0]
994 rb = self.get_dimensions()[0] * self.__grad[0] + self.get_dimensions()[1] * self.__grad[1]
995 tmp = [lt, lb, rt, rb]
996 maxi = max(tmp)
997 mini = min(tmp)
998 self.__offset = 0 - mini
999 if self.__grad[0] != 0 or self.__grad[1] != 0:
1000 self.__koef = self.__max_minus / (maxi - mini)
1001 else:
1002 self.__koef = 0
1003 self.__max_minus = 0
1004