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