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.02
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 REWARD_NORMAL = -0.04
66 REWARD_DANGER = -1
67 REWARD_GOAL = 1
68
69 -class SHOW(enum.Enum):
70 '''
71 Enum class used for storing what is displayed in GUI - everything higher includes everything lower (except NONE, of course).
72 So if SHOW is NODE_REWARDS, it automatically means, that it will display FULL_MAZE (and EXPLORED), however it won't display ACTION_COSTS
73 '''
74 NONE = 0
75 EXPLORED = 1
76 FULL_MAZE = 2
77
78
80 '''
81 Enum class to represent actions in a grid-world.
82 '''
83 UP = 0
84 RIGHT = 1
85 DOWN = 2
86 LEFT = 3
87
97
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]]
182 __ACTIONS = [ACTION.UP, ACTION.RIGHT, ACTION.DOWN, ACTION.LEFT]
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 finish.append(state(x,y))
259 self.__finish = frozenset(finish)
260
261 if start_node is not None:
262 if self.__is_inside_valid(start_node):
263 if self.__start is not None:
264 warnings.warn('Replacing start state as there could be only one!')
265 self.__start = state(start_node.x, start_node.y)
266
267 if goal_nodes is not None:
268 finish = list(self.__finish)
269 warnings.warn('Adding to list of goal nodes!')
270 for point in goal_nodes:
271 if self.__is_inside_valid(point):
272 finish.append(point)
273 self.__finish = frozenset(finish)
274
275 if node_rewards is not None:
276 if isinstance(node_rewards, str):
277 node_rewards = np.load(node_rewards)
278 else:
279 node_rewards = np.array(node_rewards)
280 node_rewards = np.transpose(node_rewards)
281 print(node_rewards.shape, self.__maze.shape)
282 if node_rewards.shape == self.__maze.shape:
283 self.__node_rewards = node_rewards
284 print(self.__node_rewards)
285
286 if self.__node_rewards is None:
287 self.__node_rewards = np.zeros(self.__maze.shape, dtype=float)
288 for y, col in enumerate(maze):
289 for x, cell in enumerate(col):
290 pos = state(x,y)
291 self.__node_rewards[x, y] = REWARD_NORMAL
292 if pos in self.__finish:
293 self.__node_rewards[x,y] = REWARD_GOAL
294 if pos in self.hard_places:
295 self.__node_rewards[x,y] = REWARD_DANGER
296 print(self.__node_rewards)
297
298 if self.__node_utils is None:
299 self.__node_utils = np.zeros(self.__maze.shape, dtype=float)
300
301 if path_costs is not None:
302 if isinstance(path_costs, str):
303 path_costs = np.load(path_costs)
304 if path_costs.shape == (self.__maze.shape[0], self.__maze.shape[1], 2):
305 self.__path_costs = path_costs
306 if self.__path_costs is None:
307 self.__path_costs = np.ones((self.__maze.shape[0], self.__maze.shape[1], 2), dtype=int)
308
309 if trans_probs is not None:
310 self.__trans_probs = ProbsRoulette()
311 if self.__trans_probs is None:
312 self.__trans_probs = ProbsRoulette(0.8, 0.1, 0.1, 0)
313
314 assert (isinstance(show_level, SHOW))
315 self.show_level = show_level
316 self.__backup_show = show_level
317 self.__clear_player_data()
318
319 assert (self.__start is not None)
320 assert (self.__finish is not None)
321 assert (self.__node_rewards is not None)
322 assert (self.__path_costs is not None)
323 assert (self.__trans_probs is not None)
324 print('maze init done')
325
328
330 '''
331 Returns a start state
332 @return: start state
333 @rtype: L{namedtuple state<state>}
334 '''
335 return self.__start
336
339
341 '''
342 a visualisation method - sets an interal variable for displaying utilities
343 @param utils: dictionary of utilities, indexed by tuple - state coordinates
344 @return: None
345 '''
346 for position in utils.keys():
347 self.__node_utils[position] = utils[position]
348
350 '''
351 Check whether a C{current_node} is goal state or not
352 @param current_state: state to check.
353 @type current_state: L{namedtuple state<state>}
354 @return: True if state is a goal state, False otherwise
355 @rtype: boolean
356 '''
357 return state(current_state.x, current_state.y) in self.__finish
358
360 return state(current_state.x, current_state.y) in self.hard_places
361
363 '''
364 Returns a list of goal nodes
365 @return: list of goal nodes
366 @rtype: list
367 '''
368 return list(self.__finish)
369
371 '''
372 Returns a list of all the problem states
373 @return: list of all states
374 @rtype: list of L{namedtuple weighted_state<weighted_state>}
375 '''
376 dims = self.get_dimensions()
377 states = []
378 for x in range(dims[0]):
379 for y in range(dims[1]):
380 if self.__maze[x, y]:
381 states.append(weighted_state(x, y, self.__node_rewards[x, y]))
382 return states
383
385 '''
386 Returns dimensions of problem
387 @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.
388 @rtype: tuple
389 '''
390 return self.__maze.shape
391
393 '''
394 Generate (yield) actions possible for the current_state
395 It does not check the outcome this is left to the result method
396 @param current_state:
397 @return: action (relevant for the problem - problem in this case)
398 @rtype: L{action from ACTION<ACTION>}
399 '''
400 for action in ACTION:
401 yield action
402
403 - def result(self, current_state, action):
404 '''
405 Apply the action and get the state; deterministic version
406 @param current_state: state L{namedtuple state<state>}
407 @param action: L{action from ACTION<ACTION>}
408 @return: state (result of the action applied at the current_state)
409 @rtype: L{namedtuple state<state>}
410 '''
411 x, y = self.__deltas[action]
412 nx = current_state.x + x
413 ny = current_state.y + y
414 if self.__is_inside(state(nx, ny)) and self.__maze[nx, ny]:
415 nstate = weighted_state(nx, ny, self.__node_rewards[nx, ny])
416 else:
417 nstate = weighted_state(current_state.x, current_state.y,
418 self.__node_rewards[current_state.x, current_state.y])
419
420 return state(nstate.x, nstate.y)
421
423 '''
424 For the commanded action it generates all posiible outcomes with associated probabilities
425 @param state: state L{namedtuple state<state>}
426 @param action: L{action from ACTION<ACTION>}
427 @return: list of tuples (next_state, probability_of_ending_in_the_next_state)
428 @rtype: list of tuples
429 '''
430 states_probs = []
431 for out_action in ACTION:
432 next_state = self.result(curr, out_action.value)
433 states_probs.append((next_state, self.__trans_probs[action, out_action]))
434 return states_probs
435
437 '''
438 sets explored states list, preparation for visualisation
439 @param states: iterable of L{state<state>}
440 '''
441 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
442 for state in states:
443 self.__explored[state.x, state.y] = True
444 if self.__changed_cells is not None:
445 self.__changed_cells.append(state)
446
447 - def set_probs(self, obey, confusionL, confusionR, confusion180):
448 self.__trans_probs.set_probs(obey, confusionL, confusionR, confusion180)
449
451 self.__trans_probs = ActionProbsTable(obey, confusionL, confusionR, confusion180)
452
454 '''
455 sets seen states list, preparation for visualisation
456 @param states: iterable of L{state<state>}
457 '''
458 for state in states:
459 self.__seen[state.x, state.y] = True
460 if self.__changed_cells is not None:
461 self.__changed_cells.append(state)
462
464 real_action = self.__trans_probs.confuse_action(action)
465 return real_action
466
468 '''
469 Check whether a state is inside a problem
470 @param current_state: state to check
471 @type current_state: L{namedtuple state<state>}
472 @return: True if state is inside problem, False otherwise
473 @rtype: boolean
474 '''
475 dims = self.get_dimensions()
476 return current_state.x >= 0 and current_state.y >= 0 and current_state.x < dims[0] and current_state.y < dims[1]
477
479 '''
480 Check whether a state is inside a problem and is not a wall
481 @param current_state: state to check
482 @type current_state: L{namedtuple state<state>}
483 @return: True if state is inside problem and is not a wall, False otherwise
484 @rtype: boolean
485 '''
486 return self.__is_inside(current_state) and self.__maze[current_state.x, current_state.y]
487
489 '''
490 Clear player data for using with different player or running another find_path
491 '''
492 self.__seen = np.zeros(self.__maze.shape, dtype=bool)
493 self.__seen[self.__start.x, self.__start.y] = True
494 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
495 self.__explored[self.__start.x, self.__start.y] = True
496 self.__i = 0
497 self.__running_find = False
498 self.__renew_gui()
499 self.__changed_cells = None
500
501 self.__clear_lines()
502
504 '''
505 Clear player data for using with different player or running another find_path
506 '''
507 self.__seen = np.zeros(self.__maze.shape, dtype=bool)
508 self.__seen[self.__start.x, self.__start.y] = True
509 self.__explored = np.zeros(self.__maze.shape, dtype=bool)
510 self.__explored[self.__start.x, self.__start.y] = True
511 self.__i = 0
512 self.__running_find = False
513
515 '''
516 Set player associated with this problem.
517 @param player: player to be used for association
518 @type player: L{BaseAgent<kuimaze.BaseAgent>} or its descendant
519 @raise AssertionError: if player is not instance of L{BaseAgent<kuimaze.BaseAgent>} or its descendant
520 '''
521 assert (isinstance(player, kuimaze.baseagent.BaseAgent))
522 self.__player = player
523 self.__clear_player_data()
524
525
526 '''
527 if self.__gui_root is not None:
528 self.__gui_root.mainloop()
529 '''
530
532 '''
533 Main GUI function - call this from L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>} to update GUI and
534 break at this point to be able to step your actions.
535 Example of its usage can be found at L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>}
536
537 Don't use it too often as it is quite expensive and rendering after single exploration might be slowing your
538 code down a lot.
539
540 You can optionally set parameter C{drawed_nodes} to a list of lists of dimensions corresponding to dimensions of
541 problem and if show_level is higher or equal to L{SHOW.NODE_REWARDS}, it will plot those in state centers
542 instead of state rewards.
543 If this parameter is left unset, no redrawing of texts in center of nodes is issued, however, it can be set to
544 True which will draw node_rewards saved in the problem.
545
546 If show_level is L{SHOW.NONE}, thisets function has no effect
547
548 @param drawed_nodes: custom objects convertible to string to draw to center of nodes or True or None
549 @type drawed_nodes: list of lists of the same dimensions as problem or boolean or None
550 '''
551 assert (self.__player is not None)
552 if self.show_level is not SHOW.NONE:
553 first_run = False
554 if not self.__gui_setup:
555 self.__setup_gui()
556 first_run = True
557 if self.show_level.value >= SHOW.FULL_MAZE.value:
558 self.__gui_update_map(explored_only=False)
559 else:
560 if self.show_level.value == SHOW.EXPLORED.value:
561 self.__gui_update_map(explored_only=True)
562 if first_run:
563
564
565 first_run = False
566 if not self.__till_end and self.__running_find:
567 self.__gui_lock = True
568 self.__changed_cells = []
569 self.__gui_canvas.update()
570 '''
571 while self.__gui_lock:
572 time.sleep(0.01)
573 self.__gui_root.update()
574 '''
575
577 '''
578 Show resulting path_section given as a list of consecutive L{namedtuples path_section<path_section>} to show in GUI.
579 Example of such usage can be found in L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>}
580
581 @param full_path: path_section in a form of list of consecutive L{namedtuples path_section<path_section>}
582 @type full_path: list of consecutive L{namedtuples path_section<path_section>}
583 '''
584 if self.show_level is not SHOW.NONE and len(full_path) is not 0:
585 def coord_gen(paths):
586 paths.append(path_section(paths[-1].state_to, None, None, None))
587 for item in paths:
588 for j in range(2):
589 num = item.state_from.x if j == 0 else item.state_from.y
590 yield (num + 1.5) * self.__cell_size + BORDER_SIZE
591 size = int(self.__cell_size/3)
592 coords = list(coord_gen(full_path))
593 full_path = full_path[:-1]
594 self.__drawn_lines.append((self.__gui_canvas.create_line(
595 *coords, width=self.__line_size, capstyle='round', fill=LINE_COLOR,
596 arrow=tkinter.LAST, arrowshape=(size, size, int(size/2.5))), coords))
597 self.__text_to_top()
598
600 '''
601 Set new show level. It will redraw whole GUI, so it takes a while.
602 @param show_level: new show_level to set
603 @type show_level: L{SHOW}
604 @raise AssertionError: if show_level is not an instance of L{SHOW}
605 '''
606 assert (isinstance(show_level, SHOW))
607 self.__backup_show = show_level
608 self.__changed_cells = None
609 if self.show_level is not show_level:
610 self.__destroy_gui(unblock=False)
611 self.show_level = show_level
612 if self.show_level is SHOW.NONE:
613 self.__gui_lock = False
614 self.__show_tkinter.set(show_level.value)
615 coords = [c for i, c in self.__drawn_lines]
616 self.show_and_break()
617 if self.show_level is not SHOW.NONE:
618 self.__drawn_lines = []
619 for coord in coords:
620 self.__drawn_lines.append((self.__gui_canvas.create_line(
621 *coord, width=self.__line_size, capstyle='round', fill=LINE_COLOR), coord))
622
624 '''
625 Set folder where the EPS files will be saved.
626 @param folder: folder to save EPS files
627 @type folder: string with a valid path_section
628 '''
629 folder = os.path.join(os.path.dirname(os.path.dirname(sys.argv[0])))
630 self.__save_name = os.path.join(folder, "%04d.eps" % (self.__i,))
631
633 '''
634 Setup and draw basic GUI. Imports tkinter.
635 '''
636 self.__gui_root = tkinter.Tk()
637 self.__gui_root.title('KUI - Maze')
638 self.__gui_root.protocol('WM_DELETE_WINDOW', self.__destroy_gui)
639 self.__gui_root.resizable(0, 0)
640 w = (self.__gui_root.winfo_screenwidth() / (self.get_dimensions()[0] + 2)) * MAX_WINDOW_PERCENTAGE
641 h = (self.__gui_root.winfo_screenheight() / (self.get_dimensions()[1] + 2)) * MAX_WINDOW_PERCENTAGE
642 use_font = FONT_FAMILY + str(FONT_SIZE)
643 self.__cell_size = min(w, h, MAX_CELL_SIZE)
644 self.__show_tkinter = tkinter.IntVar()
645 self.__show_tkinter.set(self.show_level)
646 top_frame = tkinter.Frame(self.__gui_root)
647 top_frame.pack(expand=False, side=tkinter.TOP)
648 width_pixels = (self.__cell_size * (self.get_dimensions()[0] + 2) + 2 * BORDER_SIZE)
649 height_pixels = (self.__cell_size * (self.get_dimensions()[1] + 2) + 2 * BORDER_SIZE)
650 self.__gui_canvas = tkinter.Canvas(top_frame, width=width_pixels, height=height_pixels)
651 self.__gui_canvas.pack(expand=False, side=tkinter.LEFT)
652 self.__color_handles = (-np.ones(self.get_dimensions(), dtype=int)).tolist()
653 self.__text_handles = (-np.ones(self.get_dimensions(), dtype=int)).tolist()
654 self.__text_handles_four = (-np.ones([self.get_dimensions()[0], self.get_dimensions()[1], 4], dtype=int)).tolist()
655 font_size = max(2, int(0.2 * self.__cell_size))
656 font_size_small = max(1, int(0.14 * self.__cell_size))
657 self.__font = FONT_FAMILY + " " + str(font_size)
658 self.__font_small = FONT_FAMILY + " " + str(font_size_small)
659 self.__line_size = max(1, int(self.__cell_size * LINE_SIZE_PERCENTAGE))
660 self.__drawn_lines = []
661 self.__changed_cells = None
662 for x in range(self.get_dimensions()[0]):
663 draw_num = DRAW_LABELS
664 if font_size == 1 and ((x % int(self.get_dimensions()[0] / 5)) != 0 and x != self.get_dimensions()[0] - 1):
665 draw_num = False
666 if draw_num:
667 self.__gui_canvas.create_text(self.__get_cell_center(x), (BORDER_SIZE + self.__cell_size) / 2,
668 text=str(x), font=self.__font)
669 self.__gui_canvas.create_text(self.__get_cell_center(x),
670 BORDER_SIZE + self.__cell_size * (self.get_dimensions()[1] + 1) + (
671 BORDER_SIZE + self.__cell_size) / 2, text=str(x), font=self.__font)
672 for y in range(self.get_dimensions()[1]):
673 draw_num = DRAW_LABELS
674 if font_size == 1 and ((y % int(self.get_dimensions()[1] / 5)) != 0 and y != self.get_dimensions()[1] - 1):
675 draw_num = False
676 if draw_num:
677 self.__gui_canvas.create_text((BORDER_SIZE + self.__cell_size) / 2, self.__get_cell_center(y),
678 text=str(y), font=self.__font)
679 self.__gui_canvas.create_text(BORDER_SIZE + self.__cell_size * (self.get_dimensions()[0] + 1) + (
680 BORDER_SIZE + self.__cell_size) / 2, self.__get_cell_center(y), text=str(y), font=self.__font)
681 box_size = (
682 int(self.__cell_size * self.get_dimensions()[0] + 2), int(self.__cell_size * self.get_dimensions()[1] + 2))
683 self.__gui_setup = True
684
686 '''
687 Safely destroy GUI. It is possible to pass an argument whether to unblock
688 L{find_path()<kuimaze.BaseAgent.find_path()>}
689 method, by default it is unblocking.
690
691 @param unblock: Whether to unblock L{find_path()<kuimaze.BaseAgent.find_path()>} method by calling this method
692 @type unblock: boolean
693 '''
694 if unblock:
695 self.__gui_lock = False
696 if self.__gui_root is not None:
697 self.__gui_root.update()
698 self.__gui_root.destroy()
699 self.__gui_root = None
700 self.show_level = SHOW.NONE
701 self.__gui_setup = False
702
704 '''
705 Renew GUI if a new player connects to a problem object.
706 '''
707
708 self.__has_triangles = False
709 self.show_level = self.__backup_show
710
712 '''
713 Just a simple callback for tkinter radiobuttons for selecting show level
714 '''
715 self.set_show_level(SHOW(self.__show_tkinter.get()))
716
718 '''
719 Clear path_section lines if running same player twice.
720 '''
721 if self.__gui_setup:
722 for line, _ in self.__drawn_lines:
723 self.__gui_canvas.delete(line)
724 self.__drawn_lines = []
725
727 '''
728 Set collor at position given by current position. Code inspired by old implementation of RPH Maze (predecessor of kuimaze)
729 @param current_node: state at which to set a color
730 @type current_node: L{namedtuple state<state>}
731 @param color: color string recognized by tkinter (see U{http://wiki.tcl.tk/37701})
732 @type color: string
733 '''
734 assert (self.__gui_setup)
735 x, y = current_node.x, current_node.y
736 if self.__color_handles[x][y] > 0:
737 if self.__gui_canvas.itemcget(self.__color_handles[x][y], "fill") is not color:
738 self.__gui_canvas.itemconfigure(self.__color_handles[x][y], fill=color)
739 else:
740 left = self.__get_cell_center(x) - self.__cell_size / 2
741 right = left + self.__cell_size
742 up = self.__get_cell_center(y) - self.__cell_size / 2
743 down = up + self.__cell_size
744 self.__color_handles[x][y] = self.__gui_canvas.create_rectangle(left, up, right, down, fill=color)
745
747 '''
748 Save canvas as color EPS - response for third button.
749 '''
750 self.set_eps_folder()
751 if not disabled:
752 self.__gui_canvas.postscript(file=self.__save_name, colormode="color")
753 self.__i += 1
754 else:
755 raise EnvironmentError('Maze must be rendered before saving to eps!')
756
758 '''
759 Mapping from problem coordinates to GUI coordinates.
760 @param x: x coord in problem
761 @param y: y coord in problem
762 @return: (x, y) coordinates in GUI (centers of cells)
763 '''
764 return self.__get_cell_center(x), self.__get_cell_center(y)
765
767 '''
768 Mapping from problem coordinate to GUI coordinate, only one coord.
769 @param x: coord in problem (could be either x or y)
770 @return: center of cell corresponding to such coordinate in GUI
771 '''
772 return BORDER_SIZE + self.__cell_size * (x + 1.5)
773
775 '''
776 Updating cell colors depending on what has been already explored.
777
778 @param explored_only: if True, update only explored position and leave unexplored black. if False, draw everything
779 @type explored_only: boolean
780 '''
781 assert (self.__gui_setup)
782
783 def get_cells():
784 dims = self.get_dimensions()
785 if self.__changed_cells is None:
786 for x in range(dims[0]):
787 for y in range(dims[1]):
788 yield x, y
789 else:
790 for item in self.__changed_cells:
791 yield item.x, item.y
792
793 for x, y in get_cells():
794 n = state(x, y)
795 if not self.__maze[x, y]:
796 self.__set_cell_color(n, self.__color_string_depth(WALL_COLOR, x, y))
797 else:
798 if self.is_goal_state(n) and not self.is_danger_state(n):
799 self.__set_cell_color(n, self.__color_string_depth(FINISH_COLOR, x, y))
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.__explored[x, y]:
804 self.__set_cell_color(n, self.__color_string_depth(EXPLORED_COLOR, x, y))
805 else:
806 if self.__seen[x, y]:
807 self.__set_cell_color(n, self.__color_string_depth(SEEN_COLOR, x, y))
808 else:
809 if explored_only:
810 self.__set_cell_color(n, self.__color_string_depth(WALL_COLOR, x, y))
811 else:
812 self.__set_cell_color(n, self.__color_string_depth(EMPTY_COLOR, x, y))
813 if n == self.__start:
814 self.__set_cell_color(n, self.__color_string_depth(START_COLOR, x, y))
815 if self.is_danger_state(n):
816 self.__set_cell_color(n, self.__color_string_depth(DANGER_COLOR, x, y))
817
819 '''
820 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.
821 if one of these objects in list is None, then no text is printed.
822
823 If drawed_nodes is None, then node_rewards saved in Maze objects are printed instead
824
825 @param drawed_nodes: list of lists of objects to be printed in GUI instead of state rewards
826 @type drawed_nodes: list of lists of appropriate dimensions or None
827 @raise IndexError: if drawed_nodes parameter doesn't match dimensions of problem
828 '''
829 dims = self.get_dimensions()
830
831 def get_cells():
832 for x in range(dims[0]):
833 for y in range(dims[1]):
834 yield x, y
835
836 if dictionary is None:
837 for x, y in get_cells():
838 if self.__maze[x, y]:
839 n = state(x, y)
840 vector = (n.x - self.__start.x, n.y - self.__start.y)
841 ret = self.__grad[0] * vector[0] + self.__grad[1] * vector[1]
842 self.__draw_text(n, format(ret, '.2f'))
843 return
844
845 assert type(dictionary[0]) == dict, "ERROR: Visualisation input must be dictionary"
846
847 if type(dictionary[0]['value']) == tuple or type(dictionary[0]['value']) == list:
848 assert len(dictionary[0]['value']) == 4, "ERROR: When visualising list or tuple, length must be 4!"
849 if not self.__has_triangles:
850
851 for x, y in get_cells():
852 if self.__maze[x, y]:
853 center = self.__get_cell_center_coords(x, y)
854 size = int(self.__cell_size/2)
855 point1 = [center[0] - size, center[1] - size]
856 point2 = [center[0] + size, center[1] + size]
857 point3 = [center[0] + size, center[1] - size]
858 point4 = [center[0] - size, center[1] + size]
859 self.__gui_canvas.create_line(point1[0], point1[1], point2[0], point2[1], width=1.4)
860 self.__gui_canvas.create_line(point3[0], point3[1], point4[0], point4[1], width=1.4)
861 self.__has_triangles = True
862 for element in dictionary:
863 x = element['x']
864 y = element['y']
865 if self.__maze[x, y]:
866 n = state(x, y)
867 index = y * dims[0] + x
868
869 self.__draw_text_four(n, element['value'])
870 return
871
872
873 if True:
874 for element in dictionary:
875 x = element['x']
876 y = element['y']
877 if self.__maze[x, y]:
878 n = state(x, y)
879 index = y * dims[0] + x
880
881 try:
882 string_to_print = format(element['value'], '.2f')
883 except:
884 string_to_print = str(element['value'])
885 self.__draw_text(n, string_to_print)
886
887
888 - def __draw_text(self, current_node, string):
889 '''
890 Draw text in the center of cells in the same manner as draw colors is done.
891
892 @param current_node: position on which the text is to be printed in Maze coordinates
893 @type current_node: L{namedtuple state<state>}
894 @param string: string to be drawn
895 @type string: string
896 '''
897
898 x, y = current_node.x, current_node.y
899 assert self.__gui_setup
900 if self.__text_handles[x][y] > 0:
901 if self.__gui_canvas.itemcget(self.__text_handles[x][y], "text") != string:
902 self.__gui_canvas.itemconfigure(self.__text_handles[x][y], text=string)
903 else:
904 self.__text_handles[x][y] = self.__gui_canvas.create_text(*self.__get_cell_center_coords(x, y), text=string,
905 font=self.__font)
906
907 - def __text_to_top(self):
908 '''
909 Move text fields to the top layer of the canvas - to cover arrow
910 :return:
911 '''
912 if self.__has_triangles:
913 for x in range(self.get_dimensions()[0]):
914 for y in range(self.get_dimensions()[1]):
915 for i in range(4):
916 if self.__text_handles_four[x][y][i] > 0:
917 self.__gui_canvas.tag_raise(self.__text_handles_four[x][y][i])
918 else:
919 for x in range(self.get_dimensions()[0]):
920 for y in range(self.get_dimensions()[1]):
921 if self.__text_handles[x][y] > 0:
922 self.__gui_canvas.tag_raise(self.__text_handles[x][y])
923
924 - def __draw_text_four(self, current_node, my_list):
925 '''
926 Draw four text cells into one square
927
928 @param current_node: position on which the text is to be printed in Maze coordinates
929 @param my_list: list to be drawn
930 @type my_list: list of floats or ints
931 '''
932
933 x, y = current_node.x, current_node.y
934 format_string = '.2f'
935 assert self.__gui_setup
936 for i in range(4):
937 if self.__text_handles_four[x][y][i] > 0:
938 if self.__gui_canvas.itemcget(self.__text_handles_four[x][y][i], "text") != format(my_list[i], format_string):
939 self.__gui_canvas.itemconfigure(self.__text_handles_four[x][y][i], text=format(my_list[i], format_string))
940 else:
941 center = self.__get_cell_center_coords(x, y)
942 size = self.__cell_size/2
943 if i == 0:
944 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0], center[1] - int(0.7*size)],
945 text=format(my_list[i], format_string), font=self.__font_small)
946 elif i == 1:
947 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0] + int(0.565*size), center[1]],
948 text=format(my_list[i], format_string), font=self.__font_small)
949 elif i == 2:
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 == 3:
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
957 '''
958 Method adjust color due to depth of square in maze
959 :param color: color string in hexadecimal ... for example "#FFF000000" for red
960 :param x: index of square
961 :param y: index of square
962 :return: new color string
963 '''
964 assert len(color) == 10
965 rgb = [int(color[1:4], 16), int(color[4:7], 16), int(color[7:10], 16)]
966 tmp = self.__koef * (x * self.__grad[0] + y * self.__grad[1] + self.__offset)
967 strings = []
968 for i in range(3):
969 rgb[i] = rgb[i] - abs(int(tmp) - self.__max_minus)
970 if rgb[i] < 0:
971 rgb[i] = 0
972 strings.append(hex(rgb[i])[2:])
973 for i in range(3):
974 while len(strings[i]) < 3:
975 strings[i] = "0" + strings[i]
976 ret = "#" + strings[0] + strings[1] + strings[2]
977 return ret
978
980 '''
981 Sets data needed for rendering 3D ilusion
982 :return: None
983 '''
984 self.__max_minus = 2048
985 lt = 0
986 lb = self.get_dimensions()[1] * self.__grad[1]
987 rt = self.get_dimensions()[0] * self.__grad[0]
988 rb = self.get_dimensions()[0] * self.__grad[0] + self.get_dimensions()[1] * self.__grad[1]
989 tmp = [lt, lb, rt, rb]
990 maxi = max(tmp)
991 mini = min(tmp)
992 self.__offset = 0 - mini
993 if self.__grad[0] != 0 or self.__grad[1] != 0:
994 self.__koef = self.__max_minus / (maxi - mini)
995 else:
996 self.__koef = 0
997 self.__max_minus = 0
998