McRogueFace/roguelike_tutorial/rogueliketutorials.com/Part 10 - Saving and loadin...

2132 lines
141 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" style="color-scheme: dark;"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>
Part 10 - Saving and loading · Roguelike Tutorials
</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<meta name="description" content="Saving and loading is essential to almost every roguelike, but it can be a pain to manage if you dont 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.
Lets start by defining the colors well need this chapter, by opening color.py and entering the following:">
<meta name="keywords" content="">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Part 10 - Saving and loading">
<meta name="twitter:description" content="Saving and loading is essential to almost every roguelike, but it can be a pain to manage if you dont 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.
Lets start by defining the colors well need this chapter, by opening color.py and entering the following:">
<meta property="og:title" content="Part 10 - Saving and loading">
<meta property="og:description" content="Saving and loading is essential to almost every roguelike, but it can be a pain to manage if you dont 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.
Lets start by defining the colors well need this chapter, by opening color.py and entering the following:">
<meta property="og:type" content="article">
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-10/"><meta property="article:section" content="tutorials">
<meta property="article:published_time" content="2020-07-21T00:00:00+00:00">
<meta property="article:modified_time" content="2020-07-21T00:00:00+00:00">
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-10/">
<link rel="preload" href="https://rogueliketutorials.com/fonts/forkawesome-webfont.woff2?v=1.2.0" as="font" type="font/woff2" crossorigin="">
<link rel="stylesheet" href="Part%2010%20-%20Saving%20and%20loading%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%2010%20-%20Saving%20and%20loading%20%C2%B7%20Roguelike%20Tutorials_files/coder-dark.min.78b5fe3864945faf5207fb8fe3ab2320d49c3365def0e.css" integrity="sha256-eLX+OGSUX69SB/uP46sjINScM2Xe8OiKwd8N2txUoDw=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%2010%20-%20Saving%20and%20loading%20%C2%B7%20Roguelike%20Tutorials_files/style.min.9d3eb202952dddb888856ff12c83bc88de866c596286bfb4c1.css" integrity="sha256-nT6yApUt3biIhW/xLIO8iN6GbFlihr+0wfjmvq2a42Y=" crossorigin="anonymous" media="screen">
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-16x16.png" sizes="16x16">
<link rel="apple-touch-icon" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
<link rel="manifest" href="https://rogueliketutorials.com/site.webmanifest">
<link rel="mask-icon" href="https://rogueliketutorials.com/images/safari-pinned-tab.svg" color="#5bbad5">
<meta name="generator" content="Hugo 0.110.0">
<style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style></head>
<body class="colorscheme-dark vsc-initialized">
<div class="float-container">
<a id="dark-mode-toggle" class="colorscheme-toggle">
<i class="fa fa-adjust fa-fw" aria-hidden="true"></i>
</a>
</div>
<main class="wrapper">
<nav class="navigation">
<section class="container">
<a class="navigation-title" href="https://rogueliketutorials.com/">
Roguelike Tutorials
</a>
<input type="checkbox" id="menu-toggle">
<label class="menu-button float-right" for="menu-toggle">
<i class="fa fa-bars fa-fw" aria-hidden="true"></i>
</label>
<ul class="navigation-list">
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/">Home</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/">TCOD Tutorial (2020)</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/2019/">TCOD Tutorial (2019)</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/about/">About</a>
</li>
</ul>
</section>
</nav>
<div class="content">
<section class="container page">
<article>
<header>
<h1 class="title">
<a class="title-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-10/">
Part 10 - Saving and loading
</a>
</h1>
</header>
<p>Saving and loading is essential to almost every roguelike, but it
can be a pain to manage if you dont 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.</p>
<p>Lets start by defining the colors well need this chapter, by opening <code>color.py</code> and entering the following:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>bar_empty = (0x40, 0x10, 0x10)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+menu_title = (255, 255, 63)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+menu_text = white
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
bar_empty = (0x40, 0x10, 0x10)
<span class="new-text">menu_title = (255, 255, 63)
menu_text = white</span></pre>
</div>
</div>
<p>Another thing well need is a new type of exception. This will be
used when we want to quit the game, but not save it. Normally, well
save the game when the user quits, but if the game is over (because the
player is dead), we dont want to create a save file.</p>
<p>We can put this exception in <code>exceptions.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class Impossible(Exception):
</span></span><span style="display:flex;"><span> """Exception raised when an action is impossible to be performed.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> The reason is given as the exception message.
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class QuitWithoutSaving(SystemExit):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Can be raised to exit the game without automatically saving."""
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class Impossible(Exception):
"""Exception raised when an action is impossible to be performed.
The reason is given as the exception message.
"""
<span class="new-text">class QuitWithoutSaving(SystemExit):
"""Can be raised to exit the game without automatically saving."""</span></pre>
</div>
</div>
<p>Theres 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 dont need to set the engines “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 <code>Engine</code> class wont need to store the event handler anymore. This works by keeping track of the handler in the <code>main.py</code> file instead, and switching it when necessary.</p>
<p>To make the change, start by adding the following to <code>input_handlers.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from typing import Callable, Optional, Tuple, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Callable, Optional, Tuple, TYPE_CHECKING, Union
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>CONFIRM_KEYS = {
</span></span><span style="display:flex;"><span> tcod.event.K_RETURN,
</span></span><span style="display:flex;"><span> tcod.event.K_KP_ENTER,
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ActionOrHandler = Union[Action, "BaseEventHandler"]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+"""An event handler return value which can trigger an action or switch active handlers.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+If a handler is returned then it will become the active handler for future events.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+If an action is returned it will be attempted and if it's valid then
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+MainGameEventHandler will become the active handler.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+"""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def handle_events(self, event: tcod.event.Event) -&gt; BaseEventHandler:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle an event and return the next active event handler."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ state = self.dispatch(event)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if isinstance(state, BaseEventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return state
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ assert not isinstance(state, Action), f"{self!r} can not handle actions."
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_render(self, console: tcod.Console) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise NotImplementedError()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise SystemExit()
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
<span class="crossed-out-text">from typing import Callable, Optional, Tuple, TYPE_CHECKING</span>
<span class="new-text">from typing import Callable, Optional, Tuple, TYPE_CHECKING, Union</span>
import tcod
...
...
CONFIRM_KEYS = {
tcod.event.K_RETURN,
tcod.event.K_KP_ENTER,
}
<span class="new-text">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.
"""</span>
<span class="new-text">class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
def handle_events(self, event: tcod.event.Event) -&gt; 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) -&gt; None:
raise NotImplementedError()
def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
raise SystemExit()</span></pre>
</div>
</div>
<p>As the docstring explains, <code>ActionOrHandler</code> can be either an <code>Action</code> or an <code>EventHandler</code>. If its an <code>Action</code>, the action will be attempted, and if its a handler, the handler will be changed.</p>
<p><code>BaseEventHandler</code> will be the base class for all of our handlers (well change that to be the case next). It will return a new instance of <code>BaseEventHandler</code>
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.</p>
<p>We also need to adjust <code>EventHandler</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#f92672">-class EventHandler(tcod.event.EventDispatch[Action]):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+class EventHandler(BaseEventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> def __init__(self, engine: Engine):
</span></span><span style="display:flex;"><span> self.engine = engine
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self, event: tcod.event.Event) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.handle_action(self.dispatch(event))
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def handle_events(self, event: tcod.event.Event) -&gt; BaseEventHandler:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle events for input handlers with an engine."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action_or_state = self.dispatch(event)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if isinstance(action_or_state, BaseEventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return action_or_state
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.handle_action(action_or_state):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # A valid action was performed.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not self.engine.player.is_alive:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # The player was killed sometime during or after the action.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return GameOverEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MainGameEventHandler(self.engine) # Return to the main handler.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def handle_action(self, action: Optional[Action]) -&gt; bool:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- raise SystemExit()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> def on_render(self, console: tcod.Console) -&gt; None:
</span></span><span style="display:flex;"><span> self.engine.render(console)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="crossed-out-text">class EventHandler(tcod.event.EventDispatch[Action]):</span>
<span class="new-text">class EventHandler(BaseEventHandler):</span>
def __init__(self, engine: Engine):
self.engine = engine
<span class="crossed-out-text">def handle_events(self, event: tcod.event.Event) -&gt; None:</span>
<span class="crossed-out-text">self.handle_action(self.dispatch(event))</span>
<span class="new-text">def handle_events(self, event: tcod.event.Event) -&gt; 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</span>
def handle_action(self, action: Optional[Action]) -&gt; bool:
...
<span class="crossed-out-text">def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:</span>
<span class="crossed-out-text">raise SystemExit()</span>
def on_render(self, console: tcod.Console) -&gt; None:
self.engine.render(console)</pre>
</div>
</div>
<p>The <code>handle_events</code> method of <code>EventHandler</code> is similar to <code>BaseEventHandler</code>,
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.</p>
<p>To adjust our existing handlers, well need to continue editing <code>input_handlers</code>. This next code section is quite long, but the idea is consistent throughout: We want to modify our return types to return <code>Optional[ActionOrHandler]</code> instead of <code>Optional[Action]</code>, and instead of setting <code>self.engine.event_handler</code> to change the handler, well return the handler instead.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class AskUserEventHandler(EventHandler):
</span></span><span style="display:flex;"><span> """Handles user input for actions which require special input."""
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_action(self, action: Optional[Action]) -&gt; bool:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- """Return to the main event handler when a valid action was performed."""
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- if super().handle_action(action):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = MainGameEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- return True
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- return False
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """By default any key exits this input handler."""
</span></span><span style="display:flex;"><span> if event.sym in { # Ignore modifier keys.
</span></span><span style="display:flex;"><span> tcod.event.K_LSHIFT,
</span></span><span style="display:flex;"><span> tcod.event.K_RSHIFT,
</span></span><span style="display:flex;"><span> tcod.event.K_LCTRL,
</span></span><span style="display:flex;"><span> tcod.event.K_RCTRL,
</span></span><span style="display:flex;"><span> tcod.event.K_LALT,
</span></span><span style="display:flex;"><span> tcod.event.K_RALT,
</span></span><span style="display:flex;"><span> }:
</span></span><span style="display:flex;"><span> return None
</span></span><span style="display:flex;"><span> return self.on_exit()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def ev_mousebuttondown(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self, event: tcod.event.MouseButtonDown
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """By default any mouse click exits this input handler."""
</span></span><span style="display:flex;"><span> return self.on_exit()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def on_exit(self) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def on_exit(self) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """Called when the user is trying to exit or cancel an action.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> By default this returns to the main event handler.
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = MainGameEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- return None
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return MainGameEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class InventoryEventHandler(AskUserEventHandler):
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> player = self.engine.player
</span></span><span style="display:flex;"><span> key = event.sym
</span></span><span style="display:flex;"><span> index = key - tcod.event.K_a
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if 0 &lt;= index &lt;= 26:
</span></span><span style="display:flex;"><span> try:
</span></span><span style="display:flex;"><span> selected_item = player.inventory.items[index]
</span></span><span style="display:flex;"><span> except IndexError:
</span></span><span style="display:flex;"><span> self.engine.message_log.add_message("Invalid entry.", color.invalid)
</span></span><span style="display:flex;"><span> return None
</span></span><span style="display:flex;"><span> return self.on_item_selected(selected_item)
</span></span><span style="display:flex;"><span> return super().ev_keydown(event)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def on_item_selected(self, item: Item) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def on_item_selected(self, item: Item) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """Called when the user selects a valid item."""
</span></span><span style="display:flex;"><span> raise NotImplementedError()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class InventoryActivateHandler(InventoryEventHandler):
</span></span><span style="display:flex;"><span> """Handle using an inventory item."""
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> TITLE = "Select an item to use"
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def on_item_selected(self, item: Item) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def on_item_selected(self, item: Item) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """Return the action for the selected item."""
</span></span><span style="display:flex;"><span> return item.consumable.get_action(self.engine.player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class InventoryDropHandler(InventoryEventHandler):
</span></span><span style="display:flex;"><span> """Handle dropping an inventory item."""
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> TITLE = "Select an item to drop"
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def on_item_selected(self, item: Item) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def on_item_selected(self, item: Item) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """Drop this item."""
</span></span><span style="display:flex;"><span> return actions.DropItem(self.engine.player, item)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class SelectIndexHandler(AskUserEventHandler):
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def ev_mousebuttondown(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self, event: tcod.event.MouseButtonDown
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def on_index_selected(self, x: int, y: int) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def on_index_selected(self, x: int, y: int) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """Called when an index is selected."""
</span></span><span style="display:flex;"><span> raise NotImplementedError()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class LookHandler(SelectIndexHandler):
</span></span><span style="display:flex;"><span> """Lets the player look around using the keyboard."""
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def on_index_selected(self, x: int, y: int) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def on_index_selected(self, x: int, y: int) -&gt; MainGameEventHandler:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """Return to main handler."""
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = MainGameEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return MainGameEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class MainGameEventHandler(EventHandler):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> action: Optional[Action] = None
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> key = event.sym
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> player = self.engine.player
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if key in MOVE_KEYS:
</span></span><span style="display:flex;"><span> dx, dy = MOVE_KEYS[key]
</span></span><span style="display:flex;"><span> action = BumpAction(player, dx, dy)
</span></span><span style="display:flex;"><span> elif key in WAIT_KEYS:
</span></span><span style="display:flex;"><span> action = WaitAction(player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_ESCAPE:
</span></span><span style="display:flex;"><span> raise SystemExit()
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_v:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = HistoryViewer(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return HistoryViewer(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_g:
</span></span><span style="display:flex;"><span> action = PickupAction(player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_i:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = InventoryActivateHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return InventoryActivateHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_d:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = InventoryDropHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return InventoryDropHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_SLASH:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = LookHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return LookHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> # No valid key was pressed
</span></span><span style="display:flex;"><span> return action
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>class HistoryViewer(EventHandler):
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_keydown(self, event: tcod.event.KeyDown) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[MainGameEventHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> # Fancy conditional movement to make it feel right.
</span></span><span style="display:flex;"><span> if event.sym in CURSOR_Y_KEYS:
</span></span><span style="display:flex;"><span> adjust = CURSOR_Y_KEYS[event.sym]
</span></span><span style="display:flex;"><span> if adjust &lt; 0 and self.cursor == 0:
</span></span><span style="display:flex;"><span> # Only move from the top to the bottom when you're on the edge.
</span></span><span style="display:flex;"><span> self.cursor = self.log_length - 1
</span></span><span style="display:flex;"><span> elif adjust &gt; 0 and self.cursor == self.log_length - 1:
</span></span><span style="display:flex;"><span> # Same with bottom to top movement.
</span></span><span style="display:flex;"><span> self.cursor = 0
</span></span><span style="display:flex;"><span> else:
</span></span><span style="display:flex;"><span> # Otherwise move while staying clamped to the bounds of the history log.
</span></span><span style="display:flex;"><span> self.cursor = max(0, min(self.cursor + adjust, self.log_length - 1))
</span></span><span style="display:flex;"><span> elif event.sym == tcod.event.K_HOME:
</span></span><span style="display:flex;"><span> self.cursor = 0 # Move directly to the top message.
</span></span><span style="display:flex;"><span> elif event.sym == tcod.event.K_END:
</span></span><span style="display:flex;"><span> self.cursor = self.log_length - 1 # Move directly to the last message.
</span></span><span style="display:flex;"><span> else: # Any other key moves back to the main game state.
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = MainGameEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return MainGameEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return None
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class AskUserEventHandler(EventHandler):
"""Handles user input for actions which require special input."""
<span class="crossed-out-text">def handle_action(self, action: Optional[Action]) -&gt; bool:</span>
<span class="crossed-out-text">"""Return to the main event handler when a valid action was performed."""</span>
<span class="crossed-out-text">if super().handle_action(action):</span>
<span class="crossed-out-text">self.engine.event_handler = MainGameEventHandler(self.engine)</span>
<span class="crossed-out-text">return True</span>
<span class="crossed-out-text">return False</span>
<span class="crossed-out-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:</span>
<span class="new-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[ActionOrHandler]:</span>
"""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()
<span class="crossed-out-text">def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -&gt; Optional[Action]:</span>
<span class="new-text">def ev_mousebuttondown(
self, event: tcod.event.MouseButtonDown
) -&gt; Optional[ActionOrHandler]:</span>
"""By default any mouse click exits this input handler."""
return self.on_exit()
<span class="crossed-out-text">def on_exit(self) -&gt; Optional[Action]:</span>
<span class="new-text">def on_exit(self) -&gt; Optional[ActionOrHandler]:</span>
"""Called when the user is trying to exit or cancel an action.
By default this returns to the main event handler.
"""
<span class="crossed-out-text">self.engine.event_handler = MainGameEventHandler(self.engine)</span>
<span class="crossed-out-text">return None</span>
<span class="new-text">return MainGameEventHandler(self.engine)</span>
class InventoryEventHandler(AskUserEventHandler):
...
<span class="crossed-out-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:</span>
<span class="new-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[ActionOrHandler]:</span>
player = self.engine.player
key = event.sym
index = key - tcod.event.K_a
if 0 &lt;= index &lt;= 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)
<span class="crossed-out-text">def on_item_selected(self, item: Item) -&gt; Optional[Action]:</span>
<span class="new-text">def on_item_selected(self, item: Item) -&gt; Optional[ActionOrHandler]:</span>
"""Called when the user selects a valid item."""
raise NotImplementedError()
class InventoryActivateHandler(InventoryEventHandler):
"""Handle using an inventory item."""
TITLE = "Select an item to use"
<span class="crossed-out-text">def on_item_selected(self, item: Item) -&gt; Optional[Action]:</span>
<span class="new-text">def on_item_selected(self, item: Item) -&gt; Optional[ActionOrHandler]:</span>
"""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"
<span class="crossed-out-text">def on_item_selected(self, item: Item) -&gt; Optional[Action]:</span>
<span class="new-text">def on_item_selected(self, item: Item) -&gt; Optional[ActionOrHandler]:</span>
"""Drop this item."""
return actions.DropItem(self.engine.player, item)
class SelectIndexHandler(AskUserEventHandler):
...
<span class="crossed-out-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:</span>
<span class="new-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[ActionOrHandler]:</span>
...
<span class="crossed-out-text">def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -&gt; Optional[Action]:</span>
<span class="new-text">def ev_mousebuttondown(
self, event: tcod.event.MouseButtonDown
) -&gt; Optional[ActionOrHandler]:</span>
...
<span class="crossed-out-text">def on_index_selected(self, x: int, y: int) -&gt; Optional[Action]:</span>
<span class="new-text">def on_index_selected(self, x: int, y: int) -&gt; Optional[ActionOrHandler]:</span>
"""Called when an index is selected."""
raise NotImplementedError()
class LookHandler(SelectIndexHandler):
"""Lets the player look around using the keyboard."""
<span class="crossed-out-text">def on_index_selected(self, x: int, y: int) -&gt; None:</span>
<span class="new-text">def on_index_selected(self, x: int, y: int) -&gt; MainGameEventHandler:</span>
"""Return to main handler."""
<span class="crossed-out-text">self.engine.event_handler = MainGameEventHandler(self.engine)</span>
<span class="new-text">return MainGameEventHandler(self.engine)</span>
...
class MainGameEventHandler(EventHandler):
<span class="crossed-out-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:</span>
<span class="new-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[ActionOrHandler]:</span>
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:
<span class="crossed-out-text">self.engine.event_handler = HistoryViewer(self.engine)</span>
<span class="new-text">return HistoryViewer(self.engine)</span>
elif key == tcod.event.K_g:
action = PickupAction(player)
elif key == tcod.event.K_i:
<span class="crossed-out-text">self.engine.event_handler = InventoryActivateHandler(self.engine)</span>
<span class="new-text">return InventoryActivateHandler(self.engine)</span>
elif key == tcod.event.K_d:
<span class="crossed-out-text">self.engine.event_handler = InventoryDropHandler(self.engine)</span>
<span class="new-text">return InventoryDropHandler(self.engine)</span>
elif key == tcod.event.K_SLASH:
<span class="crossed-out-text">self.engine.event_handler = LookHandler(self.engine)</span>
<span class="new-text">return LookHandler(self.engine)</span>
# No valid key was pressed
return action
...
class HistoryViewer(EventHandler):
...
<span class="crossed-out-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; None:</span>
<span class="new-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[MainGameEventHandler]:</span>
# Fancy conditional movement to make it feel right.
if event.sym in CURSOR_Y_KEYS:
adjust = CURSOR_Y_KEYS[event.sym]
if adjust &lt; 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 &gt; 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.
<span class="crossed-out-text">self.engine.event_handler = MainGameEventHandler(self.engine)</span>
<span class="new-text">return MainGameEventHandler(self.engine)
return None</span></pre>
</div>
</div>
<p>Well also need to make a few adjustments in <code>consumable.py</code>, because some of the methods there also affected the input handlers.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>import components.ai
</span></span><span style="display:flex;"><span>import components.inventory
</span></span><span style="display:flex;"><span>from components.base_component import BaseComponent
</span></span><span style="display:flex;"><span>from exceptions import Impossible
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from input_handlers import AreaRangedAttackHandler, SingleRangedAttackHandler
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from input_handlers import (
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ActionOrHandler,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ AreaRangedAttackHandler,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ SingleRangedAttackHandler,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>class Consumable(BaseComponent):
</span></span><span style="display:flex;"><span> parent: Item
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def get_action(self, consumer: Actor) -&gt; Optional[actions.Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def get_action(self, consumer: Actor) -&gt; Optional[ActionOrHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> """Try to return the action for this item."""
</span></span><span style="display:flex;"><span> return actions.ItemAction(consumer, self.parent)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class ConfusionConsumable(Consumable):
</span></span><span style="display:flex;"><span> def __init__(self, number_of_turns: int):
</span></span><span style="display:flex;"><span> self.number_of_turns = number_of_turns
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def get_action(self, consumer: Actor) -&gt; Optional[actions.Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def get_action(self, consumer: Actor) -&gt; SingleRangedAttackHandler:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.engine.message_log.add_message(
</span></span><span style="display:flex;"><span> "Select a target location.", color.needs_target
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = SingleRangedAttackHandler(
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return SingleRangedAttackHandler(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.engine,
</span></span><span style="display:flex;"><span> callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span><span style="color:#f92672">- return None
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class FireballDamageConsumable(Consumable):
</span></span><span style="display:flex;"><span> def __init__(self, damage: int, radius: int):
</span></span><span style="display:flex;"><span> self.damage = damage
</span></span><span style="display:flex;"><span> self.radius = radius
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def get_action(self, consumer: Actor) -&gt; Optional[actions.Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def get_action(self, consumer: Actor) -&gt; AreaRangedAttackHandler:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.engine.message_log.add_message(
</span></span><span style="display:flex;"><span> "Select a target location.", color.needs_target
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = AreaRangedAttackHandler(
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return AreaRangedAttackHandler(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.engine,
</span></span><span style="display:flex;"><span> radius=self.radius,
</span></span><span style="display:flex;"><span> callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span><span style="color:#f92672">- return None
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> def activate(self, action: actions.ItemAction) -&gt; None:
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
import components.ai
import components.inventory
from components.base_component import BaseComponent
from exceptions import Impossible
<span class="crossed-out-text">from input_handlers import AreaRangedAttackHandler, SingleRangedAttackHandler</span>
<span class="new-text">from input_handlers import (
ActionOrHandler,
AreaRangedAttackHandler,
SingleRangedAttackHandler,
)</span>
if TYPE_CHECKING:
...
...
class Consumable(BaseComponent):
parent: Item
<span class="crossed-out-text">def get_action(self, consumer: Actor) -&gt; Optional[actions.Action]:</span>
<span class="new-text">def get_action(self, consumer: Actor) -&gt; Optional[ActionOrHandler]:</span>
"""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
<span class="crossed-out-text">def get_action(self, consumer: Actor) -&gt; Optional[actions.Action]:</span>
<span class="new-text">def get_action(self, consumer: Actor) -&gt; SingleRangedAttackHandler:</span>
self.engine.message_log.add_message(
"Select a target location.", color.needs_target
)
<span class="crossed-out-text">self.engine.event_handler = SingleRangedAttackHandler(</span>
<span class="new-text">return SingleRangedAttackHandler(</span>
self.engine,
callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
)
<span class="crossed-out-text">return None</span>
...
class FireballDamageConsumable(Consumable):
def __init__(self, damage: int, radius: int):
self.damage = damage
self.radius = radius
<span class="crossed-out-text">def get_action(self, consumer: Actor) -&gt; Optional[actions.Action]:</span>
<span class="new-text">def get_action(self, consumer: Actor) -&gt; AreaRangedAttackHandler:</span>
self.engine.message_log.add_message(
"Select a target location.", color.needs_target
)
<span class="crossed-out-text">self.engine.event_handler = AreaRangedAttackHandler(</span>
<span class="new-text">return AreaRangedAttackHandler(</span>
self.engine,
radius=self.radius,
callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
)
<span class="crossed-out-text">return None</span>
def activate(self, action: actions.ItemAction) -&gt; None:
...</pre>
</div>
</div>
<p>We also need to make a small adjustmet to <code>fighter.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from typing import TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import color
</span></span><span style="display:flex;"><span>from components.base_component import BaseComponent
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from input_handlers import GameOverEventHandler
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>from render_order import RenderOrder
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> if self.engine.player is self.parent:
</span></span><span style="display:flex;"><span> death_message = "You died!"
</span></span><span style="display:flex;"><span> death_message_color = color.player_die
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.event_handler = GameOverEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> else:
</span></span><span style="display:flex;"><span> death_message = f"{self.parent.name} is dead!"
</span></span><span style="display:flex;"><span> death_message_color = color.enemy_die
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from typing import TYPE_CHECKING
import color
from components.base_component import BaseComponent
<span class="crossed-out-text">from input_handlers import GameOverEventHandler</span>
from render_order import RenderOrder
...
...
if self.engine.player is self.parent:
death_message = "You died!"
death_message_color = color.player_die
<span class="crossed-out-text">self.engine.event_handler = GameOverEventHandler(self.engine)</span>
else:
death_message = f"{self.parent.name} is dead!"
death_message_color = color.enemy_die
...</pre>
</div>
</div>
<p>Since the logic associated with setting <code>GameOverEventHandler</code> is now handled in the <code>EventHandler</code> class, this line in <code>fighter.py</code> wasnt needed anymore.</p>
<p>In order to make these changes work, we need to adjust the way that
the input handlers are… well, handled. What well want to do is have the
handler exist on its own in <code>main.py</code> rather than be part of the <code>Engine</code> class.</p>
<p>Open up <code>main.py</code> and add make the following changes:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>#!/usr/bin/env python3
</span></span><span style="display:flex;"><span>import copy
</span></span><span style="display:flex;"><span>import traceback
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import color
</span></span><span style="display:flex;"><span>from engine import Engine
</span></span><span style="display:flex;"><span>import entity_factories
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import exceptions
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import input_handlers
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from procgen import generate_dungeon
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def main() -&gt; None:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> engine.message_log.add_message(
</span></span><span style="display:flex;"><span> "Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> with tcod.context.new_terminal(
</span></span><span style="display:flex;"><span> screen_width,
</span></span><span style="display:flex;"><span> screen_height,
</span></span><span style="display:flex;"><span> tileset=tileset,
</span></span><span style="display:flex;"><span> title="Yet Another Roguelike Tutorial",
</span></span><span style="display:flex;"><span> vsync=True,
</span></span><span style="display:flex;"><span> ) as context:
</span></span><span style="display:flex;"><span> root_console = tcod.Console(screen_width, screen_height, order="F")
</span></span><span style="display:flex;"><span><span style="color:#f92672">- while True:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- root_console.clear()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.event_handler.on_render(console=root_console)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- context.present(root_console)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- try:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- for event in tcod.event.wait():
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- context.convert_event(event)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.event_handler.handle_events(event)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- except Exception: # Handle exceptions in game.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- traceback.print_exc() # Print error to stderr.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- # Then print the error to the message log.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.message_log.add_message(traceback.format_exc(), color.error)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ try:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ while True:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ root_console.clear()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ handler.on_render(console=root_console)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ context.present(root_console)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ try:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for event in tcod.event.wait():
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ context.convert_event(event)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ handler = handler.handle_events(event)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except Exception: # Handle exceptions in game.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ traceback.print_exc() # Print error to stderr.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Then print the error to the message log.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if isinstance(handler, input_handlers.EventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ handler.engine.message_log.add_message(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ traceback.format_exc(), color.error
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except exceptions.QuitWithoutSaving:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except SystemExit: # Save and quit.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # TODO: Add the save function here
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except BaseException: # Save on any other unexpected exception.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # TODO: Add the save function here
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>#!/usr/bin/env python3
import copy
import traceback
import tcod
import color
from engine import Engine
import entity_factories
<span class="new-text">import exceptions</span>
<span class="new-text">import input_handlers</span>
from procgen import generate_dungeon
def main() -&gt; None:
...
engine.message_log.add_message(
"Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
)
<span class="new-text">handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)</span>
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")
<span class="crossed-out-text">while True:</span>
<span class="crossed-out-text">root_console.clear()</span>
<span class="crossed-out-text">engine.event_handler.on_render(console=root_console)</span>
<span class="crossed-out-text">context.present(root_console)</span>
<span class="crossed-out-text">try:</span>
<span class="crossed-out-text">for event in tcod.event.wait():</span>
<span class="crossed-out-text">context.convert_event(event)</span>
<span class="crossed-out-text">engine.event_handler.handle_events(event)</span>
<span class="crossed-out-text">except Exception: # Handle exceptions in game.</span>
<span class="crossed-out-text">traceback.print_exc() # Print error to stderr.</span>
<span class="crossed-out-text"># Then print the error to the message log.</span>
<span class="crossed-out-text">engine.message_log.add_message(traceback.format_exc(), color.error)</span>
<span class="new-text">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</span></pre>
</div>
</div>
<p>Were now defining our <code>handler</code> in <code>main.py</code> rather than passing it to the <code>Engine</code>. The handler can change if a different handler is returned from <code>handler.handle_events</code>.
Weve also added a few exception statements for the various exception
types. They all do the same thing at the moment, but soon, once weve
implemented our save function, the <code>SystemExit</code> and <code>BaseException</code> exceptions will save the game before exiting. <code>QuitWithoutSaving</code> will not, as this will be called when we dont want to save the game.</p>
<p>If you run the project now, things should work the same as before.</p>
<p>Lets clean up the <code>Engine</code> class by removing the <code>event_handler</code> attribute, as we dont need it anymore:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from typing import TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from tcod.console import Console
</span></span><span style="display:flex;"><span>from tcod.map import compute_fov
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import exceptions
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from input_handlers import MainGameEventHandler
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>from message_log import MessageLog
</span></span><span style="display:flex;"><span>from render_functions import (
</span></span><span style="display:flex;"><span> render_bar,
</span></span><span style="display:flex;"><span> render_names_at_mouse_location,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span> from entity import Actor
</span></span><span style="display:flex;"><span> from game_map import GameMap
</span></span><span style="display:flex;"><span><span style="color:#f92672">- from input_handlers import EventHandler
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class Engine:
</span></span><span style="display:flex;"><span> game_map: GameMap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def __init__(self, player: Actor):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.event_handler: EventHandler = MainGameEventHandler(self)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> self.message_log = MessageLog()
</span></span><span style="display:flex;"><span> self.mouse_location = (0, 0)
</span></span><span style="display:flex;"><span> self.player = player
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import TYPE_CHECKING
from tcod.console import Console
from tcod.map import compute_fov
import exceptions
<span class="crossed-out-text">from input_handlers import MainGameEventHandler</span>
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
<span class="crossed-out-text">from input_handlers import EventHandler</span>
class Engine:
game_map: GameMap
def __init__(self, player: Actor):
<span class="crossed-out-text">self.event_handler: EventHandler = MainGameEventHandler(self)</span>
self.message_log = MessageLog()
self.mouse_location = (0, 0)
self.player = player
...</pre>
</div>
</div>
<p>If you run the project again, nothing should change. The <code>event_handler</code> in <code>Engine</code> was not doing anything at this point.</p>
<p>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 wont need to call it when
we eventually get to loading a game from a save file.</p>
<p>Create a new file, called <code>setup_game.py</code>, and put the following contents into it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#e6db74">"""Handle the loading and initialization of game sessions."""</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> __future__ <span style="color:#f92672">import</span> annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> copy
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Optional
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> color
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> engine <span style="color:#f92672">import</span> Engine
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> entity_factories
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> input_handlers
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> procgen <span style="color:#f92672">import</span> generate_dungeon
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Load the background image and remove the alpha channel.</span>
</span></span><span style="display:flex;"><span>background_image <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>image<span style="color:#f92672">.</span>load(<span style="color:#e6db74">"menu_background.png"</span>)[:, :, :<span style="color:#ae81ff">3</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">new_game</span>() <span style="color:#f92672">-&gt;</span> Engine:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Return a brand new game session as an Engine instance."""</span>
</span></span><span style="display:flex;"><span> map_width <span style="color:#f92672">=</span> <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span> map_height <span style="color:#f92672">=</span> <span style="color:#ae81ff">43</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> room_max_size <span style="color:#f92672">=</span> <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span> room_min_size <span style="color:#f92672">=</span> <span style="color:#ae81ff">6</span>
</span></span><span style="display:flex;"><span> max_rooms <span style="color:#f92672">=</span> <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> max_monsters_per_room <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span> max_items_per_room <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> player <span style="color:#f92672">=</span> copy<span style="color:#f92672">.</span>deepcopy(entity_factories<span style="color:#f92672">.</span>player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine <span style="color:#f92672">=</span> Engine(player<span style="color:#f92672">=</span>player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine<span style="color:#f92672">.</span>game_map <span style="color:#f92672">=</span> generate_dungeon(
</span></span><span style="display:flex;"><span> max_rooms<span style="color:#f92672">=</span>max_rooms,
</span></span><span style="display:flex;"><span> room_min_size<span style="color:#f92672">=</span>room_min_size,
</span></span><span style="display:flex;"><span> room_max_size<span style="color:#f92672">=</span>room_max_size,
</span></span><span style="display:flex;"><span> map_width<span style="color:#f92672">=</span>map_width,
</span></span><span style="display:flex;"><span> map_height<span style="color:#f92672">=</span>map_height,
</span></span><span style="display:flex;"><span> max_monsters_per_room<span style="color:#f92672">=</span>max_monsters_per_room,
</span></span><span style="display:flex;"><span> max_items_per_room<span style="color:#f92672">=</span>max_items_per_room,
</span></span><span style="display:flex;"><span> engine<span style="color:#f92672">=</span>engine,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> engine<span style="color:#f92672">.</span>update_fov()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"Hello and welcome, adventurer, to yet another dungeon!"</span>, color<span style="color:#f92672">.</span>welcome_text
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> engine
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MainMenu</span>(input_handlers<span style="color:#f92672">.</span>BaseEventHandler):
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Handle the main menu rendering and input."""</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">on_render</span>(self, console: tcod<span style="color:#f92672">.</span>Console) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Render the main menu on a background image."""</span>
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>draw_semigraphics(background_image, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>width <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>height <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">4</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"TOMBS OF THE ANCIENT KINGS"</span>,
</span></span><span style="display:flex;"><span> fg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>menu_title,
</span></span><span style="display:flex;"><span> alignment<span style="color:#f92672">=</span>tcod<span style="color:#f92672">.</span>CENTER,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>width <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>height <span style="color:#f92672">-</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"By (Your name here)"</span>,
</span></span><span style="display:flex;"><span> fg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>menu_title,
</span></span><span style="display:flex;"><span> alignment<span style="color:#f92672">=</span>tcod<span style="color:#f92672">.</span>CENTER,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> menu_width <span style="color:#f92672">=</span> <span style="color:#ae81ff">24</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> i, text <span style="color:#f92672">in</span> enumerate(
</span></span><span style="display:flex;"><span> [<span style="color:#e6db74">"[N] Play a new game"</span>, <span style="color:#e6db74">"[C] Continue last game"</span>, <span style="color:#e6db74">"[Q] Quit"</span>]
</span></span><span style="display:flex;"><span> ):
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>width <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>height <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">+</span> i,
</span></span><span style="display:flex;"><span> text<span style="color:#f92672">.</span>ljust(menu_width),
</span></span><span style="display:flex;"><span> fg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>menu_text,
</span></span><span style="display:flex;"><span> bg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>black,
</span></span><span style="display:flex;"><span> alignment<span style="color:#f92672">=</span>tcod<span style="color:#f92672">.</span>CENTER,
</span></span><span style="display:flex;"><span> bg_blend<span style="color:#f92672">=</span>tcod<span style="color:#f92672">.</span>BKGND_ALPHA(<span style="color:#ae81ff">64</span>),
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ev_keydown</span>(
</span></span><span style="display:flex;"><span> self, event: tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>KeyDown
</span></span><span style="display:flex;"><span> ) <span style="color:#f92672">-&gt;</span> Optional[input_handlers<span style="color:#f92672">.</span>BaseEventHandler]:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> event<span style="color:#f92672">.</span>sym <span style="color:#f92672">in</span> (tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_q, tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_ESCAPE):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">SystemExit</span>()
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> event<span style="color:#f92672">.</span>sym <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_c:
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># TODO: Load the game here</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">pass</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> event<span style="color:#f92672">.</span>sym <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_n:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> input_handlers<span style="color:#f92672">.</span>MainGameEventHandler(new_game())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span></code></pre></div><p>Lets break this code down a bit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span>background_image <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>image<span style="color:#f92672">.</span>load(<span style="color:#e6db74">"menu_background.png"</span>)[:, :, :<span style="color:#ae81ff">3</span>]
</span></span></code></pre></div><p>This line loads the image file well
use for our background in the main menu. If you havent already, be
sure to download that file. You can find it <a href="https://github.com/TStand90/tcod_tutorial_v2/blob/bff29c352d76068845730ff61e62cd382e5e2adc/menu_background.png">here</a>, or download it by right-clicking and saving it from here:</p>
<p><img src="Part%2010%20-%20Saving%20and%20loading%20%C2%B7%20Roguelike%20Tutorials_files/menu_background.png" alt="Main Menu Background Image"></p>
<p>Save the file to your project directory and you should be good to go.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">new_game</span>() <span style="color:#f92672">-&gt;</span> Engine:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Return a brand new game session as an Engine instance."""</span>
</span></span><span style="display:flex;"><span> map_width <span style="color:#f92672">=</span> <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span> map_height <span style="color:#f92672">=</span> <span style="color:#ae81ff">43</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> room_max_size <span style="color:#f92672">=</span> <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span> room_min_size <span style="color:#f92672">=</span> <span style="color:#ae81ff">6</span>
</span></span><span style="display:flex;"><span> max_rooms <span style="color:#f92672">=</span> <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> max_monsters_per_room <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span> max_items_per_room <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> player <span style="color:#f92672">=</span> copy<span style="color:#f92672">.</span>deepcopy(entity_factories<span style="color:#f92672">.</span>player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine <span style="color:#f92672">=</span> Engine(player<span style="color:#f92672">=</span>player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine<span style="color:#f92672">.</span>game_map <span style="color:#f92672">=</span> generate_dungeon(
</span></span><span style="display:flex;"><span> max_rooms<span style="color:#f92672">=</span>max_rooms,
</span></span><span style="display:flex;"><span> room_min_size<span style="color:#f92672">=</span>room_min_size,
</span></span><span style="display:flex;"><span> room_max_size<span style="color:#f92672">=</span>room_max_size,
</span></span><span style="display:flex;"><span> map_width<span style="color:#f92672">=</span>map_width,
</span></span><span style="display:flex;"><span> map_height<span style="color:#f92672">=</span>map_height,
</span></span><span style="display:flex;"><span> max_monsters_per_room<span style="color:#f92672">=</span>max_monsters_per_room,
</span></span><span style="display:flex;"><span> max_items_per_room<span style="color:#f92672">=</span>max_items_per_room,
</span></span><span style="display:flex;"><span> engine<span style="color:#f92672">=</span>engine,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> engine<span style="color:#f92672">.</span>update_fov()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"Hello and welcome, adventurer, to yet another dungeon!"</span>, color<span style="color:#f92672">.</span>welcome_text
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> engine
</span></span></code></pre></div><p>This should all look very familiar: its the same code we used to initialize our engine in <code>main.py</code>. We initialize the same things here, but return the <code>Engine</code>, so that <code>main.py</code> can make use of it. This will help reduce the amount of code in <code>main.py</code> while also making sure that we dont waste time initializing the engine class if were loading from a saved file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MainMenu</span>(input_handlers<span style="color:#f92672">.</span>BaseEventHandler):
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Handle the main menu rendering and input."""</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">on_render</span>(self, console: tcod<span style="color:#f92672">.</span>Console) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Render the main menu on a background image."""</span>
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>draw_semigraphics(background_image, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>width <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>height <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">4</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"TOMBS OF THE ANCIENT KINGS"</span>,
</span></span><span style="display:flex;"><span> fg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>menu_title,
</span></span><span style="display:flex;"><span> alignment<span style="color:#f92672">=</span>tcod<span style="color:#f92672">.</span>CENTER,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>width <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>height <span style="color:#f92672">-</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"By (Your name here)"</span>,
</span></span><span style="display:flex;"><span> fg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>menu_title,
</span></span><span style="display:flex;"><span> alignment<span style="color:#f92672">=</span>tcod<span style="color:#f92672">.</span>CENTER,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> menu_width <span style="color:#f92672">=</span> <span style="color:#ae81ff">24</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> i, text <span style="color:#f92672">in</span> enumerate(
</span></span><span style="display:flex;"><span> [<span style="color:#e6db74">"[N] Play a new game"</span>, <span style="color:#e6db74">"[C] Continue last game"</span>, <span style="color:#e6db74">"[Q] Quit"</span>]
</span></span><span style="display:flex;"><span> ):
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>width <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>height <span style="color:#f92672">//</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">-</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">+</span> i,
</span></span><span style="display:flex;"><span> text<span style="color:#f92672">.</span>ljust(menu_width),
</span></span><span style="display:flex;"><span> fg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>menu_text,
</span></span><span style="display:flex;"><span> bg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>black,
</span></span><span style="display:flex;"><span> alignment<span style="color:#f92672">=</span>tcod<span style="color:#f92672">.</span>CENTER,
</span></span><span style="display:flex;"><span> bg_blend<span style="color:#f92672">=</span>tcod<span style="color:#f92672">.</span>BKGND_ALPHA(<span style="color:#ae81ff">64</span>),
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ev_keydown</span>(
</span></span><span style="display:flex;"><span> self, event: tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>KeyDown
</span></span><span style="display:flex;"><span> ) <span style="color:#f92672">-&gt;</span> Optional[input_handlers<span style="color:#f92672">.</span>BaseEventHandler]:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> event<span style="color:#f92672">.</span>sym <span style="color:#f92672">in</span> (tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_q, tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_ESCAPE):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">SystemExit</span>()
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> event<span style="color:#f92672">.</span>sym <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_c:
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># TODO: Load the game here</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">pass</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> event<span style="color:#f92672">.</span>sym <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_n:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> input_handlers<span style="color:#f92672">.</span>MainGameEventHandler(new_game())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span></code></pre></div><p>It might seem strange to put an event handler here rather than in its normal spot (<code>input_handlers.py</code>), but this makes sense for two reasons:</p>
<ol>
<li>The main menu is specific to the start of the game.</li>
<li>It wont be called during the normal course of the game, like the other input handlers in that file.</li>
</ol>
<p>Anyway, the class renders the image we specified earlier, and it displays a title, <code>"TOMBS OF THE ANCIENT KINGS"</code>. Of course, you can change this to whatever name you have in mind for your game. It also includes a <code>"By (Your name here)"</code> section, so be sure to fill your name in and let everyone know who it was that worked so hard to make this game!</p>
<p>The menu also gives three choices: new game, continue last game, and quit. The <code>ev_keydown</code> method, as you might expect, handles these inputs.</p>
<ul>
<li>If the player presses “Q”, the game just exits.</li>
<li>If the player presses “N”, a new game starts. We do this by returing the <code>MainGameEventHandler</code>, and calling the <code>new_game</code> function to create our new engine.</li>
<li>If the player presses “C”, theoretically, a saved game should load.
However, we havent gotten there yet, so as of now, nothing happens.</li>
</ul>
<p>Lets utilize our <code>MainMenu</code> function in <code>main.py</code>, like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>#!/usr/bin/env python3
</span></span><span style="display:flex;"><span><span style="color:#f92672">-import copy
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>import traceback
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import color
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from engine import Engine
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-import entity_factories
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>import exceptions
</span></span><span style="display:flex;"><span>import input_handlers
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from procgen import generate_dungeon
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+import setup_game
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def main() -&gt; None:
</span></span><span style="display:flex;"><span> screen_width = 80
</span></span><span style="display:flex;"><span> screen_height = 50
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- map_width = 80
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- map_height = 43
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- room_max_size = 10
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- room_min_size = 6
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- max_rooms = 30
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- max_monsters_per_room = 2
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- max_items_per_room = 2
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> tileset = tcod.tileset.load_tilesheet(
</span></span><span style="display:flex;"><span> "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- player = copy.deepcopy(entity_factories.player)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine = Engine(player=player)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.game_map = generate_dungeon(
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- max_rooms=max_rooms,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- room_min_size=room_min_size,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- room_max_size=room_max_size,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- map_width=map_width,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- map_height=map_height,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- max_monsters_per_room=max_monsters_per_room,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- max_items_per_room=max_items_per_room,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- engine=engine,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- )
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.update_fov()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.message_log.add_message(
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- "Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- )
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ handler: input_handlers.BaseEventHandler = setup_game.MainMenu()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> with tcod.context.new_terminal(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>#!/usr/bin/env python3
<span class="crossed-out-text">import copy</span>
import traceback
import tcod
import color
<span class="crossed-out-text">from engine import Engine</span>
<span class="crossed-out-text">import entity_factories</span>
import exceptions
import input_handlers
<span class="crossed-out-text">from procgen import generate_dungeon</span>
<span class="new-text">import setup_game</span>
def main() -&gt; None:
screen_width = 80
screen_height = 50
<span class="crossed-out-text">map_width = 80</span>
<span class="crossed-out-text">map_height = 43</span>
<span class="crossed-out-text">room_max_size = 10</span>
<span class="crossed-out-text">room_min_size = 6</span>
<span class="crossed-out-text">max_rooms = 30</span>
<span class="crossed-out-text">max_monsters_per_room = 2</span>
<span class="crossed-out-text">max_items_per_room = 2</span>
tileset = tcod.tileset.load_tilesheet(
"dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
)
<span class="crossed-out-text">player = copy.deepcopy(entity_factories.player)</span>
<span class="crossed-out-text">engine = Engine(player=player)</span>
<span class="crossed-out-text">engine.game_map = generate_dungeon(</span>
<span class="crossed-out-text">max_rooms=max_rooms,</span>
<span class="crossed-out-text">room_min_size=room_min_size,</span>
<span class="crossed-out-text">room_max_size=room_max_size,</span>
<span class="crossed-out-text">map_width=map_width,</span>
<span class="crossed-out-text">map_height=map_height,</span>
<span class="crossed-out-text">max_monsters_per_room=max_monsters_per_room,</span>
<span class="crossed-out-text">max_items_per_room=max_items_per_room,</span>
<span class="crossed-out-text">engine=engine,</span>
<span class="crossed-out-text">)</span>
<span class="crossed-out-text">engine.update_fov()</span>
<span class="crossed-out-text">engine.message_log.add_message(</span>
<span class="crossed-out-text">"Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text</span>
<span class="crossed-out-text">)</span>
<span class="crossed-out-text">handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)</span>
<span class="new-text">handler: input_handlers.BaseEventHandler = setup_game.MainMenu()</span>
with tcod.context.new_terminal(
...</pre>
</div>
</div>
<p>Were removing the code that dealt with setting up the engine, as thats been moved into the <code>new_game</code> function. All we have to do here is set our handler to <code>MainMenu</code>, and the <code>MainMenu</code> class handles the rest from there.</p>
<p>Run the game now, and you should see the main menu!</p>
<p><img src="Part%2010%20-%20Saving%20and%20loading%20%C2%B7%20Roguelike%20Tutorials_files/part-10-main-menu.png" alt="Part 10 - Main Menu"></p>
<p>*<em>Note: If you run the project and get this error: <code>Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)</code>, it means you didnt download the menu image file.</em></p>
<p>At last, weve come to the part where well write the function that will save our game! This will be a method in <code>Engine</code>, and well write it like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import lzma
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import pickle
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from typing import TYPE_CHECKING
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class Engine:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def save_as(self, filename: str) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Save this Engine instance as a compressed file."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ save_data = lzma.compress(pickle.dumps(self))
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ with open(filename, "wb") as f:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ f.write(save_data)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
<span class="new-text">import lzma
import pickle</span>
from typing import TYPE_CHECKING
...
class Engine:
...
<span class="new-text">def save_as(self, filename: str) -&gt; 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)</span></pre>
</div>
</div>
<p><code>pickle.dumps</code> serializes an object hierarchy in Python. <code>lzma.compress</code> compresses the data, so it takes up less space. We then use <code>with open(filename, "wb") as f:</code> to write the file (<code>wb</code> means “write in binary mode”), calling <code>f.write(save_data)</code> to write the data.</p>
<p>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 <code>Engine</code> class, we can pickle it, and were done!</p>
<p>Of course, its not quite <em>that</em> 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 doesnt exist.</p>
<p>To save our game, well call <code>save_as</code> from our <code>main.py</code> function. Well set up another function called <code>save_game</code> to call it, like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>import color
</span></span><span style="display:flex;"><span>import exceptions
</span></span><span style="display:flex;"><span>import setup_game
</span></span><span style="display:flex;"><span>import input_handlers
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+def save_game(handler: input_handlers.BaseEventHandler, filename: str) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """If the current event handler has an active Engine then save it."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if isinstance(handler, input_handlers.EventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ handler.engine.save_as(filename)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ print("Game saved.")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def main() -&gt; None:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> except exceptions.QuitWithoutSaving:
</span></span><span style="display:flex;"><span> raise
</span></span><span style="display:flex;"><span> except SystemExit: # Save and quit.
</span></span><span style="display:flex;"><span><span style="color:#f92672">- # TODO: Add the save function here
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ save_game(handler, "savegame.sav")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> raise
</span></span><span style="display:flex;"><span> except BaseException: # Save on any other unexpected exception.
</span></span><span style="display:flex;"><span><span style="color:#f92672">- # TODO: Add the save function here
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ save_game(handler, "savegame.sav")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> raise
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>if __name__ == "__main__":
</span></span><span style="display:flex;"><span> main()
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
import color
import exceptions
import setup_game
import input_handlers
<span class="new-text">def save_game(handler: input_handlers.BaseEventHandler, filename: str) -&gt; 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.")</span>
def main() -&gt; None:
...
...
except exceptions.QuitWithoutSaving:
raise
except SystemExit: # Save and quit.
<span class="crossed-out-text"># TODO: Add the save function here</span>
<span class="new-text">save_game(handler, "savegame.sav")</span>
raise
except BaseException: # Save on any other unexpected exception.
<span class="crossed-out-text"># TODO: Add the save function here</span>
<span class="new-text">save_game(handler, "savegame.sav")</span>
raise
if __name__ == "__main__":
main()</pre>
</div>
</div>
<p>Now when you exit the game, you should see a new <code>savegame.sav</code> in your project directory.</p>
<p>One thing that would help to handle the case where the user tries to
load a saved file when one doesnt exist would be a pop-up message. This
message will appear in the center of the screen, and disappear after
any key is pressed.</p>
<p>Add this class to <code>input_handlers.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class PopupMessage(BaseEventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Display a popup text window."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(self, parent_handler: BaseEventHandler, text: str):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent = parent_handler
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.text = text
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_render(self, console: tcod.Console) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Render the parent and dim the result, then print the message on top."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.on_render(console)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.tiles_rgb["fg"] //= 8
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.tiles_rgb["bg"] //= 8
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.width // 2,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.height // 2,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.text,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fg=color.white,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ bg=color.black,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ alignment=tcod.CENTER,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[BaseEventHandler]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Any key returns to the parent handler."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.parent
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class EventHandler(BaseEventHandler):
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
...
<span class="new-text">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) -&gt; 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) -&gt; Optional[BaseEventHandler]:
"""Any key returns to the parent handler."""
return self.parent</span>
class EventHandler(BaseEventHandler):
...</pre>
</div>
</div>
<p>This displays a message on top of the current display, whether its
the main menu or the main game. When the player presses a key (any key),
the message disappears.</p>
<p>Now lets shift our focus to loading the game. We can add a <code>load_game</code> function in our <code>setup_game.py</code> file, which will attempt to load the game. Well call it when we press the “c” key on the main menu. Open up <code>setup_game.py</code> and edit it like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import copy
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import lzma
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import pickle
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import traceback
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from typing import Optional
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def new_game() -&gt; Engine:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+def load_game(filename: str) -&gt; Engine:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Load an Engine instance from a file."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ with open(filename, "rb") as f:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine = pickle.loads(lzma.decompress(f.read()))
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ assert isinstance(engine, Engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return engine
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class MainMenu(input_handlers.BaseEventHandler):
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def ev_keydown(
</span></span><span style="display:flex;"><span> self, event: tcod.event.KeyDown
</span></span><span style="display:flex;"><span> ) -&gt; Optional[input_handlers.BaseEventHandler]:
</span></span><span style="display:flex;"><span> if event.sym in (tcod.event.K_q, tcod.event.K_ESCAPE):
</span></span><span style="display:flex;"><span> raise SystemExit()
</span></span><span style="display:flex;"><span> elif event.sym == tcod.event.K_c:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- # TODO: Load the game here
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- pass
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ try:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return input_handlers.MainGameEventHandler(load_game("savegame.sav"))
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except FileNotFoundError:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return input_handlers.PopupMessage(self, "No saved game to load.")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except Exception as exc:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ traceback.print_exc() # Print to stderr.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return input_handlers.PopupMessage(self, f"Failed to load save:\n{exc}")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif event.sym == tcod.event.K_n:
</span></span><span style="display:flex;"><span> return input_handlers.MainGameEventHandler(new_game())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> return None
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
import copy
<span class="new-text">import lzma</span>
<span class="new-text">import pickle</span>
<span class="new-text">import traceback</span>
from typing import Optional
import tcod
...
def new_game() -&gt; Engine:
...
<span class="new-text">def load_game(filename: str) -&gt; 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</span>
class MainMenu(input_handlers.BaseEventHandler):
...
def ev_keydown(
self, event: tcod.event.KeyDown
) -&gt; 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:
<span class="crossed-out-text"># TODO: Load the game here</span>
<span class="crossed-out-text">pass</span>
<span class="new-text">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}")</span>
elif event.sym == tcod.event.K_n:
return input_handlers.MainGameEventHandler(new_game())
return None</pre>
</div>
</div>
<p><code>load_game</code> essentially works the opposite of <code>save_as</code>, by opening up the file, uncompressing and unpickling it, and returning the instance of the <code>Engine</code> class. It then passes that engine to <code>MainGameEventHandler</code>. If no save game exists, or an error occured, we display a popup message.</p>
<p>And with that change, we can load our game! Try exiting your game and loading it afterwards.</p>
<p>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 cant 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.</p>
<p>To do that, we can override the <code>ev_quit</code> method in <code>GameOverEventHandler</code>. Open up <code>input_handlers.py</code> and make the following fix:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import os
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>from typing import Callable, Optional, Tuple, TYPE_CHECKING, Union
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class GameOverEventHandler(EventHandler):
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_quit(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle exiting out of a finished game."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if os.path.exists("savegame.sav"):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ os.remove("savegame.sav") # Deletes the active save file.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.QuitWithoutSaving() # Avoid saving a finished game.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_quit(self, event: tcod.event.Quit) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.on_quit()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def ev_keydown(self, event: tcod.event.KeyDown) -&gt; None:
</span></span><span style="display:flex;"><span> if event.sym == tcod.event.K_ESCAPE:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- raise SystemExit()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.on_quit()
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
<span class="new-text">import os</span>
from typing import Callable, Optional, Tuple, TYPE_CHECKING, Union
...
class GameOverEventHandler(EventHandler):
<span class="new-text">def on_quit(self) -&gt; 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) -&gt; None:
self.on_quit()</span>
def ev_keydown(self, event: tcod.event.KeyDown) -&gt; None:
if event.sym == tcod.event.K_ESCAPE:
<span class="crossed-out-text">raise SystemExit()</span>
<span class="new-text">self.on_quit()</span></pre>
</div>
</div>
<p>We use the <code>os</code> module to find the save file, and if it exists, we remove it. We then raise <code>QuitWithoutSaving</code>,
so that the game wont be saved on exiting. Now when the player meets
his or her tragic end (its a roguelike, its inevitable!), the save
file will be deleted.</p>
<p>Last thing before we wrap up: Were creating the <code>.sav</code>
files to represent our saved games, but we dont 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 <code>.gitignore</code> file:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#a6e22e">+# Saved games
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+*.sav
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="new-text"># Saved games
*.sav</span></pre>
</div>
</div>
<p><em>The rest of the .gitignore is omitted, as your .gitignore file
may look different from mine. It doesnt matter where you add this in.</em></p>
<p>If you want to see the code so far in its entirety, <a href="https://github.com/TStand90/tcod_tutorial_v2/tree/2020/part-10">click here</a>.</p>
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-11">Click here to move on to the next part of this tutorial.</a></p>
</article>
</section>
</div>
<footer class="footer">
<section class="container">
©
2023
·
Powered by <a href="https://gohugo.io/">Hugo</a> &amp; <a href="https://github.com/luizdepra/hugo-coder/">Coder</a>.
</section>
</footer>
</main>
<script src="Part%2010%20-%20Saving%20and%20loading%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
<script src="Part%2010%20-%20Saving%20and%20loading%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
</body></html>