Package kuimaze :: Module maze
[hide private]
[frames] | no frames]

Source Code for Module kuimaze.maze

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  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  # nicer warnings 
 25  fw_orig = warnings.formatwarning 
 26  warnings.formatwarning = lambda msg, categ, fname, lineno, line=None: fw_orig(msg, categ, fname, lineno, '') 
 27   
 28  # some named tuples to be used throughout the package - notice that state and weighted_state are essentially the same 
 29  #: Namedtuple to hold state position with reward. Interchangeable with L{state} 
 30  weighted_state = collections.namedtuple('State', ['x', 'y', 'reward']) 
 31  #: Namedtuple to hold state position. Mostly interchangeable with L{weighted_state} 
 32  state = collections.namedtuple('State', ['x', 'y']) 
 33  #: Namedtuple to hold path_section from state A to state B. Expects C{state_from} and C{state_to} to be of type L{state} or L{weighted_state} 
 34  path_section = collections.namedtuple('Path', ['state_from', 'state_to', 'cost', 'action']) 
 35   
 36  # constants used for GUI drawing 
 37  #: Maximum size of one cell in GUI in pixels. If problem is too large to fit on screen, the cell size will be smaller 
 38  MAX_CELL_SIZE = 200 
 39  #: Maximal percentage of smaller screen size, GUI window can occupy. 
 40  MAX_WINDOW_PERCENTAGE = 0.85 
 41  #: Border size of canvas from border of GUI window, in pixels. 
 42  BORDER_SIZE = 0 
 43  #: Percentage of actuall cell size that specifies thickness of line size used in show_path. Line thickness is then determined by C{max(1, int(LINE_SIZE_PERCENTAGE * cell_size))} 
 44  LINE_SIZE_PERCENTAGE = 0.1 
 45  #: Draw the x,y labels 
 46  # Todo: add a possibility of having the maze to the borders 
 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  # FINISH_COLOR = "#FFF000000" 
 56  FINISH_COLOR = "#000FFFFFF" 
 57  DANGER_COLOR = "#FFF000000" 
 58   
 59  #: Font family used in GUI 
 60  FONT_FAMILY = "Helvetica" 
 61   
 62  #: Text size in GUI (not on Canvas itself) 
 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
76 -class ACTION(enum.Enum):
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
85 - def __str__(self):
86 if self == ACTION.UP: 87 return "/\\" 88 if self == ACTION.RIGHT: 89 return ">" 90 if self == ACTION.DOWN: 91 return "\\/" 92 if self == ACTION.LEFT: 93 return "<"
94 95
96 -class ProbsRoulette:
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 # create bounds -> use defined method 'set_probs' outside init 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
118 - def confuse_action(self, action):
119 roulette = random.uniform(0.0, 1.0) 120 if 0 <= roulette < self._obey: 121 return action 122 else: 123 # Confused left 124 if self._obey <= roulette < self._confusionLeft: 125 return (action - 1) % 4 126 else: 127 # Confused right 128 if self._confusionLeft <= roulette < self._confusionRight: 129 return (action + 1) % 4 130 else: 131 # Confused back 132 return (action + 2) % 4
133
134 - def __str__(self):
135 return str(self.probtable)
136 137
138 -class ActionProbsTable:
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 # self.obey = obey 142 # self.confusion90 = confusion90 143 # self.confusion180 = confusion180 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
165 - def __getitem__(self, item):
166 return self.probtable[item]
167
168 - def __str__(self):
169 return str(self.probtable)
170 171 172 173
174 -class Maze:
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: # array provided directly 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
323 - def get_state_reward(self, state):
324 return self.__node_rewards[state.x, state.y]
325
326 - def get_start_state(self):
327 ''' 328 Returns a start state 329 @return: start state 330 @rtype: L{namedtuple state<state>} 331 ''' 332 return self.__start
333
334 - def close_gui(self):
335 self.__destroy_gui()
336
337 - def set_node_utils(self,utils):
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
346 - def is_goal_state(self, current_state):
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
356 - def is_danger_state(self, current_state):
357 return state(current_state.x, current_state.y) in self.hard_places
358
359 - def get_goal_nodes(self):
360 ''' 361 Returns a list of goal nodes 362 @return: list of goal nodes 363 @rtype: list 364 ''' 365 return list(self.__finish)
366
367 - def get_all_states(self):
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]: # do not include walls 378 states.append(weighted_state(x, y, self.__node_rewards[x, y])) 379 return states
380
381 - def get_dimensions(self):
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
389 - def get_actions(self, current_state):
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 # yet to be change as this is not probabilistic 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: # no outcome, just stay, thing about bouncing back, should be handled by the search agent 414 nstate = weighted_state(current_state.x, current_state.y, 415 self.__node_rewards[current_state.x, current_state.y]) 416 #return nstate, self.__get_path_cost(current_state, nstate) 417 return state(nstate.x, nstate.y)
418
419 - def get_next_states_and_probs(self, curr, action):
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
433 - def set_explored(self, states):
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
447 - def set_probs_table(self, obey, confusionL, confusionR, confusion180):
448 self.__trans_probs = ActionProbsTable(obey, confusionL, confusionR, confusion180)
449
450 - def set_visited(self, states):
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
460 - def non_det_result(self, action):
461 real_action = self.__trans_probs.confuse_action(action) 462 return real_action
463
464 - def __is_inside(self, current_state):
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
475 - def __is_inside_valid(self, current_state):
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
485 - def clear_player_data(self):
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 # self.show_and_break() 498 self.__clear_lines()
499
500 - def __clear_player_data(self):
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
511 - def set_player(self, player):
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 #self.__renew_gui() 522 #self.show_and_break() 523 ''' 524 if self.__gui_root is not None: 525 self.__gui_root.mainloop() 526 '''
527
528 - def show_and_break(self, drawed_nodes=None):
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 #self.__gui_canvas.create_image(self.__cell_size + BORDER_SIZE, self.__cell_size + BORDER_SIZE 561 # , anchor=tkinter.NW, image=self._image) 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
573 - def show_path(self, full_path):
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, # stipple='gray75', 593 arrow=tkinter.LAST, arrowshape=(size, size, int(size/2.5))), coords)) 594 self.__text_to_top()
595
596 - def set_show_level(self, show_level):
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
620 - def set_eps_folder(self):
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
629 - def __setup_gui(self):
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
682 - def __destroy_gui(self, unblock=True):
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
700 - def __renew_gui(self):
701 ''' 702 Renew GUI if a new player connects to a problem object. 703 ''' 704 #self.__destroy_gui() 705 self.__has_triangles = False 706 self.show_level = self.__backup_show
707
708 - def __set_show_level_cb(self):
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
714 - def __clear_lines(self):
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
723 - def __set_cell_color(self, current_node, color):
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
743 - def save_as_eps(self, disabled):
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
754 - def __get_cell_center_coords(self, x, y):
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
763 - def __get_cell_center(self, x):
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
771 - def __gui_update_map(self, explored_only=True):
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
815 - def visualise(self, dictionary):
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 # assert len(dictionary) == dims[0]*dims[1], "ERROR: Visualisation input must have same size as maze!" 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 # create triangles 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 # self.__draw_text_four(n, dictionary[index]['value']) 866 self.__draw_text_four(n, element['value']) 867 return 868 869 # if type(dictionary[0]['value']) == int or type(dictionary[0]['value']) == float: 870 if True: # at the moment for everything else 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 # self.__draw_text(n, format(dictionary[index]['value'], '.2f')) 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
953 - def __color_string_depth(self, color, x, y):
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
976 - def __set_grad_data(self):
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