# Part 5 - Placing Enemies and kicking them (harmlessly)

What good is a dungeon with no monsters to bash? This chapter will focus on placing the enemies throughout the dungeon, and setting them up to be attacked (the actual attacking part we’ll save for next time).

When we’re building our dungeon, we’ll need to place the enemies in the rooms. In order to do that, we will need to make a change to the way `entities` are stored in our game. Currently, they’re saved in the `Engine` class. However, for the sake of placing enemies in the dungeon, and when we get to the part where we move between dungeon floors, it will be better to store them in the `GameMap` class. That way, the map has access to the entities directly, and we can preserve which entities are on which floors fairly easily.

Start by modifying `GameMap`:

    +from __future__ import annotations

    +from typing import Iterable, TYPE_CHECKING

    import numpy as np  # type: ignore
    from tcod.console import Console

    import tile_types

    +if TYPE_CHECKING:
    +   from entity import Entity

    class GameMap:
    -   def __init__(self, width: int, height: int):
    +   def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
            self.width, self.height = width, height
    +       self.entities = set(entities)
            self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")

    from __future__ import annotations

    from typing import Iterable, TYPE_CHECKING

    import numpy as np  # type: ignore
    from tcod.console import Console

    import tile_types

    if TYPE_CHECKING:
        from entity import Entity

    class GameMap:
        def __init__(self, width: int, height: int):
        def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
            self.width, self.height = width, height
            self.entities = set(entities)
            self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")

Then, let’s modify `Engine` to remove the `entities` from it:

    -from typing import Set, Iterable, Any
    +from typing import Iterable, Any

    class Engine:
    -   def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):
    +   def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
    -       self.entities = entities
            self.event_handler = event_handler
            self.game_map = game_map
            self.player = player
            self.update_fov()

    from typing import Set, Iterable, Any
    from typing import Iterable, Any

    class Engine:
        def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):
        def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
            self.entities = entities
            self.event_handler = event_handler
            self.game_map = game_map
            self.player = player
            self.update_fov()

Because we’ve modified the definition of `Engine.__init__`, we need to modify `main.py` where we create our `game_map` variable. We might as well remove that `npc` as well, since we won’t be needing it anymore.

        ...
        player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
    -   npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
    -   entities = {npc, player}

        game_map = generate_dungeon(
            max_rooms=max_rooms,
            room_min_size=room_min_size,
            room_max_size=room_max_size,
            map_width=map_width,
            map_height=map_height,
            player=player,
        )

    -   engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
    +   engine = Engine(event_handler=event_handler, game_map=game_map, player=player)

        with tcod.context.new_terminal(
            ...

        ...
        player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
        npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
        entities = {npc, player}

        game_map = generate_dungeon(
            max_rooms=max_rooms,
            room_min_size=room_min_size,
            room_max_size=room_max_size,
            map_width=map_width,
            map_height=map_height,
            player=player,
        )

        engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
        engine = Engine(event_handler=event_handler, game_map=game_map, player=player)

        with tcod.context.new_terminal(
            ...

We can remove the part in `Engine.render` that loops through the entities and renders the ones that are visible. That part will also be handled by the `GameMap` from now on.

    class Engine:
        ...

        def render(self, console: Console, context: Context) -> None:
            self.game_map.render(console)

    -       for entity in self.entities:
    -           # Only print entities that are in the FOV
    -           if self.game_map.visible[entity.x, entity.y]:
    -               console.print(entity.x, entity.y, entity.char, fg=entity.color)

    class Engine:
        ...

        def render(self, console: Console, context: Context) -> None:
            self.game_map.render(console)

            for entity in self.entities:
                # Only print entities that are in the FOV
                if self.game_map.visible[entity.x, entity.y]:
                    console.print(entity.x, entity.y, entity.char, fg=entity.color)

We can move this block into `GameMap.render`, though take note that the line that checks for visibility has a slight change: it goes from:

`if self.game_map.visible[entity.x, entity.y]:`

To:

`if self.visible[entity.x, entity.y]:`.

    class GameMap:
        ...

        def render(self, console: Console) -> None:
            """
            Renders the map.

            If a tile is in the "visible" array, then draw it with the "light" colors.
            If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
            Otherwise, the default is "SHROUD".
            """
            console.tiles_rgb[0:self.width, 0:self.height] = np.select(
                condlist=[self.visible, self.explored],
                choicelist=[self.tiles["light"], self.tiles["dark"]],
                default=tile_types.SHROUD
            )

    +       for entity in self.entities:
    +           # Only print entities that are in the FOV
    +           if self.visible[entity.x, entity.y]:
    +               console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)

    class GameMap:
        ...

        def render(self, console: Console) -> None:
            """
            Renders the map.

            If a tile is in the "visible" array, then draw it with the "light" colors.
            If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
            Otherwise, the default is "SHROUD".
            """
            console.tiles_rgb[0:self.width, 0:self.height] = np.select(
                condlist=[self.visible, self.explored],
                choicelist=[self.tiles["light"], self.tiles["dark"]],
                default=tile_types.SHROUD
            )

            for entity in self.entities:
                # Only print entities that are in the FOV
                if self.visible[entity.x, entity.y]:
                    console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)

