# Part 6 - Doing (and taking) some damage

## Check your TCOD installation Link to heading

Before proceeding any further, you’ll want to upgrade to TCOD version 11.15, if you don’t already have it. This version of TCOD was released during the tutorial event, so if you’re following along on a weekly basis, you probably don’t have this version installed!

## Refactoring previous code Link to heading

After parts 1-5 for this tutorial were written, we decided to change a few things around, to hopefully make the codebase a bit cleaner and easier to extend in the future. Unfortunately, this means that code written in previous parts now has to be modified.

I would go back and edit the tutorial text and Github branches to reflect these changes, except for two things:

1. I don’t have time at the moment. Writing the sections that get published every week is taking all of my time as it is.
2. It wouldn’t be fair to those who are following this tutorial on a weekly basis.

Someday, when the event is over, the previous parts will be rewritten, and all will be well. But until then, there’s several changes that need to be made before proceeding with Part 6.

I won’t explain all of the changes (again, time is a limiting factor), but here’s the basic ideas:

- Event handlers will have the `handle_events` method instead of `Engine`.
- The game map will have a reference to `Engine`, and entities will have a reference to the map.
- Actions will be initialized with the entity doing the action
- Because of the above points, Actions will have a reference to the `Engine`, through `Entity`->`GameMap`->`Engine`

Make the changes to each file, and when you’re finished, verify the project works as it did before.

`input_handlers.py`

    +from __future__ import annotations

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

    import tcod.event

    from actions import Action, BumpAction, EscapeAction

    +if TYPE_CHECKING:
    +   from engine import Engine

    class EventHandler(tcod.event.EventDispatch[Action]):
    +   def __init__(self, engine: Engine):
    +       self.engine = engine

    +   def handle_events(self) -> None:
    +       for event in tcod.event.wait():
    +           action = self.dispatch(event)

    +           if action is None:
    +               continue

    +           action.perform()

    +           self.engine.handle_enemy_turns()
    +           self.engine.update_fov()  # Update the FOV before the players next action.

        def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
            ...

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

            key = event.sym

    +       player = self.engine.player

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

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

    from __future__ import annotations

    from typing import Optional
    from typing import Optional, TYPE_CHECKING

    import tcod.event

    from actions import Action, BumpAction, EscapeAction

    if TYPE_CHECKING:
        from engine import Engine

    class EventHandler(tcod.event.EventDispatch[Action]):
        def __init__(self, engine: Engine):
            self.engine = engine

        def handle_events(self) -> None:
            for event in tcod.event.wait():
                action = self.dispatch(event)

                if action is None:
                    continue

                action.perform()

                self.engine.handle_enemy_turns()
                self.engine.update_fov()  # Update the FOV before the players next action.

        def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
            ...

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

            key = event.sym

            player = self.engine.player

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

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

`actions.py`

    from __future__ import annotations

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

    if TYPE_CHECKING:
        from engine import Engine
        from entity import Entity

    class Action:
    +   def __init__(self, entity: Entity) -> None:
    +       super().__init__()
    +       self.entity = entity

    +   @property
    +   def engine(self) -> Engine:
    +       """Return the engine this action belongs to."""
    +       return self.entity.gamemap.engine

    +   def perform(self) -> None:
    -   def perform(self, engine: Engine, entity: Entity) -> None:
            """Perform this action with the objects needed to determine its scope.

    +       `self.engine` is the scope this action is being performed in.
    -       `engine` is the scope this action is being performed in.

    +       `self.entity` is the object performing the action.
    -       `entity` is the object performing the action.

            This method must be overridden by Action subclasses.
            """
            raise NotImplementedError()

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

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

            self.dx = dx
            self.dy = dy

    +   @property
    +   def dest_xy(self) -> Tuple[int, int]:
    +       """Returns this actions destination."""
    +       return self.entity.x + self.dx, self.entity.y + self.dy

    +   @property
    +   def blocking_entity(self) -> Optional[Entity]:
    +       """Return the blocking entity at this actions destination.."""
    +       return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)

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

    class MeleeAction(ActionWithDirection):
    +   def perform(self) -> None:
    +       target = self.blocking_entity
    -   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) -> None:
    +       dest_x, dest_y = self.dest_xy
    -   def perform(self, engine: Engine, entity: Entity) -> None:
    -       dest_x = entity.x + self.dx
    -       dest_y = entity.y + self.dy

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

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

    class BumpAction(ActionWithDirection):
    +   def perform(self) -> None:
    +       if self.blocking_entity:
    +           return MeleeAction(self.entity, self.dx, self.dy).perform()
    -   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.entity, self.dx, self.dy).perform()
    -           return MovementAction(self.dx, self.dy).perform(engine, entity)

    from __future__ import annotations

    from typing import Optional, Tuple, TYPE_CHECKING
    from typing import TYPE_CHECKING

    if TYPE_CHECKING:
        from engine import Engine
        from entity import Entity

    class Action:
        def __init__(self, entity: Entity) -> None:
            super().__init__()
            self.entity = entity

        @property
        def engine(self) -> Engine:
            """Return the engine this action belongs to."""
            return self.entity.gamemap.engine

        def perform(self) -> None:
        def perform(self, engine: Engine, entity: Entity) -> None:
            """Perform this action with the objects needed to determine its scope.

            `self.engine` is the scope this action is being performed in.
            `engine` is the scope this action is being performed in.

            `self.entity` is the object performing the action.
            `entity` is the object performing the action.

            This method must be overridden by Action subclasses.
            """
            raise NotImplementedError()

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

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

            self.dx = dx
            self.dy = dy

        @property
        def dest_xy(self) -> Tuple[int, int]:
            """Returns this actions destination."""
            return self.entity.x + self.dx, self.entity.y + self.dy

        @property
        def blocking_entity(self) -> Optional[Entity]:
            """Return the blocking entity at this actions destination.."""
            return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)

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

    class MeleeAction(ActionWithDirection):
        def perform(self) -> None:
            target = self.blocking_entity
        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) -> None:
            dest_x, dest_y = self.dest_xy
        def perform(self, engine: Engine, entity: Entity) -> None:
            dest_x = entity.x + self.dx
            dest_y = entity.y + self.dy

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

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

    class BumpAction(ActionWithDirection):
        def perform(self) -> None:
            if self.blocking_entity:
                return MeleeAction(self.entity, self.dx, self.dy).perform()
        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.entity, self.dx, self.dy).perform()
                return MovementAction(self.dx, self.dy).perform(engine, entity)

`game_map.py`

    from __future__ import annotations

    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 engine import Engine
        from entity import Entity

    class GameMap:
    -   def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
    +   def __init__(
    +       self, engine: Engine, width: int, height: int, entities: Iterable[Entity] = ()
    +   ):
    +       self.engine = engine
            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.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
    +       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]:
    +   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:
    +           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:
            """Return True if x and y are inside of the bounds of this map."""
            return 0 <= x < self.width and 0 <= y < self.height

        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(
    +       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
    +           default=tile_types.SHROUD,
            )

    from __future__ import annotations

    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 engine import Engine
        from entity import Entity

    class GameMap:
        def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
        def __init__(
            self, engine: Engine, width: int, height: int, entities: Iterable[Entity] = ()
        ):
            self.engine = engine
            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.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
            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]:
        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:
                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:
            """Return True if x and y are inside of the bounds of this map."""
            return 0 <= x < self.width and 0 <= y < self.height

        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(
            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
                default=tile_types.SHROUD,
            )

