# Part 2 - The generic Entity, the render functions, and the map

Now that we can move our little ‘@’ symbol around, we need to give it something to move around in. But before that, let’s stop for a moment and think about the player object itself.

Right now, we just represent the player with the ‘@’ symbol, and its x and y coordinates. Shouldn’t we tie those things together in an object, along with some other data and functions that pertain to it?

Let’s create a generic class to represent not just the player, but just about everything in our game world. Enemies, items, and whatever other foreign entities we can dream of will be part of this class, which we’ll call `Entity`.

Create a new file, and call it `entity.py`. In that file, put the following class:

    from typing import Tuple

    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]):
            self.x = x
            self.y = y
            self.char = char
            self.color = color

        def move(self, dx: int, dy: int) -> None:
            # Move the entity by a given amount
            self.x += dx
            self.y += dy

The initializer (`__init__`) takes four arguments: `x`, `y`, `char`, and `color`.

- `x` and `y` are pretty self explanatory: They represent the Entity’s “x” and “y” coordinates on the map.
- `char` is the character we’ll use to represent the entity. Our player will be an “@” symbol, whereas something like a Troll (coming in a later chapter) can be the letter “T”.
- `color` is the color we’ll use when drawing the Entity. We define `color` as a Tuple of three integers, representing the entity’s RGB values.

The other method is `move`, which takes `dx` and `dy` as arguments, and uses them to modify the Entity’s position. This should look familiar to what we did in the last chapter.