Finally, we need to alter the part in `generate_dungeon` that creates the instance of `GameMap`, so that the `player` is passed into the `entities` argument.

    def generate_dungeon(
        max_rooms: int,
        room_min_size: int,
        room_max_size: int,
        map_width: int,
        map_height: int,
        player: Entity,
    ) -> GameMap:
        """Generate a new dungeon map."""
    -   dungeon = GameMap(map_width, map_height)
    +   dungeon = GameMap(map_width, map_height, entities=[player])

        rooms: List[RectangularRoom] = []
        ...

    def generate_dungeon(
        max_rooms: int,
        room_min_size: int,
        room_max_size: int,
        map_width: int,
        map_height: int,
        player: Entity,
    ) -> GameMap:
        """Generate a new dungeon map."""
        dungeon = GameMap(map_width, map_height)
        dungeon = GameMap(map_width, map_height, entities=[player])

        rooms: List[RectangularRoom] = []
        ...

If you run the project now, things should look the same as before, minus the NPC that we had earlier for testing.

Now, moving on to actually placing monsters in our dungeon. Our logic will be simple enough: For each room that’s created in our dungeon, we’ll place a random number of enemies, between 0 and a maximum (2 for now). We’ll make it so that there’s an 80% chance of spawning an Orc (a weaker enemy) and a 20% chance of it being a Troll (a stronger enemy).

In order to specify the maximum number of monsters that can be spawned into a room, let’s create a new variable, `max_monsters_per_room`, and place it in `main.py`. We’ll also modify our call to `generate_dungeon` to pass this new variable in.

        ...
        max_rooms = 30

    +   max_monsters_per_room = 2

        tileset = tcod.tileset.load_tilesheet(
            "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
        )

        event_handler = EventHandler()

        player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))

        game_map = generate_dungeon(
            max_rooms=max_rooms,
            room_min_size=room_min_size,
            room_max_size=room_max_size,
            map_width=map_width,
            map_height=map_height,
    +       max_monsters_per_room=max_monsters_per_room,
            player=player
        )

        engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
        ...

        ...
        max_rooms = 30

        max_monsters_per_room = 2

        tileset = tcod.tileset.load_tilesheet(
            "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
        )

        event_handler = EventHandler()

        player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))

        game_map = generate_dungeon(
            max_rooms=max_rooms,
            room_min_size=room_min_size,
            room_max_size=room_max_size,
            map_width=map_width,
            map_height=map_height,
            max_monsters_per_room=max_monsters_per_room,
            player=player
        )

        engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
        ...

Pretty straightforward. Now we’ll need to modify the definition of `generate_dungeon` to take this new variable, like this:

    def generate_dungeon(
        max_rooms: int,
        room_min_size: int,
        room_max_size: int,
        map_width: int,
        map_height: int,
    +   max_monsters_per_room: int,
        player: Entity,
    ) -> GameMap:
        """Generate a new dungeon map."""
        dungeon = GameMap(map_width, map_height, entities=[player])

    def generate_dungeon(
        max_rooms: int,
        room_min_size: int,
        room_max_size: int,
        map_width: int,
        map_height: int,
        max_monsters_per_room: int,
        player: Entity,
    ) -> GameMap:
        """Generate a new dungeon map."""
        dungeon = GameMap(map_width, map_height, entities=[player])

Easy enough, but now how do we actually place the enemies?

After we’ve created our room, we’ll want to call a function to put the entities in their places. Let’s call the function `place_entities`, and it will take three arguments: The `RectangularRoom` that we’ve created, the `dungeon` so that it can add the entities to it (remember that `dungeon` is an instance of `GameMap`, which now holds entities), and the `max_monsters_per_room`, so that we know how many monsters to make.