`main.py`

    #!/usr/bin/env python3
    import copy

    import tcod

    from engine import Engine
    import entity_factories
    -from input_handlers import EventHandler
    from procgen import generate_dungeon

        ...
    +   player = copy.deepcopy(entity_factories.player)
    -   event_handler = EventHandler()

    +   engine = Engine(player=player)
    -   player = copy.deepcopy(entity_factories.player)

    +   engine.game_map = generate_dungeon(
    -   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,
    +       engine=engine,
    -       player=player,
        )
    +   engine.update_fov()

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

        with tcod.context.new_terminal(
            ...
            while True:
                engine.render(console=root_console, context=context)

    +           engine.event_handler.handle_events()
    -           events = tcod.event.wait()

    -           engine.handle_events(events)

    if __name__ == "__main__":
        main()

    #!/usr/bin/env python3
    import copy

    import tcod

    from engine import Engine
    import entity_factories
    from input_handlers import EventHandler
    from procgen import generate_dungeon

        ...
        player = copy.deepcopy(entity_factories.player)
        event_handler = EventHandler()

        engine = Engine(player=player)
        player = copy.deepcopy(entity_factories.player)

        engine.game_map = generate_dungeon(
        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,
            engine=engine,
            player=player,
        )
        engine.update_fov()

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

        with tcod.context.new_terminal(
            ...
            while True:
                engine.render(console=root_console, context=context)

                engine.event_handler.handle_events()
                events = tcod.event.wait()

                engine.handle_events(events)

    if __name__ == "__main__":
        main()

`entity.py`:

    from __future__ import annotations

    import copy
    -from typing import Tuple, TypeVar, TYPE_CHECKING
    +from typing import Optional, 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.
        """

    +   gamemap: GameMap

        def __init__(
            self,
    +       gamemap: Optional[GameMap] = None,
            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
    +       if gamemap:
    +           # If gamemap isn't provided now then it will be set later.
    +           self.gamemap = gamemap
    +           gamemap.entities.add(self)

        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
    +       clone.gamemap = gamemap
            gamemap.entities.add(clone)
            return clone

    +   def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None:
    +       """Place this entity at a new location.  Handles moving across GameMaps."""
    +       self.x = x
    +       self.y = y
    +       if gamemap:
    +           if hasattr(self, "gamemap"):  # Possibly uninitialized.
    +               self.gamemap.entities.remove(self)
    +           self.gamemap = gamemap
    +           gamemap.entities.add(self)

    from __future__ import annotations

    import copy
    from typing import Tuple, TypeVar, TYPE_CHECKING
    from typing import Optional, 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.
        """

        gamemap: GameMap

        def __init__(
            self,
            gamemap: Optional[GameMap] = None,
            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
            if gamemap:
                # If gamemap isn't provided now then it will be set later.
                self.gamemap = gamemap
                gamemap.entities.add(self)

        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
            clone.gamemap = gamemap
            gamemap.entities.add(clone)
            return clone

        def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None:
            """Place this entity at a new location.  Handles moving across GameMaps."""
            self.x = x
            self.y = y
            if gamemap:
                if hasattr(self, "gamemap"):  # Possibly uninitialized.
                    self.gamemap.entities.remove(self)
                self.gamemap = gamemap
                gamemap.entities.add(self)

`procgen.py`:

    import tile_types

    if TYPE_CHECKING:
    +   from engine import Engine
    -   from entity import Entity

    ...
    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,
    +   engine: Engine,
    -   player: Entity,
    ) -> GameMap:
        """Generate a new dungeon map."""
    +   player = engine.player
    +   dungeon = GameMap(engine, map_width, map_height, entities=[player])
    -   dungeon = GameMap(map_width, map_height, entities=[player])

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

            ...
            if len(rooms) == 0:
                # The first room, where the player starts.
    +           player.place(*new_room.center, dungeon)
    -           player.x, player.y = new_room.center
            else:  # All rooms after the first.
                ...

    import tile_types

    if TYPE_CHECKING:
        from engine import Engine
        from entity import Entity

    ...
    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,
        engine: Engine,
        player: Entity,
    ) -> GameMap:
        """Generate a new dungeon map."""
        player = engine.player
        dungeon = GameMap(engine, map_width, map_height, entities=[player])
        dungeon = GameMap(map_width, map_height, entities=[player])

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

            ...
            if len(rooms) == 0:
                # The first room, where the player starts.
                player.place(*new_room.center, dungeon)
                player.x, player.y = new_room.center
            else:  # All rooms after the first.
                ...

`engine.py`:

    +from __future__ import annotations

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

    from tcod.context import Context
    from tcod.console import Console
    from tcod.map import compute_fov

    -from entity import Entity
    -from game_map import GameMap
    from input_handlers import EventHandler

    +if TYPE_CHECKING:
    +   from entity import Entity
    +   from game_map import GameMap

    class Engine:
    +   game_map: GameMap

    +   def __init__(self, player: Entity):
    +       self.event_handler: EventHandler = EventHandler(self)
    +       self.player = player
    -   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.

        def update_fov(self) -> None:
            ...

    from __future__ import annotations

    from typing import TYPE_CHECKING
    from typing import Iterable, Any

    from tcod.context import Context
    from tcod.console import Console
    from tcod.map import compute_fov

    from entity import Entity
    from game_map import GameMap
    from input_handlers import EventHandler

    if TYPE_CHECKING:
        from entity import Entity
        from game_map import GameMap

    class Engine:
        game_map: GameMap

        def __init__(self, player: Entity):
            self.event_handler: EventHandler = EventHandler(self)
            self.player = player
        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.

        def update_fov(self) -> None:
            ...

## Onwards to Part 6 Link to heading

The last part of this tutorial set us up for combat, so now it’s time to actually implement it.

In order to make “killable” Entities, rather than attaching hit points to each Entity we create, we’ll create a component, called `Fighter`, which will hold information related to combat, like HP, max HP, attack, and defense. If an Entity can fight, it will have this component attached to it, and if not, it won’t. This way of doing things is called composition, and it’s an alternative to your typical inheritance-based programming model. (This tutorial uses both composition and inheritance).

Create a new Python package (a folder with an empty __init__.py file), called `components`. In that new directory, add two new files, one called `base_component.py`, and another called `fighter.py`. The `Fighter` class in `fighter.py` will inherit from the class we put in `base_component.py`, so let’s start with that one:

    from __future__ import annotations

    from typing import TYPE_CHECKING

    if TYPE_CHECKING:
        from engine import Engine
        from entity import Entity

    class BaseComponent:
        entity: Entity  # Owning entity instance.

        @property
        def engine(self) -> Engine:
            return self.entity.gamemap.engine

With that, let’s now open up `fighter.py` and put the following into it:

    from components.base_component import BaseComponent

    class Fighter(BaseComponent):
        def __init__(self, hp: int, defense: int, power: int):
            self.max_hp = hp
            self._hp = hp
            self.defense = defense
            self.power = power

        @property
        def hp(self) -> int:
            return self._hp

        @hp.setter
        def hp(self, value: int) -> None:
            self._hp = max(0, min(value, self.max_hp))

We import and inherit from `BaseComponent`, which gives us access to the parent entity and the engine, which will be useful later on.

The `__init__` function takes a few arguments. `hp` represents the entity’s hit points. `defense` is how much taken damage will be reduced. `power` is the entity’s raw attack power.

What’s with the `hp` property? We define both a getter and setter, which will allow the class to access `hp` like a normal variable. The getter (the one with the `@property` thing above the method) doesn’t do anything special: it just returns the HP. The `setter` (`@hp.setter`) is where things get more interesting.

By defining HP this way, we can modify the value as it’s set within the method. This line:

            self._hp = max(0, min(value, self.max_hp))

Means that `_hp` (which we access through `hp`) will never be set to less than 0, but also won’t ever go higher than the `max_hp` attribute.

So that’s our `Fighter` component. It won’t do us much good at the moment, because the entities in our game still don’t move or do much of anything (besides the player, anyway). To give some life to our entities, we can add another component, which, when attached to our entities, will allow them to take turns and move around.

Create a file in the `components` directory called `ai.py`, and put the following contents into it:

    from __future__ import annotations

    from typing import List, Tuple

    import numpy as np  # type: ignore
    import tcod

    from actions import Action
    from components.base_component import BaseComponent

    class BaseAI(Action, BaseComponent):
        def perform(self) -> None:
            raise NotImplementedError()

        def get_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]:
            """Compute and return a path to the target position.

            If there is no valid path then returns an empty list.
            """
            # Copy the walkable array.
            cost = np.array(self.entity.gamemap.tiles["walkable"], dtype=np.int8)

            for entity in self.entity.gamemap.entities:
                # Check that an enitiy blocks movement and the cost isn't zero (blocking.)
                if entity.blocks_movement and cost[entity.x, entity.y]:
                    # Add to the cost of a blocked position.
                    # A lower number means more enemies will crowd behind each other in
                    # hallways.  A higher number means enemies will take longer paths in
                    # order to surround the player.
                    cost[entity.x, entity.y] += 10

            # Create a graph from the cost array and pass that graph to a new pathfinder.
            graph = tcod.path.SimpleGraph(cost=cost, cardinal=2, diagonal=3)
            pathfinder = tcod.path.Pathfinder(graph)

            pathfinder.add_root((self.entity.x, self.entity.y))  # Start position.

            # Compute the path to the destination and remove the starting point.
            path: List[List[int]] = pathfinder.path_to((dest_x, dest_y))[1:].tolist()

            # Convert from List[List[int]] to List[Tuple[int, int]].
            return [(index[0], index[1]) for index in path]

