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