While we haven’t written the function yet, let’s place our call to it in `generate_dungeon`:

                ...
                    dungeon.tiles[x, y] = tile_types.floor

    +       place_entities(new_room, dungeon, max_monsters_per_room)

            # Finally, append the new room to the list.
            rooms.append(new_room)

        return dungeon

                ...
                    dungeon.tiles[x, y] = tile_types.floor

            place_entities(new_room, dungeon, max_monsters_per_room)

            # Finally, append the new room to the list.
            rooms.append(new_room)

        return dungeon

Now, let’s write the `place_entities` function so that this actually works.

Our first version of `place_entities` won’t actually place the entities. Why not? Because we’ll need to do a few other things to make spawning the entities here work. However, we can at least fill in most of the function, and skip over the part that actually creates the entities for the moment.

Create the function like this:

    class RectangularRoom:
        ...

    +def place_entities(
    +   room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,
    +) -> None:
    +   number_of_monsters = random.randint(0, maximum_monsters)

    +   for i in range(number_of_monsters):
    +       x = random.randint(room.x1 + 1, room.x2 - 1)
    +       y = random.randint(room.y1 + 1, room.y2 - 1)

    +       if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
    +           if random.random() < 0.8:
    +               pass  # TODO: Place an Orc here
    +           else:
    +               pass  # TODO: Place a Troll here

    def tunnel_between(
        ...

    class RectangularRoom:
        ...

    def place_entities(
        room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,
    ) -> None:
        number_of_monsters = random.randint(0, maximum_monsters)

        for i in range(number_of_monsters):
            x = random.randint(room.x1 + 1, room.x2 - 1)
            y = random.randint(room.y1 + 1, room.y2 - 1)

            if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
                if random.random() < 0.8:
                    pass  # TODO: Place an Orc here
                else:
                    pass  # TODO: Place a Troll here

    def tunnel_between(
        ...

The first line in the function takes a random number between 0 and the provided maximum (2, in this case). From there, it iterates from 0 to the number.

We select a random `x` and `y` to place the entity, and do a quick check to make sure there’s no other entities in that location before dropping the enemy there. This is to ensure we don’t get stacks of enemies.

As described earlier, there should be an 80% chance of there being an Orc, and 20% chance for a Troll. For now, we’re using `pass` to skip over actually putting them down, because that requires a bit more work first.

There’s a few ways we could go about creating the new entities. Assuming that every Orc and Troll we spawn will always have the same attributes as their brethren, we can create initial instances of `orc` and `troll`, then copy those every time we want to create a new one.

Why not just create the entities right here in the function? We could (the 1st version of this tutorial does, in fact), but that’s a bit of a pain to go back and edit. Imagine if you had 100 enemies in your game at some point in the future. Would you rather search for those entity definitions in one file that only exists to define entities, or try finding it in the file that generates our dungeon? Not to mention, what happens if you want to create a new dungeon generator? Are you going to copy over the entity definitions and have them defined in two places?

Let’s modify `Entity` to prepare for this new copying method. Modify `entity.py` like this:

    +from __future__ import annotations

    +import copy
    -from typing import Tuple
    +from typing import Tuple, TypeVar, TYPE_CHECKING

    +if TYPE_CHECKING:
    +   from game_map import GameMap

    +T = TypeVar("T", bound="Entity")

    class Entity:
        """
        A generic object to represent players, enemies, items, etc.
        """
    -   def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]):
    +   def __init__(
    +       self,
    +       x: int = 0,
    +       y: int = 0,
    +       char: str = "?",
    +       color: Tuple[int, int, int] = (255, 255, 255),
    +       name: str = "<Unnamed>",
    +       blocks_movement: bool = False,
    +   ):
            self.x = x
            self.y = y
            self.char = char
            self.color = color
    +       self.name = name
    +       self.blocks_movement = blocks_movement

    +   def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
    +       """Spawn a copy of this instance at the given location."""
    +       clone = copy.deepcopy(self)
    +       clone.x = x
    +       clone.y = y
    +       gamemap.entities.add(clone)
    +       return clone

        def move(self, dx: int, dy: int) -> None:
            ...

    from __future__ import annotations

    import copy
    from typing import Tuple
    from typing import Tuple, TypeVar, TYPE_CHECKING

    if TYPE_CHECKING:
        from game_map import GameMap

    T = TypeVar("T", bound="Entity")

    class Entity:
        """
        A generic object to represent players, enemies, items, etc.
        """
        def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]):
        def __init__(
            self,
            x: int = 0,
            y: int = 0,
            char: str = "?",
            color: Tuple[int, int, int] = (255, 255, 255),
            name: str = "<Unnamed>",
            blocks_movement: bool = False,
        ):
            self.x = x
            self.y = y
            self.char = char
            self.color = color
            self.name = name
            self.blocks_movement = blocks_movement

        def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
            """Spawn a copy of this instance at the given location."""
            clone = copy.deepcopy(self)
            clone.x = x
            clone.y = y
            gamemap.entities.add(clone)
            return clone

        def move(self, dx: int, dy: int) -> None:
            ...

