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_dict = {} 437 for out_action in ACTION: 438 next_state = self.result(curr, out_action.value) 439 if not(next_state in states_dict): 440 states_dict[next_state] = 0 441 states_dict[next_state] += self.__trans_probs[action, out_action] # different actions may end up in the same state 442 443 # a list is tuples is required as output, fill the list then 444 states_probs = [] 445 for key in states_dict: 446 states_probs.append((key, states_dict[key])) 447 return states_probs
448
449 - def set_explored(self, states):
450 ''' 451 sets explored states list, preparation for visualisation 452 @param states: iterable of L{state<state>} 453 ''' 454 self.__explored = np.zeros(self.__maze.shape, dtype=bool) 455 for state in states: 456 self.__explored[state.x, state.y] = True 457 if self.__changed_cells is not None: 458 self.__changed_cells.append(state)
459
460 - def set_probs(self, obey, confusionL, confusionR, confusion180):
461 self.__trans_probs.set_probs(obey, confusionL, confusionR, confusion180)
462
463 - def set_probs_table(self, obey, confusionL, confusionR, confusion180):
464 self.__trans_probs = ActionProbsTable(obey, confusionL, confusionR, confusion180)
465
466 - def set_visited(self, states):
467 ''' 468 sets seen states list, preparation for visualisation 469 @param states: iterable of L{state<state>} 470 ''' 471 for state in states: 472 self.__seen[state.x, state.y] = True 473 if self.__changed_cells is not None: 474 self.__changed_cells.append(state)
475
476 - def non_det_result(self, action):
477 real_action = self.__trans_probs.confuse_action(action) 478 return real_action
479
480 - def __is_inside(self, current_state):
481 ''' 482 Check whether a state is inside a problem 483 @param current_state: state to check 484 @type current_state: L{namedtuple state<state>} 485 @return: True if state is inside problem, False otherwise 486 @rtype: boolean 487 ''' 488 dims = self.get_dimensions() 489 return current_state.x >= 0 and current_state.y >= 0 and current_state.x < dims[0] and current_state.y < dims[1]
490
491 - def __is_inside_valid(self, current_state):
492 ''' 493 Check whether a state is inside a problem and is not a wall 494 @param current_state: state to check 495 @type current_state: L{namedtuple state<state>} 496 @return: True if state is inside problem and is not a wall, False otherwise 497 @rtype: boolean 498 ''' 499 return self.__is_inside(current_state) and self.__maze[current_state.x, current_state.y]
500
501 - def clear_player_data(self):
502 ''' 503 Clear player data for using with different player or running another find_path 504 ''' 505 self.__seen = np.zeros(self.__maze.shape, dtype=bool) 506 self.__seen[self.__start.x, self.__start.y] = True 507 self.__explored = np.zeros(self.__maze.shape, dtype=bool) 508 self.__explored[self.__start.x, self.__start.y] = True 509 self.__i = 0 510 self.__running_find = False 511 self.__renew_gui() 512 self.__changed_cells = None 513 # self.show_and_break() 514 self.__clear_lines()
515
516 - def __clear_player_data(self):
517 ''' 518 Clear player data for using with different player or running another find_path 519 ''' 520 self.__seen = np.zeros(self.__maze.shape, dtype=bool) 521 self.__seen[self.__start.x, self.__start.y] = True 522 self.__explored = np.zeros(self.__maze.shape, dtype=bool) 523 self.__explored[self.__start.x, self.__start.y] = True 524 self.__i = 0 525 self.__running_find = False
526
527 - def set_player(self, player):
528 ''' 529 Set player associated with this problem. 530 @param player: player to be used for association 531 @type player: L{BaseAgent<kuimaze.BaseAgent>} or its descendant 532 @raise AssertionError: if player is not instance of L{BaseAgent<kuimaze.BaseAgent>} or its descendant 533 ''' 534 assert (isinstance(player, kuimaze.baseagent.BaseAgent)) 535 self.__player = player 536 self.__clear_player_data() 537 #self.__renew_gui() 538 #self.show_and_break() 539 ''' 540 if self.__gui_root is not None: 541 self.__gui_root.mainloop() 542 '''
543
544 - def show_and_break(self, drawed_nodes=None):
545 ''' 546 Main GUI function - call this from L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>} to update GUI and 547 break at this point to be able to step your actions. 548 Example of its usage can be found at L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>} 549 550 Don't use it too often as it is quite expensive and rendering after single exploration might be slowing your 551 code down a lot. 552 553 You can optionally set parameter C{drawed_nodes} to a list of lists of dimensions corresponding to dimensions of 554 problem and if show_level is higher or equal to L{SHOW.NODE_REWARDS}, it will plot those in state centers 555 instead of state rewards. 556 If this parameter is left unset, no redrawing of texts in center of nodes is issued, however, it can be set to 557 True which will draw node_rewards saved in the problem. 558 559 If show_level is L{SHOW.NONE}, thisets function has no effect 560 561 @param drawed_nodes: custom objects convertible to string to draw to center of nodes or True or None 562 @type drawed_nodes: list of lists of the same dimensions as problem or boolean or None 563 ''' 564 assert (self.__player is not None) 565 if self.show_level is not SHOW.NONE: 566 first_run = False 567 if not self.__gui_setup: 568 self.__setup_gui() 569 first_run = True 570 if self.show_level.value >= SHOW.FULL_MAZE.value: 571 self.__gui_update_map(explored_only=False) 572 else: 573 if self.show_level.value == SHOW.EXPLORED.value: 574 self.__gui_update_map(explored_only=True) 575 if first_run: 576 #self.__gui_canvas.create_image(self.__cell_size + BORDER_SIZE, self.__cell_size + BORDER_SIZE 577 # , anchor=tkinter.NW, image=self._image) 578 first_run = False 579 if not self.__till_end and self.__running_find: 580 self.__gui_lock = True 581 self.__changed_cells = [] 582 self.__gui_canvas.update() 583 ''' 584 while self.__gui_lock: 585 time.sleep(0.01) 586 self.__gui_root.update() 587 '''
588
589 - def show_path(self, full_path):
590 ''' 591 Show resulting path_section given as a list of consecutive L{namedtuples path_section<path_section>} to show in GUI. 592 Example of such usage can be found in L{C{BaseAgent.find_path()}<kuimaze.BaseAgent.find_path()>} 593 594 @param full_path: path_section in a form of list of consecutive L{namedtuples path_section<path_section>} 595 @type full_path: list of consecutive L{namedtuples path_section<path_section>} 596 ''' 597 if self.show_level is not SHOW.NONE and len(full_path) is not 0: 598 def coord_gen(paths): 599 paths.append(path_section(paths[-1].state_to, None, None, None)) 600 for item in paths: 601 for j in range(2): 602 num = item.state_from.x if j == 0 else item.state_from.y 603 yield (num + 1.5) * self.__cell_size + BORDER_SIZE
604 size = int(self.__cell_size/3) 605 coords = list(coord_gen(full_path)) 606 full_path = full_path[:-1] 607 self.__drawn_lines.append((self.__gui_canvas.create_line( 608 *coords, width=self.__line_size, capstyle='round', fill=LINE_COLOR, # stipple='gray75', 609 arrow=tkinter.LAST, arrowshape=(size, size, int(size/2.5))), coords)) 610 self.__text_to_top()
611
612 - def set_show_level(self, show_level):
613 ''' 614 Set new show level. It will redraw whole GUI, so it takes a while. 615 @param show_level: new show_level to set 616 @type show_level: L{SHOW} 617 @raise AssertionError: if show_level is not an instance of L{SHOW} 618 ''' 619 assert (isinstance(show_level, SHOW)) 620 self.__backup_show = show_level 621 self.__changed_cells = None 622 if self.show_level is not show_level: 623 self.__destroy_gui(unblock=False) 624 self.show_level = show_level 625 if self.show_level is SHOW.NONE: 626 self.__gui_lock = False 627 self.__show_tkinter.set(show_level.value) 628 coords = [c for i, c in self.__drawn_lines] 629 self.show_and_break() 630 if self.show_level is not SHOW.NONE: 631 self.__drawn_lines = [] 632 for coord in coords: 633 self.__drawn_lines.append((self.__gui_canvas.create_line( 634 *coord, width=self.__line_size, capstyle='round', fill=LINE_COLOR), coord))
635
636 - def set_eps_folder(self):
637 ''' 638 Set folder where the EPS files will be saved. 639 @param folder: folder to save EPS files 640 @type folder: string with a valid path_section 641 ''' 642 folder = os.path.join(os.path.dirname(os.path.dirname(sys.argv[0]))) 643 self.__save_name = os.path.join(folder, "%04d.eps" % (self.__i,))
644
645 - def __setup_gui(self):
646 ''' 647 Setup and draw basic GUI. Imports tkinter. 648 ''' 649 self.__gui_root = tkinter.Tk() 650 self.__gui_root.title('KUI - Maze') 651 self.__gui_root.protocol('WM_DELETE_WINDOW', self.__destroy_gui) 652 self.__gui_root.resizable(0, 0) 653 w = (self.__gui_root.winfo_screenwidth() / (self.get_dimensions()[0] + 2)) * MAX_WINDOW_PERCENTAGE 654 h = (self.__gui_root.winfo_screenheight() / (self.get_dimensions()[1] + 2)) * MAX_WINDOW_PERCENTAGE 655 use_font = FONT_FAMILY + str(FONT_SIZE) 656 self.__cell_size = min(w, h, MAX_CELL_SIZE) 657 self.__show_tkinter = tkinter.IntVar() 658 self.__show_tkinter.set(self.show_level) 659 top_frame = tkinter.Frame(self.__gui_root) 660 top_frame.pack(expand=False, side=tkinter.TOP) 661 width_pixels = (self.__cell_size * (self.get_dimensions()[0] + 2) + 2 * BORDER_SIZE) 662 height_pixels = (self.__cell_size * (self.get_dimensions()[1] + 2) + 2 * BORDER_SIZE) 663 self.__gui_canvas = tkinter.Canvas(top_frame, width=width_pixels, height=height_pixels) 664 self.__gui_canvas.pack(expand=False, side=tkinter.LEFT) 665 self.__color_handles = (-np.ones(self.get_dimensions(), dtype=int)).tolist() 666 self.__text_handles = (-np.ones(self.get_dimensions(), dtype=int)).tolist() 667 self.__text_handles_four = (-np.ones([self.get_dimensions()[0], self.get_dimensions()[1], 4], dtype=int)).tolist() 668 font_size = max(2, int(0.2 * self.__cell_size)) 669 font_size_small = max(1, int(0.14 * self.__cell_size)) 670 self.__font = FONT_FAMILY + " " + str(font_size) 671 self.__font_small = FONT_FAMILY + " " + str(font_size_small) 672 self.__line_size = max(1, int(self.__cell_size * LINE_SIZE_PERCENTAGE)) 673 self.__drawn_lines = [] 674 self.__changed_cells = None 675 for x in range(self.get_dimensions()[0]): 676 draw_num = DRAW_LABELS 677 if font_size == 1 and ((x % int(self.get_dimensions()[0] / 5)) != 0 and x != self.get_dimensions()[0] - 1): 678 draw_num = False 679 if draw_num: 680 self.__gui_canvas.create_text(self.__get_cell_center(x), (BORDER_SIZE + self.__cell_size) / 2, 681 text=str(x), font=self.__font) 682 self.__gui_canvas.create_text(self.__get_cell_center(x), 683 BORDER_SIZE + self.__cell_size * (self.get_dimensions()[1] + 1) + ( 684 BORDER_SIZE + self.__cell_size) / 2, text=str(x), font=self.__font) 685 for y in range(self.get_dimensions()[1]): 686 draw_num = DRAW_LABELS 687 if font_size == 1 and ((y % int(self.get_dimensions()[1] / 5)) != 0 and y != self.get_dimensions()[1] - 1): 688 draw_num = False 689 if draw_num: 690 self.__gui_canvas.create_text((BORDER_SIZE + self.__cell_size) / 2, self.__get_cell_center(y), 691 text=str(y), font=self.__font) 692 self.__gui_canvas.create_text(BORDER_SIZE + self.__cell_size * (self.get_dimensions()[0] + 1) + ( 693 BORDER_SIZE + self.__cell_size) / 2, self.__get_cell_center(y), text=str(y), font=self.__font) 694 box_size = ( 695 int(self.__cell_size * self.get_dimensions()[0] + 2), int(self.__cell_size * self.get_dimensions()[1] + 2)) 696 self.__gui_setup = True
697
698 - def __destroy_gui(self, unblock=True):
699 ''' 700 Safely destroy GUI. It is possible to pass an argument whether to unblock 701 L{find_path()<kuimaze.BaseAgent.find_path()>} 702 method, by default it is unblocking. 703 704 @param unblock: Whether to unblock L{find_path()<kuimaze.BaseAgent.find_path()>} method by calling this method 705 @type unblock: boolean 706 ''' 707 if unblock: 708 self.__gui_lock = False 709 if self.__gui_root is not None: 710 self.__gui_root.update() 711 self.__gui_root.destroy() 712 self.__gui_root = None 713 self.show_level = SHOW.NONE 714 self.__gui_setup = False
715
716 - def __renew_gui(self):
717 ''' 718 Renew GUI if a new player connects to a problem object. 719 ''' 720 #self.__destroy_gui() 721 self.__has_triangles = False 722 self.show_level = self.__backup_show
723
724 - def __set_show_level_cb(self):
725 ''' 726 Just a simple callback for tkinter radiobuttons for selecting show level 727 ''' 728 self.set_show_level(SHOW(self.__show_tkinter.get()))
729
730 - def __clear_lines(self):
731 ''' 732 Clear path_section lines if running same player twice. 733 ''' 734 if self.__gui_setup: 735 for line, _ in self.__drawn_lines: 736 self.__gui_canvas.delete(line) 737 self.__drawn_lines = []
738
739 - def __set_cell_color(self, current_node, color):
740 ''' 741 Set collor at position given by current position. Code inspired by old implementation of RPH Maze (predecessor of kuimaze) 742 @param current_node: state at which to set a color 743 @type current_node: L{namedtuple state<state>} 744 @param color: color string recognized by tkinter (see U{http://wiki.tcl.tk/37701}) 745 @type color: string 746 ''' 747 assert (self.__gui_setup) 748 x, y = current_node.x, current_node.y 749 if self.__color_handles[x][y] > 0: 750 if self.__gui_canvas.itemcget(self.__color_handles[x][y], "fill") is not color: 751 self.__gui_canvas.itemconfigure(self.__color_handles[x][y], fill=color) 752 else: 753 left = self.__get_cell_center(x) - self.__cell_size / 2 754 right = left + self.__cell_size 755 up = self.__get_cell_center(y) - self.__cell_size / 2 756 down = up + self.__cell_size 757 self.__color_handles[x][y] = self.__gui_canvas.create_rectangle(left, up, right, down, fill=color)
758
759 - def save_as_eps(self, disabled):
760 ''' 761 Save canvas as color EPS - response for third button. 762 ''' 763 self.set_eps_folder() 764 if not disabled: 765 self.__gui_canvas.postscript(file=self.__save_name, colormode="color") 766 self.__i += 1 767 else: 768 raise EnvironmentError('Maze must be rendered before saving to eps!')
769
770 - def __get_cell_center_coords(self, x, y):
771 ''' 772 Mapping from problem coordinates to GUI coordinates. 773 @param x: x coord in problem 774 @param y: y coord in problem 775 @return: (x, y) coordinates in GUI (centers of cells) 776 ''' 777 return self.__get_cell_center(x), self.__get_cell_center(y)
778
779 - def __get_cell_center(self, x):
780 ''' 781 Mapping from problem coordinate to GUI coordinate, only one coord. 782 @param x: coord in problem (could be either x or y) 783 @return: center of cell corresponding to such coordinate in GUI 784 ''' 785 return BORDER_SIZE + self.__cell_size * (x + 1.5)
786
787 - def __gui_update_map(self, explored_only=True):
788 ''' 789 Updating cell colors depending on what has been already explored. 790 791 @param explored_only: if True, update only explored position and leave unexplored black. if False, draw everything 792 @type explored_only: boolean 793 ''' 794 assert (self.__gui_setup) 795 796 def get_cells(): 797 dims = self.get_dimensions() 798 if self.__changed_cells is None: 799 for x in range(dims[0]): 800 for y in range(dims[1]): 801 yield x, y 802 else: 803 for item in self.__changed_cells: 804 yield item.x, item.y
805 806 for x, y in get_cells(): 807 n = state(x, y) 808 if not self.__maze[x, y]: 809 self.__set_cell_color(n, self.__color_string_depth(WALL_COLOR, x, y)) 810 else: 811 if self.is_goal_state(n) and not self.is_danger_state(n): 812 self.__set_cell_color(n, self.__color_string_depth(FINISH_COLOR, x, y)) 813 if self.__explored[x, y]: 814 self.__set_cell_color(n, self.__color_string_depth(EXPLORED_COLOR, x, y)) 815 else: 816 if self.__explored[x, y]: 817 self.__set_cell_color(n, self.__color_string_depth(EXPLORED_COLOR, x, y)) 818 else: 819 if self.__seen[x, y]: 820 self.__set_cell_color(n, self.__color_string_depth(SEEN_COLOR, x, y)) 821 else: 822 if explored_only: 823 self.__set_cell_color(n, self.__color_string_depth(WALL_COLOR, x, y)) 824 else: 825 self.__set_cell_color(n, self.__color_string_depth(EMPTY_COLOR, x, y)) 826 if n == self.__start: 827 self.__set_cell_color(n, self.__color_string_depth(START_COLOR, x, y)) 828 if self.is_danger_state(n): 829 self.__set_cell_color(n, self.__color_string_depth(DANGER_COLOR, x, y)) 830
831 - def visualise(self, dictionary):
832 ''' 833 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. 834 if one of these objects in list is None, then no text is printed. 835 836 If drawed_nodes is None, then node_rewards saved in Maze objects are printed instead 837 838 @param drawed_nodes: list of lists of objects to be printed in GUI instead of state rewards 839 @type drawed_nodes: list of lists of appropriate dimensions or None 840 @raise IndexError: if drawed_nodes parameter doesn't match dimensions of problem 841 ''' 842 dims = self.get_dimensions() 843 844 def get_cells(): 845 for x in range(dims[0]): 846 for y in range(dims[1]): 847 yield x, y
848 849 if dictionary is None: 850 for x, y in get_cells(): 851 if self.__maze[x, y]: 852 n = state(x, y) 853 vector = (n.x - self.__start.x, n.y - self.__start.y) 854 ret = self.__grad[0] * vector[0] + self.__grad[1] * vector[1] 855 self.__draw_text(n, format(ret, '.2f')) 856 return 857 858 assert type(dictionary[0]) == dict, "ERROR: Visualisation input must be dictionary" 859 # assert len(dictionary) == dims[0]*dims[1], "ERROR: Visualisation input must have same size as maze!" 860 if type(dictionary[0]['value']) == tuple or type(dictionary[0]['value']) == list: 861 assert len(dictionary[0]['value']) == 4, "ERROR: When visualising list or tuple, length must be 4!" 862 if not self.__has_triangles: 863 # create triangles 864 for x, y in get_cells(): 865 if self.__maze[x, y]: 866 center = self.__get_cell_center_coords(x, y) 867 size = int(self.__cell_size/2) 868 point1 = [center[0] - size, center[1] - size] 869 point2 = [center[0] + size, center[1] + size] 870 point3 = [center[0] + size, center[1] - size] 871 point4 = [center[0] - size, center[1] + size] 872 self.__gui_canvas.create_line(point1[0], point1[1], point2[0], point2[1], width=1.4) 873 self.__gui_canvas.create_line(point3[0], point3[1], point4[0], point4[1], width=1.4) 874 self.__has_triangles = True 875 for element in dictionary: 876 x = element['x'] 877 y = element['y'] 878 if self.__maze[x, y]: 879 n = state(x, y) 880 index = y * dims[0] + x 881 # self.__draw_text_four(n, dictionary[index]['value']) 882 self.__draw_text_four(n, element['value']) 883 return 884 885 # if type(dictionary[0]['value']) == int or type(dictionary[0]['value']) == float: 886 if True: # at the moment for everything else 887 for element in dictionary: 888 x = element['x'] 889 y = element['y'] 890 if self.__maze[x, y]: 891 n = state(x, y) 892 index = y * dims[0] + x 893 # self.__draw_text(n, format(dictionary[index]['value'], '.2f')) 894 try: 895 string_to_print = format(element['value'], '.2f') 896 except: 897 string_to_print = str(element['value']) 898 self.__draw_text(n, string_to_print) 899 900
901 - def __draw_text(self, current_node, string):
902 ''' 903 Draw text in the center of cells in the same manner as draw colors is done. 904 905 @param current_node: position on which the text is to be printed in Maze coordinates 906 @type current_node: L{namedtuple state<state>} 907 @param string: string to be drawn 908 @type string: string 909 ''' 910 911 x, y = current_node.x, current_node.y 912 assert self.__gui_setup 913 if self.__text_handles[x][y] > 0: 914 if self.__gui_canvas.itemcget(self.__text_handles[x][y], "text") != string: 915 self.__gui_canvas.itemconfigure(self.__text_handles[x][y], text=string) 916 else: 917 self.__text_handles[x][y] = self.__gui_canvas.create_text(*self.__get_cell_center_coords(x, y), text=string, 918 font=self.__font)
919
920 - def __text_to_top(self):
921 ''' 922 Move text fields to the top layer of the canvas - to cover arrow 923 :return: 924 ''' 925 if self.__has_triangles: 926 for x in range(self.get_dimensions()[0]): 927 for y in range(self.get_dimensions()[1]): 928 for i in range(4): 929 if self.__text_handles_four[x][y][i] > 0: 930 self.__gui_canvas.tag_raise(self.__text_handles_four[x][y][i]) 931 else: 932 for x in range(self.get_dimensions()[0]): 933 for y in range(self.get_dimensions()[1]): 934 if self.__text_handles[x][y] > 0: 935 self.__gui_canvas.tag_raise(self.__text_handles[x][y])
936
937 - def __draw_text_four(self, current_node, my_list):
938 ''' 939 Draw four text cells into one square 940 941 @param current_node: position on which the text is to be printed in Maze coordinates 942 @param my_list: list to be drawn 943 @type my_list: list of floats or ints 944 ''' 945 946 x, y = current_node.x, current_node.y 947 format_string = '.2f' 948 assert self.__gui_setup 949 for i in range(4): 950 if self.__text_handles_four[x][y][i] > 0: 951 if self.__gui_canvas.itemcget(self.__text_handles_four[x][y][i], "text") != format(my_list[i], format_string): 952 self.__gui_canvas.itemconfigure(self.__text_handles_four[x][y][i], text=format(my_list[i], format_string)) 953 else: 954 center = self.__get_cell_center_coords(x, y) 955 size = self.__cell_size/2 956 if i == 0: 957 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0], center[1] - int(0.7*size)], 958 text=format(my_list[i], format_string), font=self.__font_small) 959 elif i == 1: 960 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0] + int(0.565*size), center[1]], 961 text=format(my_list[i], format_string), font=self.__font_small) 962 elif i == 2: 963 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0], center[1] + int(0.7*size)], 964 text=format(my_list[i], format_string), font=self.__font_small) 965 elif i == 3: 966 self.__text_handles_four[x][y][i] = self.__gui_canvas.create_text([center[0] - int(0.565*size), center[1]], 967 text=format(my_list[i], format_string), font=self.__font_small)
968
969 - def __color_string_depth(self, color, x, y):
970 ''' 971 Method adjust color due to depth of square in maze 972 :param color: color string in hexadecimal ... for example "#FFF000000" for red 973 :param x: index of square 974 :param y: index of square 975 :return: new color string 976 ''' 977 assert len(color) == 10 978 rgb = [int(color[1:4], 16), int(color[4:7], 16), int(color[7:10], 16)] 979 tmp = self.__koef * (x * self.__grad[0] + y * self.__grad[1] + self.__offset) 980 strings = [] 981 for i in range(3): 982 rgb[i] = rgb[i] - abs(int(tmp) - self.__max_minus) 983 if rgb[i] < 0: 984 rgb[i] = 0 985 strings.append(hex(rgb[i])[2:]) 986 for i in range(3): 987 while len(strings[i]) < 3: 988 strings[i] = "0" + strings[i] 989 ret = "#" + strings[0] + strings[1] + strings[2] 990 return ret
991
992 - def __set_grad_data(self):
993 ''' 994 Sets data needed for rendering 3D ilusion 995 :return: None 996 ''' 997 self.__max_minus = 2048 998 lt = 0 999 lb = self.get_dimensions()[1] * self.__grad[1] 1000 rt = self.get_dimensions()[0] * self.__grad[0] 1001 rb = self.get_dimensions()[0] * self.__grad[0] + self.get_dimensions()[1] * self.__grad[1] 1002 tmp = [lt, lb, rt, rb] 1003 maxi = max(tmp) 1004 mini = min(tmp) 1005 self.__offset = 0 - mini 1006 if self.__grad[0] != 0 or self.__grad[1] != 0: 1007 self.__koef = self.__max_minus / (maxi - mini) 1008 else: 1009 self.__koef = 0 1010 self.__max_minus = 0
1011