`BaseAI` doesn’t implement a `perform` method, since the entities which will be using AI to act will have to have an AI class that inherits from this one.

`get_path_to` uses the “walkable” tiles in our map, along with some TCOD pathfinding tools to get the path from the `BaseAI`’s parent entity to whatever their target might be. In the case of this tutorial, the target will always be the player, though you could theoretically write a monster that cares more about food or treasure than attacking the player.

The pathfinder first builds an array of `cost`, which is how “costly” (time consuming) it will take to get to the target. If a piece of terrain takes longer to traverse, its cost will be higher. In the case of our simple game, all parts of the map have the same cost, but what this cost array allows us to do is take other entities into account.

How? Well, if an entity exists at a spot on the map, we increase the cost of moving there to “10”. What this does is encourages the entity to move around the entity that’s blocking them from their target. Higher values will cause the entity to take a longer path around; shorter values will cause groups to gather into crowds, since they don’t want to move around.

More information about TCOD’s pathfinding can be found here.

To make use of our new `Fighter` and `AI` components, we could attach them directly onto the `Entity` class. However, it might be useful to differentiate between entities that can act, and those that can’t. Right now, our game only consists of acting entities, but soon enough, we’ll be adding things like consumable items and, eventually, equipment, which won’t be able to take turns or take damage.

One way to handle this is to create a new subclass of `Entity`, called `Actor`, and give it all the same attributes as `Entity`, plus the `ai` and `fighter` components it will need. Modify `entity.py` like this:

    from __future__ import annotations

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

    if TYPE_CHECKING:
    +   from components.ai import BaseAI
    +   from components.fighter import Fighter
        from game_map import GameMap

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

    class Entity:
        ...

    +class Actor(Entity):
    +   def __init__(
    +       self,
    +       *,
    +       x: int = 0,
    +       y: int = 0,
    +       char: str = "?",
    +       color: Tuple[int, int, int] = (255, 255, 255),
    +       name: str = "<Unnamed>",
    +       ai_cls: Type[BaseAI],
    +       fighter: Fighter
    +   ):
    +       super().__init__(
    +           x=x,
    +           y=y,
    +           char=char,
    +           color=color,
    +           name=name,
    +           blocks_movement=True,
    +       )

    +       self.ai: Optional[BaseAI] = ai_cls(self)

    +       self.fighter = fighter
    +       self.fighter.entity = self

    +   @property
    +   def is_alive(self) -> bool:
    +       """Returns True as long as this actor can perform actions."""
    +       return bool(self.ai)

    from __future__ import annotations

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

    if TYPE_CHECKING:
        from components.ai import BaseAI
        from components.fighter import Fighter
        from game_map import GameMap

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

    class Entity:
        ...

    class Actor(Entity):
        def __init__(
            self,
            *,
            x: int = 0,
            y: int = 0,
            char: str = "?",
            color: Tuple[int, int, int] = (255, 255, 255),
            name: str = "<Unnamed>",
            ai_cls: Type[BaseAI],
            fighter: Fighter
        ):
            super().__init__(
                x=x,
                y=y,
                char=char,
                color=color,
                name=name,
                blocks_movement=True,
            )

            self.ai: Optional[BaseAI] = ai_cls(self)

            self.fighter = fighter
            self.fighter.entity = self

        @property
        def is_alive(self) -> bool:
            """Returns True as long as this actor can perform actions."""
            return bool(self.ai)

The first thing our `Actor` class does in its `__init__()` function is call its superclass’s `__init__()`, which in this case, is the `Entity` class. We’re passing `blocks_movement` as `True` every time, because we can assume that all the “actors” will block movement.

Besides calling the `Entity.__init__()`, we also set the two components for the `Actor` class: `ai` and `fighter`. The idea is that each actor will need two things to function: the ability to move around and make decisions, and the ability to take (and receive) damage.