We’ve added two new attributes to `Entity`: `name` and `blocks_movement`. `name` is straightforward: it’s what the Entity is called. `blocks_movement` describes whether or not this `Entity` can be moved over or not. Enemies will have `blocks_movement` set to `True`, while in the future, things like consumable items and equipment will be set to `False`.

Notice that we’ve also provided defaults for each of the attributes in the `__init__` function as well, whereas we were not before. This is because we’ll soon not need to pass `x` and `y` during the initialization. More on that in a second.

The more complex section is the `spawn` method. It takes the `GameMap` instance, along with `x` and `y` for locations. It then creates a `clone` of the instance of `Entity`, and assigns the `x` and `y` variables to it (this is why we don’t need `x` and `y` in the initializer anymore, they’re set here). It then adds the entity to the `gamemap`’s entities, and returns the `clone`.

This new `spawn` method will probably make a lot more sense by putting it to use. To do that, let’s create a new file, called `entity_factories.py`, and fill it with the following contents:

    from entity import Entity

    player = Entity(char="@", color=(255, 255, 255), name="Player", blocks_movement=True)

    orc = Entity(char="o", color=(63, 127, 63), name="Orc", blocks_movement=True)
    troll = Entity(char="T", color=(0, 127, 0), name="Troll", blocks_movement=True)

This is where we’re defining our entities. `player` should look familiar, and `orc` and `troll` are not all that different, besides their characters and colors.

These are the instances we’ll be cloning to create our new entities. Using these, we can at last fill in our `place_entities` function back in `procgen.py`.

    ...
    import tcod

    +import entity_factories
    from game_map import GameMap
    ...

            ...
                if random.random() < 0.8:
    -               pass  # TODO: Place an Orc here
    +               entity_factories.orc.spawn(dungeon, x, y)
                else:
    -               pass  # TODO: Place a Troll here
    +               entity_factories.troll.spawn(dungeon, x, y)

    ...
    import tcod

    import entity_factories
    from game_map import GameMap
    ...

            ...
                if random.random() < 0.8:
                    pass  # TODO: Place an Orc here
                    entity_factories.orc.spawn(dungeon, x, y)
                else:
                    pass  # TODO: Place a Troll here
                    entity_factories.troll.spawn(dungeon, x, y)

