# Part 7 - Creating the Interface

Our game is looking more and more playable by the chapter, but before we move forward with the gameplay, we ought to take a moment to focus on how the project looks. Despite what some roguelike traditionalists may tell you, a good UI goes a long way.

One of the first things we can do is define a file that will hold our RGB colors. We’ve just been hard-coding them up until now, but it would be nice if they were all in one place and then imported when needed, so that we could easily update them if need be.

Create a new file, called `color.py`, and fill it with the following:

    white = (0xFF, 0xFF, 0xFF)
    black = (0x0, 0x0, 0x0)

    player_atk = (0xE0, 0xE0, 0xE0)
    enemy_atk = (0xFF, 0xC0, 0xC0)

    player_die = (0xFF, 0x30, 0x30)
    enemy_die = (0xFF, 0xA0, 0x30)

    welcome_text = (0x20, 0xA0, 0xFF)

    bar_text = white
    bar_filled = (0x0, 0x60, 0x0)
    bar_empty = (0x40, 0x10, 0x10)

Some of these colors, like `welcome_text` and `bar_filled` are things we haven’t added yet, but don’t worry, we’ll utilize them by the end of the chapter.

Last chapter, we implemented a basic HP tracker for the player, with the promise that we’d revisit in this chapter to make it look better. And now, the time has come!

We’ll create a bar that will gradually decrease as the player loses HP. This will help the player visualize how much HP is remaining. To do this, we’ll create a generic `render_bar` function, which can accept different values and change the bar’s length based on the `current_value` and `maximum_value` we give to it.

To house this new function (as well as some other functions that are coming soon), let’s create a new file, called `render_functions.py`. Put the following into it:

    from __future__ import annotations

    from typing import TYPE_CHECKING

    import color

    if TYPE_CHECKING:
        from tcod import Console

    def render_bar(
        console: Console, current_value: int, maximum_value: int, total_width: int
    ) -> None:
        bar_width = int(float(current_value) / maximum_value * total_width)

        console.draw_rect(x=0, y=45, width=total_width, height=1, ch=1, bg=color.bar_empty)

        if bar_width > 0:
            console.draw_rect(
                x=0, y=45, width=bar_width, height=1, ch=1, bg=color.bar_filled
            )

        console.print(
            x=1, y=45, string=f"HP: {current_value}/{maximum_value}", fg=color.bar_text
        )

We’re utilizing the `draw_rect` functions provided by TCOD to draw rectangular bars. We’re actually drawing two bars, one on top of the other. The first one will be the background color, which in the case of our health bar, will be a red color. The second goes on top, and is green. The one on top will gradually decrease as the player drops hit points, as its width is determined by the `bar_width` variable, which is itself determined by the `current_value` over the `maximum_value`.

We also print the “HP” value over the bar, so the player knows the exact number.

In order to utilize this new function, make the following changes to `engine.py`:

    ...
    from input_handlers import MainGameEventHandler
    +from render_functions import render_bar

    if TYPE_CHECKING:
        ...

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

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

            context.present(console)

            console.clear()

    ...
    from input_handlers import MainGameEventHandler
    from render_functions import render_bar

    if TYPE_CHECKING:
        ...

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

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

            context.present(console)

            console.clear()

Run the project now, and you should have a functioning health bar!

What next? One obvious problem with our project at the moment is that the messages get printed to the terminal rather than showing up in the actual game. We can fix that by adding a message log, which can display messages along with different colors for a bit of flash.

