def test_rectangle_contains():
    """Test the rectangle can find a containing point."""
    rectangle = Rectangle(width=4, height=5, x=2, y=1)
    contains = rectangle.contains(x=3, y=3)
    assert contains is True
def test_rectangle_does_not_contains():
    """Test the rectangle can determine a point is not contained."""
    rectangle = Rectangle(width=4, height=5, x=2, y=1)
    contains = rectangle.contains(x=-4, y=3)
    assert contains is False
Exemple #3
0
class Dungeon(object):
    """A class to contain all the information about a dungeon that will be generated."""

    DUNGEON_ROOM = ' '
    DUNGEON_HALL = '*'
    DUNGEON_WALL = 'X'
    DUNGEON_DOORWAY = '$'

    def __init__(self,
                 width,
                 height,
                 random_seed=None,
                 room_attempts=100,
                 max_room_width=10,
                 max_room_height=10,
                 min_room_width=5,
                 min_room_height=5,
                 randomness=0.5):
        """The __init__ function for the Dungeon class."""
        self._width = width
        self._height = height
        self._room_attempts = room_attempts
        self._max_room_width = max_room_width
        self._min_room_width = min_room_width
        self._max_room_height = max_room_height
        self._min_room_height = min_room_height
        self.dungeon = []  # This is in the form dungeon[y][x]

        self._randomness = randomness

        self._dungeon_rectangle = Rectangle(width=self._width,
                                            height=self._height)
        self.rooms = []

        if random_seed is not None:
            random.seed(random_seed)

        # Fill the Dungeon Array
        for _ in range(self._height):
            row = [self.DUNGEON_WALL for _ in range(self._width)]
            self.dungeon.append(row)

        # Add Rooms
        self._generate_rooms()

        # Add Passages
        self._carve_passages()
        self._open_rooms()

        # Remove Dead Ends
        self._remove_dead_ends()

    @property
    def width(self):
        """The width of the dungeon."""
        return self._width

    @property
    def height(self):
        """The height of the dungeon."""
        return self._height

    @property
    def randomness(self):
        """The randomness of the dungeon passages."""
        if self._randomness > 1:
            return 1
        if self._randomness < 0:
            return 0
        return self._randomness

    def _set_dungeon_cell(self, x, y, value):
        """A function to set the value of a specified dungeon cell.

        Args:
            x: The specified x coordinate.
            y: The specified y coordinate.
            value: The new value of the cell.
        """
        self.dungeon[y][x] = value

    def _get_dungeon_cell(self, x, y):
        """A function to get the value of a specified dungeon cell."""
        return self.dungeon[y][x]

    def _generate_rooms(self):
        """A function to generate random rooms."""
        for _ in range(self._room_attempts):
            # Get Random Dimensions for New Room
            x = random.randint(1, self._width - 1)
            y = random.randint(1, self._height - 1)
            room_width = random.randint(self._min_room_width,
                                        self._max_room_width)
            room_height = random.randint(self._min_room_height,
                                         self._max_room_height)
            new_room = Rectangle(width=room_width,
                                 height=room_height,
                                 x=x,
                                 y=y)

            # Make sure the room doesn't overlap other rooms
            overlaps = False
            for room in self.rooms:
                if room.overlaps(new_room):
                    overlaps = True
                    break

            in_dungeon = False
            if not (not self._dungeon_rectangle.contains(
                    new_room.x, new_room.y)
                    or not self._dungeon_rectangle.contains(
                        new_room.x_max, new_room.y_max)):
                in_dungeon = True
            if not overlaps and in_dungeon:
                self.rooms.append(new_room)
        # Add Rooms to Dungeons
        for room in self.rooms:
            for x in range(room.x, room.x + room.width):
                for y in range(room.y, room.y + room.height):
                    self._set_dungeon_cell(x, y, self.DUNGEON_ROOM)

    def _carve_passages(self):
        """A function to carve passages in the maze."""
        # Loop through the dungeon to create maze.
        for y in range(1, self._height):
            for x in range(1, self._width):
                self._carve_passage_helper(x, y)

    def _carve_passage_helper(self, start_x, start_y, last_direction=None):
        """A helper for carving the dungeon passages.

        Args:
            start_x: the starting x coordinate for the passageway.
            start_y: the starting y coordinate for the passageway.
            last_direction: the last direction a passage was carved.
        """
        # Check if the cell can be carved
        if not self._can_carve_passage(start_x, start_y, last_direction):
            return

        # Carve the current cell
        self._set_dungeon_cell(start_x, start_y, self.DUNGEON_HALL)

        available_directions = [
            direction for direction in directions.DPAD_DIRECTIONS
            if self._can_carve_passage(start_x + direction.x, start_y +
                                       direction.y, direction)
        ]
        # Pick a new direction
        while len(available_directions) > 0:
            change_direction = random.random() < self._randomness
            if last_direction not in available_directions:
                change_direction = True

            new_direction = last_direction
            if change_direction or new_direction is None:
                new_direction_index = random.randint(
                    0,
                    len(available_directions) - 1)
                new_direction = available_directions[new_direction_index]
            available_directions.remove(new_direction)
            self._carve_passage_helper(start_x + new_direction.x,
                                       start_y + new_direction.y,
                                       new_direction)

    def _can_carve_passage(self, x, y, incoming_direction=None):
        """Determine if a passage can be carved at a specified location in the dungeon.

        Args:
            x: the x coordinate of the cell to check
            y: the y coordinate of the cell to check
            incoming_direction: The direction that the passage has been moving.

        Returns:
            True if the cell can be carved, or false if it cannot.
        """
        # Make sure the section is a wall
        if not self._get_dungeon_cell(x, y) == self.DUNGEON_WALL:
            return False
        # Check surrounding cells
        for direction in directions.CARDINAL_DIRECTIONS:
            # Check the coordinates are in the dungeon
            if not self._dungeon_rectangle.contains(x + direction.x,
                                                    y + direction.y):
                return False
            # We can ignore cells behind us.
            if incoming_direction is not None:
                if incoming_direction.y == directions.UP.y or incoming_direction.y == directions.DOWN.y:
                    if incoming_direction.y * -1 == direction.y:
                        continue
                if incoming_direction.x == directions.LEFT.x or incoming_direction.x == directions.RIGHT.x:
                    if incoming_direction.x * -1 == direction.x:
                        continue
            # Check the coordinates are a wall
            if not self._get_dungeon_cell(
                    x + direction.x, y + direction.y) == self.DUNGEON_WALL:
                return False
        return True

    def _open_rooms(self):
        """A function to open the rooms to the passageways."""
        # Loop Through Rooms
        for room in self.rooms:
            possible_directions = [x for x in directions.DPAD_DIRECTIONS]
            multiple_openings = random.random() > 0.75
            has_opening = False
            while len(possible_directions) > 0:
                direction = possible_directions[random.randint(
                    0,
                    len(possible_directions) - 1)]
                possible_directions.remove(direction)

                possible_locations = []
                if direction == directions.UP:
                    possible_locations = [
                        (x, room.y_min - 1)
                        for x in range(room.x_min, room.x_max)
                    ]
                elif direction == directions.DOWN:
                    possible_locations = [
                        (x, room.y_max) for x in range(room.x_min, room.x_max)
                    ]
                elif direction == directions.LEFT:
                    possible_locations = [
                        (room.x_min - 1, y)
                        for y in range(room.y_min, room.y_max)
                    ]
                elif direction == directions.RIGHT:
                    possible_locations = [
                        (room.x_max, y) for y in range(room.y_min, room.y_max)
                    ]

                # Loop through random locations till you find one that can be opened.
                while len(possible_locations) > 0:
                    location = possible_locations[random.randint(
                        0,
                        len(possible_locations) - 1)]
                    possible_locations.remove(location)
                    if self._can_carve_entry(location[0], location[1],
                                             direction):
                        self._set_dungeon_cell(location[0], location[1],
                                               self.DUNGEON_DOORWAY)
                        has_opening = True
                        break

                # Break if done
                if has_opening and not multiple_openings:
                    break

    def _can_carve_entry(self, x, y, incoming_direction):
        # Is the cell I am trying to change a wall
        if self._get_dungeon_cell(x, y) != self.DUNGEON_WALL:
            return False

        # Is the next cell in the rectangle
        if not self._dungeon_rectangle.contains(x + incoming_direction.x,
                                                y + incoming_direction.y):
            return False

        # Does the room I am trying to change connect to a room or hall?
        if not self._get_dungeon_cell(x + incoming_direction.x, y + incoming_direction.y) == self.DUNGEON_HALL and \
                not self._get_dungeon_cell(x + incoming_direction.x, y + incoming_direction.y) == self.DUNGEON_ROOM:
            return False

        if incoming_direction == directions.UP or incoming_direction == directions.DOWN:
            if not self._get_dungeon_cell(x - 1, y) == self.DUNGEON_WALL or \
                    not self._get_dungeon_cell(x + 1, y) == self.DUNGEON_WALL:
                return False
        if incoming_direction == directions.LEFT or incoming_direction == directions.RIGHT:
            if not self._get_dungeon_cell(x, y - 1) == self.DUNGEON_WALL or \
                    not self._get_dungeon_cell(x, y + 1) == self.DUNGEON_WALL:
                return False
        return True

    def _remove_dead_ends(self):
        """Remove maze dead ends."""
        for y in range(0, self._height):
            for x in range(0, self._width):
                self._remove_dead_ends_helper(x, y)

    def _remove_dead_ends_helper(self, x, y):
        cell = self._get_dungeon_cell(x, y)
        if cell == self.DUNGEON_HALL:
            d_pad_directions = [x for x in directions.DPAD_DIRECTIONS]
            connections = []
            for direction in d_pad_directions:
                connected_cell = self._get_dungeon_cell(
                    x + direction.x, y + direction.y)
                if connected_cell in [
                        self.DUNGEON_HALL, self.DUNGEON_DOORWAY,
                        self.DUNGEON_ROOM
                ]:
                    connections.append((x + direction.x, y + direction.y))
            if len(connections) <= 1:
                self._set_dungeon_cell(x, y, self.DUNGEON_WALL)
                if len(connections) == 1:
                    self._remove_dead_ends_helper(connections[0][0],
                                                  connections[0][1])

    def print_dungeon(self):
        """A function to print the dungeon to standard out."""
        # Print Room in dungeon
        for y in range(0, self._height):
            for x in range(0, self._width):
                print(self._get_dungeon_cell(x, y), end='')
            print('')