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