Create a new file, called `message_log.py`, and put the following contents inside:

    from typing import List, Reversible, Tuple
    import textwrap

    import tcod

    import color

    class Message:
        def __init__(self, text: str, fg: Tuple[int, int, int]):
            self.plain_text = text
            self.fg = fg
            self.count = 1

        @property
        def full_text(self) -> str:
            """The full text of this message, including the count if necessary."""
            if self.count > 1:
                return f"{self.plain_text} (x{self.count})"
            return self.plain_text

    class MessageLog:
        def __init__(self) -> None:
            self.messages: List[Message] = []

        def add_message(
            self, text: str, fg: Tuple[int, int, int] = color.white, *, stack: bool = True,
        ) -> None:
            """Add a message to this log.
            `text` is the message text, `fg` is the text color.
            If `stack` is True then the message can stack with a previous message
            of the same text.
            """
            if stack and self.messages and text == self.messages[-1].plain_text:
                self.messages[-1].count += 1
            else:
                self.messages.append(Message(text, fg))

        def render(
            self, console: tcod.Console, x: int, y: int, width: int, height: int,
        ) -> None:
            """Render this log over the given area.
            `x`, `y`, `width`, `height` is the rectangular region to render onto
            the `console`.
            """
            self.render_messages(console, x, y, width, height, self.messages)

        @staticmethod
        def render_messages(
            console: tcod.Console,
            x: int,
            y: int,
            width: int,
            height: int,
            messages: Reversible[Message],
        ) -> None:
            """Render the messages provided.
            The `messages` are rendered starting at the last message and working
            backwards.
            """
            y_offset = height - 1

            for message in reversed(messages):
                for line in reversed(textwrap.wrap(message.full_text, width)):
                    console.print(x=x, y=y + y_offset, string=line, fg=message.fg)
                    y_offset -= 1
                    if y_offset < 0:
                        return  # No more space to print messages.

Let’s go through the additions piece by piece.

    class Message:
        def __init__(self, text: str, fg: Tuple[int, int, int]):
            self.plain_text = text
            self.fg = fg
            self.count = 1

        @property
        def full_text(self) -> str:
            """The full text of this message, including the count if necessary."""
            if self.count > 1:
                return f"{self.plain_text} (x{self.count})"
            return self.plain_text

The `Message` will be used to save and display messages in our log. It includes three pieces of information:

- `plain_text`: The actual message text.
- `fg`: The “foreground” color of the message.
- `count`: This is used to display something like “The Orc attacks (x3).” Rather than crowding our message log with the same message over and over, we can “stack” the messages by increasing a message’s count. This only happens when the same message appears several times in a row.

The `full_text` property returns the text with its count, if the count is greater than 1. Otherwise, it just returns the message as-is.

Now, the actual message log:

    class MessageLog:
        def __init__(self) -> None:
            self.messages: List[Message] = []

It keeps a list of the `Message`s received. Nothing too complex here.

        def add_message(
            self, text: str, fg: Tuple[int, int, int] = color.white, *, stack: bool = True,
        ) -> None:
            """Add a message to this log.
            `text` is the message text, `fg` is the text color.
            If `stack` is True then the message can stack with a previous message
            of the same text.
            """
            if stack and self.messages and text == self.messages[-1].plain_text:
                self.messages[-1].count += 1
            else:
                self.messages.append(Message(text, fg))

`add_message` is what adds the message to the log. `text` is required, but `fg` will just default to white if nothing is given. `stack` tells us whether to stack messages or not (which allows us to disable this behavior, if desired).

If we are allowing stacking, and the added message matches the previous message, we just increment the previous message’s count by 1. If it’s not a match, we add it to the list.

        def render(
            self, console: tcod.Console, x: int, y: int, width: int, height: int,
        ) -> None:
            """Render this log over the given area.
            `x`, `y`, `width`, `height` is the rectangular region to render onto
            the `console`.
            """
            self.render_messages(console, x, y, width, height, self.messages)

        @staticmethod
        def render_messages(
            console: tcod.Console,
            x: int,
            y: int,
            width: int,
            height: int,
            messages: Reversible[Message],
        ) -> None:
            """Render the messages provided.
            The `messages` are rendered starting at the last message and working
            backwards.
            """
            y_offset = height - 1

            for message in reversed(messages):
                for line in reversed(textwrap.wrap(message.full_text, width)):
                    console.print(x=x, y=y + y_offset, string=line, fg=message.fg)
                    y_offset -= 1
                    if y_offset < 0:
                        return  # No more space to print messages.

This `render` calls `render_messages`, which is a static method that actually renders the messages to the screen. It renders them in reverse order, to make it appear that the messages are scrolling in an upwards direction. We use the `textwrap.wrap` function to wrap the text to fit within the given area, and then print each line to the console. We can only print so many messages to the console, however, so if `y_offset` reaches -1, we stop.

