# Part 10 - Saving and loading

Saving and loading is essential to almost every roguelike, but it can be a pain to manage if you don’t start early. By the end of this chapter, our game will be able to save and load one file to the disk, which you could easily expand to multiple saves if you wanted to.

Let’s start by defining the colors we’ll need this chapter, by opening `color.py` and entering the following:

    ...
    bar_empty = (0x40, 0x10, 0x10)

    +menu_title = (255, 255, 63)
    +menu_text = white

    ...
    bar_empty = (0x40, 0x10, 0x10)

    menu_title = (255, 255, 63)
    menu_text = white

Another thing we’ll need is a new type of exception. This will be used when we want to quit the game, but not save it. Normally, we’ll save the game when the user quits, but if the game is over (because the player is dead), we don’t want to create a save file.

We can put this exception in `exceptions.py`:

    class Impossible(Exception):
        """Exception raised when an action is impossible to be performed.

        The reason is given as the exception message.
        """

    +class QuitWithoutSaving(SystemExit):
    +   """Can be raised to exit the game without automatically saving."""

    class Impossible(Exception):
        """Exception raised when an action is impossible to be performed.

        The reason is given as the exception message.
        """

    class QuitWithoutSaving(SystemExit):
        """Can be raised to exit the game without automatically saving."""

There’s a bit of refactoring we can do to make things easier for ourselves in the future: By creating a base class that can be either an Action or an EventHandler, we don’t need to set the engine’s “event handler” to the new handler when we want to switch, we can just return that event handler instead. A benefit of this is that the `Engine` class won’t need to store the event handler anymore. This works by keeping track of the handler in the `main.py` file instead, and switching it when necessary.

To make the change, start by adding the following to `input_handlers.py`:

    from __future__ import annotations

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

    import tcod
    ...

    ...
    CONFIRM_KEYS = {
        tcod.event.K_RETURN,
        tcod.event.K_KP_ENTER,
    }

    +ActionOrHandler = Union[Action, "BaseEventHandler"]
    +"""An event handler return value which can trigger an action or switch active handlers.

    +If a handler is returned then it will become the active handler for future events.
    +If an action is returned it will be attempted and if it's valid then
    +MainGameEventHandler will become the active handler.
    +"""

    +class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
    +   def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
    +       """Handle an event and return the next active event handler."""
    +       state = self.dispatch(event)
    +       if isinstance(state, BaseEventHandler):
    +           return state
    +       assert not isinstance(state, Action), f"{self!r} can not handle actions."
    +       return self

    +   def on_render(self, console: tcod.Console) -> None:
    +       raise NotImplementedError()

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

    from __future__ import annotations

    from typing import Callable, Optional, Tuple, TYPE_CHECKING
    from typing import Callable, Optional, Tuple, TYPE_CHECKING, Union

    import tcod
    ...

    ...
    CONFIRM_KEYS = {
        tcod.event.K_RETURN,
        tcod.event.K_KP_ENTER,
    }

    ActionOrHandler = Union[Action, "BaseEventHandler"]
    """An event handler return value which can trigger an action or switch active handlers.

    If a handler is returned then it will become the active handler for future events.
    If an action is returned it will be attempted and if it's valid then
    MainGameEventHandler will become the active handler.
    """

    class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
        def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
            """Handle an event and return the next active event handler."""
            state = self.dispatch(event)
            if isinstance(state, BaseEventHandler):
                return state
            assert not isinstance(state, Action), f"{self!r} can not handle actions."
            return self

        def on_render(self, console: tcod.Console) -> None:
            raise NotImplementedError()

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

As the docstring explains, `ActionOrHandler` can be either an `Action` or an `EventHandler`. If it’s an `Action`, the action will be attempted, and if its a handler, the handler will be changed.

`BaseEventHandler` will be the base class for all of our handlers (we’ll change that to be the case next). It will return a new instance of `BaseEventHandler` or its subclasses if one was returned, or return itself. This allows us to change event handlers based on the context of what happens in the actions.

We also need to adjust `EventHandler`:

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

    -   def handle_events(self, event: tcod.event.Event) -> None:
    -       self.handle_action(self.dispatch(event))
    +   def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
    +       """Handle events for input handlers with an engine."""
    +       action_or_state = self.dispatch(event)
    +       if isinstance(action_or_state, BaseEventHandler):
    +           return action_or_state
    +       if self.handle_action(action_or_state):
    +           # A valid action was performed.
    +           if not self.engine.player.is_alive:
    +               # The player was killed sometime during or after the action.
    +               return GameOverEventHandler(self.engine)
    +           return MainGameEventHandler(self.engine)  # Return to the main handler.
    +       return self

        def handle_action(self, action: Optional[Action]) -> bool:
            ...

    -   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 EventHandler(tcod.event.EventDispatch[Action]):
    class EventHandler(BaseEventHandler):
        def __init__(self, engine: Engine):
            self.engine = engine

        def handle_events(self, event: tcod.event.Event) -> None:
            self.handle_action(self.dispatch(event))
        def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
            """Handle events for input handlers with an engine."""
            action_or_state = self.dispatch(event)
            if isinstance(action_or_state, BaseEventHandler):
                return action_or_state
            if self.handle_action(action_or_state):
                # A valid action was performed.
                if not self.engine.player.is_alive:
                    # The player was killed sometime during or after the action.
                    return GameOverEventHandler(self.engine)
                return MainGameEventHandler(self.engine)  # Return to the main handler.
            return self

        def handle_action(self, action: Optional[Action]) -> bool:
            ...

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

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

