Example #1
0
    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
Example #2
0
    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
Example #3
0
 def testGetOutOfRangeCellTooHighIndexFromCoordinate(self):
     maze = Maze(1, 1)
     with self.assertRaises(
             ValueError,
             msg="Coordinate (72, 0) is not valid.",
     ):
         _ = maze.getIndexFromCoordinates(XY(72, 0))
Example #4
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)
Example #7
0
    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(),
        )
Example #8
0
    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
Example #9
0
    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)
Example #10
0
    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
Example #11
0
    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)
Example #12
0
    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.")
Example #13
0
    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)
Example #14
0
    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
Example #15
0
            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)
Example #16
0
 def testGetFirstCellIndexFromCoordinates(self):
     maze = Maze(2, 2)
     index = maze.getIndexFromCoordinates(XY(0, 0))
     self.assertEqual(index, 0)
Example #17
0
 def testGetCentreCellIndexFromCoordinates(self):
     maze = Maze(3, 3)
     index = maze.getIndexFromCoordinates(XY(1, 1))
     self.assertEqual(index, 4)
Example #18
0
            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)
Example #19
0
            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)
Example #20
0
 def testGetLastCellIndexFromCoordinates(self):
     maze = Maze(3, 3)
     index = maze.getIndexFromCoordinates(XY(2, 2))
     self.assertEqual(index, 8)
Example #21
0
    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
Example #22
0
    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)