McRogueFace/roguelike_tutorial/rogueliketutorials.com/Part 8 - Items and Inventor...

3235 lines
181 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 8 - Items and Inventory · 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="Quick refactors Link to heading Once again, apologies to everyone reading this right now. After publishing the last two parts, there were once again a few refactors on code written in those parts, like at the beginning of part 6. Luckily, the changes are much less extensive this time.
ai.py
Diff Original ... import numpy as np # type: ignore import tcod from actions import Action, MeleeAction, MovementAction, WaitAction -from components.">
<meta name="keywords" content="">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Part 8 - Items and Inventory">
<meta name="twitter:description" content="Quick refactors Link to heading Once again, apologies to everyone reading this right now. After publishing the last two parts, there were once again a few refactors on code written in those parts, like at the beginning of part 6. Luckily, the changes are much less extensive this time.
ai.py
Diff Original ... import numpy as np # type: ignore import tcod from actions import Action, MeleeAction, MovementAction, WaitAction -from components.">
<meta property="og:title" content="Part 8 - Items and Inventory">
<meta property="og:description" content="Quick refactors Link to heading Once again, apologies to everyone reading this right now. After publishing the last two parts, there were once again a few refactors on code written in those parts, like at the beginning of part 6. Luckily, the changes are much less extensive this time.
ai.py
Diff Original ... import numpy as np # type: ignore import tcod from actions import Action, MeleeAction, MovementAction, WaitAction -from components.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-8/"><meta property="article:section" content="tutorials">
<meta property="article:published_time" content="2020-07-14T00:00:00+00:00">
<meta property="article:modified_time" content="2020-07-14T00:00:00+00:00">
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-8/">
<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%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%208%20-%20Items%20and%20Inventory%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%208%20-%20Items%20and%20Inventory%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-8/">
Part 8 - Items and Inventory
</a>
</h1>
</header>
<h2 id="quick-refactors">
Quick refactors
<a class="heading-link" href="#quick-refactors">
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
<span class="sr-only">Link to heading</span>
</a>
</h2>
<p>Once again, apologies to everyone reading this right now. After
publishing the last two parts, there were once again a few refactors on
code written in those parts, like at the beginning of part 6. Luckily,
the changes are much less extensive this time.</p>
<p><code>ai.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>...
</span></span><span style="display:flex;"><span>import numpy as np # type: ignore
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from actions import Action, MeleeAction, MovementAction, WaitAction
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from components.base_component import BaseComponent
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-class BaseAI(Action, BaseComponent):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+class BaseAI(Action):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> entity: Actor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def perform(self) -&gt; None:
</span></span><span style="display:flex;"><span> raise NotImplementedError()
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
import numpy as np # type: ignore
import tcod
from actions import Action, MeleeAction, MovementAction, WaitAction
<span class="crossed-out-text">from components.base_component import BaseComponent</span>
if TYPE_CHECKING:
from entity import Actor
<span class="crossed-out-text">class BaseAI(Action, BaseComponent):</span>
<span class="new-text">class BaseAI(Action):</span>
<span class="crossed-out-text">entity: Actor</span>
def perform(self) -&gt; None:
raise NotImplementedError()
...</pre>
</div>
</div>
<p><code>message_log.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><span style="color:#a6e22e">+from typing import Iterable, List, Reversible, Tuple
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import List, Reversible, Tuple
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>import textwrap
</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></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class MessageLog:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def render(
</span></span><span style="display:flex;"><span> self, console: tcod.Console, x: int, y: int, width: int, height: int,
</span></span><span style="display:flex;"><span> ) -&gt; None:
</span></span><span style="display:flex;"><span> """Render this log over the given area.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> `x`, `y`, `width`, `height` is the rectangular region to render onto
</span></span><span style="display:flex;"><span> the `console`.
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span> self.render_messages(console, x, y, width, height, self.messages)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ @staticmethod
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def wrap(string: str, width: int) -&gt; Iterable[str]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return a wrapped text message."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for line in string.splitlines(): # Handle newlines in messages.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ yield from textwrap.wrap(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ line, width, expand_tabs=True,
</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:#f92672">- @staticmethod
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ @classmethod
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> def render_messages(
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ cls,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> console: tcod.Console,
</span></span><span style="display:flex;"><span> x: int,
</span></span><span style="display:flex;"><span> y: int,
</span></span><span style="display:flex;"><span> width: int,
</span></span><span style="display:flex;"><span> height: int,
</span></span><span style="display:flex;"><span> messages: Reversible[Message],
</span></span><span style="display:flex;"><span> ) -&gt; None:
</span></span><span style="display:flex;"><span> """Render the messages provided.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> The `messages` are rendered starting at the last message and working
</span></span><span style="display:flex;"><span> backwards.
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span> y_offset = height - 1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> for message in reversed(messages):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- for line in reversed(textwrap.wrap(message.full_text, width)):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ for line in reversed(list(cls.wrap(message.full_text, width))):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> console.print(x=x, y=y + y_offset, string=line, fg=message.fg)
</span></span><span style="display:flex;"><span> y_offset -= 1
</span></span><span style="display:flex;"><span> if y_offset &lt; 0:
</span></span><span style="display:flex;"><span> return # No more space to print messages.
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="new-text">from typing import Iterable, List, Reversible, Tuple</span>
<span class="crossed-out-text">from typing import List, Reversible, Tuple</span>
import textwrap
import tcod
import color
...
class MessageLog:
...
def render(
self, console: tcod.Console, x: int, y: int, width: int, height: int,
) -&gt; None:
"""Render this log over the given area.
`x`, `y`, `width`, `height` is the rectangular region to render onto
the `console`.
"""
self.render_messages(console, x, y, width, height, self.messages)
<span class="new-text">@staticmethod
def wrap(string: str, width: int) -&gt; Iterable[str]:
"""Return a wrapped text message."""
for line in string.splitlines(): # Handle newlines in messages.
yield from textwrap.wrap(
line, width, expand_tabs=True,
)</span>
<span class="crossed-out-text">@staticmethod</span>
<span class="new-text">@classmethod</span>
def render_messages(
<span class="new-text">cls,</span>
console: tcod.Console,
x: int,
y: int,
width: int,
height: int,
messages: Reversible[Message],
) -&gt; None:
"""Render the messages provided.
The `messages` are rendered starting at the last message and working
backwards.
"""
y_offset = height - 1
for message in reversed(messages):
<span class="crossed-out-text">for line in reversed(textwrap.wrap(message.full_text, width)):</span>
<span class="new-text">for line in reversed(list(cls.wrap(message.full_text, width))):</span>
console.print(x=x, y=y + y_offset, string=line, fg=message.fg)
y_offset -= 1
if y_offset &lt; 0:
return # No more space to print messages.</pre>
</div>
</div>
<p><code>game_map.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 GameMap:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> ) # Tiles the player has seen before
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ @property
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def gamemap(self) -&gt; GameMap:
</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> @property
</span></span><span style="display:flex;"><span> def actors(self) -&gt; Iterator[Actor]:
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class GameMap:
...
) # Tiles the player has seen before
<span class="new-text">@property
def gamemap(self) -&gt; GameMap:
return self</span>
@property
def actors(self) -&gt; Iterator[Actor]:
...</pre>
</div>
</div>
<p><code>entity.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 Entity:
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span> A generic object to represent players, enemies, items, etc.
</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">- gamemap: GameMap
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: GameMap
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def __init__(
</span></span><span style="display:flex;"><span> self,
</span></span><span style="display:flex;"><span><span style="color:#f92672">- gamemap: Optional[GameMap] = None,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: Optional[GameMap] = None,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> x: int = 0,
</span></span><span style="display:flex;"><span> y: int = 0,
</span></span><span style="display:flex;"><span> char: str = "?",
</span></span><span style="display:flex;"><span> color: Tuple[int, int, int] = (255, 255, 255),
</span></span><span style="display:flex;"><span> name: str = "&lt;Unnamed&gt;",
</span></span><span style="display:flex;"><span> blocks_movement: bool = False,
</span></span><span style="display:flex;"><span> render_order: RenderOrder = RenderOrder.CORPSE,
</span></span><span style="display:flex;"><span> ):
</span></span><span style="display:flex;"><span> self.x = x
</span></span><span style="display:flex;"><span> self.y = y
</span></span><span style="display:flex;"><span> self.char = char
</span></span><span style="display:flex;"><span> self.color = color
</span></span><span style="display:flex;"><span> self.name = name
</span></span><span style="display:flex;"><span> self.blocks_movement = blocks_movement
</span></span><span style="display:flex;"><span> self.render_order = render_order
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if gamemap:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- # If gamemap isn't provided now then it will be set later.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.gamemap = gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- gamemap.entities.add(self)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if parent:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # If parent isn't provided now then it will be set later.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent = parent
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ parent.entities.add(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">+ @property
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def gamemap(self) -&gt; GameMap:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.parent.gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def spawn(self: T, gamemap: GameMap, x: int, y: int) -&gt; T:
</span></span><span style="display:flex;"><span> """Spawn a copy of this instance at the given location."""
</span></span><span style="display:flex;"><span> clone = copy.deepcopy(self)
</span></span><span style="display:flex;"><span> clone.x = x
</span></span><span style="display:flex;"><span> clone.y = y
</span></span><span style="display:flex;"><span><span style="color:#f92672">- clone.gamemap = gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ clone.parent = gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> gamemap.entities.add(clone)
</span></span><span style="display:flex;"><span> return clone
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -&gt; None:
</span></span><span style="display:flex;"><span> """Place this entity at a new location. Handles moving across GameMaps."""
</span></span><span style="display:flex;"><span> self.x = x
</span></span><span style="display:flex;"><span> self.y = y
</span></span><span style="display:flex;"><span> if gamemap:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if hasattr(self, "gamemap"): # Possibly uninitialized.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.gamemap.entities.remove(self)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.gamemap = gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if hasattr(self, "parent"): # Possibly uninitialized.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.parent is self.gamemap:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.gamemap.entities.remove(self)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent = gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> gamemap.entities.add(self)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def move(self, dx: int, dy: int) -&gt; None:
</span></span><span style="display:flex;"><span> # Move the entity by a given amount
</span></span><span style="display:flex;"><span> self.x += dx
</span></span><span style="display:flex;"><span> self.y += dy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class Actor(Entity):
</span></span><span style="display:flex;"><span> def __init__(
</span></span><span style="display:flex;"><span> self,
</span></span><span style="display:flex;"><span> *,
</span></span><span style="display:flex;"><span> x: int = 0,
</span></span><span style="display:flex;"><span> y: int = 0,
</span></span><span style="display:flex;"><span> char: str = "?",
</span></span><span style="display:flex;"><span> color: Tuple[int, int, int] = (255, 255, 255),
</span></span><span style="display:flex;"><span> name: str = "&lt;Unnamed&gt;",
</span></span><span style="display:flex;"><span> ai_cls: Type[BaseAI],
</span></span><span style="display:flex;"><span> fighter: Fighter
</span></span><span style="display:flex;"><span> ):
</span></span><span style="display:flex;"><span> super().__init__(
</span></span><span style="display:flex;"><span> x=x,
</span></span><span style="display:flex;"><span> y=y,
</span></span><span style="display:flex;"><span> char=char,
</span></span><span style="display:flex;"><span> color=color,
</span></span><span style="display:flex;"><span> name=name,
</span></span><span style="display:flex;"><span> blocks_movement=True,
</span></span><span style="display:flex;"><span> render_order=RenderOrder.ACTOR,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self.ai: Optional[BaseAI] = ai_cls(self)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self.fighter = fighter
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.fighter.entity = self
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.fighter.parent = self
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> @property
</span></span><span style="display:flex;"><span> def is_alive(self) -&gt; bool:
</span></span><span style="display:flex;"><span> """Returns True as long as this actor can perform actions."""
</span></span><span style="display:flex;"><span> return bool(self.ai)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class Entity:
"""
A generic object to represent players, enemies, items, etc.
"""
<span class="crossed-out-text">gamemap: GameMap</span>
<span class="new-text">parent: GameMap</span>
def __init__(
self,
<span class="crossed-out-text">gamemap: Optional[GameMap] = None,</span>
<span class="new-text">parent: Optional[GameMap] = None,</span>
x: int = 0,
y: int = 0,
char: str = "?",
color: Tuple[int, int, int] = (255, 255, 255),
name: str = "&lt;Unnamed&gt;",
blocks_movement: bool = False,
render_order: RenderOrder = RenderOrder.CORPSE,
):
self.x = x
self.y = y
self.char = char
self.color = color
self.name = name
self.blocks_movement = blocks_movement
self.render_order = render_order
<span class="crossed-out-text">if gamemap:</span>
<span class="crossed-out-text"># If gamemap isn't provided now then it will be set later.</span>
<span class="crossed-out-text">self.gamemap = gamemap</span>
<span class="crossed-out-text">gamemap.entities.add(self)</span>
<span class="new-text">if parent:
# If parent isn't provided now then it will be set later.
self.parent = parent
parent.entities.add(self)
@property
def gamemap(self) -&gt; GameMap:
return self.parent.gamemap</span>
def spawn(self: T, gamemap: GameMap, x: int, y: int) -&gt; T:
"""Spawn a copy of this instance at the given location."""
clone = copy.deepcopy(self)
clone.x = x
clone.y = y
<span class="crossed-out-text">clone.gamemap = gamemap</span>
<span class="new-text">clone.parent = gamemap</span>
gamemap.entities.add(clone)
return clone
def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -&gt; None:
"""Place this entity at a new location. Handles moving across GameMaps."""
self.x = x
self.y = y
if gamemap:
<span class="crossed-out-text">if hasattr(self, "gamemap"): # Possibly uninitialized.</span>
<span class="crossed-out-text">self.gamemap.entities.remove(self)</span>
<span class="crossed-out-text">self.gamemap = gamemap</span>
<span class="new-text">if hasattr(self, "parent"): # Possibly uninitialized.
if self.parent is self.gamemap:
self.gamemap.entities.remove(self)
self.parent = gamemap</span>
gamemap.entities.add(self)
def move(self, dx: int, dy: int) -&gt; None:
# Move the entity by a given amount
self.x += dx
self.y += dy
class Actor(Entity):
def __init__(
self,
*,
x: int = 0,
y: int = 0,
char: str = "?",
color: Tuple[int, int, int] = (255, 255, 255),
name: str = "&lt;Unnamed&gt;",
ai_cls: Type[BaseAI],
fighter: Fighter
):
super().__init__(
x=x,
y=y,
char=char,
color=color,
name=name,
blocks_movement=True,
render_order=RenderOrder.ACTOR,
)
self.ai: Optional[BaseAI] = ai_cls(self)
self.fighter = fighter
<span class="crossed-out-text">self.fighter.entity = self</span>
<span class="new-text">self.fighter.parent = self</span>
@property
def is_alive(self) -&gt; bool:
"""Returns True as long as this actor can perform actions."""
return bool(self.ai)</pre>
</div>
</div>
<p><code>base_component.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>from typing import TYPE_CHECKING
</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 engine import Engine
</span></span><span style="display:flex;"><span> from entity import Entity
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from game_map import GameMap
</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 BaseComponent:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- entity: Entity # Owning entity instance.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: Entity # Owning entity instance.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ @property
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def gamemap(self) -&gt; GameMap:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.parent.gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> @property
</span></span><span style="display:flex;"><span> def engine(self) -&gt; Engine:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- return self.entity.gamemap.engine
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return self.gamemap.engine
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from engine import Engine
from entity import Entity
<span class="new-text">from game_map import GameMap</span>
class BaseComponent:
<span class="crossed-out-text">entity: Entity # Owning entity instance.</span>
<span class="new-text">parent: Entity # Owning entity instance.
@property
def gamemap(self) -&gt; GameMap:
return self.parent.gamemap</span>
@property
def engine(self) -&gt; Engine:
<span class="crossed-out-text">return self.entity.gamemap.engine</span>
<span class="new-text">return self.gamemap.engine</span></pre>
</div>
</div>
<p><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>class Fighter(BaseComponent):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- entity: Actor
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: Actor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def __init__(self, hp: int, defense: int, power: int):
</span></span><span style="display:flex;"><span> self.max_hp = hp
</span></span><span style="display:flex;"><span> self._hp = hp
</span></span><span style="display:flex;"><span> self.defense = defense
</span></span><span style="display:flex;"><span> self.power = power
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> @property
</span></span><span style="display:flex;"><span> def hp(self) -&gt; int:
</span></span><span style="display:flex;"><span> return self._hp
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> @hp.setter
</span></span><span style="display:flex;"><span> def hp(self, value: int) -&gt; None:
</span></span><span style="display:flex;"><span> self._hp = max(0, min(value, self.max_hp))
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if self._hp == 0 and self.entity.ai:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if self._hp == 0 and self.parent.ai:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.die()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def die(self) -&gt; None:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if self.engine.player is self.entity:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if self.engine.player is self.parent:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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> self.engine.event_handler = GameOverEventHandler(self.engine)
</span></span><span style="display:flex;"><span> else:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- death_message = f"{self.entity.name} is dead!"
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ death_message = f"{self.parent.name} is dead!"
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> death_message_color = color.enemy_die
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.char = "%"
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.color = (191, 0, 0)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.blocks_movement = False
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.ai = None
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.name = f"remains of {self.parent.name}"
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.render_order = RenderOrder.CORPSE
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- self.entity.char = "%"
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.color = (191, 0, 0)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.blocks_movement = False
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.ai = None
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.name = f"remains of {self.entity.name}"
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.render_order = RenderOrder.CORPSE
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> self.engine.message_log.add_message(death_message, death_message_color)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class Fighter(BaseComponent):
<span class="crossed-out-text">entity: Actor</span>
<span class="new-text">parent: Actor</span>
def __init__(self, hp: int, defense: int, power: int):
self.max_hp = hp
self._hp = hp
self.defense = defense
self.power = power
@property
def hp(self) -&gt; int:
return self._hp
@hp.setter
def hp(self, value: int) -&gt; None:
self._hp = max(0, min(value, self.max_hp))
<span class="crossed-out-text">if self._hp == 0 and self.entity.ai:</span>
<span class="new-text">if self._hp == 0 and self.parent.ai:</span>
self.die()
def die(self) -&gt; None:
<span class="crossed-out-text">if self.engine.player is self.entity:</span>
<span class="new-text">if self.engine.player is self.parent:</span>
death_message = "You died!"
death_message_color = color.player_die
self.engine.event_handler = GameOverEventHandler(self.engine)
else:
<span class="crossed-out-text">death_message = f"{self.entity.name} is dead!"</span>
<span class="new-text">death_message = f"{self.parent.name} is dead!"</span>
death_message_color = color.enemy_die
<span class="new-text">self.parent.char = "%"
self.parent.color = (191, 0, 0)
self.parent.blocks_movement = False
self.parent.ai = None
self.parent.name = f"remains of {self.parent.name}"
self.parent.render_order = RenderOrder.CORPSE</span>
<span class="crossed-out-text">self.entity.char = "%"</span>
<span class="crossed-out-text">self.entity.color = (191, 0, 0)</span>
<span class="crossed-out-text">self.entity.blocks_movement = False</span>
<span class="crossed-out-text">self.entity.ai = None</span>
<span class="crossed-out-text">self.entity.name = f"remains of {self.entity.name}"</span>
<span class="crossed-out-text">self.entity.render_order = RenderOrder.CORPSE</span>
self.engine.message_log.add_message(death_message, death_message_color)</pre>
</div>
</div>
<h2 id="part-8">
Part 8
<a class="heading-link" href="#part-8">
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
<span class="sr-only">Link to heading</span>
</a>
</h2>
<p>So far, our game has movement, dungeon exploring, combat, and AI (okay, were stretching the meaning of “intelligence” in <em>artificial intelligence</em>
to its limits, but bear with me here). Now its time for another staple
of the roguelike genre: items! Why would our rogue venture into the
dungeons of doom if not for some sweet loot, after all?</p>
<p>In this part of the tutorial, well achieve a few things: a working
inventory, and a functioning healing potion. The next part will add more
items that can be picked up, but for now, just the healing potion will
suffice.</p>
<p>For this part, well need four more colors. Lets get adding those out of the way now. Open up <code>color.py</code> and add these colors:</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>white = (0xFF, 0xFF, 0xFF)
</span></span><span style="display:flex;"><span>black = (0x0, 0x0, 0x0)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>player_atk = (0xE0, 0xE0, 0xE0)
</span></span><span style="display:flex;"><span>enemy_atk = (0xFF, 0xC0, 0xC0)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>player_die = (0xFF, 0x30, 0x30)
</span></span><span style="display:flex;"><span>enemy_die = (0xFF, 0xA0, 0x30)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+invalid = (0xFF, 0xFF, 0x00)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+impossible = (0x80, 0x80, 0x80)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+error = (0xFF, 0x40, 0x40)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>welcome_text = (0x20, 0xA0, 0xFF)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+health_recovered = (0x0, 0xFF, 0x0)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>bar_text = white
</span></span><span style="display:flex;"><span>bar_filled = (0x0, 0x60, 0x0)
</span></span><span style="display:flex;"><span>bar_empty = (0x40, 0x10, 0x10)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>white = (0xFF, 0xFF, 0xFF)
black = (0x0, 0x0, 0x0)
player_atk = (0xE0, 0xE0, 0xE0)
enemy_atk = (0xFF, 0xC0, 0xC0)
player_die = (0xFF, 0x30, 0x30)
enemy_die = (0xFF, 0xA0, 0x30)
<span class="new-text">invalid = (0xFF, 0xFF, 0x00)
impossible = (0x80, 0x80, 0x80)
error = (0xFF, 0x40, 0x40)</span>
welcome_text = (0x20, 0xA0, 0xFF)
<span class="new-text">health_recovered = (0x0, 0xFF, 0x0)</span>
bar_text = white
bar_filled = (0x0, 0x60, 0x0)
bar_empty = (0x40, 0x10, 0x10)</pre>
</div>
</div>
<p>These will become useful shortly.</p>
<p>Theres another thing we can knock out right now that well use later: The ability for a <code>Fighter</code>
component to recover health, and the ability to take damage directly
(without the defense modifier). We wont use the damage function this
chapter, but since the two functions are effectively opposites, we can
get writing it over with now.</p>
<p>Open up <code>fighter.py</code> and add these two functions:</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 Fighter:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def heal(self, amount: int) -&gt; int:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.hp == self.max_hp:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return 0
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ new_hp_value = self.hp + amount
</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 new_hp_value &gt; self.max_hp:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ new_hp_value = self.max_hp
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ amount_recovered = new_hp_value - self.hp
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.hp = new_hp_value
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return amount_recovered
</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 take_damage(self, amount: int) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.hp -= amount
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class Fighter:
...
<span class="new-text">def heal(self, amount: int) -&gt; int:
if self.hp == self.max_hp:
return 0
new_hp_value = self.hp + amount
if new_hp_value &gt; self.max_hp:
new_hp_value = self.max_hp
amount_recovered = new_hp_value - self.hp
self.hp = new_hp_value
return amount_recovered
def take_damage(self, amount: int) -&gt; None:
self.hp -= amount</span></pre>
</div>
</div>
<p><code>heal</code> will restore a certain amount of HP, up to the
maximum, and return the amount that was healed. If the entitys health
is at full, then just return 0. The function that handles this should
display an error if the returned amount is 0, since the entity cant be
healed.</p>
<p>One thing were going to need is a way to <em>not</em> consume an
item or take a turn if something goes wrong during the process. For our
health potion, think about what should happen if the player declares
they want to use a health potion, but their health is already full. What
should happen?</p>
<p>We could just consume the potion anyway, and have it go to waste, but
if youve played a game that does that, you know how frustrating it can
be, especially if the player clicked the health potion on accident. A
better way would be to warn the user that theyre trying to do something
that makes no sense, and save the player from wasting both the potion
and their turn.</p>
<p>But how can we achieve that? Well discuss it a bit more later on,
but the idea is that if we do something impossible, we should raise an
exception. Which one? Well, we can define a custom exception, which can
give us details on what happened. Create a new file called <code>exceptions.py</code> and put the following class 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:#66d9ef">class</span> <span style="color:#a6e22e">Impossible</span>(<span style="color:#a6e22e">Exception</span>):
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Exception raised when an action is impossible to be performed.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> The reason is given as the exception message.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
</span></span></code></pre></div><p>… And thats it! When we write <code>raise Impossible("An exception message")</code> in our program, the <code>Impossible</code> exception will be raised, with the given message.</p>
<p>So what do we do with the raised exception? Well, we should catch it! But where?</p>
<p>Lets modify the <code>main.py</code> file to catch the exceptions, 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>import copy
</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>
</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> context.present(root_console)
</span></span><span style="display:flex;"><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">+ engine.event_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">+ engine.message_log.add_message(traceback.format_exc(), color.error)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- engine.event_handler.handle_events(context)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>#!/usr/bin/env python3
import copy
<span class="new-text">import traceback</span>
import tcod
...
context.present(root_console)
<span class="new-text">try:
for event in tcod.event.wait():
context.convert_event(event)
engine.event_handler.handle_events(event)
except Exception: # Handle exceptions in game.
traceback.print_exc() # Print error to stderr.
# Then print the error to the message log.
engine.message_log.add_message(traceback.format_exc(), color.error)</span>
<span class="crossed-out-text">engine.event_handler.handle_events(context)</span></pre>
</div>
</div>
<p>This is a generalized, catch all solution, which will print <em>all</em> exceptions to the message log, not just instances of <code>Impossible</code>. This can be helpful for debugging your game, or getting error reports from users.</p>
<p>However, this solution doesnt mesh with our current implementation of the <code>EventHandler</code>. <code>EventHandler</code> currently loops through the events and converts them (to get the mouse information). Well need to edit a few things in <code>input_handlers.py</code> to get back on track.</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>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from actions import (
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Action,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ BumpAction,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ EscapeAction,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ WaitAction
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import color
</span></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"></span><span style="color:#f92672">-from actions import Action, BumpAction, EscapeAction, WaitAction
</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 EventHandler(tcod.event.EventDispatch[Action]):
</span></span><span style="display:flex;"><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:#a6e22e">+ def handle_events(self, event: tcod.event.Event) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.handle_action(self.dispatch(event))
</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 handle_action(self, action: Optional[Action]) -&gt; bool:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle actions returned from event methods.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Returns True if the action will advance a turn.
</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 action is None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return False
</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">+ action.perform()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except exceptions.Impossible as exc:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.add_message(exc.args[0], color.impossible)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return False # Skip enemy turn on exceptions.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.handle_enemy_turns()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.update_fov()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return True
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self, context: tcod.context.Context) -&gt; None:
</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">- self.dispatch(event)
</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>
</span></span><span style="display:flex;"><span>class MainGameEventHandler(EventHandler):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self, context: tcod.context.Context) -&gt; None:
</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"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = self.dispatch(event)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if action is None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- continue
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action.perform()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.handle_enemy_turns()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.update_fov() # Update the FOV before the players next action.
</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 GameOverEventHandler(EventHandler):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self, context: tcod.context.Context) -&gt; None:
</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">- action = self.dispatch(event)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if action is None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- continue
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action.perform()
</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">- action: Optional[Action] = None
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- key = event.sym
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if key == tcod.event.K_ESCAPE:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = EscapeAction(self.engine.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">- # No valid key was pressed
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- return 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; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if event.sym == tcod.event.K_ESCAPE:
</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>import tcod
<span class="new-text">from actions import (
Action,
BumpAction,
EscapeAction,
WaitAction
)
import color
import exceptions</span>
<span class="crossed-out-text">from actions import Action, BumpAction, EscapeAction, WaitAction</span>
class EventHandler(tcod.event.EventDispatch[Action]):
def __init__(self, engine: Engine):
self.engine = engine
<span class="new-text">def handle_events(self, event: tcod.event.Event) -&gt; None:
self.handle_action(self.dispatch(event))
def handle_action(self, action: Optional[Action]) -&gt; bool:
"""Handle actions returned from event methods.
Returns True if the action will advance a turn.
"""
if action is None:
return False
try:
action.perform()
except exceptions.Impossible as exc:
self.engine.message_log.add_message(exc.args[0], color.impossible)
return False # Skip enemy turn on exceptions.
self.engine.handle_enemy_turns()
self.engine.update_fov()
return True</span>
<span class="crossed-out-text">def handle_events(self, context: tcod.context.Context) -&gt; None:</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">self.dispatch(event)</span>
...
class MainGameEventHandler(EventHandler):
<span class="crossed-out-text">def handle_events(self, context: tcod.context.Context) -&gt; None:</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">action = self.dispatch(event)</span>
<span class="crossed-out-text">if action is None:</span>
<span class="crossed-out-text">continue</span>
<span class="crossed-out-text">action.perform()</span>
<span class="crossed-out-text">self.engine.handle_enemy_turns()</span>
<span class="crossed-out-text">self.engine.update_fov() # Update the FOV before the players next action.</span>
...
class GameOverEventHandler(EventHandler):
<span class="crossed-out-text">def handle_events(self, context: tcod.context.Context) -&gt; None:</span>
<span class="crossed-out-text">for event in tcod.event.wait():</span>
<span class="crossed-out-text">action = self.dispatch(event)</span>
<span class="crossed-out-text">if action is None:</span>
<span class="crossed-out-text">continue</span>
<span class="crossed-out-text">action.perform()</span>
<span class="crossed-out-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:</span>
<span class="crossed-out-text">action: Optional[Action] = None</span>
<span class="crossed-out-text">key = event.sym</span>
<span class="crossed-out-text">if key == tcod.event.K_ESCAPE:</span>
<span class="crossed-out-text">action = EscapeAction(self.engine.player)</span>
<span class="crossed-out-text"># No valid key was pressed</span>
<span class="crossed-out-text">return action</span>
<span class="new-text">def ev_keydown(self, event: tcod.event.KeyDown) -&gt; None:
if event.sym == tcod.event.K_ESCAPE:
raise SystemExit()</span></pre>
</div>
</div>
<p>Now that weve got our event handlers updated, lets actually put the <code>Impossible</code> exception to good use. We can start by editing <code>actions.py</code> to make use of it when the player tries to move into an invalid area:</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><span style="color:#a6e22e">+import exceptions
</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 MeleeAction(ActionWithDirection):
</span></span><span style="display:flex;"><span> def perform(self) -&gt; None:
</span></span><span style="display:flex;"><span> target = self.target_actor
</span></span><span style="display:flex;"><span> if not target:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- return # No entity to attack.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ raise exceptions.Impossible("Nothing to attack.")
</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 MovementAction(ActionWithDirection):
</span></span><span style="display:flex;"><span> def perform(self) -&gt; None:
</span></span><span style="display:flex;"><span> dest_x, dest_y = self.dest_xy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if not self.engine.game_map.in_bounds(dest_x, dest_y):
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Destination is out of bounds.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("That way is blocked.")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- return # Destination is out of bounds.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Destination is blocked by a tile.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("That way is blocked.")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- return # Destination is blocked by a tile.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Destination is blocked by an entity.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("That way is blocked.")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- return # Destination is blocked by an entity.
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
import color
<span class="new-text">import exceptions</span>
if TYPE_CHECKING:
...
class MeleeAction(ActionWithDirection):
def perform(self) -&gt; None:
target = self.target_actor
if not target:
<span class="crossed-out-text">return # No entity to attack.</span>
<span class="new-text">raise exceptions.Impossible("Nothing to attack.")</span>
...
class MovementAction(ActionWithDirection):
def perform(self) -&gt; None:
dest_x, dest_y = self.dest_xy
if not self.engine.game_map.in_bounds(dest_x, dest_y):
<span class="new-text"># Destination is out of bounds.
raise exceptions.Impossible("That way is blocked.")</span>
<span class="crossed-out-text">return # Destination is out of bounds.</span>
if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:
<span class="new-text"># Destination is blocked by a tile.
raise exceptions.Impossible("That way is blocked.")</span>
<span class="crossed-out-text">return # Destination is blocked by a tile.</span>
if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
<span class="new-text"># Destination is blocked by an entity.
raise exceptions.Impossible("That way is blocked.")</span>
<span class="crossed-out-text">return # Destination is blocked by an entity.</span></pre>
</div>
</div>
<p>Now, if you try moving into a wall, youll get a message in the log, and the players turn wont be wasted.</p>
<p>So what about when the enemies try doing something impossible? You
might want to know when that happens for debugging purposes, but during
normal execution of our game, we can simply ignore it, and have the
enemy skip their turn. To do this, modify <code>engine.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>...
</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><span style="color:#a6e22e">+import exceptions
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from input_handlers import MainGameEventHandler
</span></span><span style="display:flex;"><span>from message_log import MessageLog
</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 handle_enemy_turns(self) -&gt; None:
</span></span><span style="display:flex;"><span> for entity in set(self.game_map.actors) - {self.player}:
</span></span><span style="display:flex;"><span> if entity.ai:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ try:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity.ai.perform()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except exceptions.Impossible:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ pass # Ignore impossible action exceptions from AI.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- entity.ai.perform()
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
from tcod.map import compute_fov
<span class="new-text">import exceptions</span>
from input_handlers import MainGameEventHandler
from message_log import MessageLog
...
def handle_enemy_turns(self) -&gt; None:
for entity in set(self.game_map.actors) - {self.player}:
if entity.ai:
<span class="new-text">try:
entity.ai.perform()
except exceptions.Impossible:
pass # Ignore impossible action exceptions from AI.</span>
<span class="crossed-out-text">entity.ai.perform()</span></pre>
</div>
</div>
<p>This is great and all, but wasnt this chapter supposed to be about
implementing items? And, yes, thats true, and were going to transition
to that now, but itll be helpful to have a way to stop the player from
wasting a turn in just a moment.</p>
<p>The way well implement our health potions will be similar to how we
implemented enemies: Well create a component that holds the
functionality we want, and well create a subclass of <code>Entity</code> that holds the relevant component. From the <code>Consumable</code>
component, we can create subclasses that implement the specific
functionality we want for each item. In this case, itll be a health
potion, but in the next chapter, well be implementing other types of
consumables, so well want to stay flexible.</p>
<p>In the <code>components</code> directory, create a file called <code>consumable.py</code> and fill it with the following contents:</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:#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">from</span> typing <span style="color:#f92672">import</span> Optional, TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> actions
</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> components.base_component <span style="color:#f92672">import</span> BaseComponent
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> exceptions <span style="color:#f92672">import</span> Impossible
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> TYPE_CHECKING:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">from</span> entity <span style="color:#f92672">import</span> Actor, Item
</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">Consumable</span>(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:#66d9ef">def</span> <span style="color:#a6e22e">get_action</span>(self, consumer: Actor) <span style="color:#f92672">-&gt;</span> Optional[actions<span style="color:#f92672">.</span>Action]:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Try to return the action for this item."""</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> actions<span style="color:#f92672">.</span>ItemAction(consumer, self<span style="color:#f92672">.</span>parent)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">activate</span>(self, action: actions<span style="color:#f92672">.</span>ItemAction) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Invoke this items ability.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> `action` is the context for this activation.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">NotImplementedError</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">class</span> <span style="color:#a6e22e">HealingConsumable</span>(Consumable):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, amount: int):
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>amount <span style="color:#f92672">=</span> amount
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">activate</span>(self, action: actions<span style="color:#f92672">.</span>ItemAction) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> consumer <span style="color:#f92672">=</span> action<span style="color:#f92672">.</span>entity
</span></span><span style="display:flex;"><span> amount_recovered <span style="color:#f92672">=</span> consumer<span style="color:#f92672">.</span>fighter<span style="color:#f92672">.</span>heal(self<span style="color:#f92672">.</span>amount)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> amount_recovered <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</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">f</span><span style="color:#e6db74">"You consume the </span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">, and recover </span><span style="color:#e6db74">{</span>amount_recovered<span style="color:#e6db74">}</span><span style="color:#e6db74"> HP!"</span>,
</span></span><span style="display:flex;"><span> color<span style="color:#f92672">.</span>health_recovered,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> Impossible(<span style="color:#e6db74">f</span><span style="color:#e6db74">"Your health is already full."</span>)
</span></span></code></pre></div><p>The <code>Consumable</code> class knows its parent, and it defines two methods: <code>get_action</code> and <code>activate</code>.</p>
<p><code>get_action</code> gets <code>ItemAction</code>, which we havent defined just yet (we will soon). Subclasses can override this to provide more information to <code>ItemAction</code> if needed, such as the position of a potential target (this will be useful when we have ranged targeting).</p>
<p><code>activate</code> is just an abstract method, its up to the
subclasses to define their own implementation. The subclasses should
call this method when theyre trying to actually cause the effect that
theyve defined for themselves (healing for healing potions, damage for
lightning scrolls, etc.).</p>
<p><code>HealingConsumable</code> is initialized with an <code>amount</code>, which is how much the user will be healed when using the item. The <code>activate</code> function calls <code>fighter.heal</code>,
and logs a message to the message log, if the entity recovered health.
If not (because the user had full health already), we return that <code>Impossible</code>
exception we defined earlier. This will give us a message in the log
that the players health is already full, and it wont waste the health
potion.</p>
<p>So what does this component get attached to? In order to create our health potions, we can create another subclass of <code>Entity</code>, which will represent non-actor items. Open up <code>entity.py</code> and add the following class:</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 render_order import RenderOrder
</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 components.ai import BaseAI
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.consumable import Consumable
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> from components.fighter import Fighter
</span></span><span style="display:flex;"><span> from game_map import GameMap
</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> @property
</span></span><span style="display:flex;"><span> def is_alive(self) -&gt; bool:
</span></span><span style="display:flex;"><span> """Returns True as long as this actor can perform actions."""
</span></span><span style="display:flex;"><span> return bool(self.ai)
</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 Item(Entity):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ 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">+ x: int = 0,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y: int = 0,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char: str = "?",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color: Tuple[int, int, int] = (255, 255, 255),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name: str = "&lt;Unnamed&gt;",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ consumable: Consumable,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().__init__(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=y,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char=char,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color=color,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name=name,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ blocks_movement=False,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ render_order=RenderOrder.ITEM,
</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">+ self.consumable = consumable
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.consumable.parent = self
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from render_order import RenderOrder
if TYPE_CHECKING:
from components.ai import BaseAI
<span class="new-text">from components.consumable import Consumable</span>
from components.fighter import Fighter
from game_map import GameMap
...
...
@property
def is_alive(self) -&gt; bool:
"""Returns True as long as this actor can perform actions."""
return bool(self.ai)
<span class="new-text">class Item(Entity):
def __init__(
self,
*,
x: int = 0,
y: int = 0,
char: str = "?",
color: Tuple[int, int, int] = (255, 255, 255),
name: str = "&lt;Unnamed&gt;",
consumable: Consumable,
):
super().__init__(
x=x,
y=y,
char=char,
color=color,
name=name,
blocks_movement=False,
render_order=RenderOrder.ITEM,
)
self.consumable = consumable
self.consumable.parent = self</span></pre>
</div>
</div>
<p><code>Item</code> isnt too different from <code>Actor</code>, except instead of implementing <code>fighter</code> and <code>ai</code>, it does <code>consumable</code>. When we create an item, well assign the <code>consumable</code>, which will determine what actually happens when the item gets used.</p>
<p>The next thing we need to implement that we used in the <code>Consumable</code> class is the <code>ItemAction</code> class. Open up <code>actions.py</code> and put 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>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span> from engine import Engine
</span></span><span style="display:flex;"><span><span style="color:#f92672">- from entity import Actor, Entity
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ from entity import Actor, Entity, Item
</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 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><span style="color:#a6e22e">+class ItemAction(Action):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self, entity: Actor, item: Item, target_xy: Optional[Tuple[int, int]] = None
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().__init__(entity)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.item = item
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not target_xy:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ target_xy = entity.x, entity.y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.target_xy = target_xy
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ @property
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def target_actor(self) -&gt; Optional[Actor]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return the actor at this actions destination."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.engine.game_map.get_actor_at_location(*self.target_xy)
</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 perform(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Invoke the items ability, this action will be given to provide context."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.item.consumable.activate(self)
</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 EscapeAction(Action):
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>if TYPE_CHECKING:
from engine import Engine
<span class="crossed-out-text">from entity import Actor, Entity</span>
<span class="new-text">from entity import Actor, Entity, Item</span>
...
class Action:
...
<span class="new-text">class ItemAction(Action):
def __init__(
self, entity: Actor, item: Item, target_xy: Optional[Tuple[int, int]] = None
):
super().__init__(entity)
self.item = item
if not target_xy:
target_xy = entity.x, entity.y
self.target_xy = target_xy
@property
def target_actor(self) -&gt; Optional[Actor]:
"""Return the actor at this actions destination."""
return self.engine.game_map.get_actor_at_location(*self.target_xy)
def perform(self) -&gt; None:
"""Invoke the items ability, this action will be given to provide context."""
self.item.consumable.activate(self)</span>
class EscapeAction(Action):
...</pre>
</div>
</div>
<p><code>ItemAction</code> takes several arguments in its <code>__init__</code> function: <code>entity</code>, which is the entity using the item, <code>item</code>, which is the item itself, and <code>target_xy</code>,
which is the x and y coordinates of the “target” of the item, if there
is one. We wont actually use this in this chapter, but itll come in
handy soon.</p>
<p><code>target_actor</code> gets the actor at the target location.
Again, we wont actually use it this chapter, since health potions dont
“target” anything.</p>
<p><code>perform</code> activates the consumable, with its <code>activate</code> method we defined earlier.</p>
<p>To utilize our new <code>Item</code>, lets add the health potion to <code>entity_factories.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 components.ai import HostileEnemy
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from components.consumable import HealingConsumable
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from components.fighter import Fighter
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from entity import Actor
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from entity import Actor, Item
</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>player = Actor(
</span></span><span style="display:flex;"><span> char="@",
</span></span><span style="display:flex;"><span> color=(255, 255, 255),
</span></span><span style="display:flex;"><span> name="Player",
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=30, defense=2, power=5),
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>orc = Actor(
</span></span><span style="display:flex;"><span> char="o",
</span></span><span style="display:flex;"><span> color=(63, 127, 63),
</span></span><span style="display:flex;"><span> name="Orc",
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=10, defense=0, power=3),
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>troll = Actor(
</span></span><span style="display:flex;"><span> char="T",
</span></span><span style="display:flex;"><span> color=(0, 127, 0),
</span></span><span style="display:flex;"><span> name="Troll",
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=16, defense=1, power=4),
</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">+health_potion = Item(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char="!",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color=(127, 0, 255),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name="Health Potion",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ consumable=HealingConsumable(amount=4),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from components.ai import HostileEnemy
<span class="new-text">from components.consumable import HealingConsumable</span>
from components.fighter import Fighter
<span class="crossed-out-text">from entity import Actor</span>
<span class="new-text">from entity import Actor, Item</span>
player = Actor(
char="@",
color=(255, 255, 255),
name="Player",
ai_cls=HostileEnemy,
fighter=Fighter(hp=30, defense=2, power=5),
)
orc = Actor(
char="o",
color=(63, 127, 63),
name="Orc",
ai_cls=HostileEnemy,
fighter=Fighter(hp=10, defense=0, power=3),
)
troll = Actor(
char="T",
color=(0, 127, 0),
name="Troll",
ai_cls=HostileEnemy,
fighter=Fighter(hp=16, defense=1, power=4),
)
<span class="new-text">health_potion = Item(
char="!",
color=(127, 0, 255),
name="Health Potion",
consumable=HealingConsumable(amount=4),
)</span></pre>
</div>
</div>
<p>Were defining a new entity type, called <code>health_potion</code> (no surprises there), and utilizing the <code>Item</code> and <code>HealingConsumable</code>
classes we just wrote. The health potion will recover 4 HP of the
users health. Feel free to adjust that value however you see fit.</p>
<p>Alright, were now ready to put some health potions in the dungeon. As you may have already guessed, well need to adjust the <code>generate_dungeon</code> and <code>place_entities</code> functions in <code>procgen.py</code> to actually put the potions in. Edit <code>procgen.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>def place_entities(
</span></span><span style="display:flex;"><span><span style="color:#f92672">- room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ room: RectangularRoom, dungeon: GameMap, maximum_monsters: int, maximum_items: int
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>) -&gt; None:
</span></span><span style="display:flex;"><span> number_of_monsters = random.randint(0, maximum_monsters)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ number_of_items = random.randint(0, maximum_items)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> for i in range(number_of_monsters):
</span></span><span style="display:flex;"><span> x = random.randint(room.x1 + 1, room.x2 - 1)
</span></span><span style="display:flex;"><span> y = random.randint(room.y1 + 1, room.y2 - 1)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
</span></span><span style="display:flex;"><span> if random.random() &lt; 0.8:
</span></span><span style="display:flex;"><span> entity_factories.orc.spawn(dungeon, x, y)
</span></span><span style="display:flex;"><span> else:
</span></span><span style="display:flex;"><span> entity_factories.troll.spawn(dungeon, x, y)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for i in range(number_of_items):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = random.randint(room.x1 + 1, room.x2 - 1)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y = random.randint(room.y1 + 1, room.y2 - 1)
</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 not any(entity.x == x and entity.y == y for entity in dungeon.entities):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity_factories.health_potion.spawn(dungeon, x, y)
</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 tunnel_between(
</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 generate_dungeon(
</span></span><span style="display:flex;"><span> map_width: int,
</span></span><span style="display:flex;"><span> map_height: int,
</span></span><span style="display:flex;"><span> max_monsters_per_room: int,
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_items_per_room: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> engine: Engine,
</span></span><span style="display:flex;"><span>) -&gt; GameMap:
</span></span><span style="display:flex;"><span> """Generate a new dungeon map."""
</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:#f92672">- place_entities(new_room, dungeon, max_monsters_per_room)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ place_entities(new_room, dungeon, max_monsters_per_room, max_items_per_room)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>def place_entities(
<span class="crossed-out-text">room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,</span>
<span class="new-text">room: RectangularRoom, dungeon: GameMap, maximum_monsters: int, maximum_items: int</span>
) -&gt; None:
number_of_monsters = random.randint(0, maximum_monsters)
<span class="new-text">number_of_items = random.randint(0, maximum_items)</span>
for i in range(number_of_monsters):
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
if random.random() &lt; 0.8:
entity_factories.orc.spawn(dungeon, x, y)
else:
entity_factories.troll.spawn(dungeon, x, y)
<span class="new-text">for i in range(number_of_items):
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
entity_factories.health_potion.spawn(dungeon, x, y)</span>
def tunnel_between(
...
def generate_dungeon(
map_width: int,
map_height: int,
max_monsters_per_room: int,
<span class="new-text">max_items_per_room: int,</span>
engine: Engine,
) -&gt; GameMap:
"""Generate a new dungeon map."""
...
...
<span class="crossed-out-text">place_entities(new_room, dungeon, max_monsters_per_room)</span>
<span class="new-text">place_entities(new_room, dungeon, max_monsters_per_room, max_items_per_room)</span></pre>
</div>
</div>
<p>Were doing essentially the same thing we did to create our enemies:
Giving a maximum possible number for the number of items in each room,
selecting a random number between that and 0, and spawning the items in a
random spot in the room, assuming nothing else already exists there.</p>
<p>Lastly, to make the health potions appear, we need to update our call in <code>main.py</code> to <code>generate_dungeon</code>, since weve added the <code>max_items_per_room</code> argument. Open up <code>main.py</code> and add the following lines:</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> max_monsters_per_room = 2
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_items_per_room = 2
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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> player = copy.deepcopy(entity_factories.player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine = Engine(player=player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine.game_map = generate_dungeon(
</span></span><span style="display:flex;"><span> max_rooms=max_rooms,
</span></span><span style="display:flex;"><span> room_min_size=room_min_size,
</span></span><span style="display:flex;"><span> room_max_size=room_max_size,
</span></span><span style="display:flex;"><span> map_width=map_width,
</span></span><span style="display:flex;"><span> map_height=map_height,
</span></span><span style="display:flex;"><span> max_monsters_per_room=max_monsters_per_room,
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_items_per_room=max_items_per_room,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> engine=engine,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
max_monsters_per_room = 2
<span class="new-text">max_items_per_room = 2</span>
tileset = tcod.tileset.load_tilesheet(
"dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
)
player = copy.deepcopy(entity_factories.player)
engine = Engine(player=player)
engine.game_map = generate_dungeon(
max_rooms=max_rooms,
room_min_size=room_min_size,
room_max_size=room_max_size,
map_width=map_width,
map_height=map_height,
max_monsters_per_room=max_monsters_per_room,
<span class="new-text">max_items_per_room=max_items_per_room,</span>
engine=engine,
)
...</pre>
</div>
</div>
<p>Run the project now, and you should see a few health potions laying around. Success! Well, not really…</p>
<p><img src="Part%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/part-8-health-potions.png" alt="Part 8 - Health Potions"></p>
<p>Those potions dont do our rogue any good right now, because we cant
pick them up! We need to add the items to an inventory before we can
start chugging them.</p>
<p>To implement the inventory, we can create a new component, called <code>Inventory</code>. Create a new file in the <code>components</code> directory, called <code>inventory.py</code>, and add this class:</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:#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">from</span> typing <span style="color:#f92672">import</span> List, TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> components.base_component <span style="color:#f92672">import</span> BaseComponent
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> TYPE_CHECKING:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">from</span> entity <span style="color:#f92672">import</span> Actor, Item
</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">Inventory</span>(BaseComponent):
</span></span><span style="display:flex;"><span> parent: Actor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, capacity: int):
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>capacity <span style="color:#f92672">=</span> capacity
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>items: List[Item] <span style="color:#f92672">=</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">drop</span>(self, item: Item) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> Removes an item from the inventory and restores it to the game map, at the player's current location.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>items<span style="color:#f92672">.</span>remove(item)
</span></span><span style="display:flex;"><span> item<span style="color:#f92672">.</span>place(self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>x, self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>y, self<span style="color:#f92672">.</span>gamemap)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(<span style="color:#e6db74">f</span><span style="color:#e6db74">"You dropped the </span><span style="color:#e6db74">{</span>item<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">."</span>)
</span></span></code></pre></div><p>The <code>Inventory</code> class belongs to an <code>Actor</code>, and its initialized with a <code>capacity</code>, which is the maximum number of items that can be held, and the <code>items</code> list, which will actually hold the items. The <code>drop</code>
method, as the name implies, will be called when the player decides to
drop something out of the inventory, back onto the ground.</p>
<p>Lets add this new component to our <code>Actor</code> class. Open up <code>entity.py</code> and modify <code>Actor</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>...
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span> from components.ai import BaseAI
</span></span><span style="display:flex;"><span> from components.consumable import Consumable
</span></span><span style="display:flex;"><span> from components.fighter import Fighter
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.inventory import Inventory
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> from game_map import GameMap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>class Actor(Entity):
</span></span><span style="display:flex;"><span> def __init__(
</span></span><span style="display:flex;"><span> self,
</span></span><span style="display:flex;"><span> *,
</span></span><span style="display:flex;"><span> x: int = 0,
</span></span><span style="display:flex;"><span> y: int = 0,
</span></span><span style="display:flex;"><span> char: str = "?",
</span></span><span style="display:flex;"><span> color: Tuple[int, int, int] = (255, 255, 255),
</span></span><span style="display:flex;"><span> name: str = "&lt;Unnamed&gt;",
</span></span><span style="display:flex;"><span> ai_cls: Type[BaseAI],
</span></span><span style="display:flex;"><span> fighter: Fighter,
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory: Inventory,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> ):
</span></span><span style="display:flex;"><span> super().__init__(
</span></span><span style="display:flex;"><span> x=x,
</span></span><span style="display:flex;"><span> y=y,
</span></span><span style="display:flex;"><span> char=char,
</span></span><span style="display:flex;"><span> color=color,
</span></span><span style="display:flex;"><span> name=name,
</span></span><span style="display:flex;"><span> blocks_movement=True,
</span></span><span style="display:flex;"><span> render_order=RenderOrder.ACTOR,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self.ai: Optional[BaseAI] = ai_cls(self)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self.fighter = fighter
</span></span><span style="display:flex;"><span> self.fighter.parent = self
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.inventory = inventory
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.inventory.parent = self
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
if TYPE_CHECKING:
from components.ai import BaseAI
from components.consumable import Consumable
from components.fighter import Fighter
<span class="new-text">from components.inventory import Inventory</span>
from game_map import GameMap
...
class Actor(Entity):
def __init__(
self,
*,
x: int = 0,
y: int = 0,
char: str = "?",
color: Tuple[int, int, int] = (255, 255, 255),
name: str = "&lt;Unnamed&gt;",
ai_cls: Type[BaseAI],
fighter: Fighter,
<span class="new-text">inventory: Inventory,</span>
):
super().__init__(
x=x,
y=y,
char=char,
color=color,
name=name,
blocks_movement=True,
render_order=RenderOrder.ACTOR,
)
self.ai: Optional[BaseAI] = ai_cls(self)
self.fighter = fighter
self.fighter.parent = self
<span class="new-text">self.inventory = inventory
self.inventory.parent = self</span></pre>
</div>
</div>
<p>Now, each actor will have their own inventory. Our tutorial wont
implement monster inventories (they wont pick up, hold, or use items),
but hopefully this setup gives you a good starting place to implement it
yourself, if you so choose.</p>
<p>Well need to update <code>entity_factories.py</code> to take the new component into account:</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 components.ai import HostileEnemy
</span></span><span style="display:flex;"><span>from components.consumable import HealingConsumable
</span></span><span style="display:flex;"><span>from components.fighter import Fighter
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from components.inventory import Inventory
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from entity import Actor, Item
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>player = Actor(
</span></span><span style="display:flex;"><span> char="@",
</span></span><span style="display:flex;"><span> color=(255, 255, 255),
</span></span><span style="display:flex;"><span> name="Player",
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=30, defense=2, power=5),
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory=Inventory(capacity=26),
</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>orc = Actor(
</span></span><span style="display:flex;"><span> char="o",
</span></span><span style="display:flex;"><span> color=(63, 127, 63),
</span></span><span style="display:flex;"><span> name="Orc",
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=10, defense=0, power=3),
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory=Inventory(capacity=0),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>)
</span></span><span style="display:flex;"><span>troll = Actor(
</span></span><span style="display:flex;"><span> char="T",
</span></span><span style="display:flex;"><span> color=(0, 127, 0),
</span></span><span style="display:flex;"><span> name="Troll",
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=16, defense=1, power=4),
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory=Inventory(capacity=0),
</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>health_potion = Item(
</span></span><span style="display:flex;"><span> char="!",
</span></span><span style="display:flex;"><span> color=(127, 0, 255),
</span></span><span style="display:flex;"><span> name="Health Potion",
</span></span><span style="display:flex;"><span> consumable=HealingConsumable(amount=4),
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from components.ai import HostileEnemy
from components.consumable import HealingConsumable
from components.fighter import Fighter
<span class="new-text">from components.inventory import Inventory</span>
from entity import Actor, Item
player = Actor(
char="@",
color=(255, 255, 255),
name="Player",
ai_cls=HostileEnemy,
fighter=Fighter(hp=30, defense=2, power=5),
<span class="new-text">inventory=Inventory(capacity=26),</span>
)
orc = Actor(
char="o",
color=(63, 127, 63),
name="Orc",
ai_cls=HostileEnemy,
fighter=Fighter(hp=10, defense=0, power=3),
<span class="new-text">inventory=Inventory(capacity=0),</span>
)
troll = Actor(
char="T",
color=(0, 127, 0),
name="Troll",
ai_cls=HostileEnemy,
fighter=Fighter(hp=16, defense=1, power=4),
<span class="new-text">inventory=Inventory(capacity=0),</span>
)
health_potion = Item(
char="!",
color=(127, 0, 255),
name="Health Potion",
consumable=HealingConsumable(amount=4),
)</pre>
</div>
</div>
<p>Were setting the players inventory to 26, because when we implement
the menu system, each letter in the (English) alphabet will correspond
to one item slot. You can expand the inventory if you want, though
youll need to come up with an alternative menu system to accommodate
having more choices.</p>
<p>In order to actually pick up an item of the floor, well require the
rogue to move onto the same tile and press a key. First, well want an
easy way to grab all the items that currently exist in the map. Open up <code>game_map.py</code> and add 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>import numpy as np # type: ignore
</span></span><span style="display:flex;"><span>from tcod.console import Console
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from entity import Actor
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from entity import Actor, Item
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>import tile_types
</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> @property
</span></span><span style="display:flex;"><span> def actors(self) -&gt; Iterator[Actor]:
</span></span><span style="display:flex;"><span> """Iterate over this maps living actors."""
</span></span><span style="display:flex;"><span> yield from (
</span></span><span style="display:flex;"><span> entity
</span></span><span style="display:flex;"><span> for entity in self.entities
</span></span><span style="display:flex;"><span> if isinstance(entity, Actor) and entity.is_alive
</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">+ @property
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def items(self) -&gt; Iterator[Item]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ yield from (entity for entity in self.entities if isinstance(entity, Item))
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def get_blocking_entity_at_location(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
import numpy as np # type: ignore
from tcod.console import Console
<span class="crossed-out-text">from entity import Actor</span>
<span class="new-text">from entity import Actor, Item</span>
import tile_types
...
...
@property
def actors(self) -&gt; Iterator[Actor]:
"""Iterate over this maps living actors."""
yield from (
entity
for entity in self.entities
if isinstance(entity, Actor) and entity.is_alive
)
<span class="new-text">@property
def items(self) -&gt; Iterator[Item]:
yield from (entity for entity in self.entities if isinstance(entity, Item))</span>
def get_blocking_entity_at_location(
...</pre>
</div>
</div>
<p>We can use this new property in an action to find the item(s) on the same tile as the player. Lets define a <code>PickupAction</code>, which will handle picking up the item and adding it to the inventory.</p>
<p>Open up <code>actions.py</code> and define <code>PickupAction</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>class 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><span style="color:#a6e22e">+class PickupAction(Action):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Pickup an item and add it to the inventory, if there is room for it."""
</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, entity: Actor):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().__init__(entity)
</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 perform(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ actor_location_x = self.entity.x
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ actor_location_y = self.entity.y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory = self.entity.inventory
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for item in self.engine.game_map.items:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if actor_location_x == item.x and actor_location_y == item.y:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if len(inventory.items) &gt;= inventory.capacity:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("Your inventory is full.")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.game_map.entities.remove(item)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ item.parent = self.entity.inventory
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory.items.append(item)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.add_message(f"You picked up the {item.name}!")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("There is nothing here to pick up.")
</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 ItemAction(Action):
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class Action:
...
<span class="new-text">class PickupAction(Action):
"""Pickup an item and add it to the inventory, if there is room for it."""
def __init__(self, entity: Actor):
super().__init__(entity)
def perform(self) -&gt; None:
actor_location_x = self.entity.x
actor_location_y = self.entity.y
inventory = self.entity.inventory
for item in self.engine.game_map.items:
if actor_location_x == item.x and actor_location_y == item.y:
if len(inventory.items) &gt;= inventory.capacity:
raise exceptions.Impossible("Your inventory is full.")
self.engine.game_map.entities.remove(item)
item.parent = self.entity.inventory
inventory.items.append(item)
self.engine.message_log.add_message(f"You picked up the {item.name}!")
return
raise exceptions.Impossible("There is nothing here to pick up.")</span>
class ItemAction(Action):
...</pre>
</div>
</div>
<p>The action gets the entitys location, and tries to find an item that exists in the same location, iterating through <code>self.engine.game_map.items</code> (which we just defined). If an item is found, we try to add it to the inventory, checking the capacity first, and returning <code>Impossible</code>
if its full. When adding an item to the inventory, we remove it from
the game map and store it in the inventory, and print out a message. We
then return, since only one item can be picked up per turn (itll be
possible later for multiple items to be on the same spot).</p>
<p>If no item is found in the location, we just return <code>Impossible</code>, informing the player that theres nothing there.</p>
<p>Lets add our new action to the event handler. Open up <code>input_handlers.py</code> and edit the key checking section of <code>MainGameEventHandler</code> to add the key for picking up items:</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 actions import (
</span></span><span style="display:flex;"><span> Action,
</span></span><span style="display:flex;"><span> BumpAction,
</span></span><span style="display:flex;"><span> EscapeAction,
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ PickupAction,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> WaitAction,
</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> elif key == tcod.event.K_v:
</span></span><span style="display:flex;"><span> self.engine.event_handler = HistoryViewer(self.engine)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key == tcod.event.K_g:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = PickupAction(player)
</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></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from actions import (
Action,
BumpAction,
EscapeAction,
<span class="new-text">PickupAction,</span>
WaitAction,
)
...
...
elif key == tcod.event.K_v:
self.engine.event_handler = HistoryViewer(self.engine)
<span class="new-text">elif key == tcod.event.K_g:
action = PickupAction(player)</span>
# No valid key was pressed
return action</pre>
</div>
</div>
<p>Simple enough, if the player presses the “g” key (“g” for “get”), we call the <code>PickupAction</code>. Run the project now, and pick up those potions!</p>
<p>Now that the player can pick up items, well need to create our
inventory menu, where the player can see what items are in the
inventory, and select which one to use. This will require a few steps.</p>
<p>First, we need a way to get input from the user. When the user opens
the inventory menu, we need to get the input from the user, and if it
was valid, we return to the main games event handler, so the enemies
can take their turns.</p>
<p>To start, lets create a new event handler, which will return to the <code>MainGameEventHandler</code> when it handles an action successfully. Open <code>input_handlers.py</code> and add the following class:</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 EventHandler(tcod.event.EventDispatch[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><span style="color:#a6e22e">+class AskUserEventHandler(EventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handles user input for actions which require special input."""
</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 handle_action(self, action: Optional[Action]) -&gt; bool:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return to the main event handler when a valid action was performed."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if super().handle_action(action):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = MainGameEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return True
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return False
</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[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """By default any key exits this input handler."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if event.sym in { # Ignore modifier keys.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_LSHIFT,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_RSHIFT,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_LCTRL,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_RCTRL,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_LALT,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_RALT,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ }:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return None
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.on_exit()
</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_mousebuttondown(self, event: tcod.event.MouseButtonDown) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """By default any mouse click exits this input handler."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.on_exit()
</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_exit(self) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Called when the user is trying to exit or cancel an action.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ By default this returns to the main event 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">+ self.engine.event_handler = 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 EventHandler(tcod.event.EventDispatch[Action]):
...
<span class="new-text">class AskUserEventHandler(EventHandler):
"""Handles user input for actions which require special input."""
def handle_action(self, action: Optional[Action]) -&gt; bool:
"""Return to the main event handler when a valid action was performed."""
if super().handle_action(action):
self.engine.event_handler = MainGameEventHandler(self.engine)
return True
return False
def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
"""By default any key exits this input handler."""
if event.sym in { # Ignore modifier keys.
tcod.event.K_LSHIFT,
tcod.event.K_RSHIFT,
tcod.event.K_LCTRL,
tcod.event.K_RCTRL,
tcod.event.K_LALT,
tcod.event.K_RALT,
}:
return None
return self.on_exit()
def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -&gt; Optional[Action]:
"""By default any mouse click exits this input handler."""
return self.on_exit()
def on_exit(self) -&gt; Optional[Action]:
"""Called when the user is trying to exit or cancel an action.
By default this returns to the main event handler.
"""
self.engine.event_handler = MainGameEventHandler(self.engine)
return None</span></pre>
</div>
</div>
<p><code>AskUserEventHandler</code>, by default, just exits itself when
any key is pressed, besides one of the “modifier” keys (shift, control,
and alt). It also exits when clicking the mouse.</p>
<p>Whats the point of this class? By itself, nothing really. But we can
create subclasses of it that actually do something useful, which is
what well do now. Lets keep editing <code>input_handlers.py</code> and add this class:</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>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span> from engine import Engine
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from entity import Item
</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 AskUserEventHandler(EventHandler):
</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 InventoryEventHandler(AskUserEventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """This handler lets the user select an item.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ What happens then depends on the subclass.
</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">+ TITLE = "&lt;missing title&gt;"
</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 an inventory menu, which displays the items in the inventory, and the letter to select them.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Will move to a different position based on where the player is located, so the player can always see where
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ they are.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().on_render(console)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ number_of_items_in_inventory = len(self.engine.player.inventory.items)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ height = number_of_items_in_inventory + 2
</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 height &lt;= 3:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ height = 3
</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 self.engine.player.x &lt;= 30:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = 40
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = 0
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y = 0
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ width = len(self.TITLE) + 4
</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.draw_frame(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=y,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ width=width,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ height=height,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ title=self.TITLE,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clear=True,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fg=(255, 255, 255),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ bg=(0, 0, 0),
</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">+ if number_of_items_in_inventory &gt; 0:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for i, item in enumerate(self.engine.player.inventory.items):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ item_key = chr(ord("a") + i)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x + 1, y + i + 1, f"({item_key}) {item.name}")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x + 1, y + 1, "(Empty)")
</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[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player = self.engine.player
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ key = event.sym
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ index = key - tcod.event.K_a
</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 0 &lt;= index &lt;= 26:
</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">+ selected_item = player.inventory.items[index]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except IndexError:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.add_message("Invalid entry.", color.invalid)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return None
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.on_item_selected(selected_item)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return super().ev_keydown(event)
</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_item_selected(self, item: Item) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Called when the user selects a valid item."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise NotImplementedError()
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>if TYPE_CHECKING:
from engine import Engine
<span class="new-text">from entity import Item</span>
...
class AskUserEventHandler(EventHandler):
...
<span class="new-text">class InventoryEventHandler(AskUserEventHandler):
"""This handler lets the user select an item.
What happens then depends on the subclass.
"""
TITLE = "&lt;missing title&gt;"
def on_render(self, console: tcod.Console) -&gt; None:
"""Render an inventory menu, which displays the items in the inventory, and the letter to select them.
Will move to a different position based on where the player is located, so the player can always see where
they are.
"""
super().on_render(console)
number_of_items_in_inventory = len(self.engine.player.inventory.items)
height = number_of_items_in_inventory + 2
if height &lt;= 3:
height = 3
if self.engine.player.x &lt;= 30:
x = 40
else:
x = 0
y = 0
width = len(self.TITLE) + 4
console.draw_frame(
x=x,
y=y,
width=width,
height=height,
title=self.TITLE,
clear=True,
fg=(255, 255, 255),
bg=(0, 0, 0),
)
if number_of_items_in_inventory &gt; 0:
for i, item in enumerate(self.engine.player.inventory.items):
item_key = chr(ord("a") + i)
console.print(x + 1, y + i + 1, f"({item_key}) {item.name}")
else:
console.print(x + 1, y + 1, "(Empty)")
def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
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)
def on_item_selected(self, item: Item) -&gt; Optional[Action]:
"""Called when the user selects a valid item."""
raise NotImplementedError()</span></pre>
</div>
</div>
<p><code>InventoryEventHandler</code> subclasses <code>AskUserEventHandler</code>, and renders the items within the players <code>Inventory</code>.
Depending on where the player is standing, the menu will render off to
the side, so the menu wont cover the player. If theres nothing in the
inventory, it just prints “Empty”. Notice that it doesnt give itself a
title, as that will be defined in a different subclass (more on that in a
bit).</p>
<p>The <code>ev_keydown</code> function takes the users input, from
letters a - z, and associates that with an index in the inventory. If
the player pressed “b”, for example, the second item in the inventory
will be selected and returned. If the player presses a key like “c”
(item 3) but only has one item, then the message “Invalid entry” will
display. If any other key is pressed, the menu will close.</p>
<p>This class, still, does not actually do anything for us right now,
but I promise were close. Before we implement the menus to use and drop
items, well need to define the <code>Action</code> that drops items. Add the following class to <code>actions.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 EscapeAction(Action):
</span></span><span style="display:flex;"><span> def perform(self) -&gt; None:
</span></span><span style="display:flex;"><span> raise SystemExit()
</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 DropItem(ItemAction):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def perform(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.inventory.drop(self.item)
</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 WaitAction(Action):
</span></span><span style="display:flex;"><span> def perform(self) -&gt; None:
</span></span><span style="display:flex;"><span> pass
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class EscapeAction(Action):
def perform(self) -&gt; None:
raise SystemExit()
<span class="new-text">class DropItem(ItemAction):
def perform(self) -&gt; None:
self.entity.inventory.drop(self.item)</span>
class WaitAction(Action):
def perform(self) -&gt; None:
pass
...</pre>
</div>
</div>
<p><code>DropItem</code> will be used to drop something from the inventory. It just calls the <code>drop</code> method of the <code>Inventory</code> component.</p>
<p>Now, lets put this new action into… well… action! Open up <code>input_handlers.py</code> once again, and lets add the handlers that will handle both selecting an item and dropping one.</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 tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import actions
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from actions import (
</span></span><span style="display:flex;"><span> Action,
</span></span><span style="display:flex;"><span> BumpAction,
</span></span><span style="display:flex;"><span> EscapeAction,
</span></span><span style="display:flex;"><span> PickupAction,
</span></span><span style="display:flex;"><span> WaitAction,
</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>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></span><span style="display:flex;"><span><span style="color:#a6e22e">+class InventoryActivateHandler(InventoryEventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle using an inventory item."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ TITLE = "Select an item to use"
</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_item_selected(self, item: Item) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return the action for the selected item."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return item.consumable.get_action(self.engine.player)
</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 InventoryDropHandler(InventoryEventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle dropping an inventory item."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ TITLE = "Select an item to drop"
</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_item_selected(self, item: Item) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Drop this item."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return actions.DropItem(self.engine.player, item)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
import tcod
<span class="new-text">import actions</span>
from actions import (
Action,
BumpAction,
EscapeAction,
PickupAction,
WaitAction,
)
...
class InventoryEventHandler(AskUserEventHandler):
...
<span class="new-text">class InventoryActivateHandler(InventoryEventHandler):
"""Handle using an inventory item."""
TITLE = "Select an item to use"
def on_item_selected(self, item: Item) -&gt; Optional[Action]:
"""Return the action for the selected item."""
return item.consumable.get_action(self.engine.player)
class InventoryDropHandler(InventoryEventHandler):
"""Handle dropping an inventory item."""
TITLE = "Select an item to drop"
def on_item_selected(self, item: Item) -&gt; Optional[Action]:
"""Drop this item."""
return actions.DropItem(self.engine.player, item)</span></pre>
</div>
</div>
<p>At long last, weve got the <code>InventoryActivateHandler</code> and the <code>InventoryDropHandler</code>, which will handle using and dropping items, respectively. They both inherit from <code>InventoryEventHandler</code>,
allowing the player to select an item in both menus using what we wrote
in that class (selecting an item with a letter), but both handlers
display a different title and call different actions, depending on the
selection.</p>
<p>All thats left now is to utilize these event handlers, based on the
key we press. Lets set the game up to open the inventory menu when
pressing “i”, and the drop menu when pressing “d”. Open <code>input_handlers.py</code>, and add the following lines to <code>MainGameEventHandler</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></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><span style="color:#a6e22e">+ elif key == tcod.event.K_i:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = InventoryActivateHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key == tcod.event.K_d:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = InventoryDropHandler(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></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
elif key == tcod.event.K_g:
action = PickupAction(player)
<span class="new-text">elif key == tcod.event.K_i:
self.engine.event_handler = InventoryActivateHandler(self.engine)
elif key == tcod.event.K_d:
self.engine.event_handler = InventoryDropHandler(self.engine)</span>
# No valid key was pressed
return action</pre>
</div>
</div>
<p>Now, when you run the project, you can, at long last, use and drop the health potions!</p>
<p><img src="Part%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/part-8-using-items.png" alt="Part 8 - Using items"></p>
<p>Theres a major bug with our implementation though: used items wont
disappear after using them. This means the player could keep consuming
the same health potion over and over!</p>
<p>Lets fix that, by opening up <code>consumable.py</code> and add 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>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from typing import Optional, TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import actions
</span></span><span style="display:flex;"><span>import color
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import components.inventory
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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></span><span style="display:flex;"><span>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span> from entity import Actor, Item
</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> def get_action(self, consumer: Actor) -&gt; Optional[actions.Action]:
</span></span><span style="display:flex;"><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> def activate(self, action: actions.ItemAction) -&gt; None:
</span></span><span style="display:flex;"><span> """Invoke this items ability.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> `action` is the context for this activation.
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span> raise NotImplementedError()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def consume(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Remove the consumed item from its containing inventory."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity = self.parent
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory = entity.parent
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if isinstance(inventory, components.inventory.Inventory):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory.items.remove(entity)
</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 HealingConsumable(Consumable):
</span></span><span style="display:flex;"><span> def __init__(self, amount: int):
</span></span><span style="display:flex;"><span> self.amount = amount
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def activate(self, action: actions.ItemAction) -&gt; None:
</span></span><span style="display:flex;"><span> consumer = action.entity
</span></span><span style="display:flex;"><span> amount_recovered = consumer.fighter.heal(self.amount)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if amount_recovered &gt; 0:
</span></span><span style="display:flex;"><span> self.engine.message_log.add_message(
</span></span><span style="display:flex;"><span> f"You consume the {self.parent.name}, and recover {amount_recovered} HP!",
</span></span><span style="display:flex;"><span> color.health_recovered,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.consume()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> else:
</span></span><span style="display:flex;"><span> raise Impossible(f"Your health is already full.")
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import Optional, TYPE_CHECKING
import actions
import color
<span class="new-text">import components.inventory</span>
from components.base_component import BaseComponent
from exceptions import Impossible
if TYPE_CHECKING:
from entity import Actor, Item
class Consumable(BaseComponent):
parent: Item
def get_action(self, consumer: Actor) -&gt; Optional[actions.Action]:
"""Try to return the action for this item."""
return actions.ItemAction(consumer, self.parent)
def activate(self, action: actions.ItemAction) -&gt; None:
"""Invoke this items ability.
`action` is the context for this activation.
"""
raise NotImplementedError()
<span class="new-text">def consume(self) -&gt; None:
"""Remove the consumed item from its containing inventory."""
entity = self.parent
inventory = entity.parent
if isinstance(inventory, components.inventory.Inventory):
inventory.items.remove(entity)</span>
class HealingConsumable(Consumable):
def __init__(self, amount: int):
self.amount = amount
def activate(self, action: actions.ItemAction) -&gt; None:
consumer = action.entity
amount_recovered = consumer.fighter.heal(self.amount)
if amount_recovered &gt; 0:
self.engine.message_log.add_message(
f"You consume the {self.parent.name}, and recover {amount_recovered} HP!",
color.health_recovered,
)
<span class="new-text">self.consume()</span>
else:
raise Impossible(f"Your health is already full.")</pre>
</div>
</div>
<p><code>consume</code> removes the item from the <code>Inventory</code> container it occupies. Since it no longer belongs to the inventory or the map, it disappears from the game. We use the <code>consume</code> method when the health potion is successfully used, and we dont if its not.</p>
<p>With that, the health potions will disappear after use.</p>
<p>Theres two last bits of housekeeping we need to do before moving on to the next part. The <code>parent</code> class attribute in the <code>Entity</code> class has a bit of a problem: its designated as a <code>GameMap</code> type right now, but when an item moves from the map to the inventory, that isnt really true any more. Lets fix that now:</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:#f92672">-from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Union
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><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>class Entity:
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span> A generic object to represent players, enemies, items, etc.
</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">- parent: GameMap
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: Union[GameMap, Inventory]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def __init__(
</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
import copy
<span class="crossed-out-text">from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING</span>
<span class="new-text">from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Union</span>
from render_order import RenderOrder
...
class Entity:
"""
A generic object to represent players, enemies, items, etc.
"""
<span class="crossed-out-text">parent: GameMap</span>
<span class="new-text">parent: Union[GameMap, Inventory]</span>
def __init__(
...</pre>
</div>
</div>
<p>Lastly, we can actually remove <code>EscapeAction</code>, as it can just be handled by the event handlers. Open <code>actions.py</code> and remove <code>EscapeAction</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 PickupAction(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><span style="color:#f92672">-class EscapeAction(Action):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- def perform(self) -&gt; None:
</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>
</span></span><span style="display:flex;"><span>class ItemAction(Action):
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class PickupAction(Action):
...
<span class="crossed-out-text">class EscapeAction(Action):</span>
<span class="crossed-out-text">def perform(self) -&gt; None:</span>
<span class="crossed-out-text">raise SystemExit()</span>
class ItemAction(Action):
...</pre>
</div>
</div>
<p>Then, remove <code>EscapeAction</code> from <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>...
</span></span><span style="display:flex;"><span>from actions import (
</span></span><span style="display:flex;"><span> Action,
</span></span><span style="display:flex;"><span> BumpAction,
</span></span><span style="display:flex;"><span><span style="color:#f92672">- EscapeAction,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> PickupAction,
</span></span><span style="display:flex;"><span> WaitAction
</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> elif key == tcod.event.K_ESCAPE:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = EscapeAction(player)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ raise SystemExit()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_v:
</span></span><span style="display:flex;"><span> self.engine.event_handler = HistoryViewer(self.engine)
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
from actions import (
Action,
BumpAction,
<span class="crossed-out-text">EscapeAction,</span>
PickupAction,
WaitAction
)
...
...
elif key == tcod.event.K_ESCAPE:
<span class="crossed-out-text">action = EscapeAction(player)</span>
<span class="new-text">raise SystemExit()</span>
elif key == tcod.event.K_v:
self.engine.event_handler = HistoryViewer(self.engine)
...</pre>
</div>
</div>
<p>This was another long chapter, but this is an important step towards a
functioning game. Next chapter, well add a few more item types to use.</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-8">click here</a>.</p>
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-9">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%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
<script src="Part%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
</body></html>