McRogueFace/roguelike_tutorial/rogueliketutorials.com/Part 6 - Doing (and taking)...

3061 lines
188 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 6 - Doing (and taking) some damage · 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="Check your TCOD installation Link to heading Before proceeding any further, youll want to upgrade to TCOD version 11.15, if you dont already have it. This version of TCOD was released during the tutorial event, so if youre following along on a weekly basis, you probably dont have this version installed!
Refactoring previous code Link to heading After parts 1-5 for this tutorial were written, we decided to change a few things around, to hopefully make the codebase a bit cleaner and easier to extend in the future.">
<meta name="keywords" content="">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Part 6 - Doing (and taking) some damage">
<meta name="twitter:description" content="Check your TCOD installation Link to heading Before proceeding any further, youll want to upgrade to TCOD version 11.15, if you dont already have it. This version of TCOD was released during the tutorial event, so if youre following along on a weekly basis, you probably dont have this version installed!
Refactoring previous code Link to heading After parts 1-5 for this tutorial were written, we decided to change a few things around, to hopefully make the codebase a bit cleaner and easier to extend in the future.">
<meta property="og:title" content="Part 6 - Doing (and taking) some damage">
<meta property="og:description" content="Check your TCOD installation Link to heading Before proceeding any further, youll want to upgrade to TCOD version 11.15, if you dont already have it. This version of TCOD was released during the tutorial event, so if youre following along on a weekly basis, you probably dont have this version installed!
Refactoring previous code Link to heading After parts 1-5 for this tutorial were written, we decided to change a few things around, to hopefully make the codebase a bit cleaner and easier to extend in the future.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-6/"><meta property="article:section" content="tutorials">
<meta property="article:published_time" content="2020-07-07T00:00:00+00:00">
<meta property="article:modified_time" content="2020-07-07T00:00:00+00:00">
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-6/">
<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%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%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%206%20-%20Doing%20(and%20taking)%20some%20damage%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-6/">
Part 6 - Doing (and taking) some damage
</a>
</h1>
</header>
<h2 id="check-your-tcod-installation">
Check your TCOD installation
<a class="heading-link" href="#check-your-tcod-installation">
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
<span class="sr-only">Link to heading</span>
</a>
</h2>
<p>Before proceeding any further, youll want to upgrade to TCOD version
11.15, if you dont already have it. This version of TCOD was released <em>during</em> the tutorial event, so if youre following along on a weekly basis, you probably <em>dont</em> have this version installed!</p>
<h2 id="refactoring-previous-code">
Refactoring previous code
<a class="heading-link" href="#refactoring-previous-code">
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
<span class="sr-only">Link to heading</span>
</a>
</h2>
<p>After parts 1-5 for this tutorial were written, we decided to change a
few things around, to hopefully make the codebase a bit cleaner and
easier to extend in the future. Unfortunately, this means that code
written in previous parts now has to be modified.</p>
<p>I would go back and edit the tutorial text and Github branches to reflect these changes, except for two things:</p>
<ol>
<li>I dont have time at the moment. Writing the sections that get published every week is taking all of my time as it is.</li>
<li>It wouldnt be fair to those who are following this tutorial on a weekly basis.</li>
</ol>
<p>Someday, when the event is over, the previous parts will be
rewritten, and all will be well. But until then, theres several changes
that need to be made before proceeding with Part 6.</p>
<p>I wont explain all of the changes (again, time is a limiting factor), but heres the basic ideas:</p>
<ul>
<li>Event handlers will have the <code>handle_events</code> method instead of <code>Engine</code>.</li>
<li>The game map will have a reference to <code>Engine</code>, and entities will have a reference to the map.</li>
<li>Actions will be initialized with the entity doing the action</li>
<li>Because of the above points, Actions will have a reference to the <code>Engine</code>, through <code>Entity</code>-&gt;<code>GameMap</code>-&gt;<code>Engine</code></li>
</ul>
<p>Make the changes to each file, and when youre finished, verify the project works as it did before.</p>
<p><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 style="color:#a6e22e">+from __future__ import annotations
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from typing import Optional
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Optional, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>import tcod.event
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from actions import Action, BumpAction, EscapeAction
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+if TYPE_CHECKING:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from engine import Engine
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class EventHandler(tcod.event.EventDispatch[Action]):
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(self, engine: Engine):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine = engine
</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_events(self) -&gt; None:
</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">+ 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">+ if action is None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ continue
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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"></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">+ self.engine.update_fov() # Update the FOV before the players next action.
</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 ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
</span></span><span style="display:flex;"><span> action: Optional[Action] = None
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> key = event.sym
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player = self.engine.player
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> if key == tcod.event.K_UP:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=0, dy=-1)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(player, dx=0, dy=-1)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_DOWN:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=0, dy=1)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(player, dx=0, dy=1)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_LEFT:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=-1, dy=0)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(player, dx=-1, dy=0)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_RIGHT:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=1, dy=0)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(player, dx=1, dy=0)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_ESCAPE:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = EscapeAction()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = EscapeAction(player)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="new-text">from __future__ import annotations</span>
<span class="crossed-out-text">from typing import Optional</span>
<span class="new-text">from typing import Optional, TYPE_CHECKING</span>
import tcod.event
from actions import Action, BumpAction, EscapeAction
<span class="new-text">if TYPE_CHECKING:
from engine import Engine</span>
class EventHandler(tcod.event.EventDispatch[Action]):
<span class="new-text">def __init__(self, engine: Engine):
self.engine = engine
def handle_events(self) -&gt; None:
for event in tcod.event.wait():
action = self.dispatch(event)
if action is None:
continue
action.perform()
self.engine.handle_enemy_turns()
self.engine.update_fov() # Update the FOV before the players next action.</span>
def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
...
def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
action: Optional[Action] = None
key = event.sym
<span class="new-text">player = self.engine.player</span>
if key == tcod.event.K_UP:
<span class="crossed-out-text">action = BumpAction(dx=0, dy=-1)</span>
<span class="new-text">action = BumpAction(player, dx=0, dy=-1)</span>
elif key == tcod.event.K_DOWN:
<span class="crossed-out-text">action = BumpAction(dx=0, dy=1)</span>
<span class="new-text">action = BumpAction(player, dx=0, dy=1)</span>
elif key == tcod.event.K_LEFT:
<span class="crossed-out-text">action = BumpAction(dx=-1, dy=0)</span>
<span class="new-text">action = BumpAction(player, dx=-1, dy=0)</span>
elif key == tcod.event.K_RIGHT:
<span class="crossed-out-text">action = BumpAction(dx=1, dy=0)</span>
<span class="new-text">action = BumpAction(player, dx=1, dy=0)</span>
elif key == tcod.event.K_ESCAPE:
<span class="crossed-out-text">action = EscapeAction()</span>
<span class="new-text">action = EscapeAction(player)</span></pre>
</div>
</div>
<p><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>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from typing import Optional, Tuple, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import TYPE_CHECKING
</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 engine import Engine
</span></span><span style="display:flex;"><span> from entity import Entity
</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 style="color:#a6e22e">+ def __init__(self, entity: Entity) -&gt; None:
</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">+ self.entity = 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">+ @property
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def engine(self) -&gt; Engine:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return the engine this action belongs to."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.entity.gamemap.engine
</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"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> """Perform this action with the objects needed to determine its scope.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ `self.engine` is the scope this action is being performed in.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- `engine` is the scope this action is being performed in.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ `self.entity` is the object performing the action.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- `entity` is the object performing the action.
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> This method must be overridden by Action subclasses.
</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></span><span style="display:flex;"><span>class EscapeAction(Action):
</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"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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></span><span style="display:flex;"><span>class ActionWithDirection(Action):
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(self, entity: Entity, dx: int, dy: int):
</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 style="color:#f92672">- def __init__(self, dx: int, dy: int):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- super().__init__()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> self.dx = dx
</span></span><span style="display:flex;"><span> self.dy = dy
</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 dest_xy(self) -&gt; Tuple[int, int]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Returns this actions destination."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.entity.x + self.dx, self.entity.y + self.dy
</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 blocking_entity(self) -&gt; Optional[Entity]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return the blocking entity at this actions destination.."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.engine.game_map.get_blocking_entity_at_location(*self.dest_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"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> raise NotImplementedError()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class MeleeAction(ActionWithDirection):
</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">+ target = self.blocking_entity
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_x = entity.x + self.dx
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_y = entity.y + self.dy
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> if not target:
</span></span><span style="display:flex;"><span> return # No entity to attack.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> print(f"You kick the {target.name}, much to its annoyance!")
</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><span style="color:#a6e22e">+ def perform(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_x, dest_y = self.dest_xy
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_x = entity.x + self.dx
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_y = entity.y + self.dy
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not self.engine.game_map.in_bounds(dest_x, dest_y):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- if not engine.game_map.in_bounds(dest_x, dest_y):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> return # Destination is out of bounds.
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> return # Destination is blocked by a tile.
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> return # Destination is blocked by an entity.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.move(self.dx, self.dy)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- entity.move(self.dx, self.dy)
</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 BumpAction(ActionWithDirection):
</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">+ if self.blocking_entity:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MeleeAction(self.entity, self.dx, self.dy).perform()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_x = entity.x + self.dx
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_y = entity.y + self.dy
</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 engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- return MeleeAction(self.dx, self.dy).perform(engine, entity)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> else:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MovementAction(self.entity, self.dx, self.dy).perform()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- return MovementAction(self.dx, self.dy).perform(engine, entity)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
<span class="new-text">from typing import Optional, Tuple, TYPE_CHECKING</span>
<span class="crossed-out-text">from typing import TYPE_CHECKING</span>
if TYPE_CHECKING:
from engine import Engine
from entity import Entity
class Action:
<span class="new-text">def __init__(self, entity: Entity) -&gt; None:
super().__init__()
self.entity = entity
@property
def engine(self) -&gt; Engine:
"""Return the engine this action belongs to."""
return self.entity.gamemap.engine
def perform(self) -&gt; None:</span>
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -&gt; None:</span>
"""Perform this action with the objects needed to determine its scope.
<span class="new-text">`self.engine` is the scope this action is being performed in.</span>
<span class="crossed-out-text">`engine` is the scope this action is being performed in.</span>
<span class="new-text">`self.entity` is the object performing the action.</span>
<span class="crossed-out-text">`entity` is the object performing the action.</span>
This method must be overridden by Action subclasses.
"""
raise NotImplementedError()
class EscapeAction(Action):
<span class="new-text">def perform(self) -&gt; None:</span>
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -&gt; None:</span>
raise SystemExit()
class ActionWithDirection(Action):
<span class="new-text">def __init__(self, entity: Entity, dx: int, dy: int):
super().__init__(entity)</span>
<span class="crossed-out-text">def __init__(self, dx: int, dy: int):</span>
<span class="crossed-out-text">super().__init__()</span>
self.dx = dx
self.dy = dy
<span class="new-text">@property
def dest_xy(self) -&gt; Tuple[int, int]:
"""Returns this actions destination."""
return self.entity.x + self.dx, self.entity.y + self.dy
@property
def blocking_entity(self) -&gt; Optional[Entity]:
"""Return the blocking entity at this actions destination.."""
return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)
def perform(self) -&gt; None:</span>
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -&gt; None:</span>
raise NotImplementedError()
class MeleeAction(ActionWithDirection):
<span class="new-text">def perform(self) -&gt; None:
target = self.blocking_entity</span>
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -&gt; None:</span>
<span class="crossed-out-text">dest_x = entity.x + self.dx</span>
<span class="crossed-out-text">dest_y = entity.y + self.dy</span>
<span class="crossed-out-text">target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)</span>
if not target:
return # No entity to attack.
print(f"You kick the {target.name}, much to its annoyance!")
class MovementAction(ActionWithDirection):
<span class="new-text">def perform(self) -&gt; None:
dest_x, dest_y = self.dest_xy</span>
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -&gt; None:</span>
<span class="crossed-out-text">dest_x = entity.x + self.dx</span>
<span class="crossed-out-text">dest_y = entity.y + self.dy</span>
<span class="new-text">if not self.engine.game_map.in_bounds(dest_x, dest_y):</span>
<span class="crossed-out-text">if not engine.game_map.in_bounds(dest_x, dest_y):</span>
return # Destination is out of bounds.
<span class="new-text">if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:</span>
<span class="crossed-out-text">if not engine.game_map.tiles["walkable"][dest_x, dest_y]:</span>
return # Destination is blocked by a tile.
<span class="new-text">if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):</span>
<span class="crossed-out-text">if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):</span>
return # Destination is blocked by an entity.
<span class="new-text">self.entity.move(self.dx, self.dy)</span>
<span class="crossed-out-text">entity.move(self.dx, self.dy)</span>
class BumpAction(ActionWithDirection):
<span class="new-text">def perform(self) -&gt; None:
if self.blocking_entity:
return MeleeAction(self.entity, self.dx, self.dy).perform()</span>
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -&gt; None:</span>
<span class="crossed-out-text">dest_x = entity.x + self.dx</span>
<span class="crossed-out-text">dest_y = entity.y + self.dy</span>
<span class="crossed-out-text">if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):</span>
<span class="crossed-out-text">return MeleeAction(self.dx, self.dy).perform(engine, entity)</span>
else:
<span class="new-text">return MovementAction(self.entity, self.dx, self.dy).perform()</span>
<span class="crossed-out-text">return MovementAction(self.dx, self.dy).perform(engine, entity)</span></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>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from typing import Iterable, Optional, TYPE_CHECKING
</span></span><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>import tile_types
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from engine import Engine
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> from entity import Entity
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class GameMap:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self, engine: Engine, width: int, height: int, entities: Iterable[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">+ self.engine = engine
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.width, self.height = width, height
</span></span><span style="display:flex;"><span> self.entities = set(entities)
</span></span><span style="display:flex;"><span> self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.visible = np.full(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ (width, height), fill_value=False, order="F"
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) # Tiles the player can currently see
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.explored = np.full(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ (width, height), fill_value=False, order="F"
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) # Tiles the player has seen before
</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 get_blocking_entity_at_location(self, location_x: int, location_y: int) -&gt; Optional[Entity]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def get_blocking_entity_at_location(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self, location_x: int, location_y: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) -&gt; Optional[Entity]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> for entity in self.entities:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if entity.blocks_movement and entity.x == location_x and entity.y == location_y:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if (
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity.blocks_movement
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ and entity.x == location_x
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ and entity.y == location_y
</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> return entity
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> return None
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def in_bounds(self, x: int, y: int) -&gt; bool:
</span></span><span style="display:flex;"><span> """Return True if x and y are inside of the bounds of this map."""
</span></span><span style="display:flex;"><span> return 0 &lt;= x &lt; self.width and 0 &lt;= y &lt; self.height
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def render(self, console: Console) -&gt; None:
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span> Renders the map.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> If a tile is in the "visible" array, then draw it with the "light" colors.
</span></span><span style="display:flex;"><span> If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
</span></span><span style="display:flex;"><span> Otherwise, the default is "SHROUD".
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span><span style="color:#f92672">- console.tiles_rgb[0:self.width, 0:self.height] = np.select(
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ console.tiles_rgb[0 : self.width, 0 : self.height] = np.select(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> condlist=[self.visible, self.explored],
</span></span><span style="display:flex;"><span> choicelist=[self.tiles["light"], self.tiles["dark"]],
</span></span><span style="display:flex;"><span><span style="color:#f92672">- default=tile_types.SHROUD
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ default=tile_types.SHROUD,
</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 __future__ import annotations
from typing import Iterable, Optional, TYPE_CHECKING
import numpy as np # type: ignore
from tcod.console import Console
import tile_types
if TYPE_CHECKING:
<span class="new-text">from engine import Engine</span>
from entity import Entity
class GameMap:
<span class="crossed-out-text">def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):</span>
<span class="new-text">def __init__(
self, engine: Engine, width: int, height: int, entities: Iterable[Entity] = ()
):
self.engine = engine</span>
self.width, self.height = width, height
self.entities = set(entities)
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
<span class="crossed-out-text">self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see</span>
<span class="new-text">self.visible = np.full(
(width, height), fill_value=False, order="F"
) # Tiles the player can currently see</span>
<span class="crossed-out-text">self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before</span>
<span class="new-text">self.explored = np.full(
(width, height), fill_value=False, order="F"
) # Tiles the player has seen before</span>
<span class="crossed-out-text">def get_blocking_entity_at_location(self, location_x: int, location_y: int) -&gt; Optional[Entity]:</span>
<span class="new-text">def get_blocking_entity_at_location(
self, location_x: int, location_y: int,
) -&gt; Optional[Entity]:</span>
for entity in self.entities:
<span class="crossed-out-text">if entity.blocks_movement and entity.x == location_x and entity.y == location_y:</span>
<span class="new-text">if (
entity.blocks_movement
and entity.x == location_x
and entity.y == location_y
):</span>
return entity
return None
def in_bounds(self, x: int, y: int) -&gt; bool:
"""Return True if x and y are inside of the bounds of this map."""
return 0 &lt;= x &lt; self.width and 0 &lt;= y &lt; self.height
def render(self, console: Console) -&gt; None:
"""
Renders the map.
If a tile is in the "visible" array, then draw it with the "light" colors.
If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
Otherwise, the default is "SHROUD".
"""
<span class="crossed-out-text">console.tiles_rgb[0:self.width, 0:self.height] = np.select(</span>
<span class="new-text">console.tiles_rgb[0 : self.width, 0 : self.height] = np.select(</span>
condlist=[self.visible, self.explored],
choicelist=[self.tiles["light"], self.tiles["dark"]],
<span class="crossed-out-text">default=tile_types.SHROUD</span>
<span class="new-text">default=tile_types.SHROUD,</span>
)</pre>
</div>
</div>
<p><code>main.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>#!/usr/bin/env python3
</span></span><span style="display:flex;"><span>import copy
</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>from engine import Engine
</span></span><span style="display:flex;"><span>import entity_factories
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from input_handlers import EventHandler
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>from procgen import generate_dungeon
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player = copy.deepcopy(entity_factories.player)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- event_handler = EventHandler()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine = Engine(player=player)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- player = copy.deepcopy(entity_factories.player)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.game_map = generate_dungeon(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- game_map = generate_dungeon(
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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">+ engine=engine,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- player=player,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> )
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.update_fov()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> with tcod.context.new_terminal(
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> while True:
</span></span><span style="display:flex;"><span> engine.render(console=root_console, context=context)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.event_handler.handle_events()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- events = tcod.event.wait()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.handle_events(events)
</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>if __name__ == "__main__":
</span></span><span style="display:flex;"><span> main()
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>#!/usr/bin/env python3
import copy
import tcod
from engine import Engine
import entity_factories
<span class="crossed-out-text">from input_handlers import EventHandler</span>
from procgen import generate_dungeon
...
<span class="new-text">player = copy.deepcopy(entity_factories.player)</span>
<span class="crossed-out-text">event_handler = EventHandler()</span>
<span class="new-text">engine = Engine(player=player)</span>
<span class="crossed-out-text">player = copy.deepcopy(entity_factories.player)</span>
<span class="new-text">engine.game_map = generate_dungeon(</span>
<span class="crossed-out-text">game_map = generate_dungeon(</span>
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">engine=engine,</span>
<span class="crossed-out-text">player=player,</span>
)
<span class="new-text">engine.update_fov()</span>
<span class="crossed-out-text">engine = Engine(event_handler=event_handler, game_map=game_map, player=player)</span>
with tcod.context.new_terminal(
...
while True:
engine.render(console=root_console, context=context)
<span class="new-text">engine.event_handler.handle_events()</span>
<span class="crossed-out-text">events = tcod.event.wait()</span>
<span class="crossed-out-text">engine.handle_events(events)</span>
if __name__ == "__main__":
main()</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>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 Tuple, 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, TypeVar, TYPE_CHECKING
</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> from game_map import GameMap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>T = TypeVar("T", bound="Entity")
</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:#a6e22e">+ gamemap: 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:#a6e22e">+ gamemap: 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> ):
</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><span style="color:#a6e22e">+ if gamemap:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # If gamemap isn't provided now then it will be set later.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.gamemap = gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ gamemap.entities.add(self)
</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:#a6e22e">+ clone.gamemap = 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><span style="color:#a6e22e">+ def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Place this entity at a new location. Handles moving across GameMaps."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.x = x
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.y = y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if gamemap:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if hasattr(self, "gamemap"): # Possibly uninitialized.
</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.gamemap = gamemap
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ gamemap.entities.add(self)
</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 Tuple, TypeVar, TYPE_CHECKING</span>
<span class="new-text">from typing import Optional, Tuple, TypeVar, TYPE_CHECKING</span>
if TYPE_CHECKING:
from game_map import GameMap
T = TypeVar("T", bound="Entity")
class Entity:
"""
A generic object to represent players, enemies, items, etc.
"""
<span class="new-text">gamemap: GameMap</span>
def __init__(
self,
<span class="new-text">gamemap: 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,
):
self.x = x
self.y = y
self.char = char
self.color = color
self.name = name
self.blocks_movement = blocks_movement
<span class="new-text">if gamemap:
# If gamemap isn't provided now then it will be set later.
self.gamemap = gamemap
gamemap.entities.add(self)</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="new-text">clone.gamemap = gamemap</span>
gamemap.entities.add(clone)
return clone
<span class="new-text">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:
if hasattr(self, "gamemap"): # Possibly uninitialized.
self.gamemap.entities.remove(self)
self.gamemap = gamemap
gamemap.entities.add(self)</span></pre>
</div>
</div>
<p><code>procgen.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>import tile_types
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from engine import Engine
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- from entity import Entity
</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>def generate_dungeon(
</span></span><span style="display:flex;"><span> max_rooms: int,
</span></span><span style="display:flex;"><span> room_min_size: int,
</span></span><span style="display:flex;"><span> room_max_size: int,
</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">+ engine: Engine,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- player: Entity,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>) -&gt; GameMap:
</span></span><span style="display:flex;"><span> """Generate a new dungeon map."""
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player = engine.player
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon = GameMap(engine, map_width, map_height, entities=[player])
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- dungeon = GameMap(map_width, map_height, entities=[player])
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> rooms: List[RectangularRoom] = []
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> if len(rooms) == 0:
</span></span><span style="display:flex;"><span> # The first room, where the player starts.
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player.place(*new_room.center, dungeon)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- player.x, player.y = new_room.center
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> else: # All rooms after the first.
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>import tile_types
if TYPE_CHECKING:
<span class="new-text">from engine import Engine</span>
<span class="crossed-out-text">from entity import Entity</span>
...
def generate_dungeon(
max_rooms: int,
room_min_size: int,
room_max_size: int,
map_width: int,
map_height: int,
max_monsters_per_room: int,
<span class="new-text">engine: Engine,</span>
<span class="crossed-out-text">player: Entity,</span>
) -&gt; GameMap:
"""Generate a new dungeon map."""
<span class="new-text">player = engine.player</span>
<span class="new-text">dungeon = GameMap(engine, map_width, map_height, entities=[player])</span>
<span class="crossed-out-text">dungeon = GameMap(map_width, map_height, entities=[player])</span>
rooms: List[RectangularRoom] = []
...
...
if len(rooms) == 0:
# The first room, where the player starts.
<span class="new-text">player.place(*new_room.center, dungeon)</span>
<span class="crossed-out-text">player.x, player.y = new_room.center</span>
else: # All rooms after the first.
...</pre>
</div>
</div>
<p><code>engine.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 __future__ import annotations
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from typing import TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import Iterable, Any
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span>from tcod.context import Context
</span></span><span style="display:flex;"><span>from tcod.console import Console
</span></span><span style="display:flex;"><span>from tcod.map import compute_fov
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from entity import Entity
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-from game_map import GameMap
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>from input_handlers import EventHandler
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+if TYPE_CHECKING:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from entity import Entity
</span></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 Engine:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ game_map: GameMap
</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, player: Entity):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.event_handler: EventHandler = EventHandler(self)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.player = player
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.event_handler = event_handler
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.game_map = game_map
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.player = player
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.update_fov()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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 self.game_map.entities - {self.player}:
</span></span><span style="display:flex;"><span> print(f'The {entity.name} wonders when it will get to take a real turn.')
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self, events: Iterable[Any]) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- for event in events:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = self.event_handler.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(self, self.player)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.handle_enemy_turns()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.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> def update_fov(self) -&gt; None:
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="new-text">from __future__ import annotations</span>
<span class="new-text">from typing import TYPE_CHECKING</span>
<span class="crossed-out-text">from typing import Iterable, Any</span>
from tcod.context import Context
from tcod.console import Console
from tcod.map import compute_fov
<span class="crossed-out-text">from entity import Entity</span>
<span class="crossed-out-text">from game_map import GameMap</span>
from input_handlers import EventHandler
<span class="new-text">if TYPE_CHECKING:
from entity import Entity
from game_map import GameMap</span>
class Engine:
<span class="new-text">game_map: GameMap</span>
<span class="new-text">def __init__(self, player: Entity):
self.event_handler: EventHandler = EventHandler(self)
self.player = player</span>
<span class="crossed-out-text">def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):</span>
<span class="crossed-out-text">self.event_handler = event_handler</span>
<span class="crossed-out-text">self.game_map = game_map</span>
<span class="crossed-out-text">self.player = player</span>
<span class="crossed-out-text">self.update_fov()</span>
def handle_enemy_turns(self) -&gt; None:
for entity in self.game_map.entities - {self.player}:
print(f'The {entity.name} wonders when it will get to take a real turn.')
<span class="crossed-out-text">def handle_events(self, events: Iterable[Any]) -&gt; None:</span>
<span class="crossed-out-text">for event in events:</span>
<span class="crossed-out-text">action = self.event_handler.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(self, self.player)</span>
<span class="crossed-out-text">self.handle_enemy_turns()</span>
<span class="crossed-out-text">self.update_fov() # Update the FOV before the players next action.</span>
def update_fov(self) -&gt; None:
...</pre>
</div>
</div>
<h2 id="onwards-to-part-6">
Onwards to Part 6
<a class="heading-link" href="#onwards-to-part-6">
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
<span class="sr-only">Link to heading</span>
</a>
</h2>
<p>The last part of this tutorial set us up for combat, so now its time to actually implement it.</p>
<p>In order to make “killable” Entities, rather than attaching hit points to each Entity we create, well create a <strong>component</strong>, called <code>Fighter</code>,
which will hold information related to combat, like HP, max HP, attack,
and defense. If an Entity can fight, it will have this component
attached to it, and if not, it wont. This way of doing things is called
<strong>composition</strong>, and its an alternative to your typical inheritance-based programming model. (This tutorial uses both composition <em>and</em> inheritance).</p>
<p>Create a new Python package (a folder with an empty __init__.py file), called <code>components</code>. In that new directory, add two new files, one called <code>base_component.py</code>, and another called <code>fighter.py</code>. The <code>Fighter</code> class in <code>fighter.py</code> will inherit from the class we put in <code>base_component.py</code>, so lets start with that one:</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> TYPE_CHECKING
</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> engine <span style="color:#f92672">import</span> Engine
</span></span><span style="display:flex;"><span> <span style="color:#f92672">from</span> entity <span style="color:#f92672">import</span> Entity
</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">BaseComponent</span>:
</span></span><span style="display:flex;"><span> entity: Entity <span style="color:#75715e"># Owning entity instance.</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:#66d9ef">def</span> <span style="color:#a6e22e">engine</span>(self) <span style="color:#f92672">-&gt;</span> Engine:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>gamemap<span style="color:#f92672">.</span>engine
</span></span></code></pre></div><p>With that, lets now open up <code>fighter.py</code> and put the following 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:#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></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Fighter</span>(BaseComponent):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, hp: int, defense: int, power: int):
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>max_hp <span style="color:#f92672">=</span> hp
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>_hp <span style="color:#f92672">=</span> hp
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>defense <span style="color:#f92672">=</span> defense
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>power <span style="color:#f92672">=</span> power
</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:#66d9ef">def</span> <span style="color:#a6e22e">hp</span>(self) <span style="color:#f92672">-&gt;</span> int:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>_hp
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@hp.setter</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">hp</span>(self, value: int) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>_hp <span style="color:#f92672">=</span> max(<span style="color:#ae81ff">0</span>, min(value, self<span style="color:#f92672">.</span>max_hp))
</span></span></code></pre></div><p>We import and inherit from <code>BaseComponent</code>, which gives us access to the parent entity and the engine, which will be useful later on.</p>
<p>The <code>__init__</code> function takes a few arguments. <code>hp</code> represents the entitys hit points. <code>defense</code> is how much taken damage will be reduced. <code>power</code> is the entitys raw attack power.</p>
<p>Whats with the <code>hp</code> property? We define both a getter and setter, which will allow the class to access <code>hp</code> like a normal variable. The getter (the one with the <code>@property</code> thing above the method) doesnt do anything special: it just returns the HP. The <code>setter</code> (<code>@hp.setter</code>) is where things get more interesting.</p>
<p>By defining HP this way, we can modify the value as its set within the method. This line:</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> self<span style="color:#f92672">.</span>_hp <span style="color:#f92672">=</span> max(<span style="color:#ae81ff">0</span>, min(value, self<span style="color:#f92672">.</span>max_hp))
</span></span></code></pre></div><p>Means that <code>_hp</code> (which we access through <code>hp</code>) will never be set to less than 0, but also wont ever go higher than the <code>max_hp</code> attribute.</p>
<p>So thats our <code>Fighter</code> component. It wont do us much
good at the moment, because the entities in our game still dont move or
do much of anything (besides the player, anyway). To give some life to
our entities, we can add another component, which, when attached to our
entities, will allow them to take turns and move around.</p>
<p>Create a file in the <code>components</code> directory called <code>ai.py</code>, and put the following contents into it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#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, Tuple
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> numpy <span style="color:#66d9ef">as</span> np <span style="color:#75715e"># type: ignore</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> actions <span style="color:#f92672">import</span> Action
</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></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BaseAI</span>(Action, BaseComponent):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">perform</span>(self) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</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 style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_path_to</span>(self, dest_x: int, dest_y: int) <span style="color:#f92672">-&gt;</span> List[Tuple[int, int]]:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Compute and return a path to the target position.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> If there is no valid path then returns an empty list.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Copy the walkable array.</span>
</span></span><span style="display:flex;"><span> cost <span style="color:#f92672">=</span> np<span style="color:#f92672">.</span>array(self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>gamemap<span style="color:#f92672">.</span>tiles[<span style="color:#e6db74">"walkable"</span>], dtype<span style="color:#f92672">=</span>np<span style="color:#f92672">.</span>int8)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> entity <span style="color:#f92672">in</span> self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>gamemap<span style="color:#f92672">.</span>entities:
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Check that an enitiy blocks movement and the cost isn't zero (blocking.)</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> entity<span style="color:#f92672">.</span>blocks_movement <span style="color:#f92672">and</span> cost[entity<span style="color:#f92672">.</span>x, entity<span style="color:#f92672">.</span>y]:
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Add to the cost of a blocked position.</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># A lower number means more enemies will crowd behind each other in</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># hallways. A higher number means enemies will take longer paths in</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># order to surround the player.</span>
</span></span><span style="display:flex;"><span> cost[entity<span style="color:#f92672">.</span>x, entity<span style="color:#f92672">.</span>y] <span style="color:#f92672">+=</span> <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Create a graph from the cost array and pass that graph to a new pathfinder.</span>
</span></span><span style="display:flex;"><span> graph <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>SimpleGraph(cost<span style="color:#f92672">=</span>cost, cardinal<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>, diagonal<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span>)
</span></span><span style="display:flex;"><span> pathfinder <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>Pathfinder(graph)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> pathfinder<span style="color:#f92672">.</span>add_root((self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>x, self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>y)) <span style="color:#75715e"># Start position.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Compute the path to the destination and remove the starting point.</span>
</span></span><span style="display:flex;"><span> path: List[List[int]] <span style="color:#f92672">=</span> pathfinder<span style="color:#f92672">.</span>path_to((dest_x, dest_y))[<span style="color:#ae81ff">1</span>:]<span style="color:#f92672">.</span>tolist()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Convert from List[List[int]] to List[Tuple[int, int]].</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> [(index[<span style="color:#ae81ff">0</span>], index[<span style="color:#ae81ff">1</span>]) <span style="color:#66d9ef">for</span> index <span style="color:#f92672">in</span> path]
</span></span></code></pre></div><p><code>BaseAI</code> doesnt implement a <code>perform</code> method, since the entities which will be using AI to act will have to have an AI class that inherits from this one.</p>
<p><code>get_path_to</code> uses the “walkable” tiles in our map, along with some TCOD pathfinding tools to get the path from the <code>BaseAI</code>s
parent entity to whatever their target might be. In the case of this
tutorial, the target will always be the player, though you could
theoretically write a monster that cares more about food or treasure
than attacking the player.</p>
<p>The pathfinder first builds an array of <code>cost</code>, which is
how “costly” (time consuming) it will take to get to the target. If a
piece of terrain takes longer to traverse, its cost will be higher. In
the case of our simple game, all parts of the map have the same cost,
but what this cost array allows us to do is take other entities into
account.</p>
<p>How? Well, if an entity exists at a spot on the map, we increase the
cost of moving there to “10”. What this does is encourages the entity to
move around the entity thats blocking them from their target. Higher
values will cause the entity to take a longer path around; shorter
values will cause groups to gather into crowds, since they dont want to
move around.</p>
<p>More information about TCODs pathfinding can be <a href="https://python-tcod.readthedocs.io/en/latest/tcod/path.html">found here</a>.</p>
<p>To make use of our new <code>Fighter</code> and <code>AI</code> components, we could attach them directly onto the <code>Entity</code>
class. However, it might be useful to differentiate between entities
that can act, and those that cant. Right now, our game only consists of
acting entities, but soon enough, well be adding things like
consumable items and, eventually, equipment, which wont be able to take
turns or take damage.</p>
<p>One way to handle this is to create a new subclass of <code>Entity</code>, called <code>Actor</code>, and give it all the same attributes as <code>Entity</code>, plus the <code>ai</code> and <code>fighter</code> components it will need. Modify <code>entity.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>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 Tuple, 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
</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>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.ai import BaseAI
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.fighter import Fighter
</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>T = TypeVar("T", bound="Entity")
</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>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class Actor(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">+ ai_cls: Type[BaseAI],
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fighter: Fighter
</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=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:#a6e22e">+ self.ai: Optional[BaseAI] = ai_cls(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">+ self.fighter = fighter
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.fighter.entity = 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 is_alive(self) -&gt; bool:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Returns True as long as this actor can perform actions."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return bool(self.ai)
</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 Tuple, TypeVar, TYPE_CHECKING</span>
<span class="new-text">from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING</span>
if TYPE_CHECKING:
<span class="new-text">from components.ai import BaseAI
from components.fighter import Fighter</span>
from game_map import GameMap
T = TypeVar("T", bound="Entity")
class Entity:
...
<span class="new-text">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,
)
self.ai: Optional[BaseAI] = ai_cls(self)
self.fighter = fighter
self.fighter.entity = self
@property
def is_alive(self) -&gt; bool:
"""Returns True as long as this actor can perform actions."""
return bool(self.ai)</span></pre>
</div>
</div>
<p>The first thing our <code>Actor</code> class does in its <code>__init__()</code> function is call its superclasss <code>__init__()</code>, which in this case, is the <code>Entity</code> class. Were passing <code>blocks_movement</code> as <code>True</code> every time, because we can assume that all the “actors” will block movement.</p>
<p>Besides calling the <code>Entity.__init__()</code>, we also set the two components for the <code>Actor</code> class: <code>ai</code> and <code>fighter</code>.
The idea is that each actor will need two things to function: the
ability to move around and make decisions, and the ability to take (and
receive) damage.</p>
<p>This new <code>Actor</code> class isnt quite enough to get our enemies up and moving around, but were getting there. We actually need to revisit <code>ai.py</code>, and add a new class there to handle hostile enemies. Enter the following changes in <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>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from typing import List, Tuple
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import List, Tuple, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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><span style="color:#f92672">-from actions import Action
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from actions import Action, MeleeAction, MovementAction, WaitAction
</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>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+if TYPE_CHECKING:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from entity import Actor
</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 BaseAI(Action, BaseComponent):
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity: Actor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def perform(self) -&gt; None:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class HostileEnemy(BaseAI):
</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">+ self.path: List[Tuple[int, int]] = []
</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">+ target = self.engine.player
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dx = target.x - self.entity.x
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dy = target.y - self.entity.y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ distance = max(abs(dx), abs(dy)) # Chebyshev distance.
</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.game_map.visible[self.entity.x, self.entity.y]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if distance &lt;= 1:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MeleeAction(self.entity, dx, dy).perform()
</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.path = self.get_path_to(target.x, target.y)
</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.path:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_x, dest_y = self.path.pop(0)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MovementAction(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ).perform()
</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 WaitAction(self.entity).perform()
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
<span class="crossed-out-text">from typing import List, Tuple</span>
<span class="new-text">from typing import List, Tuple, TYPE_CHECKING</span>
import numpy as np # type: ignore
import tcod
<span class="crossed-out-text">from actions import Action</span>
<span class="new-text">from actions import Action, MeleeAction, MovementAction, WaitAction</span>
from components.base_component import BaseComponent
<span class="new-text">if TYPE_CHECKING:
from entity import Actor</span>
class BaseAI(Action, BaseComponent):
<span class="new-text">entity: Actor</span>
def perform(self) -&gt; None:
...
<span class="new-text">class HostileEnemy(BaseAI):
def __init__(self, entity: Actor):
super().__init__(entity)
self.path: List[Tuple[int, int]] = []
def perform(self) -&gt; None:
target = self.engine.player
dx = target.x - self.entity.x
dy = target.y - self.entity.y
distance = max(abs(dx), abs(dy)) # Chebyshev distance.
if self.engine.game_map.visible[self.entity.x, self.entity.y]:
if distance &lt;= 1:
return MeleeAction(self.entity, dx, dy).perform()
self.path = self.get_path_to(target.x, target.y)
if self.path:
dest_x, dest_y = self.path.pop(0)
return MovementAction(
self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
).perform()
return WaitAction(self.entity).perform()</span></pre>
</div>
</div>
<p><code>HostileEnemy</code> is the AI class well use for our enemies. It defines the <code>perform</code> method, which does the following:</p>
<ul>
<li>If the entity is not in the players vision, simply wait.</li>
<li>If the player is right next to the entity (<code>distance &lt;= 1</code>), attack the player.</li>
<li>If the player can see the entity, but the entity is too far away to attack, then move towards the player.</li>
</ul>
<p>The last line actually calls an action that we havent defined yet: <code>WaitAction</code>. This action will be used when the player or an enemy decides to wait where they are rather than taking a turn.</p>
<p>Implement <code>WaitAction</code> by opening <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> ...
</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 WaitAction(Action):
</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">+ pass
</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 ActionWithDirection(Action):
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class EscapeAction(Action):
...
<span class="new-text">class WaitAction(Action):
def perform(self) -&gt; None:
pass</span>
class ActionWithDirection(Action):
...</pre>
</div>
</div>
<p>As you can see, <code>WaitAction</code> does… well, nothing. And thats what we want it to do, as it represents an actor saying “Ill do nothing this turn.”</p>
<p>With all that in place, well need to refactor our <code>entity_factories.py</code> file to make use of the new <code>Actor</code> class, as well as its components. Modify <code>entity_factories.py</code> to look 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 style="color:#a6e22e">+from components.ai import HostileEnemy
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from components.fighter import Fighter
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from entity import Actor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from entity import Entity
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+player = Actor(
</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=(255, 255, 255),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name="Player",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ai_cls=HostileEnemy,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fighter=Fighter(hp=30, defense=2, power=5),
</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 style="color:#f92672">-player = Entity(char="@", color=(255, 255, 255), name="Player", blocks_movement=True)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+orc = Actor(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char="o",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color=(63, 127, 63),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name="Orc",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ai_cls=HostileEnemy,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fighter=Fighter(hp=10, defense=0, power=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">+troll = Actor(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char="T",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color=(0, 127, 0),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name="Troll",
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ai_cls=HostileEnemy,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fighter=Fighter(hp=16, defense=1, power=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"></span><span style="color:#f92672">-orc = Entity(char="o", color=(63, 127, 63), name="Orc", blocks_movement=True)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-troll = Entity(char="T", color=(0, 127, 0), name="Troll", blocks_movement=True)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="new-text">from components.ai import HostileEnemy
from components.fighter import Fighter
from entity import Actor</span>
<span class="crossed-out-text">from entity import Entity</span>
<span class="new-text">player = Actor(
char="@",
color=(255, 255, 255),
name="Player",
ai_cls=HostileEnemy,
fighter=Fighter(hp=30, defense=2, power=5),
)</span>
<span class="crossed-out-text">player = Entity(char="@", color=(255, 255, 255), name="Player", blocks_movement=True)</span>
<span class="new-text">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>
<span class="crossed-out-text">orc = Entity(char="o", color=(63, 127, 63), name="Orc", blocks_movement=True)</span>
<span class="crossed-out-text">troll = Entity(char="T", color=(0, 127, 0), name="Troll", blocks_movement=True)</span></pre>
</div>
</div>
<p>Weve changed each entity to make use of the <code>Actor</code> class, and used the <code>HostileEnemy</code> AI class for the Orc and the Troll types.
The player doesnt use the AI, so the AI given to it doesnt matter other than that an AI must be specified for all <code>Actor</code>s.
Also, we defined the <code>Fighter</code> component for each, giving a
few different values to make the Trolls stronger than the Orcs. Feel
free to modify these values to your liking.</p>
<p>How do enemies actually take their turns, though? Its actually
pretty simple: rather than printing the message we were before, we just
check if the entity has an AI, and if it does, we call the <code>perform</code> method from that AI component. Modify <code>engine.py</code> to do 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 handle_enemy_turns(self) -&gt; None:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for entity in set(self.game_map.actors) - {self.player}:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if entity.ai:
</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"></span><span style="color:#f92672">- for entity in self.game_map.entities - {self.player}:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- print(f'The {entity.name} wonders when it will get to take a real turn.')
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> def handle_enemy_turns(self) -&gt; None:
<span class="new-text">for entity in set(self.game_map.actors) - {self.player}:
if entity.ai:
entity.ai.perform()</span>
<span class="crossed-out-text">for entity in self.game_map.entities - {self.player}:</span>
<span class="crossed-out-text">print(f'The {entity.name} wonders when it will get to take a real turn.')</span></pre>
</div>
</div>
<p>But wait, <code>game_map.actors</code> isnt defined. What should it do, though? Same thing as <code>game_map.entities</code>, except it should return only the <code>Actor</code> entities.</p>
<p>Lets add this method to <code>GameMap</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from typing import Iterable, Optional, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Iterable, Iterator, Optional, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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:#a6e22e">+from entity import Actor
</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>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></span><span style="display:flex;"><span>class GameMap:
</span></span><span style="display:flex;"><span> def __init__(
</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 actors(self) -&gt; Iterator[Actor]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Iterate over this maps living actors."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ yield from (
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for entity in self.entities
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if isinstance(entity, Actor) and entity.is_alive
</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> def get_blocking_entity_at_location(
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def get_actor_at_location(self, x: int, y: int) -&gt; Optional[Actor]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for actor in self.actors:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if actor.x == x and actor.y == y:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return actor
</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></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
<span class="crossed-out-text">from typing import Iterable, Optional, TYPE_CHECKING</span>
<span class="new-text">from typing import Iterable, Iterator, Optional, TYPE_CHECKING</span>
import numpy as np # type: ignore
from tcod.console import Console
<span class="new-text">from entity import Actor</span>
import tile_types
if TYPE_CHECKING:
from engine import Engine
from entity import Entity
class GameMap:
def __init__(
...
<span class="new-text">@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>
def get_blocking_entity_at_location(
...
<span class="new-text">def get_actor_at_location(self, x: int, y: int) -&gt; Optional[Actor]:
for actor in self.actors:
if actor.x == x and actor.y == y:
return actor
return None</span></pre>
</div>
</div>
<p>Our <code>actors</code> property will return all the <code>Actor</code> entities in the map, but only those that are currently “alive”.</p>
<p>Weve also went ahead and added a <code>get_actor_at_location</code>, which, as the name implies, acts similarly to <code>get_blocking_entity_at_location</code>, but returns only an <code>Actor</code>. This will come in handy later on.</p>
<p>Run the project now, and the enemies should chase you around! They cant really attack just yet, but were getting there.</p>
<p><img src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/part-6-chase.png" alt="Part 6 - The Chase"></p>
<p>One thing you might have noticed is that were letting our enemies
move and attack in diagonal directions, but our player can only move in
the four cardinal directions (up, down, left, right). We can fix that by
adjusting <code>input_handlers.py</code>. While were at it, we might want to define a more flexible way of defining the movement keys rather than the <code>if...elif</code>
structure weve used so far. While that does work, it gets a bit clunky
after more than just a few options. We can fix this by modifying <code>input_handlers.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>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 tcod.event
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from actions import Action, BumpAction, EscapeAction
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from actions import Action, BumpAction, EscapeAction, WaitAction
</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> from engine import Engine
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+MOVE_KEYS = {
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Arrow keys.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_UP: (0, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_DOWN: (0, 1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_LEFT: (-1, 0),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_RIGHT: (1, 0),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_HOME: (-1, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_END: (-1, 1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_PAGEUP: (1, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_PAGEDOWN: (1, 1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Numpad keys.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_1: (-1, 1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_2: (0, 1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_3: (1, 1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_4: (-1, 0),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_6: (1, 0),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_7: (-1, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_8: (0, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_9: (1, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Vi keys.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_h: (-1, 0),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_j: (0, 1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_k: (0, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_l: (1, 0),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_y: (-1, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_u: (1, -1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_b: (-1, 1),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_n: (1, 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"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+WAIT_KEYS = {
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_PERIOD,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_5,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_CLEAR,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+}
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if key == tcod.event.K_UP:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=0, dy=-1)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- elif key == tcod.event.K_DOWN:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=0, dy=1)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- elif key == tcod.event.K_LEFT:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=-1, dy=0)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- elif key == tcod.event.K_RIGHT:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=1, dy=0)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if key in MOVE_KEYS:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dx, dy = MOVE_KEYS[key]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = BumpAction(player, dx, dy)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key in WAIT_KEYS:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = WaitAction(player)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import Optional, TYPE_CHECKING
import tcod.event
<span class="crossed-out-text">from actions import Action, BumpAction, EscapeAction</span>
<span class="new-text">from actions import Action, BumpAction, EscapeAction, WaitAction</span>
if TYPE_CHECKING:
from engine import Engine
<span class="new-text">MOVE_KEYS = {
# Arrow keys.
tcod.event.K_UP: (0, -1),
tcod.event.K_DOWN: (0, 1),
tcod.event.K_LEFT: (-1, 0),
tcod.event.K_RIGHT: (1, 0),
tcod.event.K_HOME: (-1, -1),
tcod.event.K_END: (-1, 1),
tcod.event.K_PAGEUP: (1, -1),
tcod.event.K_PAGEDOWN: (1, 1),
# Numpad keys.
tcod.event.K_KP_1: (-1, 1),
tcod.event.K_KP_2: (0, 1),
tcod.event.K_KP_3: (1, 1),
tcod.event.K_KP_4: (-1, 0),
tcod.event.K_KP_6: (1, 0),
tcod.event.K_KP_7: (-1, -1),
tcod.event.K_KP_8: (0, -1),
tcod.event.K_KP_9: (1, -1),
# Vi keys.
tcod.event.K_h: (-1, 0),
tcod.event.K_j: (0, 1),
tcod.event.K_k: (0, -1),
tcod.event.K_l: (1, 0),
tcod.event.K_y: (-1, -1),
tcod.event.K_u: (1, -1),
tcod.event.K_b: (-1, 1),
tcod.event.K_n: (1, 1),
}
WAIT_KEYS = {
tcod.event.K_PERIOD,
tcod.event.K_KP_5,
tcod.event.K_CLEAR,
}</span>
...
<span class="crossed-out-text">if key == tcod.event.K_UP:</span>
<span class="crossed-out-text">action = BumpAction(player, dx=0, dy=-1)</span>
<span class="crossed-out-text">elif key == tcod.event.K_DOWN:</span>
<span class="crossed-out-text">action = BumpAction(player, dx=0, dy=1)</span>
<span class="crossed-out-text">elif key == tcod.event.K_LEFT:</span>
<span class="crossed-out-text">action = BumpAction(player, dx=-1, dy=0)</span>
<span class="crossed-out-text">elif key == tcod.event.K_RIGHT:</span>
<span class="crossed-out-text">action = BumpAction(player, dx=1, dy=0)</span>
<span class="new-text">if key in MOVE_KEYS:
dx, dy = MOVE_KEYS[key]
action = BumpAction(player, dx, dy)
elif key in WAIT_KEYS:
action = WaitAction(player)</span>
...</pre>
</div>
</div>
<p>The <code>MOVE_KEYS</code> dictionary holds various different
possibilities for movement. Some roguelikes utilize the numpad for
movement, some use “Vi Keys.” Ours will actually use both for the time
being. Feel free to change the key scheme if youre not a fan of it.</p>
<p>Where we used to do <code>if...elif</code> statements for each direction, we can now just check if the key was part of <code>MOVE_KEYS</code>, and if it was, we return the <code>dx</code> and <code>dy</code> values from the dictionary. This is a lot simpler and cleaner than our previous format.</p>
<p>So now that our enemies can chase us down, its time to make them do some real damage.</p>
<p>Open up <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>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from typing import Optional, Tuple, 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><span style="color:#f92672">- from entity import Entity
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ from entity import Actor, 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 Action:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, entity: Actor) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> super().__init__()
</span></span><span style="display:flex;"><span> self.entity = entity
</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 ActionWithDirection(Action):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, entity: Entity, dx: int, dy: int):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, entity: Actor, dx: int, dy: int):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> super().__init__(entity)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self.dx = dx
</span></span><span style="display:flex;"><span> self.dy = dy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> @property
</span></span><span style="display:flex;"><span> def dest_xy(self) -&gt; Tuple[int, int]:
</span></span><span style="display:flex;"><span> """Returns this actions destination."""
</span></span><span style="display:flex;"><span> return self.entity.x + self.dx, self.entity.y + self.dy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> @property
</span></span><span style="display:flex;"><span> def blocking_entity(self) -&gt; Optional[Entity]:
</span></span><span style="display:flex;"><span> """Return the blocking entity at this actions destination.."""
</span></span><span style="display:flex;"><span> return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)
</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 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.dest_xy)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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><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><span style="color:#a6e22e">+ target = self.target_actor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- target = self.blocking_entity
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> if not target:
</span></span><span style="display:flex;"><span> return # No entity to attack.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ damage = self.entity.fighter.power - target.fighter.defense
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if damage &gt; 0:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ print(f"{attack_desc} for {damage} hit points.")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ target.fighter.hp -= damage
</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">+ print(f"{attack_desc} but does no damage.")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- print(f"You kick the {target.name}, much to its annoyance!")
</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 MovementAction(ActionWithDirection):
</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 BumpAction(ActionWithDirection):
</span></span><span style="display:flex;"><span> def perform(self) -&gt; None:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if self.blocking_entity:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if self.target_actor:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> return MeleeAction(self.entity, self.dx, self.dy).perform()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> else:
</span></span><span style="display:flex;"><span> return MovementAction(self.entity, self.dx, self.dy).perform()
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import Optional, Tuple, TYPE_CHECKING
if TYPE_CHECKING:
from engine import Engine
<span class="crossed-out-text">from entity import Entity</span>
<span class="new-text">from entity import Actor, Entity</span>
class Action:
<span class="crossed-out-text">def __init__(self, entity: Entity) -&gt; None:</span>
<span class="new-text">def __init__(self, entity: Actor) -&gt; None:</span>
super().__init__()
self.entity = entity
...
class ActionWithDirection(Action):
<span class="crossed-out-text">def __init__(self, entity: Entity, dx: int, dy: int):</span>
<span class="new-text">def __init__(self, entity: Actor, dx: int, dy: int):</span>
super().__init__(entity)
self.dx = dx
self.dy = dy
@property
def dest_xy(self) -&gt; Tuple[int, int]:
"""Returns this actions destination."""
return self.entity.x + self.dx, self.entity.y + self.dy
@property
def blocking_entity(self) -&gt; Optional[Entity]:
"""Return the blocking entity at this actions destination.."""
return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)
<span class="new-text">@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.dest_xy)</span>
def perform(self) -&gt; None:
raise NotImplementedError()
class MeleeAction(ActionWithDirection):
def perform(self) -&gt; None:
<span class="new-text">target = self.target_actor</span>
<span class="crossed-out-text">target = self.blocking_entity</span>
if not target:
return # No entity to attack.
<span class="new-text">damage = self.entity.fighter.power - target.fighter.defense
attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
if damage &gt; 0:
print(f"{attack_desc} for {damage} hit points.")
target.fighter.hp -= damage
else:
print(f"{attack_desc} but does no damage.")</span>
<span class="crossed-out-text">print(f"You kick the {target.name}, much to its annoyance!")</span>
class MovementAction(ActionWithDirection):
...
class BumpAction(ActionWithDirection):
def perform(self) -&gt; None:
<span class="crossed-out-text">if self.blocking_entity:</span>
<span class="new-text">if self.target_actor:</span>
return MeleeAction(self.entity, self.dx, self.dy).perform()
else:
return MovementAction(self.entity, self.dx, self.dy).perform()</pre>
</div>
</div>
<p>Were replacing the type hint for <code>entity</code> in <code>Action</code> and <code>ActionWithDirection</code> with <code>Actor</code> instead of <code>Entity</code>, since only <code>Actor</code>s should be taking actions.</p>
<p>Weve also added the <code>target_actor</code> property to <code>ActionWithDirection</code>, which will give us the <code>Actor</code> at the destination were moving to, if there is one. We utilize that property instead of <code>blocking_entity</code> in both <code>BumpAction</code> and <code>MeleeAction</code>.</p>
<p>Lastly, we modify <code>MeleeAction</code> to actually do an attack,
instead of just printing a message. We calculate the damage (attackers
power minus defenders defense), and assign a description to the attack,
based on whether any damage was done or not. If the damage is greater
than 0, we subtract it from the defenders HP.</p>
<p>If you run the project now, youll see the print statements
indicating that the player and the enemies are doing damage to each
other. But since neither side can actually die, combat doesnt feel all
that high stakes just yet.</p>
<p>What do we do when an Entity reaches 0 HP or lower? Well, it should drop dead, obviously! But what should our <em>code</em> do to make this happen? To handle this, we can refer back to our <code>Fighter</code> component.</p>
<p>Remember when we created a setter for <code>hp</code>? It will come
in handy right now, as we can utilize it to automatically “kill” the
actor when their HP drops to zero. Add the following to <code>fighter.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#a6e22e">+from __future__ import annotations
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from typing import TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>from components.base_component import BaseComponent
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+if TYPE_CHECKING:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from entity import Actor
</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 Fighter(BaseComponent):
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity: 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:#a6e22e">+ if self._hp == 0 and self.entity.ai:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.die()
</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 die(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.engine.player is self.entity:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ death_message = "You died!"
</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">+ death_message = f"{self.entity.name} is dead!"
</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.entity.char = "%"
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.color = (191, 0, 0)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.blocks_movement = False
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.ai = None
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.name = f"remains of {self.entity.name}"
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ print(death_message)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="new-text">from __future__ import annotations
from typing import TYPE_CHECKING</span>
from components.base_component import BaseComponent
<span class="new-text">if TYPE_CHECKING:
from entity import Actor</span>
class Fighter(BaseComponent):
<span class="new-text">entity: 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="new-text">if self._hp == 0 and self.entity.ai:
self.die()
def die(self) -&gt; None:
if self.engine.player is self.entity:
death_message = "You died!"
else:
death_message = f"{self.entity.name} is dead!"
self.entity.char = "%"
self.entity.color = (191, 0, 0)
self.entity.blocks_movement = False
self.entity.ai = None
self.entity.name = f"remains of {self.entity.name}"
print(death_message)</span></pre>
</div>
</div>
<p>When the actor dies, we use the <code>die</code> method to do several things:</p>
<ul>
<li>Print out a message, indicating the death of the entity</li>
<li>Set the entitys character to “%” (most roguelikes use this for corpses)</li>
<li>Set its color to red (for a bloody, gory mess)</li>
<li>Set <code>blocks_movement</code> to <code>False</code>, so that the entities can walk over the corpse</li>
<li>Remove the AI from the entity, so itll be marked as dead and wont take any more turns.</li>
<li>Change the name to “remains of {entity name}”</li>
</ul>
<p>Run the project now, and enjoy slaughtering some Orcs and Trolls!</p>
<p><img src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/part-6-killing-enemies.png" alt="Part 6 - Killing Enemies"></p>
<p>As satisfying as it would be to end here, our work is not quite done. If you play the game a bit, youll notice two problems.</p>
<p>The first is that, sometimes, corpses actually cover up entities.</p>
<p><img src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/part-6-player-under-corpse.png" alt="Part 6 - Player under a Corpse"></p>
<p><em>The player is currently under the corpse in the screenshot.</em></p>
<p>This not only makes no sense, since the entities should be walking <em>over</em> the corpses, but it can confuse the player rather easily.</p>
<p>The other issue is much more severe. Try playing the game and letting yourself die on purpose.</p>
<p><img src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/part-6-dead-player.png" alt="Part 6 - Dead Player"></p>
<p>The player does indeed turn into a corpse, but… you can still move
around, and even attack enemies! This is because the game doesnt really
“end” at the moment when the player dies. The only thing that changes
is that the players AI component is set to <code>None</code>, but that isnt actually what controls the player, the <code>EventHandler</code> class does that.</p>
<p>Lets focus on the first issue first. Solving it is actually pretty easy. What well do is assign a value to each <code>Entity</code>,
and this value will represent which order the entities should be
rendered in. Lower values will be rendered first, and higher values will
be rendered after. Therefore, if we assign a low value to a corpse, it
will get drawn before an entity. If two things are on the same tile,
whatever gets drawn last will be what the player sees.</p>
<p>To create the render values well need, create a new file, called <code>render_order.py</code>, and put the following class in 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:#f92672">from</span> enum <span style="color:#f92672">import</span> auto, Enum
</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">RenderOrder</span>(Enum):
</span></span><span style="display:flex;"><span> CORPSE <span style="color:#f92672">=</span> auto()
</span></span><span style="display:flex;"><span> ITEM <span style="color:#f92672">=</span> auto()
</span></span><span style="display:flex;"><span> ACTOR <span style="color:#f92672">=</span> auto()
</span></span></code></pre></div><p><em>Note: Youll need Python 3.6 or higher for the <code>auto</code> function to work.</em></p>
<p><code>RenderOrder</code> is an <code>Enum</code>. An “Enum” is a set of named values that wont change, so its perfect for things like this. <code>auto</code> assigns incrementing integer values automatically, so we dont need to retype them if we add more values later on.</p>
<p>To use this new Enum, lets edit <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>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>from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from render_order import RenderOrder
</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> from components.ai import BaseAI
</span></span><span style="display:flex;"><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>T = TypeVar("T", bound="Entity")
</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> gamemap: GameMap
</span></span><span style="display:flex;"><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> gamemap: Optional[GameMap] = None,
</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> blocks_movement: bool = False,
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ render_order: RenderOrder = RenderOrder.CORPSE,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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><span style="color:#a6e22e">+ self.render_order = render_order
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> if gamemap:
</span></span><span style="display:flex;"><span> # If gamemap isn't provided now then it will be set later.
</span></span><span style="display:flex;"><span> self.gamemap = gamemap
</span></span><span style="display:flex;"><span> gamemap.entities.add(self)
</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><span style="color:#a6e22e">+ render_order=RenderOrder.ACTOR,
</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 __future__ import annotations
import copy
from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING
<span class="new-text">from render_order import RenderOrder</span>
if TYPE_CHECKING:
from components.ai import BaseAI
from components.fighter import Fighter
from game_map import GameMap
T = TypeVar("T", bound="Entity")
class Entity:
"""
A generic object to represent players, enemies, items, etc.
"""
gamemap: GameMap
def __init__(
self,
gamemap: Optional[GameMap] = None,
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,
<span class="new-text">render_order: RenderOrder = RenderOrder.CORPSE,</span>
):
self.x = x
self.y = y
self.char = char
self.color = color
self.name = name
self.blocks_movement = blocks_movement
<span class="new-text">self.render_order = render_order</span>
if gamemap:
# If gamemap isn't provided now then it will be set later.
self.gamemap = gamemap
gamemap.entities.add(self)
...
class Actor(Entity):
def __init__(
self,
*,
x: int = 0,
y: int = 0,
char: str = "?",
color: Tuple[int, int, int] = (255, 255, 255),
name: str = "<unnamed>",
ai_cls: Type[BaseAI],
fighter: Fighter
):
super().__init__(
x=x,
y=y,
char=char,
color=color,
name=name,
blocks_movement=True,
<span class="new-text">render_order=RenderOrder.ACTOR,</span>
)</unnamed></pre>
</div>
</div>
<p>Were now passing the render order to the <code>Entity</code> class, with a default of <code>CORPSE</code>. Notice that we dont pass it to <code>Actor</code>, and instead, assume that the actors default will be the <code>ACTOR</code> value.</p>
<p>In order to actually take advantage of the rendering order, well need to modify the part of <code>GameMap</code> that renders the entities to the screen. Modify the <code>render</code> method in <code>GameMap</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> def render(self, console: Console) -&gt; None:
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span> Renders the map.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> If a tile is in the "visible" array, then draw it with the "light" colors.
</span></span><span style="display:flex;"><span> If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
</span></span><span style="display:flex;"><span> Otherwise, the default is "SHROUD".
</span></span><span style="display:flex;"><span> """
</span></span><span style="display:flex;"><span> console.tiles_rgb[0:self.width, 0:self.height] = np.select(
</span></span><span style="display:flex;"><span> condlist=[self.visible, self.explored],
</span></span><span style="display:flex;"><span> choicelist=[self.tiles["light"], self.tiles["dark"]],
</span></span><span style="display:flex;"><span> default=tile_types.SHROUD
</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">+ entities_sorted_for_rendering = sorted(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entities, key=lambda x: x.render_order.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"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- for entity in self.entities:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ for entity in entities_sorted_for_rendering:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> if self.visible[entity.x, entity.y]:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ console.print(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=entity.x, y=entity.y, string=entity.char, fg=entity.color
</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> ...
def render(self, console: Console) -&gt; None:
"""
Renders the map.
If a tile is in the "visible" array, then draw it with the "light" colors.
If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
Otherwise, the default is "SHROUD".
"""
console.tiles_rgb[0:self.width, 0:self.height] = np.select(
condlist=[self.visible, self.explored],
choicelist=[self.tiles["light"], self.tiles["dark"]],
default=tile_types.SHROUD
)
<span class="new-text">entities_sorted_for_rendering = sorted(
self.entities, key=lambda x: x.render_order.value
)</span>
<span class="crossed-out-text">for entity in self.entities:</span>
<span class="new-text">for entity in entities_sorted_for_rendering:</span>
if self.visible[entity.x, entity.y]:
<span class="crossed-out-text">console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)</span>
<span class="new-text">console.print(
x=entity.x, y=entity.y, string=entity.char, fg=entity.color
)</span></pre>
</div>
</div>
<p></p>
<p>The <code>sorted</code> function takes two arguments: The collection to sort, and the function used to sort it. By using <code>key</code> in <code>sorted</code>, were defining a custom way to sort the <code>self.entities</code>, which in this case, were using a <code>lambda</code>
function (basically, a function thats limited to one line that we
dont need to write a formal definition for). The lambda function itself
tells <code>sorted</code> to sort by the value of <code>render_order</code>. Since the <code>RenderOrder</code>
enum defines its order from 1 (Corpse, lowest) to 3 (Actor, highest),
corpses should be sent to the front of the sorted list. That way, when
rendering, theyll get drawn first, so if theres something else on top
of them, theyll get overwritten, and well just see the <code>Actor</code> instead of the corpse.</p>
<p>Last thing we need to do is rewrite the <code>render_order</code> of an entity when it dies. Go back to the <code>Fighter</code> class 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 TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from components.base_component import BaseComponent
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from render_order import RenderOrder
</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> 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>class Fighter(BaseComponent):
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> self.entity.ai = None
</span></span><span style="display:flex;"><span> self.entity.name = f"remains of {self.entity.name}"
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.render_order = RenderOrder.CORPSE
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> print(death_message)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import TYPE_CHECKING
from components.base_component import BaseComponent
<span class="new-text">from render_order import RenderOrder</span>
if TYPE_CHECKING:
from entity import Actor
class Fighter(BaseComponent):
...
...
self.entity.ai = None
self.entity.name = f"remains of {self.entity.name}"
<span class="new-text">self.entity.render_order = RenderOrder.CORPSE</span>
print(death_message)</pre>
</div>
</div>
<p>Run the project now, and the corpse ordering issue should be resolved.</p>
<p>Now, onto the more important issue: solving the players death.</p>
<p>One thing that would be helpful right now is being able to see the
players HP. Otherwise, the player will just kinda drop dead after a
while, and itll be difficult for the player to know how close they are
to deaths door.</p>
<p>Add the following line to the <code>render</code> function in the <code>Engine</code> 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><span style="color:#f92672">- from entity import Entity
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ from entity import Actor
</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 Engine:
</span></span><span style="display:flex;"><span> game_map: GameMap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, player: Entity):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, player: Actor):
</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 render(self, console: Console, context: Context) -&gt; None:
</span></span><span style="display:flex;"><span> self.game_map.render(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=1,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=47,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ string=f"HP: {self.player.fighter.hp}/{self.player.fighter.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"></span>
</span></span><span style="display:flex;"><span> context.present(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console.clear()
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>if TYPE_CHECKING:
<span class="crossed-out-text">from entity import Entity</span>
<span class="new-text">from entity import Actor</span>
from game_map import GameMap
class Engine:
game_map: GameMap
<span class="crossed-out-text">def __init__(self, player: Entity):</span>
<span class="new-text">def __init__(self, player: Actor):</span>
...
def render(self, console: Console, context: Context) -&gt; None:
self.game_map.render(console)
<span class="new-text">console.print(
x=1,
y=47,
string=f"HP: {self.player.fighter.hp}/{self.player.fighter.max_hp}",
)</span>
context.present(console)
console.clear()</pre>
</div>
</div>
<p>Pretty simple. Were printing the players HP current health over
maximum health below the map. Its not the most attractive looking
health display, thats for sure, but it should suffice for now. A better
looking way to show the characters health is coming shortly anyway, in
the next chapter.</p>
<p>Notice that we also updated the type hint for the <code>player</code> argument in the Engines <code>__init__</code> function.</p>
<p>The health indicator is great and all, but our player is still
animated after death. Theres a few ways to handle this, but the way
well go with is swapping out the <code>EventHandler</code> class. Why?
Because what we want to do right now is disallow the player from moving
around after dying. An easy way to do that is to stop reacting to the
movement keypresses. By switching to a different <code>EventHandler</code>, we can do just that.</p>
<p>What well want to do is actually modify our existing <code>EventHandler</code> to be a base class, and inherit from it in two new classes: <code>MainGameEventHandler</code>, and <code>GameOverEventHandler</code>. <code>MainGameEventHandler</code> will actually do what our current implementation of <code>EventHandler</code> does, and <code>GameOverEventHandler</code> will handle things when the main character meets his or her untimely demise.</p>
<p>Open up <code>input_handlers.py</code> and make the following adjustments:</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> 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) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise NotImplementedError()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise SystemExit()
</span></span></span><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 MainGameEventHandler(EventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> def handle_events(self) -&gt; None:
</span></span><span style="display:flex;"><span> for event in tcod.event.wait():
</span></span><span style="display:flex;"><span> action = self.dispatch(event)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if action is None:
</span></span><span style="display:flex;"><span> continue
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> action.perform()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self.engine.handle_enemy_turns()
</span></span><span style="display:flex;"><span> self.engine.update_fov() # Update the FOV before the players next action.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- raise SystemExit()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
</span></span><span style="display:flex;"><span> action: Optional[Action] = None
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> key = event.sym
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> player = self.engine.player
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if key in MOVE_KEYS:
</span></span><span style="display:flex;"><span> dx, dy = MOVE_KEYS[key]
</span></span><span style="display:flex;"><span> action = BumpAction(player, dx, dy)
</span></span><span style="display:flex;"><span> elif key in WAIT_KEYS:
</span></span><span style="display:flex;"><span> action = WaitAction(player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_ESCAPE:
</span></span><span style="display:flex;"><span> action = EscapeAction(player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> # No valid key was pressed
</span></span><span style="display:flex;"><span> return action
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class GameOverEventHandler(EventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def handle_events(self) -&gt; None:
</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">+ 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">+ if action is None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ continue
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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"></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">+ action: Optional[Action] = 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">+ key = event.sym
</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 key == tcod.event.K_ESCAPE:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = EscapeAction(self.engine.player)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # No valid key was pressed
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return action
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class EventHandler(tcod.event.EventDispatch[Action]):
def __init__(self, engine: Engine):
self.engine = engine
<span class="new-text">def handle_events(self) -&gt; None:
raise NotImplementedError()
def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
raise SystemExit()</span>
<span class="new-text">class MainGameEventHandler(EventHandler):</span>
def handle_events(self) -&gt; None:
for event in tcod.event.wait():
action = self.dispatch(event)
if action is None:
continue
action.perform()
self.engine.handle_enemy_turns()
self.engine.update_fov() # Update the FOV before the players next action.
<span class="crossed-out-text">def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:</span>
<span class="crossed-out-text">raise SystemExit()</span>
def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
action: Optional[Action] = None
key = event.sym
player = self.engine.player
if key in MOVE_KEYS:
dx, dy = MOVE_KEYS[key]
action = BumpAction(player, dx, dy)
elif key in WAIT_KEYS:
action = WaitAction(player)
elif key == tcod.event.K_ESCAPE:
action = EscapeAction(player)
# No valid key was pressed
return action
<span class="new-text">class GameOverEventHandler(EventHandler):
def handle_events(self) -&gt; None:
for event in tcod.event.wait():
action = self.dispatch(event)
if action is None:
continue
action.perform()
def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
action: Optional[Action] = None
key = event.sym
if key == tcod.event.K_ESCAPE:
action = EscapeAction(self.engine.player)
# No valid key was pressed
return action</span></pre>
</div>
</div>
<p><code>EventHandler</code> is now the base class for our other two classes.</p>
<p><code>MainGameEventHandler</code> is almost identical to our original <code>EventHandler</code> class, except that it doesnt need to implement <code>ev_quit</code>, as <code>EventHandler</code> takes care of that just fine.</p>
<p><code>GameOverEventHandler</code> is whats really new here. It doesnt look terribly different from <code>MainGameEventHandler</code>, except for a few key differences.</p>
<ul>
<li>After performing its actions, it doesnt call the enemy turns nor update the FOV.</li>
<li>It also doesnt respond to the movement keys, just <code>Esc</code>, so the player can still exit the game.</li>
</ul>
<p>Because were replacing our old implementation of <code>EventHandler</code> with <code>MainGameEventHandler</code>, well need to adjust <code>engine.py</code> to use <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>from tcod.map import compute_fov
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from input_handlers import EventHandler
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from input_handlers import MainGameEventHandler
</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> from entity import Actor
</span></span><span style="display:flex;"><span> from game_map import GameMap
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from input_handlers import EventHandler
</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 Engine:
</span></span><span style="display:flex;"><span> game_map: GameMap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def __init__(self, player: Actor):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.event_handler: EventHandler = EventHandler(self)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.event_handler: EventHandler = MainGameEventHandler(self)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.player = player
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from tcod.map import compute_fov
<span class="crossed-out-text">from input_handlers import EventHandler</span>
<span class="new-text">from input_handlers import MainGameEventHandler</span>
if TYPE_CHECKING:
from entity import Actor
from game_map import GameMap
<span class="new-text">from input_handlers import EventHandler</span>
class Engine:
game_map: GameMap
def __init__(self, player: Actor):
<span class="crossed-out-text">self.event_handler: EventHandler = EventHandler(self)</span>
<span class="new-text">self.event_handler: EventHandler = MainGameEventHandler(self)</span>
self.player = player</pre>
</div>
</div>
<p>Lastly, we can use the <code>GameOverEventHandler</code> in <code>fighter.py</code> to ensure the player cannot move after death:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from typing import TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from components.base_component import BaseComponent
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from input_handlers import GameOverEventHandler
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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 entity import Actor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class Fighter(BaseComponent):
</span></span><span style="display:flex;"><span> ...
</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> if self.engine.player is self.entity:
</span></span><span style="display:flex;"><span> death_message = "You died!"
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = GameOverEventHandler(self.engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> else:
</span></span><span style="display:flex;"><span> death_message = f"{self.entity.name} is dead!"
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import TYPE_CHECKING
from components.base_component import BaseComponent
<span class="new-text">from input_handlers import GameOverEventHandler</span>
from render_order import RenderOrder
if TYPE_CHECKING:
from entity import Actor
class Fighter(BaseComponent):
...
def die(self) -&gt; None:
if self.engine.player is self.entity:
death_message = "You died!"
<span class="new-text">self.engine.event_handler = GameOverEventHandler(self.engine)</span>
else:
death_message = f"{self.entity.name} is dead!"</pre>
</div>
</div>
<p>And with that last change, the main character should die, for real
this time! Youll be unable to move or attack, but you can still exit
the game as normal.</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-6">click
here</a>.</p>
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-7">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%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
<script src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
</body></html>