def getNeighboursOfCell(self, cellIndex: int) -> List[int]: """Return a list of indices of the neighbour of a cell at specified index. Args: cellIndex (int): The index of the cell whose neighbours you want. Returns: List[int]: The list of indices of its neighbours. """ # empty list of neighbours thisCellsNeighboursIndices: List[int] = [] # create (unvalidated) indices of this cell's neighbours coordinates = self.__getCoordinateFromIndex(cellIndex) unvalidatedNeighbourCoordinates = [ XY(coordinates.x, coordinates.y - 1), # north XY(coordinates.x, coordinates.y + 1), # south XY(coordinates.x - 1, coordinates.y), # west XY(coordinates.x + 1, coordinates.y), # east ] # only add the neighbours that are not out of range of maze for thisCoordinate in unvalidatedNeighbourCoordinates: # if the coordinate is valid... if self.__coordinateIsValid(thisCoordinate): # then calculate its index indexOfCellAtThisCoordinate = self.__getIndexFromCoordinate( thisCoordinate) # and add it to this cell's neighbours thisCellsNeighboursIndices.append(indexOfCellAtThisCoordinate) return thisCellsNeighboursIndices
def generate(self) -> MazeProtocol: # generate a simply-connected maze with the recursive backtracker self.__maze = RecursiveBacktracker(self.__size).generate() # the probability of removing each wall probabilityOfRemoveWall = 0.2 for x in range(0, self.__size.x): for y in range(0, self.__size.y): currentPosition = XY(x, y) currentIndex = self.__maze.getIndexFromCoordinates( currentPosition) connectionsWithoutWalls = set( self.__maze.getConnectionsOfCellAtIndex(currentIndex)) allNeighbours = set( self.__maze.getNeighboursOfCell(currentIndex)) walledNeighbours = allNeighbours - connectionsWithoutWalls for neighbour in walledNeighbours: # get a true or false based off the probability of before shouldDeleteThisWall = random.random( ) < probabilityOfRemoveWall if shouldDeleteThisWall: # delete the wall between the two cells self.__maze.removeWallBetween( cellAIndex=currentIndex, cellBIndex=neighbour, ) return self.__maze
def testGetOutOfRangeCellTooHighIndexFromCoordinate(self): maze = Maze(1, 1) with self.assertRaises( ValueError, msg="Coordinate (72, 0) is not valid.", ): _ = maze.getIndexFromCoordinates(XY(72, 0))
def getNeighbourCoordinatesOfCell(self, cellIndex: int) -> Iterator[XY]: # get the coordinates of this cell coordinates = self.getCoordinatesFromIndex(cellIndex) # make a list of neighbours by filtering the invalid neighbours neighbours = filter( self.__checkCoordinateIsValid, [ XY(coordinates.x, coordinates.y - 1), # north XY(coordinates.x, coordinates.y + 1), # south XY(coordinates.x - 1, coordinates.y), # west XY(coordinates.x + 1, coordinates.y), # east ], ) return neighbours
def __init__( self, parent: Optional[QWidget] = None, *args: Tuple[Any, Any], **kwargs: Tuple[Any, Any], ) -> None: """ Grouped controls box for generating mazes """ super(GenerateMazeGroupView, self).__init__(parent=parent, *args, **kwargs) self.setContentsMargins(0, 0, 0, 0) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) groupbox = QGroupBox("Generate Maze") layout.addWidget(groupbox) vbox = QFormLayout() groupbox.setLayout(vbox) self.__mazeSizePicker = XYPicker( minimum=XY(2, 2), maximum=XY(250, 250), initialValue=XY(2, 2), parent=self, ) self.__simplyConnectedCheckbox = QCheckBox() self.__simplyConnectedCheckbox.setChecked(True) generateButton = QPushButton("Generate") generateButton.clicked.connect( self.__onGenerateButtonPressed) # type: ignore vbox.addRow("Size", self.__mazeSizePicker) vbox.addRow("Simply Connected", self.__simplyConnectedCheckbox) vbox.addRow(generateButton) self.setLayout(layout)
def testMazeIsLoadedCorrectly(self): FILEPATH = self.FILE_PATH_PREFIX + "test_save.maze" size = XY(13, 3) # save the maze fileHandler = MazeFileHandler(FILEPATH) fileHandler.save(Maze(size.x, size.y, False)) # try to load the maze newMaze = fileHandler.load() self.assertEqual(newMaze.size, size)
def getValues(self) -> XY: """Get the X and Y values of this input widget. Returns ------- Tuple[int, int] The X and Y values of the number picker spin boxes. """ return XY( self.__xSpinBox.value(), self.__ySpinBox.value(), )
def getConnectionsAndDirectionsOfConnectionsOfCellAtIndex( self, cellIndex: int) -> List[Tuple[XY, AbsoluteDirection]]: # get the directions of the connections of a cell at index # check the given index is valid self.__checkIndexIsValidWithException(cellIndex) # get a list of the connections of this cell connectionIndices = self.getConnectionsOfCellAtIndex(cellIndex) # get this cell's coordinates coordinates = self.__getXYFromIndex(cellIndex) unvalidatedNeighbourCoordinates = { XY(coordinates.x, coordinates.y - 1): AbsoluteDirection.north, # north XY(coordinates.x, coordinates.y + 1): AbsoluteDirection.south, # south XY(coordinates.x - 1, coordinates.y): AbsoluteDirection.west, # west XY(coordinates.x + 1, coordinates.y): AbsoluteDirection.east, # east } neighbourCoordinates: List[XY] = [] for connectionIndex in connectionIndices: # add this neighbour's coordinate to the list of neighbour coordinates neighbourCoordinates.append( self.getCoordinatesFromIndex(connectionIndex)) coordinatesAndDirections: List[Tuple[XY, AbsoluteDirection]] = [] # get the north, east, south, west from each coordinate for coordinate in neighbourCoordinates: # create a tuple of (COORDINATE and DIRECTION) and append it to the `coordinatesAndDirections` list coordinatesAndDirections.append( (coordinate, unvalidatedNeighbourCoordinates[coordinate])) return coordinatesAndDirections
def __getCoordinateFromIndex(self, index: int) -> XY: """Calculate the coordinates of a cell at a given index Args: index (int): The index of the cell whose coordinates you want. Returns: XY: The coordinates of the cell. """ Y = floor(index / self.size.y) X = index % self.size.x return XY(X, Y)
def __checkIndexIsValidWithException(self, cellIndex: int) -> bool: if not self.__checkIndexIsValid(cellIndex): # it is not valid so raise error # calculate the index of the last cell in maze lastCellCoordinate = XY(self.size.x - 1, self.size.y - 1) lastCellIndex = self.getIndexFromCoordinates(lastCellCoordinate) # raise appropriate error with message: raise IndexError( f"`cellIndex` out of range ({cellIndex} not between 0 and {lastCellIndex})." ) return True
def __init__(self, size: XY) -> None: # check size is valid self._testSizeIsValidWithException(size) self.__size = size # initialize a Maze full of walls self.__maze = Maze(self.__size.x, self.__size.y, walls=True) # get the last cell index of the maze, so we can initialise __visitedOrNotCells to a list of False (nothing visited yet) lastIndex = self.__maze.getIndexFromCoordinates( XY(self.__size.x - 1, self.__size.y - 1)) # init __visitedOrNotCells to a list of False with the length of the max index of cells self.__visitedOrNotCells = [False] * (lastIndex + 1)
def _testSizeIsValidWithException(self, size: XY) -> None: """Check that a given size for a maze is valid. A valid maze size is at least 1 in each dimension (X and Y). Parameters ---------- size : XY The given size. Raises ------ ValueError Size is invalid. """ valid = True # size must be at least (1, 1) if size.toTuple()[0] <= 1: valid = False elif size.toTuple()[1] <= 1: valid = False if not valid: raise ValueError( "Maze size must be at least (1, 1) in each dimension.")
def __init__(self, sizeX: int, sizeY: int, fullyConnected: bool = False): """Initialize a maze from a given size. The given maze will have all walls – i.e., no cells will have connections between them. This would look like a grid. Args: size (XY): The XY size of the desired maze. fullyConnected(bool, optional): Whether to create a fully connected maze (i.e., no walls between adjacent cells). Defaults to False. """ size = XY(sizeX, sizeY) self.size = size # initialize `self.__maze` to an empty graph so we can operate on it self.__maze = Graph[MazeCell].createSquareGraph(size.x, size.y) # initialize as list of maze cells for cellIndex in range(len(self.__maze)): # set this cell's node data to a new MazeCell with index self.__maze.setNodeData( index=cellIndex, newValue=MazeCell(cellIndex), ) if fullyConnected: # list of indices for this cell's neighbours thisCellsNeighboursIndices: List[ int] = self.getNeighboursOfCell(cellIndex) # and add its neighbouring cells to the graph for connection in thisCellsNeighboursIndices: # bidirectional FALSE because the other cell will add this one later on # and we don't want multiple of the same cell in the list of neighbours if not self.__maze.connectionExistsFrom( cellIndex, connection): self.__maze.addLinkBetween(cellIndex, connection, bidirectional=False)
def __createMazePath(self) -> QPainterPath: cellSize = ( self.width() / self.__maze.size.x, self.height() / self.__maze.size.y, ) path = QPainterPath(QPointF(0, 0)) # draw outline of maze path.addRect( 0, 0, self.width() - 1, self.height() - 1, ) for y in range(self.__maze.size.y): currentY = y * cellSize[1] for x in range(self.__maze.size.x): currentX = x * cellSize[0] # get the list of walls surrounding this cell thisCellsWalls: Set[ AbsoluteDirection] = self.__maze.getWallsOfCellAtCoordinate( XY(x, y)) # draw north and west walls only, because the next iterations will draw the south and east walls for us (don't wanna waste paint /s) if AbsoluteDirection.west in thisCellsWalls: path.moveTo(currentX, currentY) path.lineTo(currentX, currentY + cellSize[1]) if AbsoluteDirection.north in thisCellsWalls: path.moveTo(currentX, currentY) path.lineTo(currentX + cellSize[0], currentY) return path
humanDescription=commandHumanDescription, commandType=commandType, commandResult=result, ) # log the command and result logging.debug(command.humanDescription) if command.commandResult is not None: logging.debug(command.commandResult.humanDescription) return (command, result) if __name__ == "__main__": # Test out the Pledge maze solver maze = NonSimple(XY(5, 5)).generate() pledge = PledgeSolver(maze, XY(2, 3), XY(0, 0)) FORMAT = "%(asctime)s - %(name)-20s - %(levelname)-5s - %(message)s" LEVEL = 0 logging.basicConfig(format=FORMAT, level=LEVEL) logging.getLogger().setLevel(LEVEL) log = logging.getLogger() from time import sleep while True: result = pledge.advance() sleep(0.25)
def testGetFirstCellIndexFromCoordinates(self): maze = Maze(2, 2) index = maze.getIndexFromCoordinates(XY(0, 0)) self.assertEqual(index, 0)
def testGetCentreCellIndexFromCoordinates(self): maze = Maze(3, 3) index = maze.getIndexFromCoordinates(XY(1, 1)) self.assertEqual(index, 4)
forward.success, f"Turned {movementDirection} and attempted to move forward", solver._state, ) command = MazeSolverCommand( "Move randomly", MazeSolverCommandType.movement, result, ) return (command, result) if __name__ == "__main__": # Test out the random mouse maze solver from modules.data_structures.maze.maze import Maze maze = Maze(10, 10, False) rm = RandomMouse(maze, XY(0, 0), XY(9, 9)) FORMAT = "%(asctime)s - %(name)-20s - %(levelname)-5s - %(message)s" LEVEL = 0 logging.basicConfig(format=FORMAT, level=LEVEL) logging.getLogger().setLevel(LEVEL) log = logging.getLogger() while True: rm.advance() print(rm.getCurrentState().currentCell)
result.success, result.humanDescription, solver._state, ) command = MazeSolverCommand( humanDescription=command.humanDescription, commandType=command.commandType, commandResult=wallFollowerCommandResult, ) return (command, wallFollowerCommandResult) if __name__ == "__main__": # Test out the wall follower maze solver from modules.data_structures.maze.maze import Maze maze = Maze(10, 10, False) wf = WallFollower(maze, XY(0, 0), XY(9, 9)) FORMAT = "%(asctime)s - %(name)-20s - %(levelname)-5s - %(message)s" LEVEL = 0 logging.basicConfig(format=FORMAT, level=LEVEL) logging.getLogger().setLevel(LEVEL) log = logging.getLogger() while True: wf.advance() print(wf.getCurrentState().currentCell)
def testGetLastCellIndexFromCoordinates(self): maze = Maze(3, 3) index = maze.getIndexFromCoordinates(XY(2, 2)) self.assertEqual(index, 8)
def generate(self) -> MazeProtocol: # start our maze generator in the top left of the maze start = XY(0, 0) # init positionsStack to a new empty stack of type int self.__positionsStack = Stack[int]() # Convert the `start` position to the same cell's index in the maze, and push to the positions stack self.__positionsStack.push( # get the index of the `start` posotion self.__maze.getIndexFromCoordinates(start)) # set the starting cell as visited self.__visitedOrNotCells[self.__positionsStack.peek()] = True # while the positions stack is not empty, so we automatically exit the loop when visited all cells and back at start while not self.__positionsStack.isEmpty(): randomCellChoice: Optional[int] = None # this loop tries to find a random cell to visit out of the unvisited neighbours of the current cell while randomCellChoice is None: # if we've ran out of positions to backtrack to, and therefore made the entire maze if self.__positionsStack.isEmpty(): break # get a list of unvisited adjacent cells neighbourCells = self.__maze.getNeighboursOfCell( # get the current position by peeking the stack. # don't pop because we want the item to remain on the stack, # so we can backtrach through it. self.__positionsStack.peek()) # Filter the neighbourCells by whether they've been visited or not # create a lambda to return whether or not a cell at index has been visited, but return the inverse because we are _filtering_ the cells! checkIsVisited: Callable[ [int], bool] = lambda cellIndex: not self.__visitedOrNotCells[ cellIndex] # …and filter the neighbourCells by this lambda, and put it into the list `unvisitedWalls` unvisitedWalls = list(filter(checkIsVisited, neighbourCells)) # check that there are unvisited walls if len(unvisitedWalls) > 0: # choose a random wall randomCellChoice = randomChoice(unvisitedWalls) # set the next cell to visited self.__visitedOrNotCells[randomCellChoice] = True else: # all the cells here have been visited # so back up to the last cell, by popping the positionsStack self.__positionsStack.pop() # if the cell hasn't been chosen, and therefore we've explored the whole maze if randomCellChoice is None: # break so we can return the completed maze break # carve a passage through to the adjacent cell self.__maze.removeWallBetween( cellAIndex=self.__positionsStack.peek(), cellBIndex=randomCellChoice, ) # push the choice to the positionsStack so it is our next one self.__positionsStack.push(randomCellChoice) return self.__maze
def __init__( self, onPlayButtonPressed: Callable[[], None], onPauseButtonPressed: Callable[[], None], onStepButtonPressed: Callable[[], None], onRestartButtonPressed: Callable[[], None], onSpeedControlValueChanged: Callable[[int], None], onOpenLogButtonPressed: Callable[[], None], onAgentVarsButtonPressed: Callable[[], None], onSolveButtonPressed: Callable[[MazeSolverSpecification], None], mazeSize: XY, parent: Optional[QWidget] = None, *args: Tuple[Any, Any], **kwargs: Tuple[Any, Any], ) -> None: """ Grouped controls box for controls for solving mazes """ self.__onSolveButtonPressed = onSolveButtonPressed self.__mazeSize = mazeSize self.__maximumXY = XY( self.__mazeSize.x - 1, self.__mazeSize.y - 1, ) super().__init__(parent=parent, *args, **kwargs) self.setContentsMargins(0, 0, 0, 0) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) groupbox = QGroupBox("Solve Maze") layout.addWidget(groupbox) vbox = QFormLayout() groupbox.setLayout(vbox) self.__startPosition = XYPicker( minimum=XY(0, 0), maximum=self.__maximumXY, initialValue=XY(0, 0), parent=self, label="•", ) self.__endPosition = XYPicker( minimum=XY(0, 0), maximum=self.__maximumXY, initialValue=self.__maximumXY, parent=self, label="•", ) self.__solverTypePicker = QComboBox(self) self.__solverTypePicker.addItem("Wall Follower", WallFollower) self.__solverTypePicker.addItem("Pledge Solver", PledgeSolver) self.__solverTypePicker.addItem("Random Mouse", RandomMouse) solveButton = QPushButton("Solve") solveButton.clicked.connect( # type: ignore lambda: self.__onSolveButtonPressed( MazeSolverSpecification( startPosition=self.__startPosition.getValues(), endPosition=self.__endPosition.getValues(), solverType=self.__solverTypePicker.currentData(), ), ) ) self.__solverControlsDropdown = SolverControlsView( onPlayButtonPressed=onPlayButtonPressed, onPauseButtonPressed=onPauseButtonPressed, onStepButtonPressed=onStepButtonPressed, onRestartButtonPressed=onRestartButtonPressed, onSpeedControlValueChanged=onSpeedControlValueChanged, onOpenLogButtonPressed=onOpenLogButtonPressed, onAgentVarsButtonPressed=onAgentVarsButtonPressed, parent=self, ) # connect enable/disable signal to child view self.setMazeSolverControlsEnabled.connect( self.__solverControlsDropdown.setMazeSolverControlsEnabled ) vbox.addRow("Start Position", self.__startPosition) vbox.addRow("End Position", self.__endPosition) vbox.addRow("Solver Type", self.__solverTypePicker) vbox.addRow(solveButton) vbox.addRow(self.__solverControlsDropdown) self.setLayout(layout)