Let’s put our fancy new class into action! Modify the first part of `main.py` to look like this:

    #!/usr/bin/env python3
    import tcod

    from actions import EscapeAction, MovementAction
    +from entity import Entity
    from input_handlers import EventHandler

    def main() -> None:
        screen_width = 80
        screen_height = 50

    -   player_x = int(screen_width / 2)
    -   player_y = int(screen_height / 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))
    +   npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
    +   entities = {npc, player}

        with tcod.context.new_terminal(
            ...

    #!/usr/bin/env python3
    import tcod

    from actions import EscapeAction, MovementAction
    from entity import Entity
    from input_handlers import EventHandler

    def main() -> None:
        screen_width = 80
        screen_height = 50

        player_x = int(screen_width / 2)
        player_y = int(screen_height / 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))
        npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
        entities = {npc, player}

        with tcod.context.new_terminal(
            ...

We’re importing the `Entity` class into `main.py`, and using it to initialize the player and a new NPC. We store these two in a set, that will eventually hold all our entities on the map.

Also modify the part where we handle movement so that the Entity class handles the actual movement.

                    if isinstance(action, MovementAction):
    -                   player_x += action.dx
    -                   player_y += action.dy
    +                   player.move(dx=action.dx, dy=action.dy)

                    if isinstance(action, MovementAction):
                        player_x += action.dx
                        player_y += action.dy
                        player.move(dx=action.dx, dy=action.dy)

Lastly, update the drawing functions to use the new player object:

            while True:
    -           root_console.print(x=player_x, y=player_y, string="@")
    +           root_console.print(x=player.x, y=player.y, string=player.char, fg=player.color)

                context.present(root_console)

            while True:
                root_console.print(x=player_x, y=player_y, string="@")
                root_console.print(x=player.x, y=player.y, string=player.char, fg=player.color)

                context.present(root_console)

If you run the project now, only the player gets drawn. We’ll need to modify things to draw both entities, and eventually, draw the map we’re going to create as well.

Before doing that, it’s worth stopping and taking a moment to think about our overall design. Currently, our `main.py` file is responsible for:

- Setting up the initial variables, like screen size and the tileset.
- Creating the entities
- Drawing the screen and everything on it.
- Reacting to the player’s input.

Soon, we’re going to need to add a map as well. It’s starting to become a bit much.

One thing we can do is pass of some of these responsibilities to another class, which will be responsible for “running” our game. The `main.py` file can still set things up and tell that new class what to do, but this design should help keep the `main.py` file from getting too large over time.

Let’s create an `Engine` class, which will take the responsibilities of drawing the map and entities, as well as handling the player’s input. Create a new file, and call it `engine.py`. In that file, put the following contents:

    from typing import Set, Iterable, Any

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

    from actions import EscapeAction, MovementAction
    from entity import Entity
    from input_handlers import EventHandler

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

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

                if action is None:
                    continue

                if isinstance(action, MovementAction):
                    self.player.move(dx=action.dx, dy=action.dy)

                elif isinstance(action, EscapeAction):
                    raise SystemExit()

        def render(self, console: Console, context: Context) -> None:
            for entity in self.entities:
                console.print(entity.x, entity.y, entity.char, fg=entity.color)

            context.present(console)

            console.clear()

Let’s walk through the class a bit, to understand what we’re trying to get at here.

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

The `__init__` function takes three arguments:

- `entities` is a set (of entities), which behaves kind of like a list that enforces uniqueness. That is, we can’t add an Entity to the set twice, whereas a list would allow that. In our case, having an entity in `entities` twice doesn’t make sense.
- `event_handler` is the same `event_handler` that we used in `main.py`. It will handle our events.
- `player` is the player Entity. We have a separate reference to it outside of `entities` for ease of access. We’ll need to access `player` a lot more than a random entity in `entities`.

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

                if action is None:
                    continue

                if isinstance(action, MovementAction):
                    self.player.move(dx=action.dx, dy=action.dy)

                elif isinstance(action, EscapeAction):
                    raise SystemExit()

This should look familiar: It’s almost identical to our event processing in `main.py`. We pass the `events` to it so it can iterate through them, and it uses `self.event_handler` to handle the events.

        def render(self, console: Console, context: Context) -> None:
            for entity in self.entities:
                console.print(entity.x, entity.y, entity.char, fg=entity.color)

            context.present(console)

            console.clear()

This handles drawing our screen. We iterate through the `self.entities` and print them to their proper locations, then present the context, and clear the console, like we did in `main.py`.

To make use of our new `Engine` class, we’ll need to modify `main.py` quite a bit.

    #!/usr/bin/env python3
    import tcod

    -from actions import EscapeAction, MovementAction
    +from engine import Engine
    from entity import Entity
    from input_handlers import EventHandler

    def main() -> None:
        screen_width = 80
        screen_height = 50

        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))
        npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
        entities = {npc, player}

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

        with tcod.context.new_terminal(
            screen_width,
            screen_height,
            tileset=tileset,
            title="Yet Another Roguelike Tutorial",
            vsync=True,
        ) as context:
            root_console = tcod.Console(screen_width, screen_height, order="F")
            while True:
    -           root_console.print(x=player_x, y=player_y, string="@")
    +           engine.render(console=root_console, context=context)

    -           context.present(root_console)
    +           events = tcod.event.wait()

    +           engine.handle_events(events)
    -           root_console.clear()

    -           for event in tcod.event.wait():
    -               action = event_handler.dispatch(event)

    -               if action is None:
    -                   continue

    -               if isinstance(action, MovementAction):
    -                   player_x += action.dx
    -                   player_y += action.dy

    -               elif isinstance(action, EscapeAction):
    -                   raise SystemExit()

    if __name__ == "__main__":
        main()

    #!/usr/bin/env python3
    import tcod

    from actions import EscapeAction, MovementAction
    from engine import Engine
    from entity import Entity
    from input_handlers import EventHandler

    def main() -> None:
        screen_width = 80
        screen_height = 50

        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))
        npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
        entities = {npc, player}

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

        with tcod.context.new_terminal(
            screen_width,
            screen_height,
            tileset=tileset,
            title="Yet Another Roguelike Tutorial",
            vsync=True,
        ) as context:
            root_console = tcod.Console(screen_width, screen_height, order="F")
            while True:
                root_console.print(x=player_x, y=player_y, string="@")
                engine.render(console=root_console, context=context)

                context.present(root_console)
                events = tcod.event.wait()

                engine.handle_events(events)
                root_console.clear()

                for event in tcod.event.wait():
                    action = event_handler.dispatch(event)

                    if action is None:
                        continue

                    if isinstance(action, MovementAction):
                        player_x += action.dx
                        player_y += action.dy

                    elif isinstance(action, EscapeAction):
                        raise SystemExit()

    if __name__ == "__main__":
        main()

