Doors #156
@@ -2,17 +2,17 @@
 | 
			
		||||
    #######                    #############        
 | 
			
		||||
    #.H...#                    #...........#        
 | 
			
		||||
    #.....#                #####...........#        
 | 
			
		||||
    #.....#                #............H..#        
 | 
			
		||||
    #.....#                #...&........H..#        
 | 
			
		||||
    #.#####                #.###...........#        
 | 
			
		||||
    #.#                    #.# #...........#        
 | 
			
		||||
    #.#                    #.# #############        
 | 
			
		||||
    #.#                    #.#                      
 | 
			
		||||
    #.####                 #.#                      
 | 
			
		||||
    #....#                 #.#                      
 | 
			
		||||
    ####.###################.#                      
 | 
			
		||||
    ####&###################&#                      
 | 
			
		||||
       #.....................#     #################
 | 
			
		||||
       #.....................#     #...............#
 | 
			
		||||
       #.....................#######...............#
 | 
			
		||||
       #...........................................#
 | 
			
		||||
       #.....................&.....&...............#
 | 
			
		||||
       #.....................#######...............#
 | 
			
		||||
       #######################     #################
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,7 @@ TexturePack.ASCII_PACK = TexturePack(
 | 
			
		||||
    BOW=')',
 | 
			
		||||
    CHEST='□',
 | 
			
		||||
    CHESTPLATE='(',
 | 
			
		||||
    DOOR='&',
 | 
			
		||||
    EAGLE='µ',
 | 
			
		||||
    EMPTY=' ',
 | 
			
		||||
    EXPLOSION='%',
 | 
			
		||||
@@ -124,6 +125,8 @@ TexturePack.SQUIRREL_PACK = TexturePack(
 | 
			
		||||
    BOW='🏹',
 | 
			
		||||
    CHEST='🧰',
 | 
			
		||||
    CHESTPLATE='🦺',
 | 
			
		||||
    DOOR=('🚪', curses.COLOR_WHITE, (1000, 1000, 1000),
 | 
			
		||||
          curses.COLOR_WHITE, (1000, 1000, 1000)),
 | 
			
		||||
    EAGLE='🦅',
 | 
			
		||||
    EMPTY='  ',
 | 
			
		||||
    EXPLOSION='💥',
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ from random import randint
 | 
			
		||||
from typing import Dict, Optional, Tuple
 | 
			
		||||
 | 
			
		||||
from .items import Item
 | 
			
		||||
from ..interfaces import FightingEntity, InventoryHolder
 | 
			
		||||
from ..interfaces import FightingEntity, InventoryHolder, Tile
 | 
			
		||||
from ..translations import gettext as _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -152,6 +152,12 @@ class Player(InventoryHolder, FightingEntity):
 | 
			
		||||
                    return True
 | 
			
		||||
                elif entity.is_item():
 | 
			
		||||
                    entity.hold(self)
 | 
			
		||||
        tile = self.map.tiles[y][x]
 | 
			
		||||
        if tile == Tile.DOOR and move_if_possible:
 | 
			
		||||
            # Open door
 | 
			
		||||
            self.map.tiles[y][x] = Tile.FLOOR
 | 
			
		||||
            self.map.compute_visibility(y, x, self.vision)
 | 
			
		||||
            return super().check_move(y, x, move_if_possible)
 | 
			
		||||
        return super().check_move(y, x, move_if_possible)
 | 
			
		||||
 | 
			
		||||
    def save_state(self) -> dict:
 | 
			
		||||
 
 | 
			
		||||
@@ -199,7 +199,9 @@ class Game:
 | 
			
		||||
            self.map_index = 0
 | 
			
		||||
            return
 | 
			
		||||
        while self.map_index >= len(self.maps):
 | 
			
		||||
            self.maps.append(broguelike.Generator().run())
 | 
			
		||||
            m = broguelike.Generator().run()
 | 
			
		||||
            m.logs = self.logs
 | 
			
		||||
            self.maps.append(m)
 | 
			
		||||
        new_map = self.map
 | 
			
		||||
        new_map.floor = self.map_index
 | 
			
		||||
        old_map.remove_entity(self.player)
 | 
			
		||||
@@ -417,6 +419,7 @@ class Game:
 | 
			
		||||
            self.maps = [Map().load_state(map_dict) for map_dict in d["maps"]]
 | 
			
		||||
            for i, m in enumerate(self.maps):
 | 
			
		||||
                m.floor = i
 | 
			
		||||
                m.logs = self.logs
 | 
			
		||||
        except KeyError as error:
 | 
			
		||||
            self.message = _("Some keys are missing in your save file.\n"
 | 
			
		||||
                             "Your save seems to be corrupt. It got deleted.")\
 | 
			
		||||
 
 | 
			
		||||
@@ -390,6 +390,7 @@ class Tile(Enum):
 | 
			
		||||
    WALL = auto()
 | 
			
		||||
    FLOOR = auto()
 | 
			
		||||
    LADDER = auto()
 | 
			
		||||
    DOOR = auto()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def from_ascii_char(ch: str) -> "Tile":
 | 
			
		||||
@@ -430,7 +431,7 @@ class Tile(Enum):
 | 
			
		||||
        """
 | 
			
		||||
        Is this Tile a wall?
 | 
			
		||||
        """
 | 
			
		||||
        return self == Tile.WALL
 | 
			
		||||
        return self == Tile.WALL or self == Tile.DOOR
 | 
			
		||||
 | 
			
		||||
    def is_ladder(self) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
 
 | 
			
		||||
@@ -26,9 +26,11 @@ DEFAULT_PARAMS = {
 | 
			
		||||
    "spawn_per_region": [1, 2],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def dist(level, y1, x1, y2, x2):
 | 
			
		||||
 | 
			
		||||
def dist(level: List[List[Tile]], y1: int, x1: int, y2: int, x2: int) -> int:
 | 
			
		||||
    """
 | 
			
		||||
    Compute the minimum walking distance between points (y1, x1) and (y2, x2) on a Tile grid
 | 
			
		||||
    Compute the minimum walking distance between points (y1, x1) and (y2, x2)
 | 
			
		||||
    on a Tile grid
 | 
			
		||||
    """
 | 
			
		||||
    # simple breadth first search
 | 
			
		||||
    copy = [[t for t in row] for row in level]
 | 
			
		||||
@@ -60,9 +62,9 @@ class Generator:
 | 
			
		||||
                  room: List[List[Tile]], door_y: int, door_x: int,
 | 
			
		||||
                  dy: int, dx: int) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Using point (door_y, door_x) in the room as a reference and placing it 
 | 
			
		||||
        Using point (door_y, door_x) in the room as a reference and placing it
 | 
			
		||||
        over point (y, x) in the level, returns whether or not the room fits
 | 
			
		||||
        here 
 | 
			
		||||
        here
 | 
			
		||||
        """
 | 
			
		||||
        lh, lw = len(level), len(level[0])
 | 
			
		||||
        rh, rw = len(room), len(room[0])
 | 
			
		||||
@@ -93,12 +95,11 @@ class Generator:
 | 
			
		||||
    def place_room(level: List[List[Tile]], y: int, x: int,
 | 
			
		||||
                   room: List[List[Tile]], door_y: int, door_x: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Mutates level in place to add the room. Placement is determined by 
 | 
			
		||||
        Mutates level in place to add the room. Placement is determined by
 | 
			
		||||
        making (door_y, door_x) in the room correspond with (y, x) in the level
 | 
			
		||||
        """
 | 
			
		||||
        rh, rw = len(room), len(room[0])
 | 
			
		||||
        # maybe place Tile.DOOR here instead ?
 | 
			
		||||
        level[y][x] = Tile.FLOOR
 | 
			
		||||
        level[y][x] = Tile.DOOR
 | 
			
		||||
        for ry in range(rh):
 | 
			
		||||
            for rx in range(rw):
 | 
			
		||||
                if room[ry][rx] == Tile.FLOOR:
 | 
			
		||||
@@ -107,11 +108,11 @@ class Generator:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def add_loop(level: List[List[Tile]], y: int, x: int) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Try to add a corridor between two far apart floor tiles, passing 
 | 
			
		||||
        Try to add a corridor between two far apart floor tiles, passing
 | 
			
		||||
        through point (y, x).
 | 
			
		||||
        """
 | 
			
		||||
        h, w = len(level), len(level[0])
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if level[y][x] != Tile.EMPTY:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
@@ -129,8 +130,8 @@ class Generator:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            def verify_sides() -> bool:
 | 
			
		||||
                # switching up dy and dx here pivots the axis, so 
 | 
			
		||||
                # (y+dx, x+dy) and (y-dx, x-dy) are the tiles adjacent to 
 | 
			
		||||
                # switching up dy and dx here pivots the axis, so
 | 
			
		||||
                # (y+dx, x+dy) and (y-dx, x-dy) are the tiles adjacent to
 | 
			
		||||
                # (y, x), but not on the original axis
 | 
			
		||||
                for delta_x, delta_y in [[dy, dx], [-dy, -dx]]:
 | 
			
		||||
                    for i in range(1, y2 - y1 + x2 - x1):
 | 
			
		||||
@@ -195,8 +196,8 @@ class Generator:
 | 
			
		||||
                   dy: int, dx: int, length: int) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
        Tries to build the exit from the room at given coordinates
 | 
			
		||||
        Depending on parameter length, it will either attempt to build a 
 | 
			
		||||
        simple door, or a long corridor.  Return value is a boolean 
 | 
			
		||||
        Depending on parameter length, it will either attempt to build a
 | 
			
		||||
        simple door, or a long corridor.  Return value is a boolean
 | 
			
		||||
        signifying whether or not the exit was successfully built
 | 
			
		||||
        """
 | 
			
		||||
        rh, rw = len(room), len(room[0])
 | 
			
		||||
@@ -248,15 +249,15 @@ class Generator:
 | 
			
		||||
            if room[y][x] == Tile.EMPTY and \
 | 
			
		||||
                    Generator.build_door(room, y, x, dy, dx, length):
 | 
			
		||||
                break
 | 
			
		||||
        else:
 | 
			
		||||
            return None, None
 | 
			
		||||
        else:  # pragma: no cover
 | 
			
		||||
            return None, None, None, None
 | 
			
		||||
 | 
			
		||||
        return y + length * dy, x + length * dx, dy, dx
 | 
			
		||||
 | 
			
		||||
    def create_circular_room(self, spawnable: bool = True) \
 | 
			
		||||
            -> Tuple[List[List[Tile]], int, int, int, int]:
 | 
			
		||||
        """
 | 
			
		||||
        Create and return as a tile grid a room that is circular in shape, and 
 | 
			
		||||
        Create and return as a tile grid a room that is circular in shape, and
 | 
			
		||||
        may have a center, also circular hole
 | 
			
		||||
        Also return door info so we know how to place the room in the level
 | 
			
		||||
        """
 | 
			
		||||
@@ -299,7 +300,7 @@ class Generator:
 | 
			
		||||
    def create_random_room(self, spawnable: bool = True) \
 | 
			
		||||
            -> Tuple[List[list], int, int, int, int]:
 | 
			
		||||
        """
 | 
			
		||||
        Randomly select a room shape and return one such room along with its 
 | 
			
		||||
        Randomly select a room shape and return one such room along with its
 | 
			
		||||
        door info. Set spawnable to False is the room should be marked as a
 | 
			
		||||
        potential spawning region on the map
 | 
			
		||||
        """
 | 
			
		||||
@@ -320,12 +321,12 @@ class Generator:
 | 
			
		||||
    def update_spawnable(self, y: int, x: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Convert previous spawn positions relative to the room grid to actual
 | 
			
		||||
        actual spawn positions on the level grid, using the position of the 
 | 
			
		||||
        top left corner of the room on the level, then log them as a 
 | 
			
		||||
        actual spawn positions on the level grid, using the position of the
 | 
			
		||||
        top left corner of the room on the level, then log them as a
 | 
			
		||||
        spawnable region
 | 
			
		||||
        """
 | 
			
		||||
        if self.queued_area != None:
 | 
			
		||||
            translated_area = [[y+ry, x+rx] for ry, rx in self.queued_area]
 | 
			
		||||
        if self.queued_area is not None:
 | 
			
		||||
            translated_area = [[y + ry, x + rx] for ry, rx in self.queued_area]
 | 
			
		||||
            self.spawn_areas.append(translated_area)
 | 
			
		||||
        self.queued_area = None
 | 
			
		||||
 | 
			
		||||
@@ -334,11 +335,6 @@ class Generator:
 | 
			
		||||
        Populate every spawnable area with some randomly chosen, randomly
 | 
			
		||||
        placed entity
 | 
			
		||||
        """
 | 
			
		||||
        if self.queued_area is not None:
 | 
			
		||||
            translated_area = [[y + ry, x + rx] for ry, rx in self.queued_area]
 | 
			
		||||
            self.spawn_areas.append(translated_area)
 | 
			
		||||
        self.queued_area = None
 | 
			
		||||
 | 
			
		||||
        min_c, max_c = self.params["spawn_per_region"]
 | 
			
		||||
        for region in self.spawn_areas:
 | 
			
		||||
            entity_count = randint(min_c, max_c)
 | 
			
		||||
 
 | 
			
		||||
@@ -134,13 +134,13 @@ class TestEntities(unittest.TestCase):
 | 
			
		||||
        self.map.remove_entity(entity2)
 | 
			
		||||
 | 
			
		||||
        # Test following the player and finding the player as target
 | 
			
		||||
        self.player.move(5, 5)
 | 
			
		||||
        fam.move(4, 5)
 | 
			
		||||
        self.player.move(6, 5)
 | 
			
		||||
        fam.move(5, 5)
 | 
			
		||||
        fam.target = None
 | 
			
		||||
        self.player.move_down()
 | 
			
		||||
        self.map.tick(self.player)
 | 
			
		||||
        self.assertTrue(fam.target == self.player)
 | 
			
		||||
        self.assertEqual(fam.y, 5)
 | 
			
		||||
        self.assertEqual(fam.y, 6)
 | 
			
		||||
        self.assertEqual(fam.x, 5)
 | 
			
		||||
 | 
			
		||||
        # Test random move
 | 
			
		||||
 
 | 
			
		||||
@@ -728,6 +728,7 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
        self.game.player.inventory.clear()
 | 
			
		||||
        ring = RingCritical()
 | 
			
		||||
        ring.hold(self.game.player)
 | 
			
		||||
        self.game.display_actions(DisplayActions.REFRESH)
 | 
			
		||||
        old_critical = self.game.player.critical
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.EQUIP)
 | 
			
		||||
        self.assertEqual(self.game.player.critical,
 | 
			
		||||
@@ -951,3 +952,18 @@ class TestGame(unittest.TestCase):
 | 
			
		||||
        # Exit the menu
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.SPACE)
 | 
			
		||||
        self.assertEqual(self.game.state, GameMode.PLAY)
 | 
			
		||||
 | 
			
		||||
    def test_doors(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Check that the user can open doors.
 | 
			
		||||
        """
 | 
			
		||||
        self.game.state = GameMode.PLAY
 | 
			
		||||
 | 
			
		||||
        self.game.player.move(9, 8)
 | 
			
		||||
        self.assertEqual(self.game.map.tiles[10][8], Tile.DOOR)
 | 
			
		||||
        # Open door
 | 
			
		||||
        self.game.handle_key_pressed(KeyValues.DOWN)
 | 
			
		||||
        self.assertEqual(self.game.map.tiles[10][8], Tile.FLOOR)
 | 
			
		||||
        self.assertEqual(self.game.player.y, 10)
 | 
			
		||||
        self.assertEqual(self.game.player.x, 8)
 | 
			
		||||
        self.game.display_actions(DisplayActions.REFRESH)
 | 
			
		||||
 
 | 
			
		||||
@@ -26,16 +26,17 @@ class TestBroguelike(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def is_connex(self, grid: List[List[Tile]]) -> bool:
 | 
			
		||||
        h, w = len(grid), len(grid[0])
 | 
			
		||||
        y, x = randint(0, h - 1), randint(0, w - 1)
 | 
			
		||||
        while not (grid[y][x].can_walk()):
 | 
			
		||||
        y, x = -1, -1
 | 
			
		||||
        while not grid[y][x].can_walk():
 | 
			
		||||
            y, x = randint(0, h - 1), randint(0, w - 1)
 | 
			
		||||
        queue = Map.neighbourhood(grid, y, x)
 | 
			
		||||
        while queue:
 | 
			
		||||
            y, x = queue.pop()
 | 
			
		||||
            if grid[y][x].can_walk():
 | 
			
		||||
            if grid[y][x].can_walk() or grid[y][x] == Tile.DOOR:
 | 
			
		||||
                grid[y][x] = Tile.WALL
 | 
			
		||||
                queue += Map.neighbourhood(grid, y, x)
 | 
			
		||||
        return not any([t.can_walk() for row in grid for t in row])
 | 
			
		||||
        return not any([t.can_walk() or t == Tile.DOOR
 | 
			
		||||
                        for row in grid for t in row])
 | 
			
		||||
 | 
			
		||||
    def test_build_doors(self) -> None:
 | 
			
		||||
        m = self.stom(".  .\n.  .\n.  .\n")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user