This new `Actor` class isn’t quite enough to get our enemies up and moving around, but we’re getting there. We actually need to revisit `ai.py`, and add a new class there to handle hostile enemies. Enter the following changes in `ai.py`:

    from __future__ import annotations

    -from typing import List, Tuple
    +from typing import List, Tuple, TYPE_CHECKING

    import numpy as np  # type: ignore
    import tcod

    -from actions import Action
    +from actions import Action, MeleeAction, MovementAction, WaitAction
    from components.base_component import BaseComponent

    +if TYPE_CHECKING:
    +   from entity import Actor

    class BaseAI(Action, BaseComponent):
    +   entity: Actor

        def perform(self) -> None:
            ...

    +class HostileEnemy(BaseAI):
    +   def __init__(self, entity: Actor):
    +       super().__init__(entity)
    +       self.path: List[Tuple[int, int]] = []

    +   def perform(self) -> None:
    +       target = self.engine.player
    +       dx = target.x - self.entity.x
    +       dy = target.y - self.entity.y
    +       distance = max(abs(dx), abs(dy))  # Chebyshev distance.

    +       if self.engine.game_map.visible[self.entity.x, self.entity.y]:
    +           if distance <= 1:
    +               return MeleeAction(self.entity, dx, dy).perform()

    +           self.path = self.get_path_to(target.x, target.y)

    +       if self.path:
    +           dest_x, dest_y = self.path.pop(0)
    +           return MovementAction(
    +               self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
    +           ).perform()

    +       return WaitAction(self.entity).perform()

    from __future__ import annotations

    from typing import List, Tuple
    from typing import List, Tuple, TYPE_CHECKING

    import numpy as np  # type: ignore
    import tcod

    from actions import Action
    from actions import Action, MeleeAction, MovementAction, WaitAction
    from components.base_component import BaseComponent

    if TYPE_CHECKING:
        from entity import Actor

    class BaseAI(Action, BaseComponent):
        entity: Actor

        def perform(self) -> None:
            ...

    class HostileEnemy(BaseAI):
        def __init__(self, entity: Actor):
            super().__init__(entity)
            self.path: List[Tuple[int, int]] = []

        def perform(self) -> None:
            target = self.engine.player
            dx = target.x - self.entity.x
            dy = target.y - self.entity.y
            distance = max(abs(dx), abs(dy))  # Chebyshev distance.

            if self.engine.game_map.visible[self.entity.x, self.entity.y]:
                if distance <= 1:
                    return MeleeAction(self.entity, dx, dy).perform()

                self.path = self.get_path_to(target.x, target.y)

            if self.path:
                dest_x, dest_y = self.path.pop(0)
                return MovementAction(
                    self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
                ).perform()

            return WaitAction(self.entity).perform()

`HostileEnemy` is the AI class we’ll use for our enemies. It defines the `perform` method, which does the following:

- If the entity is not in the player’s vision, simply wait.
- If the player is right next to the entity (`distance <= 1`), attack the player.
- If the player can see the entity, but the entity is too far away to attack, then move towards the player.

The last line actually calls an action that we haven’t defined yet: `WaitAction`. This action will be used when the player or an enemy decides to wait where they are rather than taking a turn.

Implement `WaitAction` by opening `actions.py`:

    class EscapeAction(Action):
        ...

    +class WaitAction(Action):
    +   def perform(self) -> None:
    +       pass

    class ActionWithDirection(Action):
        ...

    class EscapeAction(Action):
        ...

    class WaitAction(Action):
        def perform(self) -> None:
            pass

    class ActionWithDirection(Action):
        ...

As you can see, `WaitAction` does… well, nothing. And that’s what we want it to do, as it represents an actor saying “I’ll do nothing this turn.”

With all that in place, we’ll need to refactor our `entity_factories.py` file to make use of the new `Actor` class, as well as its components. Modify `entity_factories.py` to look like this:

    +from components.ai import HostileEnemy
    +from components.fighter import Fighter
    +from entity import Actor
    -from entity import Entity

    +player = Actor(
    +   char="@",
    +   color=(255, 255, 255),
    +   name="Player",
    +   ai_cls=HostileEnemy,
    +   fighter=Fighter(hp=30, defense=2, power=5),
    +)
    -player = Entity(char="@", color=(255, 255, 255), name="Player", blocks_movement=True)

    +orc = Actor(
    +   char="o",
    +   color=(63, 127, 63),
    +   name="Orc",
    +   ai_cls=HostileEnemy,
    +   fighter=Fighter(hp=10, defense=0, power=3),
    +)
    +troll = Actor(
    +   char="T",
    +   color=(0, 127, 0),
    +   name="Troll",
    +   ai_cls=HostileEnemy,
    +   fighter=Fighter(hp=16, defense=1, power=4),
    +)
    -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)

    from components.ai import HostileEnemy
    from components.fighter import Fighter
    from entity import Actor
    from entity import Entity

    player = Actor(
        char="@",
        color=(255, 255, 255),
        name="Player",
        ai_cls=HostileEnemy,
        fighter=Fighter(hp=30, defense=2, power=5),
    )
    player = Entity(char="@", color=(255, 255, 255), name="Player", blocks_movement=True)

    orc = Actor(
        char="o",
        color=(63, 127, 63),
        name="Orc",
        ai_cls=HostileEnemy,
        fighter=Fighter(hp=10, defense=0, power=3),
    )
    troll = Actor(
        char="T",
        color=(0, 127, 0),
        name="Troll",
        ai_cls=HostileEnemy,
        fighter=Fighter(hp=16, defense=1, power=4),
    )
    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)

We’ve changed each entity to make use of the `Actor` class, and used the `HostileEnemy` AI class for the Orc and the Troll types. The player doesn’t use the AI, so the AI given to it doesn’t matter other than that an AI must be specified for all `Actor`’s. Also, we defined the `Fighter` component for each, giving a few different values to make the Trolls stronger than the Orcs. Feel free to modify these values to your liking.

How do enemies actually take their turns, though? It’s actually pretty simple: rather than printing the message we were before, we just check if the entity has an AI, and if it does, we call the `perform` method from that AI component. Modify `engine.py` to do this:

        def handle_enemy_turns(self) -> None:
    +       for entity in set(self.game_map.actors) - {self.player}:
    +           if entity.ai:
    +               entity.ai.perform()
    -       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_enemy_turns(self) -> None:
            for entity in set(self.game_map.actors) - {self.player}:
                if entity.ai:
                    entity.ai.perform()
            for entity in self.game_map.entities - {self.player}:
                print(f'The {entity.name} wonders when it will get to take a real turn.')

But wait, `game_map.actors` isn’t defined. What should it do, though? Same thing as `game_map.entities`, except it should return only the `Actor` entities.