The `handle_events` method of `EventHandler` is similar to `BaseEventHandler`, except it includes logic to handle actions as well. It also contains the logic for changing our handler to a game over if the player is dead.

To adjust our existing handlers, we’ll need to continue editing `input_handlers`. This next code section is quite long, but the idea is consistent throughout: We want to modify our return types to return `Optional[ActionOrHandler]` instead of `Optional[Action]`, and instead of setting `self.engine.event_handler` to change the handler, we’ll return the handler instead.

    class AskUserEventHandler(EventHandler):
        """Handles user input for actions which require special input."""

    -   def handle_action(self, action: Optional[Action]) -> bool:
    -       """Return to the main event handler when a valid action was performed."""
    -       if super().handle_action(action):
    -           self.engine.event_handler = MainGameEventHandler(self.engine)
    -           return True
    -       return False

    -   def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
    +   def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
            """By default any key exits this input handler."""
            if event.sym in {  # Ignore modifier keys.
                tcod.event.K_LSHIFT,
                tcod.event.K_RSHIFT,
                tcod.event.K_LCTRL,
                tcod.event.K_RCTRL,
                tcod.event.K_LALT,
                tcod.event.K_RALT,
            }:
                return None
            return self.on_exit()

    -   def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[Action]:
    +   def ev_mousebuttondown(
    +       self, event: tcod.event.MouseButtonDown
    +   ) -> Optional[ActionOrHandler]:
            """By default any mouse click exits this input handler."""
            return self.on_exit()

    -   def on_exit(self) -> Optional[Action]:
    +   def on_exit(self) -> Optional[ActionOrHandler]:
            """Called when the user is trying to exit or cancel an action.

            By default this returns to the main event handler.
            """
    -       self.engine.event_handler = MainGameEventHandler(self.engine)
    -       return None
    +       return MainGameEventHandler(self.engine)

    class InventoryEventHandler(AskUserEventHandler):
        ...

    -   def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
    +   def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
            player = self.engine.player
            key = event.sym
            index = key - tcod.event.K_a

            if 0 <= index <= 26:
                try:
                    selected_item = player.inventory.items[index]
                except IndexError:
                    self.engine.message_log.add_message("Invalid entry.", color.invalid)
                    return None
                return self.on_item_selected(selected_item)
            return super().ev_keydown(event)

    -   def on_item_selected(self, item: Item) -> Optional[Action]:
    +   def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
            """Called when the user selects a valid item."""
            raise NotImplementedError()

    class InventoryActivateHandler(InventoryEventHandler):
        """Handle using an inventory item."""

        TITLE = "Select an item to use"

    -   def on_item_selected(self, item: Item) -> Optional[Action]:
    +   def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
            """Return the action for the selected item."""
            return item.consumable.get_action(self.engine.player)

    class InventoryDropHandler(InventoryEventHandler):
        """Handle dropping an inventory item."""

        TITLE = "Select an item to drop"

    -   def on_item_selected(self, item: Item) -> Optional[Action]:
    +   def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
            """Drop this item."""
            return actions.DropItem(self.engine.player, item)

    class SelectIndexHandler(AskUserEventHandler):
        ...

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

    -   def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[Action]:
    +   def ev_mousebuttondown(
    +       self, event: tcod.event.MouseButtonDown
    +   ) -> Optional[ActionOrHandler]:
            ...

    -   def on_index_selected(self, x: int, y: int) -> Optional[Action]:
    +   def on_index_selected(self, x: int, y: int) -> Optional[ActionOrHandler]:
            """Called when an index is selected."""
            raise NotImplementedError()

    class LookHandler(SelectIndexHandler):
        """Lets the player look around using the keyboard."""

    -   def on_index_selected(self, x: int, y: int) -> None:
    +   def on_index_selected(self, x: int, y: int) -> MainGameEventHandler:
            """Return to main handler."""
    -       self.engine.event_handler = MainGameEventHandler(self.engine)
    +       return MainGameEventHandler(self.engine)

    ...

    class MainGameEventHandler(EventHandler):
    -   def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
    +   def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
            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:
                raise SystemExit()
            elif key == tcod.event.K_v:
    -           self.engine.event_handler = HistoryViewer(self.engine)
    +           return HistoryViewer(self.engine)

            elif key == tcod.event.K_g:
                action = PickupAction(player)

            elif key == tcod.event.K_i:
    -           self.engine.event_handler = InventoryActivateHandler(self.engine)
    +           return InventoryActivateHandler(self.engine)
            elif key == tcod.event.K_d:
    -           self.engine.event_handler = InventoryDropHandler(self.engine)
    +           return InventoryDropHandler(self.engine)
            elif key == tcod.event.K_SLASH:
    -           self.engine.event_handler = LookHandler(self.engine)
    +           return LookHandler(self.engine)

            # No valid key was pressed
            return action

    ...
    class HistoryViewer(EventHandler):
        ...

    -   def ev_keydown(self, event: tcod.event.KeyDown) -> None:
    +   def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[MainGameEventHandler]:
            # 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)
    +           return MainGameEventHandler(self.engine)
    +       return None

    class AskUserEventHandler(EventHandler):
        """Handles user input for actions which require special input."""

        def handle_action(self, action: Optional[Action]) -> bool:
            """Return to the main event handler when a valid action was performed."""
            if super().handle_action(action):
                self.engine.event_handler = MainGameEventHandler(self.engine)
                return True
            return False

        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
            """By default any key exits this input handler."""
            if event.sym in {  # Ignore modifier keys.
                tcod.event.K_LSHIFT,
                tcod.event.K_RSHIFT,
                tcod.event.K_LCTRL,
                tcod.event.K_RCTRL,
                tcod.event.K_LALT,
                tcod.event.K_RALT,
            }:
                return None
            return self.on_exit()

        def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[Action]:
        def ev_mousebuttondown(
            self, event: tcod.event.MouseButtonDown
        ) -> Optional[ActionOrHandler]:
            """By default any mouse click exits this input handler."""
            return self.on_exit()

        def on_exit(self) -> Optional[Action]:
        def on_exit(self) -> Optional[ActionOrHandler]:
            """Called when the user is trying to exit or cancel an action.

            By default this returns to the main event handler.
            """
            self.engine.event_handler = MainGameEventHandler(self.engine)
            return None
            return MainGameEventHandler(self.engine)

    class InventoryEventHandler(AskUserEventHandler):
        ...

        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
            player = self.engine.player
            key = event.sym
            index = key - tcod.event.K_a

            if 0 <= index <= 26:
                try:
                    selected_item = player.inventory.items[index]
                except IndexError:
                    self.engine.message_log.add_message("Invalid entry.", color.invalid)
                    return None
                return self.on_item_selected(selected_item)
            return super().ev_keydown(event)

        def on_item_selected(self, item: Item) -> Optional[Action]:
        def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
            """Called when the user selects a valid item."""
            raise NotImplementedError()

    class InventoryActivateHandler(InventoryEventHandler):
        """Handle using an inventory item."""

        TITLE = "Select an item to use"

        def on_item_selected(self, item: Item) -> Optional[Action]:
        def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
            """Return the action for the selected item."""
            return item.consumable.get_action(self.engine.player)

    class InventoryDropHandler(InventoryEventHandler):
        """Handle dropping an inventory item."""

        TITLE = "Select an item to drop"

        def on_item_selected(self, item: Item) -> Optional[Action]:
        def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
            """Drop this item."""
            return actions.DropItem(self.engine.player, item)

    class SelectIndexHandler(AskUserEventHandler):
        ...

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

        def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[Action]:
        def ev_mousebuttondown(
            self, event: tcod.event.MouseButtonDown
        ) -> Optional[ActionOrHandler]:
            ...

        def on_index_selected(self, x: int, y: int) -> Optional[Action]:
        def on_index_selected(self, x: int, y: int) -> Optional[ActionOrHandler]:
            """Called when an index is selected."""
            raise NotImplementedError()

    class LookHandler(SelectIndexHandler):
        """Lets the player look around using the keyboard."""

        def on_index_selected(self, x: int, y: int) -> None:
        def on_index_selected(self, x: int, y: int) -> MainGameEventHandler:
            """Return to main handler."""
            self.engine.event_handler = MainGameEventHandler(self.engine)
            return MainGameEventHandler(self.engine)

    ...

    class MainGameEventHandler(EventHandler):
        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
            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:
                raise SystemExit()
            elif key == tcod.event.K_v:
                self.engine.event_handler = HistoryViewer(self.engine)
                return HistoryViewer(self.engine)

            elif key == tcod.event.K_g:
                action = PickupAction(player)

            elif key == tcod.event.K_i:
                self.engine.event_handler = InventoryActivateHandler(self.engine)
                return InventoryActivateHandler(self.engine)
            elif key == tcod.event.K_d:
                self.engine.event_handler = InventoryDropHandler(self.engine)
                return InventoryDropHandler(self.engine)
            elif key == tcod.event.K_SLASH:
                self.engine.event_handler = LookHandler(self.engine)
                return LookHandler(self.engine)

            # No valid key was pressed
            return action

    ...
    class HistoryViewer(EventHandler):
        ...

        def ev_keydown(self, event: tcod.event.KeyDown) -> None:
        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[MainGameEventHandler]:
            # 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)
                return MainGameEventHandler(self.engine)
            return None