Because we’ve moved the rendering and event handling code to the `Engine` class, we no longer need it in `main.py`. All we need to do is create the `Engine` instance, pass the needed variables to it, and use the methods we wrote for it.

Run the project now, and your screen should look like this:

Our `main.py` file is looking a lot smaller and simpler, and we’ve rendered both the player and the NPC to the screen. With that, we’ll want to move on to creating a map for our entity to move around in. We won’t do the procedural dungeon generation in this chapter (that’s next), but we’ll at least get our class that will hold that map set up.

We can represent the map with a new class, called `GameMap`. The map itself will be made up of tiles, which will contain certain data about if the tile is “walkable” (True if it’s a floor, False if its a wall), “transparency” (again, True for floors, False for walls), and how to render the tile to the screen.

We’ll create the `tiles` first. Create a new file called `tile_types.py` and fill it with the following contents:

    from typing import Tuple

    import numpy as np  # type: ignore

    # Tile graphics structured type compatible with Console.tiles_rgb.
    graphic_dt = np.dtype(
        [
            ("ch", np.int32),  # Unicode codepoint.
            ("fg", "3B"),  # 3 unsigned bytes, for RGB colors.
            ("bg", "3B"),
        ]
    )

    # Tile struct used for statically defined tile data.
    tile_dt = np.dtype(
        [
            ("walkable", np.bool),  # True if this tile can be walked over.
            ("transparent", np.bool),  # True if this tile doesn't block FOV.
            ("dark", graphic_dt),  # Graphics for when this tile is not in FOV.
        ]
    )

    def new_tile(
        *,  # Enforce the use of keywords, so that parameter order doesn't matter.
        walkable: int,
        transparent: int,
        dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
    ) -> np.ndarray:
        """Helper function for defining individual tile types """
        return np.array((walkable, transparent, dark), dtype=tile_dt)

    floor = new_tile(
        walkable=True, transparent=True, dark=(ord(" "), (255, 255, 255), (50, 50, 150)),
    )
    wall = new_tile(
        walkable=False, transparent=False, dark=(ord(" "), (255, 255, 255), (0, 0, 100)),
    )

That’s quite a lot to take in all at once. Let’s go through it.

    # Tile graphics structured type compatible with Console.tiles_rgb.
    graphic_dt = np.dtype(
        [
            ("ch", np.int32),  # Unicode codepoint.
            ("fg", "3B"),  # 3 unsigned bytes, for RGB colors.
            ("bg", "3B"),
        ]
    )

`dtype` creates a data type which Numpy can use, which behaves similarly to a `struct` in a language like C. Our data type is made up of three parts:

- `ch`: The character, represented in integer format. We’ll translate it from the integer into Unicode.
- `fg`: The foreground color. “3B” means 3 unsigned bytes, which can be used for RGB color codes.
- `bg`: The background color. Similar to `fg`.

We take this new data type and use it in the next bit:

    # Tile struct used for statically defined tile data.
    tile_dt = np.dtype(
        [
            ("walkable", np.bool),  # True if this tile can be walked over.
            ("transparent", np.bool),  # True if this tile doesn't block FOV.
            ("dark", graphic_dt),  # Graphics for when this tile is not in FOV.
        ]
    )

This is yet another `dtype`, which we’ll use in the actual tile itself. It’s also made up of three parts:

- `walkable`: A boolean that describes if the player can walk across this tile.
- `transparent`: A boolean that describes if this tile does or does not block the field of view. Not used in this chapter, but will be in chapter 4.
- `dark`: This uses our previously defined `dtype`, which holds the character to print, the foreground color, and the background color. Why is it called `dark`? Because later on, we’ll want to differentiate between tiles that are and aren’t in the field of view. `dark` will represent tiles that are not in the current field of view. Again, we’ll cover that in part 4.

    def new_tile(
        *,  # Enforce the use of keywords, so that parameter order doesn't matter.
        walkable: int,
        transparent: int,
        dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
    ) -> np.ndarray:
        """Helper function for defining individual tile types """
        return np.array((walkable, transparent, dark), dtype=tile_dt)