Let’s also modify the way we create the `player`:

    #!/usr/bin/env python3
    +import copy

    import tcod

    from engine import Engine
    -from entity import Entity
    +import entity_factories
    from input_handlers import EventHandler
    from procgen import generate_dungeon
    ...

        ...
        event_handler = EventHandler()

    -   player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
    +   player = copy.deepcopy(entity_factories.player)

        game_map = generate_dungeon(
            ...

    #!/usr/bin/env python3
    import copy

    import tcod

    from engine import Engine
    from entity import Entity
    import entity_factories
    from input_handlers import EventHandler
    from procgen import generate_dungeon
    ...

        ...
        event_handler = EventHandler()

        player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
        player = copy.deepcopy(entity_factories.player)

        game_map = generate_dungeon(
            ...

Note: We can’t use `player.spawn` here, because `spawn` requires the `GameMap`, which isn’t created until after we create the player.

With that, your dungeon should now be populated with enemies.

They’re… not exactly intimidating, are they? In fact, they don’t really do much of anything right now. But that’s okay, we’ll work on that.

The first step towards making our monsters scarier is making them stand their ground… literally! The player can currently walk over (or under) the enemies by simply moving into the same space. Let’s fix that, and ensure that when the player tries to move towards an enemy, we attack instead.

To begin, we need to determine if the space the player is trying to move into has an Entity in it. Not just any Entity, however: we’ll check if the Entity has “blocks_movement” set to `True`. If it does, our player can’t move there, and tries to attack instead.

Add the following to the map:

    from __future__ import annotations

    -from typing import Iterable, TYPE_CHECKING
    +from typing import Iterable, Optional, TYPE_CHECKING

    import numpy as np  # type: ignore
    from tcod.console import Console

    import tile_types

    if TYPE_CHECKING:
        from entity import Entity

    class GameMap:
        def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
            self.width, self.height = width, height
            self.entities = set(entities)
            self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")

            self.visible = np.full((width, height), fill_value=False, order="F")  # Tiles the player can currently see
            self.explored = np.full((width, height), fill_value=False, order="F")  # Tiles the player has seen before

    +   def get_blocking_entity_at_location(self, location_x: int, location_y: int) -> Optional[Entity]:
    +       for entity in self.entities:
    +           if entity.blocks_movement and entity.x == location_x and entity.y == location_y:
    +               return entity

    +       return None

        def in_bounds(self, x: int, y: int) -> bool:
            ...

    from __future__ import annotations

    from typing import Iterable, TYPE_CHECKING
    from typing import Iterable, Optional, TYPE_CHECKING

    import numpy as np  # type: ignore
    from tcod.console import Console

    import tile_types

    if TYPE_CHECKING:
        from entity import Entity

    class GameMap:
        def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
            self.width, self.height = width, height
            self.entities = set(entities)
            self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")

            self.visible = np.full((width, height), fill_value=False, order="F")  # Tiles the player can currently see
            self.explored = np.full((width, height), fill_value=False, order="F")  # Tiles the player has seen before

        def get_blocking_entity_at_location(self, location_x: int, location_y: int) -> Optional[Entity]:
            for entity in self.entities:
                if entity.blocks_movement and entity.x == location_x and entity.y == location_y:
                    return entity

            return None

        def in_bounds(self, x: int, y: int) -> bool:
            ...

This new function iterates through all the `entities`, and if one is found that both blocks movement and occupies the given `location_x` and `location_y` coordinates, it returns that Entity. Otherwise, we return `None` instead.

Where can we check if a tile is occupied or not? And what do we do if it is?

One way to handle all this is to modify our “actions” a bit. Our current `MovementAction` doesn’t take into account what occupies the tile we’re moving into. That’s fine, it doesn’t necessarily need to, but there probably should be an action that does. What if we created an `Action` subclass that could tell what was in the tile, and call either `MovementAction` if it was empty, or some other “attack” action if it wasn’t?

Let’s do a few things. We’ll start by defining a new class, called `ActionWithDirection`, which will actually become the new superclass for `MovementAction`. This new class will take the initializer from `MovementAction`, but won’t implement its own `perform` method. It looks like this:

    ...
    class EscapeAction(Action):
        def perform(self, engine: Engine, entity: Entity) -> None:
            raise SystemExit()

    +class ActionWithDirection(Action):
    +   def __init__(self, dx: int, dy: int):
    +       super().__init__()

    +       self.dx = dx
    +       self.dy = dy

    +   def perform(self, engine: Engine, entity: Entity) -> None:
    +       raise NotImplementedError()

    -class MovementAction(Action):
    +class MovementAction(ActionWithDirection):
    -   def __init__(self, dx: int, dy: int):
    -       super().__init__()

    -       self.dx = dx
    -       self.dy = dy

        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy

            if not engine.game_map.in_bounds(dest_x, dest_y):
                return  # Destination is out of bounds.
            if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
                return  # Destination is blocked by a tile.
    +       if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
    +           return  # Destination is blocked by an entity.

            entity.move(self.dx, self.dy)

    ...
    class EscapeAction(Action):
        def perform(self, engine: Engine, entity: Entity) -> None:
            raise SystemExit()

    class ActionWithDirection(Action):
        def __init__(self, dx: int, dy: int):
            super().__init__()

            self.dx = dx
            self.dy = dy

        def perform(self, engine: Engine, entity: Entity) -> None:
            raise NotImplementedError()

    class MovementAction(Action):
    class MovementAction(ActionWithDirection):
        def __init__(self, dx: int, dy: int):
            super().__init__()

            self.dx = dx
            self.dy = dy

        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy

            if not engine.game_map.in_bounds(dest_x, dest_y):
                return  # Destination is out of bounds.
            if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
                return  # Destination is blocked by a tile.
            if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
                return  # Destination is blocked by an entity.

            entity.move(self.dx, self.dy)

Notice that we’ve added an extra check in `MovementAction` to ensure we’re not moving into a space with a blocking entity. Theoretically, this bit of code won’t ever trigger, but it’s nice to have it there as a safeguard.

But wait, `MovementAction` still doesn’t do anything differently. So what’s the point? Well, now we can use the new `ActionWithDirection` class to define two more subclasses, which will do what we want.

The first one will be the action we use to actually attack. It looks like this:

    class ActionWithDirection(Action):
        def __init__(self, dx: int, dy: int):
            super().__init__()

            self.dx = dx
            self.dy = dy

        def perform(self, engine: Engine, entity: Entity) -> None:
            raise NotImplementedError()

    +class MeleeAction(ActionWithDirection):
    +   def perform(self, engine: Engine, entity: Entity) -> None:
    +       dest_x = entity.x + self.dx
    +       dest_y = entity.y + self.dy
    +       target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
    +       if not target:
    +           return  # No entity to attack.

    +       print(f"You kick the {target.name}, much to its annoyance!")

    class MovementAction(ActionWithDirection):
        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy

            if not engine.game_map.in_bounds(dest_x, dest_y):
                return  # Destination is out of bounds.
            if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
                return  # Destination is blocked by a tile.
            if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
                return  # Destination is blocked by an entity.

            entity.move(self.dx, self.dy)

    class ActionWithDirection(Action):
        def __init__(self, dx: int, dy: int):
            super().__init__()

            self.dx = dx
            self.dy = dy

        def perform(self, engine: Engine, entity: Entity) -> None:
            raise NotImplementedError()

    class MeleeAction(ActionWithDirection):
        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy
            target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
            if not target:
                return  # No entity to attack.

            print(f"You kick the {target.name}, much to its annoyance!")

    class MovementAction(ActionWithDirection):
        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy

            if not engine.game_map.in_bounds(dest_x, dest_y):
                return  # Destination is out of bounds.
            if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
                return  # Destination is blocked by a tile.
            if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
                return  # Destination is blocked by an entity.

            entity.move(self.dx, self.dy)

Just like `MovementAction`, `MeleeAction` inherits from `ActionWithDirection`. The `perform` method it implements is what we’ll use to attack… eventually. Right now, we’re just printing out a little message. The actual attacking will have to wait until the next part (this one is getting long as it is).

Still, we’re not actually using `MeleeAction` anywhere, yet. Let’s add one more class, which is what will make the determination on whether our player is moving or attacking:

    class MovementAction(ActionWithDirection):
        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy

            if not engine.game_map.in_bounds(dest_x, dest_y):
                return  # Destination is out of bounds.
            if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
                return  # Destination is blocked by a tile.
            if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
                return  # Destination is blocked by an entity.

            entity.move(self.dx, self.dy)

    +class BumpAction(ActionWithDirection):
    +   def perform(self, engine: Engine, entity: Entity) -> None:
    +       dest_x = entity.x + self.dx
    +       dest_y = entity.y + self.dy

    +       if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
    +           return MeleeAction(self.dx, self.dy).perform(engine, entity)

    +       else:
    +           return MovementAction(self.dx, self.dy).perform(engine, entity)

    class MovementAction(ActionWithDirection):
        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy

            if not engine.game_map.in_bounds(dest_x, dest_y):
                return  # Destination is out of bounds.
            if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
                return  # Destination is blocked by a tile.
            if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
                return  # Destination is blocked by an entity.

            entity.move(self.dx, self.dy)

    class BumpAction(ActionWithDirection):
        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy

            if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
                return MeleeAction(self.dx, self.dy).perform(engine, entity)

            else:
                return MovementAction(self.dx, self.dy).perform(engine, entity)

This class also inherits from `ActionWithDirection`, but its `perform` method doesn’t actually perform anything, except deciding which class, between `MeleeAction` and `MovementAction` to return. Those classes are what are actually doing the work. `BumpAction` just determines which one is appropriate to call, based on whether there is a blocking entity at the given destination or not. Notice we’re using the function we defined earlier in our map to decide if there’s a valid target or not.

Now that our new actions are in place, we need to modify our `input_handlers.py` file to use `BumpAction` instead of `MovementAction`. It’s a pretty simple change:

    from typing import Optional

    import tcod.event

    -from actions import Action, EscapeAction, MovementAction
    +from actions import Action, BumpAction, EscapeAction

    class EventHandler(tcod.event.EventDispatch[Action]):
        def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
            raise SystemExit()

        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
            action: Optional[Action] = None

            key = event.sym

            if key == tcod.event.K_UP:
    -           action = MovementAction(dx=0, dy=-1)
    +           action = BumpAction(dx=0, dy=-1)
            elif key == tcod.event.K_DOWN:
    -           action = MovementAction(dx=0, dy=1)
    +           action = BumpAction(dx=0, dy=1)
            elif key == tcod.event.K_LEFT:
    -           action = MovementAction(dx=-1, dy=0)
    +           action = BumpAction(dx=-1, dy=0)
            elif key == tcod.event.K_RIGHT:
    -           action = MovementAction(dx=1, dy=0)
    +           action = BumpAction(dx=1, dy=0)

            elif key == tcod.event.K_ESCAPE:
                action = EscapeAction()

            # No valid key was pressed
            return action

    from typing import Optional

    import tcod.event

    from actions import Action, EscapeAction, MovementAction
    from actions import Action, BumpAction, EscapeAction

    class EventHandler(tcod.event.EventDispatch[Action]):
        def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
            raise SystemExit()

        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
            action: Optional[Action] = None

            key = event.sym

            if key == tcod.event.K_UP:
                action = MovementAction(dx=0, dy=-1)
                action = BumpAction(dx=0, dy=-1)
            elif key == tcod.event.K_DOWN:
                action = MovementAction(dx=0, dy=1)
                action = BumpAction(dx=0, dy=1)
            elif key == tcod.event.K_LEFT:
                action = MovementAction(dx=-1, dy=0)
                action = BumpAction(dx=-1, dy=0)
            elif key == tcod.event.K_RIGHT:
                action = MovementAction(dx=1, dy=0)
                action = BumpAction(dx=1, dy=0)

            elif key == tcod.event.K_ESCAPE:
                action = EscapeAction()

            # No valid key was pressed
            return action

Run the project now. At this point, you shouldn’t be able to move over the enemies, and you should get a message in the terminal, indicating that you’re attacking the enemy (albeit not for any damage).

Before we wrap this part up, let’s set ourselves up to allow for enemy turns as well. They won’t actually be doing anything at the moment, we’ll just get a message in the terminal that indicates something is happening.

Add these small modifications to `engine.py`:

    class Engine:
        def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
            self.event_handler = event_handler
            self.game_map = game_map
            self.player = player
            self.update_fov()

    +   def handle_enemy_turns(self) -> None:
    +       for entity in self.game_map.entities - {self.player}:
    +           print(f'The {entity.name} wonders when it will get to take a real turn.')

        def handle_events(self, events: Iterable[Any]) -> None:
            for event in events:
                action = self.event_handler.dispatch(event)

                if action is None:
                    continue

                action.perform(self, self.player)
    +           self.handle_enemy_turns()
                self.update_fov()  # Update the FOV before the players next action.

    class Engine:
        def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
            self.event_handler = event_handler
            self.game_map = game_map
            self.player = player
            self.update_fov()

        def handle_enemy_turns(self) -> None:
            for entity in self.game_map.entities - {self.player}:
                print(f'The {entity.name} wonders when it will get to take a real turn.')

        def handle_events(self, events: Iterable[Any]) -> None:
            for event in events:
                action = self.event_handler.dispatch(event)

                if action is None:
                    continue

                action.perform(self, self.player)
                self.handle_enemy_turns()
                self.update_fov()  # Update the FOV before the players next action.

The `handle_enemy_turns` function loops through each entity (minus the player) and prints out a message for them. In the next part, we’ll replace this with some code that will allow those entities to take real turns.

We call `handle_enemy_turns` right after `action.perform`, so that the enemies move right after the player. Other roguelike games have more complex timing mechanisms for when entities take their turns, but our tutorial will stick with probably the simplest method of all: the player moves, then all the enemies move.

That’s all for this chapter. Next time, we’ll look at moving the enemies around on their turns, and doing some real damage to both the enemies and the player.

If you want to see the code so far in its entirety, click here.

Click here to move on to the next part of this tutorial.