We’ll also need to make a few adjustments in `consumable.py`, because some of the methods there also affected the input handlers.

    ...
    import components.ai
    import components.inventory
    from components.base_component import BaseComponent
    from exceptions import Impossible
    -from input_handlers import AreaRangedAttackHandler, SingleRangedAttackHandler
    +from input_handlers import (
    +   ActionOrHandler,
    +   AreaRangedAttackHandler,
    +   SingleRangedAttackHandler,
    +)

    if TYPE_CHECKING:
        ...

    ...
    class Consumable(BaseComponent):
        parent: Item

    -   def get_action(self, consumer: Actor) -> Optional[actions.Action]:
    +   def get_action(self, consumer: Actor) -> Optional[ActionOrHandler]:
            """Try to return the action for this item."""
            return actions.ItemAction(consumer, self.parent)

        ...

    class ConfusionConsumable(Consumable):
        def __init__(self, number_of_turns: int):
            self.number_of_turns = number_of_turns

    -   def get_action(self, consumer: Actor) -> Optional[actions.Action]:
    +   def get_action(self, consumer: Actor) -> SingleRangedAttackHandler:
            self.engine.message_log.add_message(
                "Select a target location.", color.needs_target
            )
    -       self.engine.event_handler = SingleRangedAttackHandler(
    +       return SingleRangedAttackHandler(
                self.engine,
                callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
            )
    -       return None

        ...

    class FireballDamageConsumable(Consumable):
        def __init__(self, damage: int, radius: int):
            self.damage = damage
            self.radius = radius

    -   def get_action(self, consumer: Actor) -> Optional[actions.Action]:
    +   def get_action(self, consumer: Actor) -> AreaRangedAttackHandler:
            self.engine.message_log.add_message(
                "Select a target location.", color.needs_target
            )
    -       self.engine.event_handler = AreaRangedAttackHandler(
    +       return AreaRangedAttackHandler(
                self.engine,
                radius=self.radius,
                callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
            )
    -       return None

        def activate(self, action: actions.ItemAction) -> None:
            ...

    ...
    import components.ai
    import components.inventory
    from components.base_component import BaseComponent
    from exceptions import Impossible
    from input_handlers import AreaRangedAttackHandler, SingleRangedAttackHandler
    from input_handlers import (
        ActionOrHandler,
        AreaRangedAttackHandler,
        SingleRangedAttackHandler,
    )

    if TYPE_CHECKING:
        ...

    ...
    class Consumable(BaseComponent):
        parent: Item

        def get_action(self, consumer: Actor) -> Optional[actions.Action]:
        def get_action(self, consumer: Actor) -> Optional[ActionOrHandler]:
            """Try to return the action for this item."""
            return actions.ItemAction(consumer, self.parent)

        ...

    class ConfusionConsumable(Consumable):
        def __init__(self, number_of_turns: int):
            self.number_of_turns = number_of_turns

        def get_action(self, consumer: Actor) -> Optional[actions.Action]:
        def get_action(self, consumer: Actor) -> SingleRangedAttackHandler:
            self.engine.message_log.add_message(
                "Select a target location.", color.needs_target
            )
            self.engine.event_handler = SingleRangedAttackHandler(
            return SingleRangedAttackHandler(
                self.engine,
                callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
            )
            return None

        ...

    class FireballDamageConsumable(Consumable):
        def __init__(self, damage: int, radius: int):
            self.damage = damage
            self.radius = radius

        def get_action(self, consumer: Actor) -> Optional[actions.Action]:
        def get_action(self, consumer: Actor) -> AreaRangedAttackHandler:
            self.engine.message_log.add_message(
                "Select a target location.", color.needs_target
            )
            self.engine.event_handler = AreaRangedAttackHandler(
            return AreaRangedAttackHandler(
                self.engine,
                radius=self.radius,
                callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
            )
            return None

        def activate(self, action: actions.ItemAction) -> None:
            ...

We also need to make a small adjustmet to `fighter.py`:

    from typing import TYPE_CHECKING

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

    ...

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

    from typing import TYPE_CHECKING

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

    ...

            ...
            if self.engine.player is self.parent:
                death_message = "You died!"
                death_message_color = color.player_die
                self.engine.event_handler = GameOverEventHandler(self.engine)
            else:
                death_message = f"{self.parent.name} is dead!"
                death_message_color = color.enemy_die
            ...

Since the logic associated with setting `GameOverEventHandler` is now handled in the `EventHandler` class, this line in `fighter.py` wasn’t needed anymore.

In order to make these changes work, we need to adjust the way that the input handlers are… well, handled. What we’ll want to do is have the handler exist on its own in `main.py` rather than be part of the `Engine` class.

Open up `main.py` and add make the following changes:

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

    import tcod

    import color
    from engine import Engine
    import entity_factories
    +import exceptions
    +import input_handlers
    from procgen import generate_dungeon

    def main() -> None:
        ...
        engine.message_log.add_message(
            "Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
        )

    +   handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)

        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.clear()
    -           engine.event_handler.on_render(console=root_console)
    -           context.present(root_console)

    -           try:
    -               for event in tcod.event.wait():
    -                   context.convert_event(event)
    -                   engine.event_handler.handle_events(event)
    -           except Exception:  # Handle exceptions in game.
    -               traceback.print_exc()  # Print error to stderr.
    -               # Then print the error to the message log.
    -               engine.message_log.add_message(traceback.format_exc(), color.error)
    +       try:
    +           while True:
    +               root_console.clear()
    +               handler.on_render(console=root_console)
    +               context.present(root_console)

    +               try:
    +                   for event in tcod.event.wait():
    +                       context.convert_event(event)
    +                       handler = handler.handle_events(event)
    +               except Exception:  # Handle exceptions in game.
    +                   traceback.print_exc()  # Print error to stderr.
    +                   # Then print the error to the message log.
    +                   if isinstance(handler, input_handlers.EventHandler):
    +                       handler.engine.message_log.add_message(
    +                           traceback.format_exc(), color.error
    +                       )
    +       except exceptions.QuitWithoutSaving:
    +           raise
    +       except SystemExit:  # Save and quit.
    +           # TODO: Add the save function here
    +           raise
    +       except BaseException:  # Save on any other unexpected exception.
    +           # TODO: Add the save function here
    +           raise

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

    import tcod

    import color
    from engine import Engine
    import entity_factories
    import exceptions
    import input_handlers
    from procgen import generate_dungeon

    def main() -> None:
        ...
        engine.message_log.add_message(
            "Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
        )

        handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)

        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.clear()
                engine.event_handler.on_render(console=root_console)
                context.present(root_console)

                try:
                    for event in tcod.event.wait():
                        context.convert_event(event)
                        engine.event_handler.handle_events(event)
                except Exception:  # Handle exceptions in game.
                    traceback.print_exc()  # Print error to stderr.
                    # Then print the error to the message log.
                    engine.message_log.add_message(traceback.format_exc(), color.error)
            try:
                while True:
                    root_console.clear()
                    handler.on_render(console=root_console)
                    context.present(root_console)

                    try:
                        for event in tcod.event.wait():
                            context.convert_event(event)
                            handler = handler.handle_events(event)
                    except Exception:  # Handle exceptions in game.
                        traceback.print_exc()  # Print error to stderr.
                        # Then print the error to the message log.
                        if isinstance(handler, input_handlers.EventHandler):
                            handler.engine.message_log.add_message(
                                traceback.format_exc(), color.error
                            )
            except exceptions.QuitWithoutSaving:
                raise
            except SystemExit:  # Save and quit.
                # TODO: Add the save function here
                raise
            except BaseException:  # Save on any other unexpected exception.
                # TODO: Add the save function here
                raise

We’re now defining our `handler` in `main.py` rather than passing it to the `Engine`. The handler can change if a different handler is returned from `handler.handle_events`. We’ve also added a few exception statements for the various exception types. They all do the same thing at the moment, but soon, once we’ve implemented our save function, the `SystemExit` and `BaseException` exceptions will save the game before exiting. `QuitWithoutSaving` will not, as this will be called when we don’t want to save the game.

If you run the project now, things should work the same as before.

Let’s clean up the `Engine` class by removing the `event_handler` attribute, as we don’t need it anymore:

    from __future__ import annotations

    from typing import TYPE_CHECKING

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

    import exceptions
    -from input_handlers import MainGameEventHandler
    from message_log import MessageLog
    from render_functions import (
        render_bar,
        render_names_at_mouse_location,
    )

    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.mouse_location = (0, 0)
            self.player = player
            ...

    from __future__ import annotations

    from typing import TYPE_CHECKING

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

    import exceptions
    from input_handlers import MainGameEventHandler
    from message_log import MessageLog
    from render_functions import (
        render_bar,
        render_names_at_mouse_location,
    )

    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.mouse_location = (0, 0)
            self.player = player
            ...

If you run the project again, nothing should change. The `event_handler` in `Engine` was not doing anything at this point.

Before we implement saving the game, we need to implement a main menu, where the user can choose to start a new game or load an existing one (or simply quit). It would also be handy to move all the logic of setting up a new game into a function, as we won’t need to call it when we eventually get to loading a game from a save file.

Create a new file, called `setup_game.py`, and put the following contents into it:

    """Handle the loading and initialization of game sessions."""
    from __future__ import annotations

    import copy
    from typing import Optional

    import tcod

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

    # Load the background image and remove the alpha channel.
    background_image = tcod.image.load("menu_background.png")[:, :, :3]

    def new_game() -> Engine:
        """Return a brand new game session as an Engine instance."""
        map_width = 80
        map_height = 43

        room_max_size = 10
        room_min_size = 6
        max_rooms = 30

        max_monsters_per_room = 2
        max_items_per_room = 2

        player = copy.deepcopy(entity_factories.player)

        engine = Engine(player=player)

        engine.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,
            max_items_per_room=max_items_per_room,
            engine=engine,
        )
        engine.update_fov()

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

    class MainMenu(input_handlers.BaseEventHandler):
        """Handle the main menu rendering and input."""

        def on_render(self, console: tcod.Console) -> None:
            """Render the main menu on a background image."""
            console.draw_semigraphics(background_image, 0, 0)

            console.print(
                console.width // 2,
                console.height // 2 - 4,
                "TOMBS OF THE ANCIENT KINGS",
                fg=color.menu_title,
                alignment=tcod.CENTER,
            )
            console.print(
                console.width // 2,
                console.height - 2,
                "By (Your name here)",
                fg=color.menu_title,
                alignment=tcod.CENTER,
            )

            menu_width = 24
            for i, text in enumerate(
                ["[N] Play a new game", "[C] Continue last game", "[Q] Quit"]
            ):
                console.print(
                    console.width // 2,
                    console.height // 2 - 2 + i,
                    text.ljust(menu_width),
                    fg=color.menu_text,
                    bg=color.black,
                    alignment=tcod.CENTER,
                    bg_blend=tcod.BKGND_ALPHA(64),
                )

        def ev_keydown(
            self, event: tcod.event.KeyDown
        ) -> Optional[input_handlers.BaseEventHandler]:
            if event.sym in (tcod.event.K_q, tcod.event.K_ESCAPE):
                raise SystemExit()
            elif event.sym == tcod.event.K_c:
                # TODO: Load the game here
                pass
            elif event.sym == tcod.event.K_n:
                return input_handlers.MainGameEventHandler(new_game())

            return None

Let’s break this code down a bit.

    background_image = tcod.image.load("menu_background.png")[:, :, :3]

This line loads the image file we’ll use for our background in the main menu. If you haven’t already, be sure to download that file. You can find it here, or download it by right-clicking and saving it from here:

Save the file to your project directory and you should be good to go.

    def new_game() -> Engine:
        """Return a brand new game session as an Engine instance."""
        map_width = 80
        map_height = 43

        room_max_size = 10
        room_min_size = 6
        max_rooms = 30

        max_monsters_per_room = 2
        max_items_per_room = 2

        player = copy.deepcopy(entity_factories.player)

        engine = Engine(player=player)

        engine.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,
            max_items_per_room=max_items_per_room,
            engine=engine,
        )
        engine.update_fov()

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

