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