This is a helper function, that we’ll use in the next section to define our tile types. It takes the parameters `walkable`, `transparent`, and `dark`, which should look familiar, since they’re the same data points we used in `tile_dt`. It creates a Numpy array of just the one `tile_dt` element, and returns it.

    floor = new_tile(
        walkable=True, transparent=True, dark=(ord(" "), (255, 255, 255), (50, 50, 150)),
    )
    wall = new_tile(
        walkable=False, transparent=False, dark=(ord(" "), (255, 255, 255), (0, 0, 100)),
    )

Finally, we arrive to our actual tile types. We’ve got two: `floor` and `wall`.

`floor` is both `walkable` and `transparent`. Its `dark` attribute consists of the space character (feel free to change this to something else, a lot of roguelikes use “#”) and defines its foreground color as white (won’t matter since it’s an empty space) and a background color.

`wall` is neither `walkable` nor `transparent`, and its `dark` attribute differs from `floor` slightly in its background color.

Now let’s use our newly created tiles by creating our map class. Create a file called `game_map.py` and fill it with the following:

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

    import tile_types

    class GameMap:
        def __init__(self, width: int, height: int):
            self.width, self.height = width, height
            self.tiles = np.full((width, height), fill_value=tile_types.floor, order="F")

            self.tiles[30:33, 22] = tile_types.wall

        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:
            console.tiles_rgb[0:self.width, 0:self.height] = self.tiles["dark"]

Let’s break down `GameMap` a bit:

        def __init__(self, width: int, height: int):
            self.width, self.height = width, height
            self.tiles = np.full((width, height), fill_value=tile_types.floor, order="F")

            self.tiles[30:33, 22] = tile_types.wall

The initializer takes `width` and `height` integers and assigns them, in one line.

The `self.tiles` line might look a little strange if you’re not used to Numpy. Basically, we create a 2D array, filled with the same values, which in this case, is the `tile_types.floor` that we created earlier. This will fill `self.tiles` with floor tiles.

`self.tiles[30:33, 22] = tile_types.wall` creates a small, three tile wide wall at the specified location. We won’t normally hard-code walls like this, the wall is just for demonstration purposes. We’ll remove it in the next part.

        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

As the docstring alludes to, this method returns `True` if the given x and y values are within the map’s boundaries. We can use this to ensure the player doesn’t move beyond the map, into the void.

        def render(self, console: Console) -> None:
            console.tiles_rgb[0:self.width, 0:self.height] = self.tiles["dark"]

Using the `Console` class’s `tiles_rgb` method, we can quickly render the entire map. This method proves much faster than using the `console.print` method that we use for the individual entities.

With our `GameMap` class ready to go, let’s modify `main.py` to make use of it. We’ll also need to modify `Engine` to hold the map. Let’s start with `main.py` though:

    #!/usr/bin/env python3
    import tcod

    from engine import Engine
    from entity import Entity
    +from game_map import GameMap
    from input_handlers import EventHandler

    def main() -> None:
        screen_width = 80
        screen_height = 50

    +   map_width = 80
    +   map_height = 45

        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))
        npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
        entities = {npc, player}

    +   game_map = GameMap(map_width, map_height)

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

        with tcod.context.new_terminal(
            screen_width,
            screen_height,
            tileset=tileset,
            title="Yet Another Roguelike Tutorial",
            vsync=True,
        ) as context:

    #!/usr/bin/env python3
    import tcod

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

    def main() -> None:
        screen_width = 80
        screen_height = 50

        map_width = 80
        map_height = 45

        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))
        npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
        entities = {npc, player}

        game_map = GameMap(map_width, map_height)

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

        with tcod.context.new_terminal(
            screen_width,
            screen_height,
            tileset=tileset,
            title="Yet Another Roguelike Tutorial",
            vsync=True,
        ) as context:

We’ve added `map_width` and `map_height`, two integers, which we use in the `GameMap` class to describe its width and height. The `game_map` variable holds our initialized `GameMap`, and we then pass it into `engine`. The `Engine` class doesn’t yet accept a `GameMap` in its `__init__` function, so let’s fix that now.

    from typing import Set, Iterable, Any

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

    from actions import EscapeAction, MovementAction
    from entity import Entity
    +from game_map import GameMap
    from input_handlers import EventHandler

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

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

                if action is None:
                    continue

                if isinstance(action, MovementAction):
    -               self.player.move(dx=action.dx, dy=action.dy)
    +               if self.game_map.tiles["walkable"][self.player.x + action.dx, self.player.y + action.dy]:
    +                   self.player.move(dx=action.dx, dy=action.dy)

                elif isinstance(action, EscapeAction):
                    raise SystemExit()

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

            for entity in self.entities:
                console.print(entity.x, entity.y, entity.char, fg=entity.color)

            context.present(console)

            console.clear()

    from typing import Set, Iterable, Any

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

    from actions import EscapeAction, MovementAction
    from entity import Entity
    from game_map import GameMap
    from input_handlers import EventHandler

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

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

                if action is None:
                    continue

                if isinstance(action, MovementAction):
                    self.player.move(dx=action.dx, dy=action.dy)
                    if self.game_map.tiles["walkable"][self.player.x + action.dx, self.player.y + action.dy]:
                        self.player.move(dx=action.dx, dy=action.dy)

                elif isinstance(action, EscapeAction):
                    raise SystemExit()

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

            for entity in self.entities:
                console.print(entity.x, entity.y, entity.char, fg=entity.color)

            context.present(console)

            console.clear()

We’ve imported the `GameMap` class, and we’re now passing an instance of it in the `Engine` class’s initializer. From there, we utilize it in two ways:

- In `handle_events`, we use it to check if the tile is “walkable”, and only then do we move the player.
- In `render`, we call the `GameMap`’s `render` method to draw it to the screen.

If you run the project now, it should look like this:

The darker squares represent the wall, which, if you try to move your character through, should prove to be impenetrable.

Before we finish this up, there’s one last improvement we can make, thanks to our new `Engine` class: We can expand our `Action` classes to do a bit more of the heavy lifting, rather than leaving it to the `Engine`. This is because we can pass the `Engine` to the `Action`, providing it with the context it needs to do what we want.

Here’s what that looks like:

    +from __future__ import annotations

    +from typing import TYPE_CHECKING

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

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

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

    +       `entity` is the object performing the action.

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

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

    class MovementAction(Action):
        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.

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

    from __future__ import annotations

    from typing import TYPE_CHECKING

    if TYPE_CHECKING:
        from engine import Engine
        from entity import Entity

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

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

            `entity` is the object performing the action.

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

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

    class MovementAction(Action):
        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.

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

Now we’re passing in the `Engine` and the `Entity` performing the action to each `Action` subclass. Each subclass needs to implement its own version of the `perform` method. In the case of `EscapeAction`, we’re just raising `SystemExit`. In the case of `MovementAction`, we double check that the move is “in bounds” and on a “walkable” tile, and if either is true, we return without doing anything. If neither of those cases prove true, then we move the entity, as before.

So what does this new technique do for us? As it turns out, we can simplify the `Engine.handle_events` method like this:

    ...
    -from actions import EscapeAction, MovementAction
    from entity import Entity
    from game_map import GameMap
    from input_handlers import EventHandler

    class Engine:
        ...

        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)
    -           if isinstance(action, MovementAction):
    -               if self.game_map.tiles["walkable"][self.player.x + action.dx, self.player.y + action.dy]:
    -                   self.player.move(dx=action.dx, dy=action.dy)

    -           elif isinstance(action, EscapeAction):
    -               raise SystemExit()

    ...
    from actions import EscapeAction, MovementAction
    from entity import Entity
    from game_map import GameMap
    from input_handlers import EventHandler

    class Engine:
        ...

        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)
                if isinstance(action, MovementAction):
                    if self.game_map.tiles["walkable"][self.player.x + action.dx, self.player.y + action.dy]:
                        self.player.move(dx=action.dx, dy=action.dy)

                elif isinstance(action, EscapeAction):
                    raise SystemExit()

Much simpler! Run the project again, and it should function the same as before.

With that, Part 2 is now complete! We’ve managed to lay the groundwork for generating dungeons and moving through them, which, as it happens, is what the next part is all about.

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.