This should all look very familiar: it’s the same code we used to initialize our engine in `main.py`. We initialize the same things here, but return the `Engine`, so that `main.py` can make use of it. This will help reduce the amount of code in `main.py` while also making sure that we don’t waste time initializing the engine class if we’re loading from a saved file.

    class MainMenu(input_handlers.BaseEventHandler):
        """Handle the main menu rendering and input."""

        def on_render(self, console: tcod.Console) -> None:
            """Render the main menu on a background image."""
            console.draw_semigraphics(background_image, 0, 0)

            console.print(
                console.width // 2,
                console.height // 2 - 4,
                "TOMBS OF THE ANCIENT KINGS",
                fg=color.menu_title,
                alignment=tcod.CENTER,
            )
            console.print(
                console.width // 2,
                console.height - 2,
                "By (Your name here)",
                fg=color.menu_title,
                alignment=tcod.CENTER,
            )

            menu_width = 24
            for i, text in enumerate(
                ["[N] Play a new game", "[C] Continue last game", "[Q] Quit"]
            ):
                console.print(
                    console.width // 2,
                    console.height // 2 - 2 + i,
                    text.ljust(menu_width),
                    fg=color.menu_text,
                    bg=color.black,
                    alignment=tcod.CENTER,
                    bg_blend=tcod.BKGND_ALPHA(64),
                )

        def ev_keydown(
            self, event: tcod.event.KeyDown
        ) -> Optional[input_handlers.BaseEventHandler]:
            if event.sym in (tcod.event.K_q, tcod.event.K_ESCAPE):
                raise SystemExit()
            elif event.sym == tcod.event.K_c:
                # TODO: Load the game here
                pass
            elif event.sym == tcod.event.K_n:
                return input_handlers.MainGameEventHandler(new_game())

            return None