Let’s add this method to `GameMap`:

    from __future__ import annotations

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

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

    +from entity import Actor
    import tile_types

    if TYPE_CHECKING:
        from engine import Engine
        from entity import Entity

    class GameMap:
        def __init__(
            ...

    +   @property
    +   def actors(self) -> Iterator[Actor]:
    +       """Iterate over this maps living actors."""
    +       yield from (
    +           entity
    +           for entity in self.entities
    +           if isinstance(entity, Actor) and entity.is_alive
    +       )

        def get_blocking_entity_at_location(
            ...

    +   def get_actor_at_location(self, x: int, y: int) -> Optional[Actor]:
    +       for actor in self.actors:
    +           if actor.x == x and actor.y == y:
    +               return actor

    +       return None

    from __future__ import annotations

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

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

    from entity import Actor
    import tile_types

    if TYPE_CHECKING:
        from engine import Engine
        from entity import Entity

    class GameMap:
        def __init__(
            ...

        @property
        def actors(self) -> Iterator[Actor]:
            """Iterate over this maps living actors."""
            yield from (
                entity
                for entity in self.entities
                if isinstance(entity, Actor) and entity.is_alive
            )

        def get_blocking_entity_at_location(
            ...

        def get_actor_at_location(self, x: int, y: int) -> Optional[Actor]:
            for actor in self.actors:
                if actor.x == x and actor.y == y:
                    return actor

            return None

Our `actors` property will return all the `Actor` entities in the map, but only those that are currently “alive”.

We’ve also went ahead and added a `get_actor_at_location`, which, as the name implies, acts similarly to `get_blocking_entity_at_location`, but returns only an `Actor`. This will come in handy later on.

Run the project now, and the enemies should chase you around! They can’t really attack just yet, but we’re getting there.

One thing you might have noticed is that we’re letting our enemies move and attack in diagonal directions, but our player can only move in the four cardinal directions (up, down, left, right). We can fix that by adjusting `input_handlers.py`. While we’re at it, we might want to define a more flexible way of defining the movement keys rather than the `if...elif` structure we’ve used so far. While that does work, it gets a bit clunky after more than just a few options. We can fix this by modifying `input_handlers.py` like this:

    from __future__ import annotations

    from typing import Optional, TYPE_CHECKING

    import tcod.event

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

    if TYPE_CHECKING:
        from engine import Engine

    +MOVE_KEYS = {
    +   # Arrow keys.
    +   tcod.event.K_UP: (0, -1),
    +   tcod.event.K_DOWN: (0, 1),
    +   tcod.event.K_LEFT: (-1, 0),
    +   tcod.event.K_RIGHT: (1, 0),
    +   tcod.event.K_HOME: (-1, -1),
    +   tcod.event.K_END: (-1, 1),
    +   tcod.event.K_PAGEUP: (1, -1),
    +   tcod.event.K_PAGEDOWN: (1, 1),
    +   # Numpad keys.
    +   tcod.event.K_KP_1: (-1, 1),
    +   tcod.event.K_KP_2: (0, 1),
    +   tcod.event.K_KP_3: (1, 1),
    +   tcod.event.K_KP_4: (-1, 0),
    +   tcod.event.K_KP_6: (1, 0),
    +   tcod.event.K_KP_7: (-1, -1),
    +   tcod.event.K_KP_8: (0, -1),
    +   tcod.event.K_KP_9: (1, -1),
    +   # Vi keys.
    +   tcod.event.K_h: (-1, 0),
    +   tcod.event.K_j: (0, 1),
    +   tcod.event.K_k: (0, -1),
    +   tcod.event.K_l: (1, 0),
    +   tcod.event.K_y: (-1, -1),
    +   tcod.event.K_u: (1, -1),
    +   tcod.event.K_b: (-1, 1),
    +   tcod.event.K_n: (1, 1),
    +}

    +WAIT_KEYS = {
    +   tcod.event.K_PERIOD,
    +   tcod.event.K_KP_5,
    +   tcod.event.K_CLEAR,
    +}

            ...

    -       if key == tcod.event.K_UP:
    -           action = BumpAction(dx=0, dy=-1)
    -       elif key == tcod.event.K_DOWN:
    -           action = BumpAction(dx=0, dy=1)
    -       elif key == tcod.event.K_LEFT:
    -           action = BumpAction(dx=-1, dy=0)
    -       elif key == tcod.event.K_RIGHT:
    -           action = BumpAction(dx=1, dy=0)
    +       if key in MOVE_KEYS:
    +           dx, dy = MOVE_KEYS[key]
    +           action = BumpAction(player, dx, dy)
    +       elif key in WAIT_KEYS:
    +           action = WaitAction(player)

            ...

    from __future__ import annotations

    from typing import Optional, TYPE_CHECKING

    import tcod.event

    from actions import Action, BumpAction, EscapeAction
    from actions import Action, BumpAction, EscapeAction, WaitAction

    if TYPE_CHECKING:
        from engine import Engine

    MOVE_KEYS = {
        # Arrow keys.
        tcod.event.K_UP: (0, -1),
        tcod.event.K_DOWN: (0, 1),
        tcod.event.K_LEFT: (-1, 0),
        tcod.event.K_RIGHT: (1, 0),
        tcod.event.K_HOME: (-1, -1),
        tcod.event.K_END: (-1, 1),
        tcod.event.K_PAGEUP: (1, -1),
        tcod.event.K_PAGEDOWN: (1, 1),
        # Numpad keys.
        tcod.event.K_KP_1: (-1, 1),
        tcod.event.K_KP_2: (0, 1),
        tcod.event.K_KP_3: (1, 1),
        tcod.event.K_KP_4: (-1, 0),
        tcod.event.K_KP_6: (1, 0),
        tcod.event.K_KP_7: (-1, -1),
        tcod.event.K_KP_8: (0, -1),
        tcod.event.K_KP_9: (1, -1),
        # Vi keys.
        tcod.event.K_h: (-1, 0),
        tcod.event.K_j: (0, 1),
        tcod.event.K_k: (0, -1),
        tcod.event.K_l: (1, 0),
        tcod.event.K_y: (-1, -1),
        tcod.event.K_u: (1, -1),
        tcod.event.K_b: (-1, 1),
        tcod.event.K_n: (1, 1),
    }

    WAIT_KEYS = {
        tcod.event.K_PERIOD,
        tcod.event.K_KP_5,
        tcod.event.K_CLEAR,
    }

            ...

            if key == tcod.event.K_UP:
                action = BumpAction(player, dx=0, dy=-1)
            elif key == tcod.event.K_DOWN:
                action = BumpAction(player, dx=0, dy=1)
            elif key == tcod.event.K_LEFT:
                action = BumpAction(player, dx=-1, dy=0)
            elif key == tcod.event.K_RIGHT:
                action = BumpAction(player, dx=1, dy=0)
            if key in MOVE_KEYS:
                dx, dy = MOVE_KEYS[key]
                action = BumpAction(player, dx, dy)
            elif key in WAIT_KEYS:
                action = WaitAction(player)

            ...

The `MOVE_KEYS` dictionary holds various different possibilities for movement. Some roguelikes utilize the numpad for movement, some use “Vi Keys.” Ours will actually use both for the time being. Feel free to change the key scheme if you’re not a fan of it.

Where we used to do `if...elif` statements for each direction, we can now just check if the key was part of `MOVE_KEYS`, and if it was, we return the `dx` and `dy` values from the dictionary. This is a lot simpler and cleaner than our previous format.

So now that our enemies can chase us down, it’s time to make them do some real damage.

Open up `actions.py`:

    from __future__ import annotations

    from typing import Optional, Tuple, TYPE_CHECKING

    if TYPE_CHECKING:
        from engine import Engine
    -   from entity import Entity
    +   from entity import Actor, Entity

    class Action:
    -   def __init__(self, entity: Entity) -> None:
    +   def __init__(self, entity: Actor) -> None:
            super().__init__()
            self.entity = entity

        ...

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

            self.dx = dx
            self.dy = dy

        @property
        def dest_xy(self) -> Tuple[int, int]:
            """Returns this actions destination."""
            return self.entity.x + self.dx, self.entity.y + self.dy

        @property
        def blocking_entity(self) -> Optional[Entity]:
            """Return the blocking entity at this actions destination.."""
            return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)

    +   @property
    +   def target_actor(self) -> Optional[Actor]:
    +       """Return the actor at this actions destination."""
    +       return self.engine.game_map.get_actor_at_location(*self.dest_xy)

        def perform(self) -> None:
            raise NotImplementedError()

    class MeleeAction(ActionWithDirection):
        def perform(self) -> None:
    +       target = self.target_actor
    -       target = self.blocking_entity
            if not target:
                return  # No entity to attack.

    +       damage = self.entity.fighter.power - target.fighter.defense

    +       attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
    +       if damage > 0:
    +           print(f"{attack_desc} for {damage} hit points.")
    +           target.fighter.hp -= damage
    +       else:
    +           print(f"{attack_desc} but does no damage.")
    -       print(f"You kick the {target.name}, much to its annoyance!")

    class MovementAction(ActionWithDirection):
        ...

    class BumpAction(ActionWithDirection):
        def perform(self) -> None:
    -       if self.blocking_entity:
    +       if self.target_actor:
                return MeleeAction(self.entity, self.dx, self.dy).perform()

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

    from __future__ import annotations

    from typing import Optional, Tuple, TYPE_CHECKING

    if TYPE_CHECKING:
        from engine import Engine
        from entity import Entity
        from entity import Actor, Entity

    class Action:
        def __init__(self, entity: Entity) -> None:
        def __init__(self, entity: Actor) -> None:
            super().__init__()
            self.entity = entity

        ...

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

            self.dx = dx
            self.dy = dy

        @property
        def dest_xy(self) -> Tuple[int, int]:
            """Returns this actions destination."""
            return self.entity.x + self.dx, self.entity.y + self.dy

        @property
        def blocking_entity(self) -> Optional[Entity]:
            """Return the blocking entity at this actions destination.."""
            return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)

        @property
        def target_actor(self) -> Optional[Actor]:
            """Return the actor at this actions destination."""
            return self.engine.game_map.get_actor_at_location(*self.dest_xy)

        def perform(self) -> None:
            raise NotImplementedError()

    class MeleeAction(ActionWithDirection):
        def perform(self) -> None:
            target = self.target_actor
            target = self.blocking_entity
            if not target:
                return  # No entity to attack.

            damage = self.entity.fighter.power - target.fighter.defense

            attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
            if damage > 0:
                print(f"{attack_desc} for {damage} hit points.")
                target.fighter.hp -= damage
            else:
                print(f"{attack_desc} but does no damage.")
            print(f"You kick the {target.name}, much to its annoyance!")

    class MovementAction(ActionWithDirection):
        ...

    class BumpAction(ActionWithDirection):
        def perform(self) -> None:
            if self.blocking_entity:
            if self.target_actor:
                return MeleeAction(self.entity, self.dx, self.dy).perform()

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

We’re replacing the type hint for `entity` in `Action` and `ActionWithDirection` with `Actor` instead of `Entity`, since only `Actor`s should be taking actions.

We’ve also added the `target_actor` property to `ActionWithDirection`, which will give us the `Actor` at the destination we’re moving to, if there is one. We utilize that property instead of `blocking_entity` in both `BumpAction` and `MeleeAction`.

Lastly, we modify `MeleeAction` to actually do an attack, instead of just printing a message. We calculate the damage (attacker’s power minus defender’s defense), and assign a description to the attack, based on whether any damage was done or not. If the damage is greater than 0, we subtract it from the defender’s HP.

If you run the project now, you’ll see the print statements indicating that the player and the enemies are doing damage to each other. But since neither side can actually die, combat doesn’t feel all that high stakes just yet.

What do we do when an Entity reaches 0 HP or lower? Well, it should drop dead, obviously! But what should our code do to make this happen? To handle this, we can refer back to our `Fighter` component.

Remember when we created a setter for `hp`? It will come in handy right now, as we can utilize it to automatically “kill” the actor when their HP drops to zero. Add the following to `fighter.py`:

    +from __future__ import annotations

    +from typing import TYPE_CHECKING

    from components.base_component import BaseComponent

    +if TYPE_CHECKING:
    +   from entity import Actor

    class Fighter(BaseComponent):
    +   entity: Actor

        def __init__(self, hp: int, defense: int, power: int):
            self.max_hp = hp
            self._hp = hp
            self.defense = defense
            self.power = power

        @property
        def hp(self) -> int:
            return self._hp

        @hp.setter
        def hp(self, value: int) -> None:
            self._hp = max(0, min(value, self.max_hp))
    +       if self._hp == 0 and self.entity.ai:
    +           self.die()

    +   def die(self) -> None:
    +       if self.engine.player is self.entity:
    +           death_message = "You died!"
    +       else:
    +           death_message = f"{self.entity.name} is dead!"

    +       self.entity.char = "%"
    +       self.entity.color = (191, 0, 0)
    +       self.entity.blocks_movement = False
    +       self.entity.ai = None
    +       self.entity.name = f"remains of {self.entity.name}"

    +       print(death_message)

    from __future__ import annotations

    from typing import TYPE_CHECKING

    from components.base_component import BaseComponent

    if TYPE_CHECKING:
        from entity import Actor

    class Fighter(BaseComponent):
        entity: Actor

        def __init__(self, hp: int, defense: int, power: int):
            self.max_hp = hp
            self._hp = hp
            self.defense = defense
            self.power = power

        @property
        def hp(self) -> int:
            return self._hp

        @hp.setter
        def hp(self, value: int) -> None:
            self._hp = max(0, min(value, self.max_hp))
            if self._hp == 0 and self.entity.ai:
                self.die()

        def die(self) -> None:
            if self.engine.player is self.entity:
                death_message = "You died!"
            else:
                death_message = f"{self.entity.name} is dead!"

            self.entity.char = "%"
            self.entity.color = (191, 0, 0)
            self.entity.blocks_movement = False
            self.entity.ai = None
            self.entity.name = f"remains of {self.entity.name}"

            print(death_message)

When the actor dies, we use the `die` method to do several things:

- Print out a message, indicating the death of the entity
- Set the entity’s character to “%” (most roguelikes use this for corpses)
- Set its color to red (for a bloody, gory mess)
- Set `blocks_movement` to `False`, so that the entities can walk over the corpse
- Remove the AI from the entity, so it’ll be marked as dead and won’t take any more turns.
- Change the name to “remains of {entity name}”

Run the project now, and enjoy slaughtering some Orcs and Trolls!

As satisfying as it would be to end here, our work is not quite done. If you play the game a bit, you’ll notice two problems.

The first is that, sometimes, corpses actually cover up entities.

The player is currently under the corpse in the screenshot.

This not only makes no sense, since the entities should be walking over the corpses, but it can confuse the player rather easily.

The other issue is much more severe. Try playing the game and letting yourself die on purpose.

The player does indeed turn into a corpse, but… you can still move around, and even attack enemies! This is because the game doesn’t really “end” at the moment when the player dies. The only thing that changes is that the player’s AI component is set to `None`, but that isn’t actually what controls the player, the `EventHandler` class does that.

Let’s focus on the first issue first. Solving it is actually pretty easy. What we’ll do is assign a value to each `Entity`, and this value will represent which order the entities should be rendered in. Lower values will be rendered first, and higher values will be rendered after. Therefore, if we assign a low value to a corpse, it will get drawn before an entity. If two things are on the same tile, whatever gets drawn last will be what the player sees.

To create the render values we’ll need, create a new file, called `render_order.py`, and put the following class in it:

    from enum import auto, Enum

    class RenderOrder(Enum):
        CORPSE = auto()
        ITEM = auto()
        ACTOR = auto()

Note: You’ll need Python 3.6 or higher for the `auto` function to work.

`RenderOrder` is an `Enum`. An “Enum” is a set of named values that won’t change, so it’s perfect for things like this. `auto` assigns incrementing integer values automatically, so we don’t need to retype them if we add more values later on.

To use this new Enum, let’s edit `entity.py`:

    from __future__ import annotations

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

    +from render_order import RenderOrder

    if TYPE_CHECKING:
        from components.ai import BaseAI
        from components.fighter import Fighter
        from game_map import GameMap

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

    class Entity:
        """
        A generic object to represent players, enemies, items, etc.
        """

        gamemap: GameMap

        def __init__(
            self,
            gamemap: Optional[GameMap] = None,
            x: int = 0,
            y: int = 0,
            char: str = "?",
            color: Tuple[int, int, int] = (255, 255, 255),
            name: str = "<Unnamed>",
            blocks_movement: bool = False,
    +       render_order: RenderOrder = RenderOrder.CORPSE,
        ):
            self.x = x
            self.y = y
            self.char = char
            self.color = color
            self.name = name
            self.blocks_movement = blocks_movement
    +       self.render_order = render_order
            if gamemap:
                # If gamemap isn't provided now then it will be set later.
                self.gamemap = gamemap
                gamemap.entities.add(self)
        ...

    class Actor(Entity):
        def __init__(
            self,
            *,
            x: int = 0,
            y: int = 0,
            char: str = "?",
            color: Tuple[int, int, int] = (255, 255, 255),
            name: str = "<Unnamed>",
            ai_cls: Type[BaseAI],
            fighter: Fighter
        ):
            super().__init__(
                x=x,
                y=y,
                char=char,
                color=color,
                name=name,
                blocks_movement=True,
    +           render_order=RenderOrder.ACTOR,
            )

    from __future__ import annotations

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

    from render_order import RenderOrder

    if TYPE_CHECKING:
        from components.ai import BaseAI
        from components.fighter import Fighter
        from game_map import GameMap

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

    class Entity:
        """
        A generic object to represent players, enemies, items, etc.
        """

        gamemap: GameMap

        def __init__(
            self,
            gamemap: Optional[GameMap] = None,
            x: int = 0,
            y: int = 0,
            char: str = "?",
            color: Tuple[int, int, int] = (255, 255, 255),
            name: str = "<Unnamed>",
            blocks_movement: bool = False,
            render_order: RenderOrder = RenderOrder.CORPSE,
        ):
            self.x = x
            self.y = y
            self.char = char
            self.color = color
            self.name = name
            self.blocks_movement = blocks_movement
            self.render_order = render_order
            if gamemap:
                # If gamemap isn't provided now then it will be set later.
                self.gamemap = gamemap
                gamemap.entities.add(self)
        ...

    class Actor(Entity):
        def __init__(
            self,
            *,
            x: int = 0,
            y: int = 0,
            char: str = "?",
            color: Tuple[int, int, int] = (255, 255, 255),
            name: str = "",
            ai_cls: Type[BaseAI],
            fighter: Fighter
        ):
            super().__init__(
                x=x,
                y=y,
                char=char,
                color=color,
                name=name,
                blocks_movement=True,
                render_order=RenderOrder.ACTOR,
            )

We’re now passing the render order to the `Entity` class, with a default of `CORPSE`. Notice that we don’t pass it to `Actor`, and instead, assume that the actor’s default will be the `ACTOR` value.

In order to actually take advantage of the rendering order, we’ll need to modify the part of `GameMap` that renders the entities to the screen. Modify the `render` method in `GameMap` like this:

        ...
        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
            )

    +       entities_sorted_for_rendering = sorted(
    +           self.entities, key=lambda x: x.render_order.value
    +       )

    -       for entity in self.entities:
    +       for entity in entities_sorted_for_rendering:
                if self.visible[entity.x, entity.y]:
    -               console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
    +               console.print(
    +                   x=entity.x, y=entity.y, string=entity.char, fg=entity.color
    +               )

        ...
        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
            )

            entities_sorted_for_rendering = sorted(
                self.entities, key=lambda x: x.render_order.value
            )

            for entity in self.entities:
            for entity in entities_sorted_for_rendering:
                if self.visible[entity.x, entity.y]:
                    console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
                    console.print(
                        x=entity.x, y=entity.y, string=entity.char, fg=entity.color
                    )

