McRogueFace/roguelike_tutorial/rogueliketutorials.com/Part 5 - Placing Enemies an...

1591 lines
87 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 5 - Placing Enemies and kicking them (harmlessly) · 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="What good is a dungeon with no monsters to bash? This chapter will focus on placing the enemies throughout the dungeon, and setting them up to be attacked (the actual attacking part well save for next time).
When were building our dungeon, well need to place the enemies in the rooms. In order to do that, we will need to make a change to the way entities are stored in our game.">
<meta name="keywords" content="">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Part 5 - Placing Enemies and kicking them (harmlessly)">
<meta name="twitter:description" content="What good is a dungeon with no monsters to bash? This chapter will focus on placing the enemies throughout the dungeon, and setting them up to be attacked (the actual attacking part well save for next time).
When were building our dungeon, well need to place the enemies in the rooms. In order to do that, we will need to make a change to the way entities are stored in our game.">
<meta property="og:title" content="Part 5 - Placing Enemies and kicking them (harmlessly)">
<meta property="og:description" content="What good is a dungeon with no monsters to bash? This chapter will focus on placing the enemies throughout the dungeon, and setting them up to be attacked (the actual attacking part well save for next time).
When were building our dungeon, well need to place the enemies in the rooms. In order to do that, we will need to make a change to the way entities are stored in our game.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-5/"><meta property="article:section" content="tutorials">
<meta property="article:published_time" content="2020-06-29T00:00:00+00:00">
<meta property="article:modified_time" content="2020-06-29T00:00:00+00:00">
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-5/">
<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%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%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%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%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-5/">
Part 5 - Placing Enemies and kicking them (harmlessly)
</a>
</h1>
</header>
<p>What good is a dungeon with no monsters to bash? This chapter
will focus on placing the enemies throughout the dungeon, and setting
them up to be attacked (the actual attacking part well save for next
time).</p>
<p>When were building our dungeon, well need to place the enemies in
the rooms. In order to do that, we will need to make a change to the way
<code>entities</code> are stored in our game. Currently, theyre saved in the <code>Engine</code>
class. However, for the sake of placing enemies in the dungeon, and
when we get to the part where we move between dungeon floors, it will be
better to store them in the <code>GameMap</code> class. That way, the
map has access to the entities directly, and we can preserve which
entities are on which floors fairly easily.</p>
<p>Start by modifying <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><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 Iterable, 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>import tile_types
</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"></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):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
</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><span style="color:#a6e22e">+ self.entities = set(entities)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
</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 Iterable, TYPE_CHECKING</span>
import numpy as np # type: ignore
from tcod.console import Console
import tile_types
<span class="new-text">if TYPE_CHECKING:
from entity import Entity</span>
class GameMap:
<span class="crossed-out-text">def __init__(self, width: int, height: int):</span>
<span class="new-text">def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):</span>
self.width, self.height = width, height
<span class="new-text">self.entities = set(entities)</span>
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")</pre>
</div>
</div>
<p>Then, lets modify <code>Engine</code> to remove the <code>entities</code> from it:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#f92672">-from typing import Set, Iterable, Any
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Iterable, Any
</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:#f92672">- def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- self.entities = entities
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> self.event_handler = event_handler
</span></span><span style="display:flex;"><span> self.game_map = game_map
</span></span><span style="display:flex;"><span> self.player = player
</span></span><span style="display:flex;"><span> self.update_fov()
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="crossed-out-text">from typing import Set, Iterable, Any</span>
<span class="new-text">from typing import Iterable, Any</span>
class Engine:
<span class="crossed-out-text">def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):</span>
<span class="new-text">def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):</span>
<span class="crossed-out-text">self.entities = entities</span>
self.event_handler = event_handler
self.game_map = game_map
self.player = player
self.update_fov()</pre>
</div>
</div>
<p>Because weve modified the definition of <code>Engine.__init__</code>, we need to modify <code>main.py</code> where we create our <code>game_map</code> variable. We might as well remove that <code>npc</code> as well, since we wont be needing it anymore.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
</span></span><span style="display:flex;"><span><span style="color:#f92672">- npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- entities = {npc, player}
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> game_map = generate_dungeon(
</span></span><span style="display:flex;"><span> max_rooms=max_rooms,
</span></span><span style="display:flex;"><span> room_min_size=room_min_size,
</span></span><span style="display:flex;"><span> room_max_size=room_max_size,
</span></span><span style="display:flex;"><span> map_width=map_width,
</span></span><span style="display:flex;"><span> map_height=map_height,
</span></span><span style="display:flex;"><span> player=player,
</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">- engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> with tcod.context.new_terminal(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
<span class="crossed-out-text">npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))</span>
<span class="crossed-out-text">entities = {npc, player}</span>
game_map = generate_dungeon(
max_rooms=max_rooms,
room_min_size=room_min_size,
room_max_size=room_max_size,
map_width=map_width,
map_height=map_height,
player=player,
)
<span class="crossed-out-text">engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)</span>
<span class="new-text">engine = Engine(event_handler=event_handler, game_map=game_map, player=player)</span>
with tcod.context.new_terminal(
...</pre>
</div>
</div>
<p>We can remove the part in <code>Engine.render</code> that loops through the entities and renders the ones that are visible. That part will also be handled by the <code>GameMap</code> from now on.</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 Engine:
</span></span><span style="display:flex;"><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:#f92672">- for entity in self.entities:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- # Only print entities that are in the FOV
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- if self.game_map.visible[entity.x, entity.y]:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- console.print(entity.x, entity.y, entity.char, fg=entity.color)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class Engine:
...
def render(self, console: Console, context: Context) -&gt; None:
self.game_map.render(console)
<span class="crossed-out-text">for entity in self.entities:</span>
<span class="crossed-out-text"># Only print entities that are in the FOV</span>
<span class="crossed-out-text">if self.game_map.visible[entity.x, entity.y]:</span>
<span class="crossed-out-text">console.print(entity.x, entity.y, entity.char, fg=entity.color)</span></pre>
</div>
</div>
<p>We can move this block into <code>GameMap.render</code>, though take note that the line that checks for visibility has a slight change: it goes from:</p>
<p><code>if self.game_map.visible[entity.x, entity.y]:</code></p>
<p>To:</p>
<p><code>if self.visible[entity.x, entity.y]:</code>.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class GameMap:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</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">+ for entity in self.entities:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Only print entities that are in the FOV
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.visible[entity.x, entity.y]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class GameMap:
...
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">for entity in self.entities:
# Only print entities that are in the FOV
if self.visible[entity.x, entity.y]:
console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)</span></pre>
</div>
</div>
<p>Finally, we need to alter the part in <code>generate_dungeon</code> that creates the instance of <code>GameMap</code>, so that the <code>player</code> is passed into the <code>entities</code> argument.</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 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> player: Entity,
</span></span><span style="display:flex;"><span>) -&gt; GameMap:
</span></span><span style="display:flex;"><span> """Generate a new dungeon map."""
</span></span><span style="display:flex;"><span><span style="color:#f92672">- dungeon = GameMap(map_width, map_height)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ dungeon = GameMap(map_width, map_height, entities=[player])
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> rooms: List[RectangularRoom] = []
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>def generate_dungeon(
max_rooms: int,
room_min_size: int,
room_max_size: int,
map_width: int,
map_height: int,
player: Entity,
) -&gt; GameMap:
"""Generate a new dungeon map."""
<span class="crossed-out-text">dungeon = GameMap(map_width, map_height)</span>
<span class="new-text">dungeon = GameMap(map_width, map_height, entities=[player])</span>
rooms: List[RectangularRoom] = []
...</pre>
</div>
</div>
<p>If you run the project now, things should look the same as before, minus the NPC that we had earlier for testing.</p>
<p>Now, moving on to actually placing monsters in our dungeon. Our logic
will be simple enough: For each room thats created in our dungeon,
well place a random number of enemies, between 0 and a maximum (2 for
now). Well make it so that theres an 80% chance of spawning an Orc (a
weaker enemy) and a 20% chance of it being a Troll (a stronger enemy).</p>
<p>In order to specify the maximum number of monsters that can be spawned into a room, lets create a new variable, <code>max_monsters_per_room</code>, and place it in <code>main.py</code>. Well also modify our call to <code>generate_dungeon</code> to pass this new variable in.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> max_rooms = 30
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_monsters_per_room = 2
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> tileset = tcod.tileset.load_tilesheet(
</span></span><span style="display:flex;"><span> "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> event_handler = EventHandler()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> game_map = generate_dungeon(
</span></span><span style="display:flex;"><span> max_rooms=max_rooms,
</span></span><span style="display:flex;"><span> room_min_size=room_min_size,
</span></span><span style="display:flex;"><span> room_max_size=room_max_size,
</span></span><span style="display:flex;"><span> map_width=map_width,
</span></span><span style="display:flex;"><span> map_height=map_height,
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_monsters_per_room=max_monsters_per_room,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> player=player
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
max_rooms = 30
<span class="new-text">max_monsters_per_room = 2</span>
tileset = tcod.tileset.load_tilesheet(
"dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
)
event_handler = EventHandler()
player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
game_map = generate_dungeon(
max_rooms=max_rooms,
room_min_size=room_min_size,
room_max_size=room_max_size,
map_width=map_width,
map_height=map_height,
<span class="new-text">max_monsters_per_room=max_monsters_per_room,</span>
player=player
)
engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
...</pre>
</div>
</div>
<p>Pretty straightforward. Now well need to modify the definition of <code>generate_dungeon</code> to take this new variable, like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>def 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><span style="color:#a6e22e">+ max_monsters_per_room: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> player: Entity,
</span></span><span style="display:flex;"><span>) -&gt; GameMap:
</span></span><span style="display:flex;"><span> """Generate a new dungeon map."""
</span></span><span style="display:flex;"><span> dungeon = GameMap(map_width, map_height, entities=[player])
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>def generate_dungeon(
max_rooms: int,
room_min_size: int,
room_max_size: int,
map_width: int,
map_height: int,
<span class="new-text">max_monsters_per_room: int,</span>
player: Entity,
) -&gt; GameMap:
"""Generate a new dungeon map."""
dungeon = GameMap(map_width, map_height, entities=[player])</pre>
</div>
</div>
<p>Easy enough, but now how do we actually place the enemies?</p>
<p>After weve created our room, well want to call a function to put the entities in their places. Lets call the function <code>place_entities</code>, and it will take three arguments: The <code>RectangularRoom</code> that weve created, the <code>dungeon</code> so that it can add the entities to it (remember that <code>dungeon</code> is an instance of <code>GameMap</code>, which now holds entities), and the <code>max_monsters_per_room</code>, so that we know how many monsters to make.</p>
<p>While we havent written the function yet, lets place our call to it in <code>generate_dungeon</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> dungeon.tiles[x, y] = tile_types.floor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ place_entities(new_room, dungeon, max_monsters_per_room)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> # Finally, append the new room to the list.
</span></span><span style="display:flex;"><span> rooms.append(new_room)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> return dungeon
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
dungeon.tiles[x, y] = tile_types.floor
<span class="new-text">place_entities(new_room, dungeon, max_monsters_per_room)</span>
# Finally, append the new room to the list.
rooms.append(new_room)
return dungeon</pre>
</div>
</div>
<p>Now, lets write the <code>place_entities</code> function so that this actually works.</p>
<p>Our first version of <code>place_entities</code> wont actually place
the entities. Why not? Because well need to do a few other things to
make spawning the entities here work. However, we can at least fill in
most of the function, and skip over the part that actually creates the
entities for the moment.</p>
<p>Create the function like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class 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><span style="color:#a6e22e">+def place_entities(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ number_of_monsters = random.randint(0, maximum_monsters)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for i in range(number_of_monsters):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = random.randint(room.x1 + 1, room.x2 - 1)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y = random.randint(room.y1 + 1, room.y2 - 1)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if random.random() &lt; 0.8:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ pass # TODO: Place an Orc here
</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">+ pass # TODO: Place a Troll here
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def tunnel_between(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class RectangularRoom:
...
<span class="new-text">def place_entities(
room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,
) -&gt; None:
number_of_monsters = random.randint(0, maximum_monsters)
for i in range(number_of_monsters):
x = random.randint(room.x1 + 1, room.x2 - 1)
y = random.randint(room.y1 + 1, room.y2 - 1)
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
if random.random() &lt; 0.8:
pass # TODO: Place an Orc here
else:
pass # TODO: Place a Troll here</span>
def tunnel_between(
...</pre>
</div>
</div>
<p>The first line in the function takes a random number between 0 and
the provided maximum (2, in this case). From there, it iterates from 0
to the number.</p>
<p>We select a random <code>x</code> and <code>y</code> to place the
entity, and do a quick check to make sure theres no other entities in
that location before dropping the enemy there. This is to ensure we
dont get stacks of enemies.</p>
<p>As described earlier, there should be an 80% chance of there being an Orc, and 20% chance for a Troll. For now, were using <code>pass</code> to skip over actually putting them down, because that requires a bit more work first.</p>
<p>Theres a few ways we could go about creating the new entities.
Assuming that every Orc and Troll we spawn will always have the same
attributes as their brethren, we can create initial instances of <code>orc</code> and <code>troll</code>, then copy those every time we want to create a new one.</p>
<p>Why not just create the entities right here in the function? We could
(the 1st version of this tutorial does, in fact), but thats a bit of a
pain to go back and edit. Imagine if you had 100 enemies in your game
at some point in the future. Would you rather search for those entity
definitions in one file that <em>only</em> exists to define entities, or
try finding it in the file that generates our dungeon? Not to mention,
what happens if you want to create a new dungeon generator? Are you
going to copy over the entity definitions and have them defined in two
places?</p>
<p>Lets modify <code>Entity</code> to prepare for this new copying method. 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><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">+import copy
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import Tuple
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Tuple, TypeVar, TYPE_CHECKING
</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 TYPE_CHECKING:
</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 style="color:#a6e22e">+T = TypeVar("T", bound="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 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 style="color:#f92672">- def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]):
</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,
</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">+ blocks_movement: bool = False,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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><span style="color:#a6e22e">+ self.name = name
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.blocks_movement = blocks_movement
</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 spawn(self: T, gamemap: GameMap, x: int, y: int) -&gt; T:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Spawn a copy of this instance at the given location."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clone = copy.deepcopy(self)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clone.x = x
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clone.y = y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ gamemap.entities.add(clone)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return clone
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def move(self, dx: int, dy: int) -&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
import copy</span>
<span class="crossed-out-text">from typing import Tuple</span>
<span class="new-text">from typing import Tuple, TypeVar, TYPE_CHECKING
if TYPE_CHECKING:
from game_map import GameMap
T = TypeVar("T", bound="Entity")</span>
class Entity:
"""
A generic object to represent players, enemies, items, etc.
"""
<span class="crossed-out-text">def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]):</span>
<span class="new-text">def __init__(
self,
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>
self.x = x
self.y = y
self.char = char
self.color = color
<span class="new-text">self.name = name
self.blocks_movement = blocks_movement</span>
<span class="new-text">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
gamemap.entities.add(clone)
return clone</span>
def move(self, dx: int, dy: int) -&gt; None:
...</pre>
</div>
</div>
<p>Weve added two new attributes to <code>Entity</code>: <code>name</code> and <code>blocks_movement</code>. <code>name</code> is straightforward: its what the Entity is called. <code>blocks_movement</code> describes whether or not this <code>Entity</code> can be moved over or not. Enemies will have <code>blocks_movement</code> set to <code>True</code>, while in the future, things like consumable items and equipment will be set to <code>False</code>.</p>
<p>Notice that weve also provided defaults for each of the attributes in the <code>__init__</code> function as well, whereas we were not before. This is because well soon not need to pass <code>x</code> and <code>y</code> during the initialization. More on that in a second.</p>
<p>The more complex section is the <code>spawn</code> method. It takes the <code>GameMap</code> instance, along with <code>x</code> and <code>y</code> for locations. It then creates a <code>clone</code> of the instance of <code>Entity</code>, and assigns the <code>x</code> and <code>y</code> variables to it (this is why we dont need <code>x</code> and <code>y</code> in the initializer anymore, theyre set here). It then adds the entity to the <code>gamemap</code>s entities, and returns the <code>clone</code>.</p>
<p>This new <code>spawn</code> method will probably make a lot more sense by putting it to use. To do that, lets create a new file, called <code>entity_factories.py</code>, and fill it with the following contents:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">from</span> entity <span style="color:#f92672">import</span> Entity
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>player <span style="color:#f92672">=</span> Entity(char<span style="color:#f92672">=</span><span style="color:#e6db74">"@"</span>, color<span style="color:#f92672">=</span>(<span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>), name<span style="color:#f92672">=</span><span style="color:#e6db74">"Player"</span>, blocks_movement<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>orc <span style="color:#f92672">=</span> Entity(char<span style="color:#f92672">=</span><span style="color:#e6db74">"o"</span>, color<span style="color:#f92672">=</span>(<span style="color:#ae81ff">63</span>, <span style="color:#ae81ff">127</span>, <span style="color:#ae81ff">63</span>), name<span style="color:#f92672">=</span><span style="color:#e6db74">"Orc"</span>, blocks_movement<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>troll <span style="color:#f92672">=</span> Entity(char<span style="color:#f92672">=</span><span style="color:#e6db74">"T"</span>, color<span style="color:#f92672">=</span>(<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">127</span>, <span style="color:#ae81ff">0</span>), name<span style="color:#f92672">=</span><span style="color:#e6db74">"Troll"</span>, blocks_movement<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span></code></pre></div><p>This is where were defining our entities. <code>player</code> should look familiar, and <code>orc</code> and <code>troll</code> are not all that different, besides their characters and colors.</p>
<p>These are the instances well be cloning to create our new entities. Using these, we can at last fill in our <code>place_entities</code> function back in <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>...
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import entity_factories
</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> ...
</span></span><span style="display:flex;"><span> if random.random() &lt; 0.8:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- pass # TODO: Place an Orc here
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ entity_factories.orc.spawn(dungeon, x, y)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> else:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- pass # TODO: Place a Troll here
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ entity_factories.troll.spawn(dungeon, x, y)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
import tcod
<span class="new-text">import entity_factories</span>
from game_map import GameMap
...
...
if random.random() &lt; 0.8:
<span class="crossed-out-text">pass # TODO: Place an Orc here</span>
<span class="new-text">entity_factories.orc.spawn(dungeon, x, y)</span>
else:
<span class="crossed-out-text">pass # TODO: Place a Troll here</span>
<span class="new-text">entity_factories.troll.spawn(dungeon, x, y)</span></pre>
</div>
</div>
<p>Lets also modify the way we create the <code>player</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><span style="color:#a6e22e">+import copy
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>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">+import entity_factories
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from input_handlers import EventHandler
</span></span><span style="display:flex;"><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></span><span style="display:flex;"><span> event_handler = EventHandler()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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></span><span style="display:flex;"><span> game_map = generate_dungeon(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>#!/usr/bin/env python3
<span class="new-text">import copy</span>
import tcod
from engine import Engine
<span class="crossed-out-text">from entity import Entity</span>
<span class="new-text">import entity_factories</span>
from input_handlers import EventHandler
from procgen import generate_dungeon
...
...
event_handler = EventHandler()
<span class="crossed-out-text">player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))</span>
<span class="new-text">player = copy.deepcopy(entity_factories.player)</span>
game_map = generate_dungeon(
...</pre>
</div>
</div>
<p><em>Note: We cant use <code>player.spawn</code> here, because <code>spawn</code> requires the <code>GameMap</code>, which isnt created until after we create the player.</em></p>
<p>With that, your dungeon should now be populated with enemies.</p>
<p><img src="Part%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%20%C2%B7%20Roguelike%20Tutorials_files/part-5-monsters.png" alt="Font File"></p>
<p>Theyre… not exactly intimidating, are they? In fact, they dont
really do much of anything right now. But thats okay, well work on
that.</p>
<p>The first step towards making our monsters scarier is making them
stand their ground… literally! The player can currently walk over (or
under) the enemies by simply moving into the same space. Lets fix that,
and ensure that when the player tries to move towards an enemy, we
attack instead.</p>
<p>To begin, we need to determine if the space the player is trying to
move into has an Entity in it. Not just any Entity, however: well check
if the Entity has “blocks_movement” set to <code>True</code>. If it does, our player cant move there, and tries to attack instead.</p>
<p>Add the following to the map:</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, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Iterable, 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>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 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> def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
</span></span><span style="display:flex;"><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> self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
</span></span><span style="display:flex;"><span> self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ 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:#a6e22e">+ for entity in self.entities:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ 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:#a6e22e">+ return 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">+ return None
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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> ...
</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, TYPE_CHECKING</span>
<span class="new-text">from typing import Iterable, Optional, TYPE_CHECKING</span>
import numpy as np # type: ignore
from tcod.console import Console
import tile_types
if TYPE_CHECKING:
from entity import Entity
class GameMap:
def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
self.width, self.height = width, height
self.entities = set(entities)
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
<span class="new-text">def get_blocking_entity_at_location(self, location_x: int, location_y: int) -&gt; Optional[Entity]:
for entity in self.entities:
if entity.blocks_movement and entity.x == location_x and entity.y == location_y:
return entity
return None</span>
def in_bounds(self, x: int, y: int) -&gt; bool:
...</pre>
</div>
</div>
<p>This new function iterates through all the <code>entities</code>, and if one is found that both blocks movement and occupies the given <code>location_x</code> and <code>location_y</code> coordinates, it returns that Entity. Otherwise, we return <code>None</code> instead.</p>
<p>Where can we check if a tile is occupied or not? And what do we do if it is?</p>
<p>One way to handle all this is to modify our “actions” a bit. Our current <code>MovementAction</code>
doesnt take into account what occupies the tile were moving into.
Thats fine, it doesnt necessarily need to, but there probably should
be an action that does. What if we created an <code>Action</code> subclass that could tell what was in the tile, and call either <code>MovementAction</code> if it was empty, or some other “attack” action if it wasnt?</p>
<p>Lets do a few things. Well start by defining a new class, called <code>ActionWithDirection</code>, which will actually become the new superclass for <code>MovementAction</code>. This new class will take the initializer from <code>MovementAction</code>, but wont implement its own <code>perform</code> method. It looks 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>class EscapeAction(Action):
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span><span style="display:flex;"><span> raise SystemExit()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class ActionWithDirection(Action):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(self, dx: int, dy: int):
</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"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.dx = dx
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.dy = 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">+ def perform(self, engine: Engine, entity: Entity) -&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></span><span style="display:flex;"><span><span style="color:#f92672">-class MovementAction(Action):
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+class MovementAction(ActionWithDirection):
</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><span style="color:#f92672">- self.dx = dx
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.dy = dy
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span><span style="display:flex;"><span> dest_x = entity.x + self.dx
</span></span><span style="display:flex;"><span> dest_y = entity.y + self.dy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if not engine.game_map.in_bounds(dest_x, dest_y):
</span></span><span style="display:flex;"><span> return # Destination is out of bounds.
</span></span><span style="display:flex;"><span> if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
</span></span><span style="display:flex;"><span> return # Destination is blocked by a tile.
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return # Destination is blocked by an entity.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> entity.move(self.dx, self.dy)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
class EscapeAction(Action):
def perform(self, engine: Engine, entity: Entity) -&gt; None:
raise SystemExit()
<span class="new-text">class ActionWithDirection(Action):
def __init__(self, dx: int, dy: int):
super().__init__()
self.dx = dx
self.dy = dy
def perform(self, engine: Engine, entity: Entity) -&gt; None:
raise NotImplementedError()</span>
<span class="crossed-out-text">class MovementAction(Action):</span>
<span class="new-text">class MovementAction(ActionWithDirection):</span>
<span class="crossed-out-text">def __init__(self, dx: int, dy: int):</span>
<span class="crossed-out-text">super().__init__()</span>
<span class="crossed-out-text">self.dx = dx</span>
<span class="crossed-out-text">self.dy = dy</span>
def perform(self, engine: Engine, entity: Entity) -&gt; None:
dest_x = entity.x + self.dx
dest_y = entity.y + self.dy
if not engine.game_map.in_bounds(dest_x, dest_y):
return # Destination is out of bounds.
if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
return # Destination is blocked by a tile.
<span class="new-text">if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
return # Destination is blocked by an entity.</span>
entity.move(self.dx, self.dy)</pre>
</div>
</div>
<p>Notice that weve added an extra check in <code>MovementAction</code>
to ensure were not moving into a space with a blocking entity.
Theoretically, this bit of code wont ever trigger, but its nice to
have it there as a safeguard.</p>
<p>But wait, <code>MovementAction</code> still doesnt do anything differently. So whats the point? Well, now we can use the new <code>ActionWithDirection</code> class to define two more subclasses, which will do what we want.</p>
<p>The first one will be the action we use to actually attack. It looks like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class ActionWithDirection(Action):
</span></span><span style="display:flex;"><span> def __init__(self, dx: int, dy: int):
</span></span><span style="display:flex;"><span> super().__init__()
</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> def perform(self, engine: Engine, entity: Entity) -&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><span style="color:#a6e22e">+class MeleeAction(ActionWithDirection):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_x = entity.x + self.dx
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_y = entity.y + self.dy
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not target:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return # No entity to attack.
</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(f"You kick the {target.name}, much to its annoyance!")
</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 MovementAction(ActionWithDirection):
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span><span style="display:flex;"><span> dest_x = entity.x + self.dx
</span></span><span style="display:flex;"><span> dest_y = entity.y + self.dy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if not engine.game_map.in_bounds(dest_x, dest_y):
</span></span><span style="display:flex;"><span> return # Destination is out of bounds.
</span></span><span style="display:flex;"><span> if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
</span></span><span style="display:flex;"><span> return # Destination is blocked by a tile.
</span></span><span style="display:flex;"><span> if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
</span></span><span style="display:flex;"><span> return # Destination is blocked by an entity.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> entity.move(self.dx, self.dy)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class ActionWithDirection(Action):
def __init__(self, dx: int, dy: int):
super().__init__()
self.dx = dx
self.dy = dy
def perform(self, engine: Engine, entity: Entity) -&gt; None:
raise NotImplementedError()
<span class="new-text">class MeleeAction(ActionWithDirection):
def perform(self, engine: Engine, entity: Entity) -&gt; None:
dest_x = entity.x + self.dx
dest_y = entity.y + self.dy
target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
if not target:
return # No entity to attack.
print(f"You kick the {target.name}, much to its annoyance!")</span>
class MovementAction(ActionWithDirection):
def perform(self, engine: Engine, entity: Entity) -&gt; None:
dest_x = entity.x + self.dx
dest_y = entity.y + self.dy
if not engine.game_map.in_bounds(dest_x, dest_y):
return # Destination is out of bounds.
if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
return # Destination is blocked by a tile.
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
return # Destination is blocked by an entity.
entity.move(self.dx, self.dy)</pre>
</div>
</div>
<p>Just like <code>MovementAction</code>, <code>MeleeAction</code> inherits from <code>ActionWithDirection</code>. The <code>perform</code>
method it implements is what well use to attack… eventually. Right
now, were just printing out a little message. The actual attacking will
have to wait until the next part (this one is getting long as it is).</p>
<p>Still, were not actually <em>using</em> <code>MeleeAction</code>
anywhere, yet. Lets add one more class, which is what will make the
determination on whether our player is moving or attacking:</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 MovementAction(ActionWithDirection):
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span><span style="display:flex;"><span> dest_x = entity.x + self.dx
</span></span><span style="display:flex;"><span> dest_y = entity.y + self.dy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if not engine.game_map.in_bounds(dest_x, dest_y):
</span></span><span style="display:flex;"><span> return # Destination is out of bounds.
</span></span><span style="display:flex;"><span> if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
</span></span><span style="display:flex;"><span> return # Destination is blocked by a tile.
</span></span><span style="display:flex;"><span> if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
</span></span><span style="display:flex;"><span> return # Destination is blocked by an entity.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> entity.move(self.dx, self.dy)
</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 BumpAction(ActionWithDirection):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def perform(self, engine: Engine, entity: Entity) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_x = entity.x + self.dx
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_y = 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">+ if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MeleeAction(self.dx, self.dy).perform(engine, 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">+ else:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MovementAction(self.dx, self.dy).perform(engine, entity)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class MovementAction(ActionWithDirection):
def perform(self, engine: Engine, entity: Entity) -&gt; None:
dest_x = entity.x + self.dx
dest_y = entity.y + self.dy
if not engine.game_map.in_bounds(dest_x, dest_y):
return # Destination is out of bounds.
if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
return # Destination is blocked by a tile.
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
return # Destination is blocked by an entity.
entity.move(self.dx, self.dy)
<span class="new-text">class BumpAction(ActionWithDirection):
def perform(self, engine: Engine, entity: Entity) -&gt; None:
dest_x = entity.x + self.dx
dest_y = entity.y + self.dy
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
return MeleeAction(self.dx, self.dy).perform(engine, entity)
else:
return MovementAction(self.dx, self.dy).perform(engine, entity)</span></pre>
</div>
</div>
<p>This class also inherits from <code>ActionWithDirection</code>, but its <code>perform</code> method doesnt actually perform anything, except deciding which class, between <code>MeleeAction</code> and <code>MovementAction</code> to return. Those classes are what are actually doing the work. <code>BumpAction</code>
just determines which one is appropriate to call, based on whether
there is a blocking entity at the given destination or not. Notice were
using the function we defined earlier in our map to decide if theres a
valid target or not.</p>
<p>Now that our new actions are in place, we need to modify our <code>input_handlers.py</code> file to use <code>BumpAction</code> instead of <code>MovementAction</code>. Its a pretty simple change:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from typing import Optional
</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, EscapeAction, MovementAction
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from actions import Action, BumpAction, EscapeAction
</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> def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
</span></span><span style="display:flex;"><span> raise SystemExit()
</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> if key == tcod.event.K_UP:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = MovementAction(dx=0, dy=-1)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(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 = MovementAction(dx=0, dy=1)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(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 = MovementAction(dx=-1, dy=0)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(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 = MovementAction(dx=1, dy=0)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(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> action = EscapeAction()
</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></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from typing import Optional
import tcod.event
<span class="crossed-out-text">from actions import Action, EscapeAction, MovementAction</span>
<span class="new-text">from actions import Action, BumpAction, EscapeAction</span>
class EventHandler(tcod.event.EventDispatch[Action]):
def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
raise SystemExit()
def ev_keydown(self, event: tcod.event.KeyDown) -&gt; Optional[Action]:
action: Optional[Action] = None
key = event.sym
if key == tcod.event.K_UP:
<span class="crossed-out-text">action = MovementAction(dx=0, dy=-1)</span>
<span class="new-text">action = BumpAction(dx=0, dy=-1)</span>
elif key == tcod.event.K_DOWN:
<span class="crossed-out-text">action = MovementAction(dx=0, dy=1)</span>
<span class="new-text">action = BumpAction(dx=0, dy=1)</span>
elif key == tcod.event.K_LEFT:
<span class="crossed-out-text">action = MovementAction(dx=-1, dy=0)</span>
<span class="new-text">action = BumpAction(dx=-1, dy=0)</span>
elif key == tcod.event.K_RIGHT:
<span class="crossed-out-text">action = MovementAction(dx=1, dy=0)</span>
<span class="new-text">action = BumpAction(dx=1, dy=0)</span>
elif key == tcod.event.K_ESCAPE:
action = EscapeAction()
# No valid key was pressed
return action</pre>
</div>
</div>
<p>Run the project now. At this point, you shouldnt be able to move
over the enemies, and you should get a message in the terminal,
indicating that youre attacking the enemy (albeit not for any damage).</p>
<p>Before we wrap this part up, lets set ourselves up to allow for
enemy turns as well. They wont actually be doing anything at the
moment, well just get a message in the terminal that indicates
something is happening.</p>
<p>Add these small modifications to <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>class Engine:
</span></span><span style="display:flex;"><span> def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
</span></span><span style="display:flex;"><span> self.event_handler = event_handler
</span></span><span style="display:flex;"><span> self.game_map = game_map
</span></span><span style="display:flex;"><span> self.player = player
</span></span><span style="display:flex;"><span> self.update_fov()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def handle_enemy_turns(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for entity in self.game_map.entities - {self.player}:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ print(f'The {entity.name} wonders when it will get to take a real turn.')
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def handle_events(self, events: Iterable[Any]) -&gt; None:
</span></span><span style="display:flex;"><span> for event in events:
</span></span><span style="display:flex;"><span> action = self.event_handler.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(self, self.player)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.handle_enemy_turns()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.update_fov() # Update the FOV before the players next action.
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class Engine:
def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
self.event_handler = event_handler
self.game_map = game_map
self.player = player
self.update_fov()
<span class="new-text">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>
def handle_events(self, events: Iterable[Any]) -&gt; None:
for event in events:
action = self.event_handler.dispatch(event)
if action is None:
continue
action.perform(self, self.player)
<span class="new-text">self.handle_enemy_turns()</span>
self.update_fov() # Update the FOV before the players next action.</pre>
</div>
</div>
<p>The <code>handle_enemy_turns</code> function loops through each
entity (minus the player) and prints out a message for them. In the next
part, well replace this with some code that will allow those entities
to take real turns.</p>
<p>We call <code>handle_enemy_turns</code> right after <code>action.perform</code>,
so that the enemies move right after the player. Other roguelike games
have more complex timing mechanisms for when entities take their turns,
but our tutorial will stick with probably the simplest method of all:
the player moves, then all the enemies move.</p>
<p>Thats all for this chapter. Next time, well look at moving the
enemies around on their turns, and doing some real damage to both the
enemies and the player.</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-5">click
here</a>.</p>
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-6">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%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
<script src="Part%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
</body></html>