It might seem strange to put an event handler here rather than in its normal spot (`input_handlers.py`), but this makes sense for two reasons:

1. The main menu is specific to the start of the game.
2. It won’t be called during the normal course of the game, like the other input handlers in that file.

Anyway, the class renders the image we specified earlier, and it displays a title, `"TOMBS OF THE ANCIENT KINGS"`. Of course, you can change this to whatever name you have in mind for your game. It also includes a `"By (Your name here)"` section, so be sure to fill your name in and let everyone know who it was that worked so hard to make this game!

The menu also gives three choices: new game, continue last game, and quit. The `ev_keydown` method, as you might expect, handles these inputs.

- If the player presses “Q”, the game just exits.
- If the player presses “N”, a new game starts. We do this by returing the `MainGameEventHandler`, and calling the `new_game` function to create our new engine.
- If the player presses “C”, theoretically, a saved game should load. However, we haven’t gotten there yet, so as of now, nothing happens.

Let’s utilize our `MainMenu` function in `main.py`, like this:

    #!/usr/bin/env python3
    -import copy
    import traceback

    import tcod

    import color
    -from engine import Engine
    -import entity_factories
    import exceptions
    import input_handlers
    -from procgen import generate_dungeon
    +import setup_game

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

    -   map_width = 80
    -   map_height = 43

    -   room_max_size = 10
    -   room_min_size = 6
    -   max_rooms = 30

    -   max_monsters_per_room = 2
    -   max_items_per_room = 2

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

    -   player = copy.deepcopy(entity_factories.player)

    -   engine = Engine(player=player)

    -   engine.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,
    -       max_items_per_room=max_items_per_room,
    -       engine=engine,
    -   )
    -   engine.update_fov()

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

    -   handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)
    +   handler: input_handlers.BaseEventHandler = setup_game.MainMenu()

        with tcod.context.new_terminal(
            ...

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

    import tcod

    import color
    from engine import Engine
    import entity_factories
    import exceptions
    import input_handlers
    from procgen import generate_dungeon
    import setup_game

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

        map_width = 80
        map_height = 43

        room_max_size = 10
        room_min_size = 6
        max_rooms = 30

        max_monsters_per_room = 2
        max_items_per_room = 2

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

        player = copy.deepcopy(entity_factories.player)

        engine = Engine(player=player)

        engine.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,
            max_items_per_room=max_items_per_room,
            engine=engine,
        )
        engine.update_fov()

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

        handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)
        handler: input_handlers.BaseEventHandler = setup_game.MainMenu()

        with tcod.context.new_terminal(
            ...

We’re removing the code that dealt with setting up the engine, as that’s been moved into the `new_game` function. All we have to do here is set our handler to `MainMenu`, and the `MainMenu` class handles the rest from there.

Run the game now, and you should see the main menu!

*Note: If you run the project and get this error: `Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)`, it means you didn’t download the menu image file.

At last, we’ve come to the part where we’ll write the function that will save our game! This will be a method in `Engine`, and we’ll write it like this:

    from __future__ import annotations

    +import lzma
    +import pickle
    from typing import TYPE_CHECKING
    ...

    class Engine:
        ...

    +   def save_as(self, filename: str) -> None:
    +       """Save this Engine instance as a compressed file."""
    +       save_data = lzma.compress(pickle.dumps(self))
    +       with open(filename, "wb") as f:
    +           f.write(save_data)

    from __future__ import annotations

    import lzma
    import pickle
    from typing import TYPE_CHECKING
    ...

    class Engine:
        ...

        def save_as(self, filename: str) -> None:
            """Save this Engine instance as a compressed file."""
            save_data = lzma.compress(pickle.dumps(self))
            with open(filename, "wb") as f:
                f.write(save_data)

`pickle.dumps` serializes an object hierarchy in Python. `lzma.compress` compresses the data, so it takes up less space. We then use `with open(filename, "wb") as f:` to write the file (`wb` means “write in binary mode”), calling `f.write(save_data)` to write the data.

It might be hard to believe, but this is all we need to save our game! Because all of the things we need to save exist in the `Engine` class, we can pickle it, and we’re done!

Of course, it’s not quite that simple. We still need to call this method, and handle a few edge cases, like when the user tries to load a save file that doesn’t exist.

To save our game, we’ll call `save_as` from our `main.py` function. We’ll set up another function called `save_game` to call it, like this:

    ...
    import color
    import exceptions
    import setup_game
    import input_handlers

    +def save_game(handler: input_handlers.BaseEventHandler, filename: str) -> None:
    +   """If the current event handler has an active Engine then save it."""
    +   if isinstance(handler, input_handlers.EventHandler):
    +       handler.engine.save_as(filename)
    +       print("Game saved.")

    def main() -> None:
        ...

            ...
            except exceptions.QuitWithoutSaving:
                raise
            except SystemExit:  # Save and quit.
    -           # TODO: Add the save function here
    +           save_game(handler, "savegame.sav")
                raise
            except BaseException:  # Save on any other unexpected exception.
    -           # TODO: Add the save function here
    +           save_game(handler, "savegame.sav")
                raise

    if __name__ == "__main__":
        main()

    ...
    import color
    import exceptions
    import setup_game
    import input_handlers

    def save_game(handler: input_handlers.BaseEventHandler, filename: str) -> None:
        """If the current event handler has an active Engine then save it."""
        if isinstance(handler, input_handlers.EventHandler):
            handler.engine.save_as(filename)
            print("Game saved.")

    def main() -> None:
        ...

            ...
            except exceptions.QuitWithoutSaving:
                raise
            except SystemExit:  # Save and quit.
                # TODO: Add the save function here
                save_game(handler, "savegame.sav")
                raise
            except BaseException:  # Save on any other unexpected exception.
                # TODO: Add the save function here
                save_game(handler, "savegame.sav")
                raise

    if __name__ == "__main__":
        main()

Now when you exit the game, you should see a new `savegame.sav` in your project directory.

One thing that would help to handle the case where the user tries to load a saved file when one doesn’t exist would be a pop-up message. This message will appear in the center of the screen, and disappear after any key is pressed.

Add this class to `input_handlers.py`:

    class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
        ...

    +class PopupMessage(BaseEventHandler):
    +   """Display a popup text window."""

    +   def __init__(self, parent_handler: BaseEventHandler, text: str):
    +       self.parent = parent_handler
    +       self.text = text

    +   def on_render(self, console: tcod.Console) -> None:
    +       """Render the parent and dim the result, then print the message on top."""
    +       self.parent.on_render(console)
    +       console.tiles_rgb["fg"] //= 8
    +       console.tiles_rgb["bg"] //= 8

    +       console.print(
    +           console.width // 2,
    +           console.height // 2,
    +           self.text,
    +           fg=color.white,
    +           bg=color.black,
    +           alignment=tcod.CENTER,
    +       )

    +   def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseEventHandler]:
    +       """Any key returns to the parent handler."""
    +       return self.parent

    class EventHandler(BaseEventHandler):
        ...

    class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
        ...

    class PopupMessage(BaseEventHandler):
        """Display a popup text window."""

        def __init__(self, parent_handler: BaseEventHandler, text: str):
            self.parent = parent_handler
            self.text = text

        def on_render(self, console: tcod.Console) -> None:
            """Render the parent and dim the result, then print the message on top."""
            self.parent.on_render(console)
            console.tiles_rgb["fg"] //= 8
            console.tiles_rgb["bg"] //= 8

            console.print(
                console.width // 2,
                console.height // 2,
                self.text,
                fg=color.white,
                bg=color.black,
                alignment=tcod.CENTER,
            )

        def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseEventHandler]:
            """Any key returns to the parent handler."""
            return self.parent

    class EventHandler(BaseEventHandler):
        ...