The `sorted` function takes two arguments: The collection to sort, and the function used to sort it. By using `key` in `sorted`, we’re defining a custom way to sort the `self.entities`, which in this case, we’re using a `lambda` function (basically, a function that’s limited to one line that we don’t need to write a formal definition for). The lambda function itself tells `sorted` to sort by the value of `render_order`. Since the `RenderOrder` enum defines its order from 1 (Corpse, lowest) to 3 (Actor, highest), corpses should be sent to the front of the sorted list. That way, when rendering, they’ll get drawn first, so if there’s something else on top of them, they’ll get overwritten, and we’ll just see the `Actor` instead of the corpse.

Last thing we need to do is rewrite the `render_order` of an entity when it dies. Go back to the `Fighter` class and add the following:

    from __future__ import annotations

    from typing import TYPE_CHECKING

    from components.base_component import BaseComponent
    +from render_order import RenderOrder

    if TYPE_CHECKING:
        from entity import Actor

    class Fighter(BaseComponent):
        ...
            ...
            self.entity.ai = None
            self.entity.name = f"remains of {self.entity.name}"
    +       self.entity.render_order = RenderOrder.CORPSE

            print(death_message)

    from __future__ import annotations

    from typing import TYPE_CHECKING

    from components.base_component import BaseComponent
    from render_order import RenderOrder

    if TYPE_CHECKING:
        from entity import Actor

    class Fighter(BaseComponent):
        ...
            ...
            self.entity.ai = None
            self.entity.name = f"remains of {self.entity.name}"
            self.entity.render_order = RenderOrder.CORPSE

            print(death_message)

