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