This displays a message on top of the current display, whether it’s the main menu or the main game. When the player presses a key (any key), the message disappears.

Now let’s shift our focus to loading the game. We can add a `load_game` function in our `setup_game.py` file, which will attempt to load the game. We’ll call it when we press the “c” key on the main menu. Open up `setup_game.py` and edit it like this:

    from __future__ import annotations

    import copy
    +import lzma
    +import pickle
    +import traceback
    from typing import Optional

    import tcod
    ...

    def new_game() -> Engine:
        ...

    +def load_game(filename: str) -> Engine:
    +   """Load an Engine instance from a file."""
    +   with open(filename, "rb") as f:
    +       engine = pickle.loads(lzma.decompress(f.read()))
    +   assert isinstance(engine, Engine)
    +   return engine

    class MainMenu(input_handlers.BaseEventHandler):
        ...

        def ev_keydown(
            self, event: tcod.event.KeyDown
        ) -> Optional[input_handlers.BaseEventHandler]:
            if event.sym in (tcod.event.K_q, tcod.event.K_ESCAPE):
                raise SystemExit()
            elif event.sym == tcod.event.K_c:
    -           # TODO: Load the game here
    -           pass
    +           try:
    +               return input_handlers.MainGameEventHandler(load_game("savegame.sav"))
    +           except FileNotFoundError:
    +               return input_handlers.PopupMessage(self, "No saved game to load.")
    +           except Exception as exc:
    +               traceback.print_exc()  # Print to stderr.
    +               return input_handlers.PopupMessage(self, f"Failed to load save:\n{exc}")
            elif event.sym == tcod.event.K_n:
                return input_handlers.MainGameEventHandler(new_game())

            return None

    from __future__ import annotations

    import copy
    import lzma
    import pickle
    import traceback
    from typing import Optional

    import tcod
    ...

    def new_game() -> Engine:
        ...

    def load_game(filename: str) -> Engine:
        """Load an Engine instance from a file."""
        with open(filename, "rb") as f:
            engine = pickle.loads(lzma.decompress(f.read()))
        assert isinstance(engine, Engine)
        return engine

    class MainMenu(input_handlers.BaseEventHandler):
        ...

        def ev_keydown(
            self, event: tcod.event.KeyDown
        ) -> Optional[input_handlers.BaseEventHandler]:
            if event.sym in (tcod.event.K_q, tcod.event.K_ESCAPE):
                raise SystemExit()
            elif event.sym == tcod.event.K_c:
                # TODO: Load the game here
                pass
                try:
                    return input_handlers.MainGameEventHandler(load_game("savegame.sav"))
                except FileNotFoundError:
                    return input_handlers.PopupMessage(self, "No saved game to load.")
                except Exception as exc:
                    traceback.print_exc()  # Print to stderr.
                    return input_handlers.PopupMessage(self, f"Failed to load save:\n{exc}")
            elif event.sym == tcod.event.K_n:
                return input_handlers.MainGameEventHandler(new_game())

            return None