To utilize the message log, we’ll first need to add it to the `Engine` class. Modify `engine.py` like this:

    ...
    from input_handlers import MainGameEventHandler
    +from message_log import MessageLog
    from render_functions import render_bar

    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 = MainGameEventHandler(self)
    +       self.message_log = MessageLog()
            self.player = player
        ...

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

    +       self.message_log.render(console=console, x=21, y=45, width=40, height=5)

            render_bar(
                ...

    ...
    from input_handlers import MainGameEventHandler
    from message_log import MessageLog
    from render_functions import render_bar

    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 = MainGameEventHandler(self)
            self.message_log = MessageLog()
            self.player = player
        ...

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

            self.message_log.render(console=console, x=21, y=45, width=40, height=5)

            render_bar(
                ...

We’re adding an instance of `MessageLog` in the initializer, and rendering the log in the Engine’s `render` method. Nothing too complicated here.

We need to make a small change to `main.py` in order to actually make room for our message log. We can also add a friendly welcome message here.

    #!/usr/bin/env python3
    import copy

    import tcod

    +import color
    from engine import Engine
    import entity_factories
    ...

        ...
        map_width = 80
    -   map_height = 45
    +   map_height = 43

        room_max_size = 10
        ...

        ...
        engine.update_fov()

    +   engine.message_log.add_message(
    +       "Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
    +   )

        with tcod.context.new_terminal(
            ...

    #!/usr/bin/env python3
    import copy

    import tcod

    import color
    from engine import Engine
    import entity_factories
    ...

        ...
        map_width = 80
        map_height = 45
        map_height = 43

        room_max_size = 10
        ...

        ...
        engine.update_fov()

        engine.message_log.add_message(
            "Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
        )

        with tcod.context.new_terminal(
            ...

Feel free to experiment with different window and map sizes, if you like.

Run the project, and you should see the welcome message.

Now that we’ve confirmed our message log accepts and displays messages, we’ll need to replace all of our previous `print` statements to push messages to the log instead.

Let’s start with our attack action, in `actions.py`:

    ...
    from typing import Optional, Tuple, TYPE_CHECKING

    +import color

    if TYPE_CHECKING:
        ...

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

            attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
    +       if self.entity is self.engine.player:
    +           attack_color = color.player_atk
    +       else:
    +           attack_color = color.enemy_atk

            if damage > 0:
    -           print(f"{attack_desc} for {damage} hit points.")
    +           self.engine.message_log.add_message(
    +               f"{attack_desc} for {damage} hit points.", attack_color
    +           )
                target.fighter.hp -= damage
            else:
    -           print(f"{attack_desc} but does no damage.")
    +           self.engine.message_log.add_message(
    +               f"{attack_desc} but does no damage.", attack_color
    +           )

    ...
    from typing import Optional, Tuple, TYPE_CHECKING

    import color

    if TYPE_CHECKING:
        ...

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

            attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
            if self.entity is self.engine.player:
                attack_color = color.player_atk
            else:
                attack_color = color.enemy_atk

            if damage > 0:
                print(f"{attack_desc} for {damage} hit points.")
                self.engine.message_log.add_message(
                    f"{attack_desc} for {damage} hit points.", attack_color
                )
                target.fighter.hp -= damage
            else:
                print(f"{attack_desc} but does no damage.")
                self.engine.message_log.add_message(
                    f"{attack_desc} but does no damage.", attack_color
                )

We determine the color based on who is doing the attacking. Other than that, there’s really nothing new here, we’re just pushing those messages to the log rather than printing them.

Now we just need to update our death messages. Open up `fighter.py` and modify it like this:

    from __future__ import annotations

    from typing import TYPE_CHECKING

    +import color
    from components.base_component import BaseComponent
    ...

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

            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}"
            self.entity.render_order = RenderOrder.CORPSE

    -       print(death_message)
    +       self.engine.message_log.add_message(death_message, death_message_color)

    from __future__ import annotations

    from typing import TYPE_CHECKING

    import color
    from components.base_component import BaseComponent
    ...

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

            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}"
            self.entity.render_order = RenderOrder.CORPSE

            print(death_message)
            self.engine.message_log.add_message(death_message, death_message_color)

Run the project now. You should see messages for both attacks and deaths!

What next? One thing that would be nice is to see the names of the different entities. This will become useful later on if you decide to add more enemy types. It’s easy enough to remember “Orc” and “Troll”, but most roguelikes have a wide variety of enemies, so it’s helpful to know what each letter on the screen means.

We can accomplish this by displaying the names of the entities that are currently under the player’s mouse. We’ll need to make a few changes to our project to capture the mouse’s current position, however.

Edit `main.py` like this:

            root_console = tcod.Console(screen_width, screen_height, order="F")
            while True:
    +           root_console.clear()
    +           engine.event_handler.on_render(console=root_console)
    +           context.present(root_console)
    -           engine.render(console=root_console, context=context)

    +           engine.event_handler.handle_events(context)
    -           engine.event_handler.handle_events()

            root_console = tcod.Console(screen_width, screen_height, order="F")
            while True:
                root_console.clear()
                engine.event_handler.on_render(console=root_console)
                context.present(root_console)
                engine.render(console=root_console, context=context)

                engine.event_handler.handle_events(context)
                engine.event_handler.handle_events()

We’re adding the console’s `clear` back to main, as well as the context’s `present`. Also, we’re calling a method that we haven’t defined yet: `on_render`, but don’t worry, we’ll define it in a moment. Basically, this method tells the engine to render.

We’re also passing the `context` to `handle_events` now, because we need to call an extra method on it to capture the mouse input.

Now let’s modify `input_handlers.py` to contain the methods we’re calling in `main.py`:

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

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

    +   def handle_events(self, context: tcod.context.Context) -> None:
    +       for event in tcod.event.wait():
    +           context.convert_event(event)
    +           self.dispatch(event)

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

    +   def on_render(self, console: tcod.Console) -> None:
    +       self.engine.render(console)

    class MainGameEventHandler(EventHandler):
    -   def handle_events(self) -> None:
    +   def handle_events(self, context: tcod.context.Context) -> None:
            for event in tcod.event.wait():
    +           context.convert_event(event)

                action = self.dispatch(event)
                ...

    class GameOverEventHandler(EventHandler):
    -   def handle_events(self) -> None:
    +   def handle_events(self, context: tcod.context.Context) -> None:
            ...

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

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

        def handle_events(self, context: tcod.context.Context) -> None:
            for event in tcod.event.wait():
                context.convert_event(event)
                self.dispatch(event)

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

        def on_render(self, console: tcod.Console) -> None:
            self.engine.render(console)

    class MainGameEventHandler(EventHandler):
        def handle_events(self) -> None:
        def handle_events(self, context: tcod.context.Context) -> None:
            for event in tcod.event.wait():
                context.convert_event(event)

                action = self.dispatch(event)
                ...

    class GameOverEventHandler(EventHandler):
        def handle_events(self) -> None:
        def handle_events(self, context: tcod.context.Context) -> None:
            ...

We’re modifying the `handle_events` method in `EventHandler` to actually have an implementation. It iterates through the events, and uses `context.convert_event` to give the event knowledge on the mouse position. It then dispatches that event, to be handled like normal.

`on_render` just tells the `Engine` class to call its render method, using the given console.

`MainGameEventHandler` and `GameOverEventHandler` have small changes to their `handle_events` methods to match the signature of `EventHandler`, and `MainGameEventHandler` also uses `context.convert_event`.

We’re no longer passing the `context` to the `Engine` class’s `render` method, so let’s change the method now:

    ...
    from typing import TYPE_CHECKING

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

    class Engine:
        ...

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

            self.message_log.render(console=console, x=21, y=45, width=40, height=5)

            render_bar(
                console=console,
                current_value=self.player.fighter.hp,
                maximum_value=self.player.fighter.max_hp,
                total_width=20,
            )

    -       context.present(console)

    -       console.clear()

    ...
    from typing import TYPE_CHECKING

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

    class Engine:
        ...

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

            self.message_log.render(console=console, x=21, y=45, width=40, height=5)

            render_bar(
                console=console,
                current_value=self.player.fighter.hp,
                maximum_value=self.player.fighter.max_hp,
                total_width=20,
            )

            context.present(console)

            console.clear()

We’ve also removed the `console.clear` call, as that’s being handled by `main.py`.

So we’re passing the context around to different classes and converting the events to capture the mouse location. But where does that information actually get stored? Let’s add a data point on to the `Engine` class to hold that information. Add the following to `engine.py`:

    class Engine:
        game_map: GameMap

        def __init__(self, player: Actor):
            self.event_handler: EventHandler = MainGameEventHandler(self)
            self.message_log = MessageLog()
    +       self.mouse_location = (0, 0)
            self.player = player

    class Engine:
        game_map: GameMap

        def __init__(self, player: Actor):
            self.event_handler: EventHandler = MainGameEventHandler(self)
            self.message_log = MessageLog()
            self.mouse_location = (0, 0)
            self.player = player

Okay, so we’ve got a place to store the mouse location, but where do we actually get that information?

There’s an easy way: by overriding a method in `EventHandler`, which is called `ev_mousemotion`. By doing that, we can write the mouse location to the engine for access later. Here’s how that looks:

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

        def handle_events(self, context: tcod.context.Context) -> None:
            for event in tcod.event.wait():
                context.convert_event(event)
                self.dispatch(event)

    +   def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
    +       if self.engine.game_map.in_bounds(event.tile.x, event.tile.y):
    +           self.engine.mouse_location = event.tile.x, event.tile.y

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

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

        def handle_events(self, context: tcod.context.Context) -> None:
            for event in tcod.event.wait():
                context.convert_event(event)
                self.dispatch(event)

        def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
            if self.engine.game_map.in_bounds(event.tile.x, event.tile.y):
                self.engine.mouse_location = event.tile.x, event.tile.y

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

Great! Now we’re saving the mouse’s location, so it’s time to actually make use of it. Our original goal was to display the entity names that are in the mouse’s current position. The hard part is already done, now all we need to do is check which entities are in the given location, get their names, and print them out to the screen.

Since this has to do with rendering, let’s put these new functions in `render_functions.py`:

    from __future__ import annotations

    from typing import TYPE_CHECKING

    import color

    if TYPE_CHECKING:
        from tcod import Console
    +   from engine import Engine
    +   from game_map import GameMap

    +def get_names_at_location(x: int, y: int, game_map: GameMap) -> str:
    +   if not game_map.in_bounds(x, y) or not game_map.visible[x, y]:
    +       return ""

    +   names = ", ".join(
    +       entity.name for entity in game_map.entities if entity.x == x and entity.y == y
    +   )

    +   return names.capitalize()

    def render_bar(
        console: Console, current_value: int, maximum_value: int, total_width: int
    ) -> None:
        bar_width = int(float(current_value) / maximum_value * total_width)

        console.draw_rect(x=0, y=45, width=20, height=1, ch=1, bg=color.bar_empty)

        if bar_width > 0:
            console.draw_rect(
                x=0, y=45, width=bar_width, height=1, ch=1, bg=color.bar_filled
            )

        console.print(
            x=1, y=45, string=f"HP: {current_value}/{maximum_value}", fg=color.bar_text
        )

    +def render_names_at_mouse_location(
    +   console: Console, x: int, y: int, engine: Engine
    +) -> None:
    +   mouse_x, mouse_y = engine.mouse_location

    +   names_at_mouse_location = get_names_at_location(
    +       x=mouse_x, y=mouse_y, game_map=engine.game_map
    +   )

    +   console.print(x=x, y=y, string=names_at_mouse_location)

    from __future__ import annotations

    from typing import TYPE_CHECKING

    import color

    if TYPE_CHECKING:
        from tcod import Console
        from engine import Engine
        from game_map import GameMap

    def get_names_at_location(x: int, y: int, game_map: GameMap) -> str:
        if not game_map.in_bounds(x, y) or not game_map.visible[x, y]:
            return ""

        names = ", ".join(
            entity.name for entity in game_map.entities if entity.x == x and entity.y == y
        )

        return names.capitalize()

    def render_bar(
        console: Console, current_value: int, maximum_value: int, total_width: int
    ) -> None:
        bar_width = int(float(current_value) / maximum_value * total_width)

        console.draw_rect(x=0, y=45, width=20, height=1, ch=1, bg=color.bar_empty)

        if bar_width > 0:
            console.draw_rect(
                x=0, y=45, width=bar_width, height=1, ch=1, bg=color.bar_filled
            )

        console.print(
            x=1, y=45, string=f"HP: {current_value}/{maximum_value}", fg=color.bar_text
        )

    def render_names_at_mouse_location(
        console: Console, x: int, y: int, engine: Engine
    ) -> None:
        mouse_x, mouse_y = engine.mouse_location

        names_at_mouse_location = get_names_at_location(
            x=mouse_x, y=mouse_y, game_map=engine.game_map
        )

        console.print(x=x, y=y, string=names_at_mouse_location)

We’ve added two new functions, `render_names_at_mouse_location` and `get_names_at_location`. Let’s discuss what each one does.

`render_names_at_mouse_location` takes the console, x and y coordinates (the location to draw the names), and the engine. From the engine, it grabs the mouse’s current x and y positions, and passes them to `get_names_at_location`, which we can assume for the moment will return the list of entity names we want. Once we have these entity names as a string, we can print that string to the given x and y location on the screen, with `console.print`.

`get_names_at_location` also takes “x” and “y” variables, though these represent a spot on the map. We first check that the x and y coordinates are within the map, and are currently visible to the player. If they are, then we create a string of the entity names at that spot, separated by a comma. We then return that string, adding `capitalize` to make sure the first letter in the string is capitalized.

Now all we need to do is modify `engine.py` to import these functions and utilize them in the `render` method. Make the following modifications:

    ...
    from message_log import MessageLog
    -from render_functions import render_bar
    +from render_functions import render_bar, render_names_at_mouse_location

    if TYPE_CHECKING:
        ...

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

            self.message_log.render(console=console, x=21, y=45, width=40, height=5)

            render_bar(
                console=console,
                current_value=self.player.fighter.hp,
                maximum_value=self.player.fighter.max_hp,
                total_width=20,
            )

    +       render_names_at_mouse_location(console=console, x=21, y=44, engine=self)

    ...
    from message_log import MessageLog
    from render_functions import render_bar
    from render_functions import render_bar, render_names_at_mouse_location

    if TYPE_CHECKING:
        ...

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

            self.message_log.render(console=console, x=21, y=45, width=40, height=5)

            render_bar(
                console=console,
                current_value=self.player.fighter.hp,
                maximum_value=self.player.fighter.max_hp,
                total_width=20,
            )

            render_names_at_mouse_location(console=console, x=21, y=44, engine=self)

Now if you hover your mouse over an entity, you’ll see its name. If you stack a few corpses up, you’ll notice that it prints a list of the names.

We’re almost finished with this chapter. Before we wrap up, let’s revisit our message log for a moment. One issue with it is that we can’t see messages that are too far back. However, HexDecimal was kind enough to provide a method for viewing the whole log, with the ability to scroll.

Add the following to `input_handlers.py`:

    class GameOverEventHandler(EventHandler):
        ...

    +CURSOR_Y_KEYS = {
    +   tcod.event.K_UP: -1,
    +   tcod.event.K_DOWN: 1,
    +   tcod.event.K_PAGEUP: -10,
    +   tcod.event.K_PAGEDOWN: 10,
    +}

    +class HistoryViewer(EventHandler):
    +   """Print the history on a larger window which can be navigated."""

    +   def __init__(self, engine: Engine):
    +       super().__init__(engine)
    +       self.log_length = len(engine.message_log.messages)
    +       self.cursor = self.log_length - 1

    +   def on_render(self, console: tcod.Console) -> None:
    +       super().on_render(console)  # Draw the main state as the background.

    +       log_console = tcod.Console(console.width - 6, console.height - 6)

    +       # Draw a frame with a custom banner title.
    +       log_console.draw_frame(0, 0, log_console.width, log_console.height)
    +       log_console.print_box(
    +           0, 0, log_console.width, 1, "┤Message history├", alignment=tcod.CENTER
    +       )

    +       # Render the message log using the cursor parameter.
    +       self.engine.message_log.render_messages(
    +           log_console,
    +           1,
    +           1,
    +           log_console.width - 2,
    +           log_console.height - 2,
    +           self.engine.message_log.messages[: self.cursor + 1],
    +       )
    +       log_console.blit(console, 3, 3)

    +   def ev_keydown(self, event: tcod.event.KeyDown) -> None:
    +       # Fancy conditional movement to make it feel right.
    +       if event.sym in CURSOR_Y_KEYS:
    +           adjust = CURSOR_Y_KEYS[event.sym]
    +           if adjust < 0 and self.cursor == 0:
    +               # Only move from the top to the bottom when you're on the edge.
    +               self.cursor = self.log_length - 1
    +           elif adjust > 0 and self.cursor == self.log_length - 1:
    +               # Same with bottom to top movement.
    +               self.cursor = 0
    +           else:
    +               # Otherwise move while staying clamped to the bounds of the history log.
    +               self.cursor = max(0, min(self.cursor + adjust, self.log_length - 1))
    +       elif event.sym == tcod.event.K_HOME:
    +           self.cursor = 0  # Move directly to the top message.
    +       elif event.sym == tcod.event.K_END:
    +           self.cursor = self.log_length - 1  # Move directly to the last message.
    +       else:  # Any other key moves back to the main game state.
    +           self.engine.event_handler = MainGameEventHandler(self.engine)

    class GameOverEventHandler(EventHandler):
        ...

    CURSOR_Y_KEYS = {
        tcod.event.K_UP: -1,
        tcod.event.K_DOWN: 1,
        tcod.event.K_PAGEUP: -10,
        tcod.event.K_PAGEDOWN: 10,
    }

    class HistoryViewer(EventHandler):
        """Print the history on a larger window which can be navigated."""

        def __init__(self, engine: Engine):
            super().__init__(engine)
            self.log_length = len(engine.message_log.messages)
            self.cursor = self.log_length - 1

        def on_render(self, console: tcod.Console) -> None:
            super().on_render(console)  # Draw the main state as the background.

            log_console = tcod.Console(console.width - 6, console.height - 6)

            # Draw a frame with a custom banner title.
            log_console.draw_frame(0, 0, log_console.width, log_console.height)
            log_console.print_box(
                0, 0, log_console.width, 1, "┤Message history├", alignment=tcod.CENTER
            )

            # Render the message log using the cursor parameter.
            self.engine.message_log.render_messages(
                log_console,
                1,
                1,
                log_console.width - 2,
                log_console.height - 2,
                self.engine.message_log.messages[: self.cursor + 1],
            )
            log_console.blit(console, 3, 3)

        def ev_keydown(self, event: tcod.event.KeyDown) -> None:
            # Fancy conditional movement to make it feel right.
            if event.sym in CURSOR_Y_KEYS:
                adjust = CURSOR_Y_KEYS[event.sym]
                if adjust < 0 and self.cursor == 0:
                    # Only move from the top to the bottom when you're on the edge.
                    self.cursor = self.log_length - 1
                elif adjust > 0 and self.cursor == self.log_length - 1:
                    # Same with bottom to top movement.
                    self.cursor = 0
                else:
                    # Otherwise move while staying clamped to the bounds of the history log.
                    self.cursor = max(0, min(self.cursor + adjust, self.log_length - 1))
            elif event.sym == tcod.event.K_HOME:
                self.cursor = 0  # Move directly to the top message.
            elif event.sym == tcod.event.K_END:
                self.cursor = self.log_length - 1  # Move directly to the last message.
            else:  # Any other key moves back to the main game state.
                self.engine.event_handler = MainGameEventHandler(self.engine)

To show this new view, all we need to do is this, in `MainGameEventHandler`:

            ...
            elif key == tcod.event.K_ESCAPE:
                action = EscapeAction(player)
    +       elif key == tcod.event.K_v:
    +           self.engine.event_handler = HistoryViewer(self.engine)

            ...
            elif key == tcod.event.K_ESCAPE:
                action = EscapeAction(player)
            elif key == tcod.event.K_v:
                self.engine.event_handler = HistoryViewer(self.engine)

Now all the player has to do is press the “v” key to see a log of all past messages. By using the up and down keys, you can scroll through the log.

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.