Run the project now, and the corpse ordering issue should be resolved.

Now, onto the more important issue: solving the player’s death.

One thing that would be helpful right now is being able to see the player’s HP. Otherwise, the player will just kinda drop dead after a while, and it’ll be difficult for the player to know how close they are to death’s door.

Add the following line to the `render` function in the `Engine` class:

    if TYPE_CHECKING:
    -   from entity import Entity
    +   from entity import Actor
        from game_map import GameMap

    class Engine:
        game_map: GameMap

    -   def __init__(self, player: Entity):
    +   def __init__(self, player: Actor):
            ...

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

    +       console.print(
    +           x=1,
    +           y=47,
    +           string=f"HP: {self.player.fighter.hp}/{self.player.fighter.max_hp}",
    +       )

            context.present(console)

            console.clear()

    if TYPE_CHECKING:
        from entity import Entity
        from entity import Actor
        from game_map import GameMap

    class Engine:
        game_map: GameMap

        def __init__(self, player: Entity):
        def __init__(self, player: Actor):
            ...

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

            console.print(
                x=1,
                y=47,
                string=f"HP: {self.player.fighter.hp}/{self.player.fighter.max_hp}",
            )

            context.present(console)

            console.clear()

Pretty simple. We’re printing the player’s HP current health over maximum health below the map. It’s not the most attractive looking health display, that’s for sure, but it should suffice for now. A better looking way to show the character’s health is coming shortly anyway, in the next chapter.