`load_game` essentially works the opposite of `save_as`, by opening up the file, uncompressing and unpickling it, and returning the instance of the `Engine` class. It then passes that engine to `MainGameEventHandler`. If no save game exists, or an error occured, we display a popup message.

And with that change, we can load our game! Try exiting your game and loading it afterwards.

The implementation as it exists now does have one major issue though: the player can load their save file after dying, and doing so actually allows the player to take an extra turn! The player can’t continue the game after that though, as our game immediately after detects that the player is dead, and the game state reverts to a game over. Still, this is an odd little bug that can be fixed quite simply: by deleting the save game file after the player dies.

To do that, we can override the `ev_quit` method in `GameOverEventHandler`. Open up `input_handlers.py` and make the following fix:

    from __future__ import annotations

    +import os

    from typing import Callable, Optional, Tuple, TYPE_CHECKING, Union
    ...

    class GameOverEventHandler(EventHandler):
    +   def on_quit(self) -> None:
    +       """Handle exiting out of a finished game."""
    +       if os.path.exists("savegame.sav"):
    +           os.remove("savegame.sav")  # Deletes the active save file.
    +       raise exceptions.QuitWithoutSaving()  # Avoid saving a finished game.

    +   def ev_quit(self, event: tcod.event.Quit) -> None:
    +       self.on_quit()

        def ev_keydown(self, event: tcod.event.KeyDown) -> None:
            if event.sym == tcod.event.K_ESCAPE:
    -           raise SystemExit()
    +           self.on_quit()

    from __future__ import annotations

    import os

    from typing import Callable, Optional, Tuple, TYPE_CHECKING, Union
    ...

    class GameOverEventHandler(EventHandler):
        def on_quit(self) -> None:
            """Handle exiting out of a finished game."""
            if os.path.exists("savegame.sav"):
                os.remove("savegame.sav")  # Deletes the active save file.
            raise exceptions.QuitWithoutSaving()  # Avoid saving a finished game.

        def ev_quit(self, event: tcod.event.Quit) -> None:
            self.on_quit()

        def ev_keydown(self, event: tcod.event.KeyDown) -> None:
            if event.sym == tcod.event.K_ESCAPE:
                raise SystemExit()
                self.on_quit()

We use the `os` module to find the save file, and if it exists, we remove it. We then raise `QuitWithoutSaving`, so that the game won’t be saved on exiting. Now when the player meets his or her tragic end (it’s a roguelike, it’s inevitable!), the save file will be deleted.

Last thing before we wrap up: We’re creating the `.sav` files to represent our saved games, but we don’t want to include these in our Git repository, since that should be reserved for just the code. The fix for this is to add this to our `.gitignore` file:

    +# Saved games
    +*.sav

    # Saved games
    *.sav

The rest of the .gitignore is omitted, as your .gitignore file may look different from mine. It doesn’t matter where you add this in.

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.