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

Source Code for Module kuimaze.maze

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