Notice that we also updated the type hint for the `player` argument in the Engine’s `__init__` function.

The health indicator is great and all, but our player is still animated after death. There’s a few ways to handle this, but the way we’ll go with is swapping out the `EventHandler` class. Why? Because what we want to do right now is disallow the player from moving around after dying. An easy way to do that is to stop reacting to the movement keypresses. By switching to a different `EventHandler`, we can do just that.

What we’ll want to do is actually modify our existing `EventHandler` to be a base class, and inherit from it in two new classes: `MainGameEventHandler`, and `GameOverEventHandler`. `MainGameEventHandler` will actually do what our current implementation of `EventHandler` does, and `GameOverEventHandler` will handle things when the main character meets his or her untimely demise.

Open up `input_handlers.py` and make the following adjustments:

    class EventHandler(tcod.event.EventDispatch[Action]):
        def __init__(self, engine: Engine):
            self.engine = engine

    +   def handle_events(self) -> None:
    +       raise NotImplementedError()

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

    +class MainGameEventHandler(EventHandler):
        def handle_events(self) -> None:
            for event in tcod.event.wait():
                action = self.dispatch(event)

                if action is None:
                    continue

                action.perform()

                self.engine.handle_enemy_turns()
                self.engine.update_fov()  # Update the FOV before the players next 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

            player = self.engine.player

            if key in MOVE_KEYS:
                dx, dy = MOVE_KEYS[key]
                action = BumpAction(player, dx, dy)
            elif key in WAIT_KEYS:
                action = WaitAction(player)

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

            # No valid key was pressed
            return action

    +class GameOverEventHandler(EventHandler):
    +   def handle_events(self) -> None:
    +       for event in tcod.event.wait():
    +           action = self.dispatch(event)

    +           if action is None:
    +               continue

    +           action.perform()

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

    +       key = event.sym

    +       if key == tcod.event.K_ESCAPE:
    +           action = EscapeAction(self.engine.player)

    +       # No valid key was pressed
    +       return action

    class EventHandler(tcod.event.EventDispatch[Action]):
        def __init__(self, engine: Engine):
            self.engine = engine

        def handle_events(self) -> None:
            raise NotImplementedError()

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

    class MainGameEventHandler(EventHandler):
        def handle_events(self) -> None:
            for event in tcod.event.wait():
                action = self.dispatch(event)

                if action is None:
                    continue

                action.perform()

                self.engine.handle_enemy_turns()
                self.engine.update_fov()  # Update the FOV before the players next 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

            player = self.engine.player

            if key in MOVE_KEYS:
                dx, dy = MOVE_KEYS[key]
                action = BumpAction(player, dx, dy)
            elif key in WAIT_KEYS:
                action = WaitAction(player)

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

            # No valid key was pressed
            return action

    class GameOverEventHandler(EventHandler):
        def handle_events(self) -> None:
            for event in tcod.event.wait():
                action = self.dispatch(event)

                if action is None:
                    continue

                action.perform()

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

            key = event.sym

            if key == tcod.event.K_ESCAPE:
                action = EscapeAction(self.engine.player)

            # No valid key was pressed
            return action

`EventHandler` is now the base class for our other two classes.

`MainGameEventHandler` is almost identical to our original `EventHandler` class, except that it doesn’t need to implement `ev_quit`, as `EventHandler` takes care of that just fine.

`GameOverEventHandler` is what’s really new here. It doesn’t look terribly different from `MainGameEventHandler`, except for a few key differences.

- After performing its actions, it doesn’t call the enemy turns nor update the FOV.
- It also doesn’t respond to the movement keys, just `Esc`, so the player can still exit the game.

Because we’re replacing our old implementation of `EventHandler` with `MainGameEventHandler`, we’ll need to adjust `engine.py` to use `MainGameEventHandler`:

    from tcod.map import compute_fov

    -from input_handlers import EventHandler
    +from input_handlers import MainGameEventHandler

    if TYPE_CHECKING:
        from entity import Actor
        from game_map import GameMap
    +   from input_handlers import EventHandler

    class Engine:
        game_map: GameMap

        def __init__(self, player: Actor):
    -       self.event_handler: EventHandler = EventHandler(self)
    +       self.event_handler: EventHandler = MainGameEventHandler(self)
            self.player = player

    from tcod.map import compute_fov

    from input_handlers import EventHandler
    from input_handlers import MainGameEventHandler

    if TYPE_CHECKING:
        from entity import Actor
        from game_map import GameMap
        from input_handlers import EventHandler

    class Engine:
        game_map: GameMap

        def __init__(self, player: Actor):
            self.event_handler: EventHandler = EventHandler(self)
            self.event_handler: EventHandler = MainGameEventHandler(self)
            self.player = player

Lastly, we can use the `GameOverEventHandler` in `fighter.py` to ensure the player cannot move after death:

    from __future__ import annotations

    from typing import TYPE_CHECKING

    from components.base_component import BaseComponent
    +from input_handlers import GameOverEventHandler
    from render_order import RenderOrder

    if TYPE_CHECKING:
        from entity import Actor

    class Fighter(BaseComponent):
        ...

        def die(self) -> None:
            if self.engine.player is self.entity:
                death_message = "You died!"
    +           self.engine.event_handler = GameOverEventHandler(self.engine)
            else:
                death_message = f"{self.entity.name} is dead!"

    from __future__ import annotations

    from typing import TYPE_CHECKING

    from components.base_component import BaseComponent
    from input_handlers import GameOverEventHandler
    from render_order import RenderOrder

    if TYPE_CHECKING:
        from entity import Actor

    class Fighter(BaseComponent):
        ...

        def die(self) -> None:
            if self.engine.player is self.entity:
                death_message = "You died!"
                self.engine.event_handler = GameOverEventHandler(self.engine)
            else:
                death_message = f"{self.entity.name} is dead!"

And with that last change, the main character should die, for real this time! You’ll be unable to move or attack, but you can still exit the game as normal.

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.