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