<h1><spanclass="section-number">12. </span>NPC and monster AI<aclass="headerlink"href="#npc-and-monster-ai"title="Permalink to this headline">¶</a></h1>
<p>The term “Artificial Intelligence” can sound daunting. It evokes images of supercomputers, machine learning, neural networks and large language models. For our use case though, you can get something that feels pretty ‘intelligent’ by just using a few if-statements.</p>
</aside>
<p>Not every entity in the game are controlled by a player. NPCs and enemies need to be controlled by the computer - that is, we need to give them artificial intelligence (AI).</p>
<p>For our game we will implement a type of AI called a ‘state machine’. It means that the entity (like an NPC or mob) is always in a given ‘state’. An example of a state could be ‘idle’, ‘roaming’ or ‘attacking’.
At regular intervals, the AI entity will be ‘ticked’ by Evennia. This ‘tick’ starts with an evaluation which determines if the entity should switch to another state, or stay and perform one (or more) actions inside the current state.</p>
<asideclass="sidebar">
<pclass="sidebar-title">Mobs and NPC</p>
<p>‘Mob’ is short for ‘Mobile’ and is a common MUD term for an entity that can move between rooms. The term is usually used for aggressive enemies. A Mob is also an ‘NPC’ (Non-Player Character), but the latter term is often used for more peaceful entities, like shopkeeprs and quest givers.</p>
</aside>
<p>For example, if a mob in a ‘roaming’ state comes upon a player character, it may switch into the ‘attack’ state. In combat it could move between different combat actions, and if it survives combat it would go back to its ‘roaming’ state.</p>
<p>The AI can be ‘ticked’ on different time scales depending on how your game works. For example, while a mob is moving, they might automatically move from room to room every 20 seconds. But once it enters turn-based combat (if you use that), the AI will ‘tick’ only on every turn.</p>
<sectionid="our-requirements">
<h2><spanclass="section-number">12.1. </span>Our requirements<aclass="headerlink"href="#our-requirements"title="Permalink to this headline">¶</a></h2>
<asideclass="sidebar">
<pclass="sidebar-title">Shopkeepers and quest givers</p>
<p>NPC shopkeepers and quest givers will be assumed to always be in the ‘idle’ state in our game - the functionality of talking to or shopping from them will be explored in a future lesson.</p>
</aside>
<p>For this tutorial game, we’ll need AI entities to be able to be in the following states:</p>
<ulclass="simple">
<li><p><em>Idle</em> - don’t do anything, just stand around.</p></li>
<li><p><em>Roam</em> - move from room to room. It’s important that we add the ability to limit where the AI can roam to. For example, if we have non-combat areas we want to be able to <aclass="reference internal"href="../../../Components/Locks.html"><spanclass="doc std std-doc">lock</span></a> all exits leading into those areas so aggressive mods doesn’t walk into them.</p></li>
<li><p><em>Combat</em> - initiate and perform combat with PCs. This state will make use of the <aclass="reference internal"href="Beginner-Tutorial-Combat-Base.html"><spanclass="doc std std-doc">Combat Tutorial</span></a> to randomly select combat actions (turn-based or tick-based as appropriately).</p></li>
<li><p><em>Flee</em> - this is like <em>Roam</em> except the AI will move so as to avoid entering rooms with PCs, if possible.</p></li>
</ul>
<p>We will organize the AI code like this:</p>
<ulclass="simple">
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">AIHandler</span></code> this will be a handler stored as <codeclass="docutils literal notranslate"><spanclass="pre">.ai</span></code> on the AI entity. It is responsible for storing the AI’s state. To ‘tick’ the AI, we run <codeclass="docutils literal notranslate"><spanclass="pre">.ai.run()</span></code>. How often we crank the wheels of the AI this way we leave up to other game systems.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">.ai_<state_name></span></code> methods on the NPC/Mob class - when the <codeclass="docutils literal notranslate"><spanclass="pre">ai.run()</span></code> method is called, it is responsible for finding a method named like its current state (e.g. <codeclass="docutils literal notranslate"><spanclass="pre">.ai_combat</span></code> if we are in the <em>combat</em> state). Having methods like this makes it easy to add new states - just add a new method named appropriately and the AI now knows how to handle that state!</p></li>
</ul>
</section>
<sectionid="the-aihandler">
<h2><spanclass="section-number">12.2. </span>The AIHandler<aclass="headerlink"href="#the-aihandler"title="Permalink to this headline">¶</a></h2>
<p>This is the core logic for managing AI states. Create a new file <codeclass="docutils literal notranslate"><spanclass="pre">evadventure/ai.py</span></code>.</p>
<spanclass="n">log_trace</span><spanclass="p">(</span><spanclass="sa">f</span><spanclass="s2">"AI error in </span><spanclass="si">{</span><spanclass="bp">self</span><spanclass="o">.</span><spanclass="n">obj</span><spanclass="o">.</span><spanclass="n">name</span><spanclass="si">}</span><spanclass="s2"> (running state: </span><spanclass="si">{</span><spanclass="n">state</span><spanclass="si">}</span><spanclass="s2">)"</span><spanclass="p">)</span>
</pre></div></td></tr></table></div>
</div>
<p>The AIHandler is an example of an <aclass="reference internal"href="../../Tutorial-Persistent-Handler.html"><spanclass="doc std std-doc">Object Handler</span></a>. This is a design style that groups all functionality together. To look-ahead a little, this handler will be added to the object like this:</p>
<asideclass="sidebar">
<pclass="sidebar-title">lazy_property</p>
<p>This is an Evennia <aclass="reference external"href="https://realpython.com/primer-on-python-decorators/">@decorator</a> that makes it so that the handler won’t be initialized until someone actually tries to access <codeclass="docutils literal notranslate"><spanclass="pre">obj.ai</span></code> for the first time. On subsequent calls, the already initialized handler is returned. This is a very useful performance optimization when you have a lot of objects and also important for the functionality of handlers.</p>
</aside>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># just an example, don't put this anywhere yet</span>
<p>So in short, accessing the <codeclass="docutils literal notranslate"><spanclass="pre">.ai</span></code> property will initialize an instance of <codeclass="docutils literal notranslate"><spanclass="pre">AIHandler</span></code>, to which we pass <codeclass="docutils literal notranslate"><spanclass="pre">self</span></code> (the current object). In the <codeclass="docutils literal notranslate"><spanclass="pre">AIHandler.__init__</span></code> we take this input and store it as <codeclass="docutils literal notranslate"><spanclass="pre">self.obj</span></code> (<strong>lines 10-13</strong>). This way the handler can always operate on the entity it’s “sitting on” by accessing <codeclass="docutils literal notranslate"><spanclass="pre">self.obj</span></code>. The <codeclass="docutils literal notranslate"><spanclass="pre">lazy_property</span></code> makes sure that this initialization only happens once per server reload.</p>
<p>More key functionality:</p>
<ulclass="simple">
<li><p><strong>Line 11</strong>: We (re)load the AI state by accessing <codeclass="docutils literal notranslate"><spanclass="pre">self.obj.attributes.get()</span></code>. This loads a database <aclass="reference internal"href="../../../Components/Attributes.html"><spanclass="doc std std-doc">Attribute</span></a> with a given name and category. If one is not (yet) saved, return “idle”. Note that we must access <codeclass="docutils literal notranslate"><spanclass="pre">self.obj</span></code> (the NPC/mob) since that is the only thing with access to the database.</p></li>
<li><p><strong>Line 16</strong>: In the <codeclass="docutils literal notranslate"><spanclass="pre">set_state</span></code> method we force the handler to switch to a given state. When we do, we make sure to save it to the database as well, so its state survives a reload. But we also store it in <codeclass="docutils literal notranslate"><spanclass="pre">self.ai_state</span></code> so we don’t need to hit the database on every fetch.</p></li>
<li><p><strong>line 23</strong>: The <codeclass="docutils literal notranslate"><spanclass="pre">getattr</span></code> function is an in-built Python function for getting a named property on an object. This allows us to, based on the current state, call a method <codeclass="docutils literal notranslate"><spanclass="pre">ai_<statename></span></code> defined on the NPC/mob. We must wrap this call in a <codeclass="docutils literal notranslate"><spanclass="pre">try...except</span></code> block to properly handle errors in the AI method. Evennia’s <codeclass="docutils literal notranslate"><spanclass="pre">log_trace</span></code> will make sure to log the error, including its traceback for debugging.</p></li>
</ul>
<sectionid="more-helpers-on-the-ai-handler">
<h3><spanclass="section-number">12.2.1. </span>More helpers on the AI handler<aclass="headerlink"href="#more-helpers-on-the-ai-handler"title="Permalink to this headline">¶</a></h3>
<p>It’s also convenient to put a few helpers on the AIHandler. This makes them easily available from inside the <codeclass="docutils literal notranslate"><spanclass="pre">ai_<state></span></code> methods, callable as e.g. <codeclass="docutils literal notranslate"><spanclass="pre">self.ai.get_targets()</span></code>.</p>
<p>The ‘traverse’ lock is the default lock-type checked by Evennia before allowing something to pass through an exit. Since only PCs have the <codeclass="docutils literal notranslate"><spanclass="pre">is_pc</span></code> property, we could lock down exits to <em>only</em> allow entities with the property to pass through.</p>
<p>In game:</p>
<divclass="highlight-none notranslate"><divclass="highlight"><pre><span></span>lock north = traverse:attr(is_pc, True)
<p>See <aclass="reference internal"href="../../../Components/Locks.html"><spanclass="doc std std-doc">Locks</span></a> for a lot more information about Evennia locks.</p>
</aside>
<ulclass="simple">
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">get_targets</span></code> checks if any of the other objects in the same location as the <codeclass="docutils literal notranslate"><spanclass="pre">is_pc</span></code> property set on their typeclass. For simplicity we assume Mobs will only ever attack PCs (no monster in-fighting!).</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">get_traversable_exits</span></code> fetches all valid exits from the current location, excluding those with a provided destination <em>or</em> those which doesn’t pass the “traverse” access check.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">get_random_probability</span></code> takes a dict <codeclass="docutils literal notranslate"><spanclass="pre">{action:</span><spanclass="pre">probability,</span><spanclass="pre">...}</span></code>. This will randomly select an action, but the higher the probability, the more likely it is that it will be picked. We will use this for the combat state later, to allow different combatants to more or less likely to perform different combat actions. This algorithm uses a few useful Python tools:</p>
<ul>
<li><p><strong>Line 41</strong>: Remember <codeclass="docutils literal notranslate"><spanclass="pre">probabilities</span></code> is a <codeclass="docutils literal notranslate"><spanclass="pre">dict</span></code><codeclass="docutils literal notranslate"><spanclass="pre">{key:</span><spanclass="pre">value,</span><spanclass="pre">...}</span></code>, where the values are the probabilities. So <codeclass="docutils literal notranslate"><spanclass="pre">probabilities.values()</span></code> gets us a list of only the probabilities. Running <codeclass="docutils literal notranslate"><spanclass="pre">sum()</span></code> on them gets us the total sum of those probabilities. We need that to normalize all probabilities between 0 and 1.0 on the line below.</p></li>
<li><p><strong>Lines 42-46</strong>: Here we create a new iterable of tuples <codeclass="docutils literal notranslate"><spanclass="pre">(key,</span><spanclass="pre">prob/prob_total)</span></code>. We sort them using the Python <codeclass="docutils literal notranslate"><spanclass="pre">sorted</span></code> helper. The <codeclass="docutils literal notranslate"><spanclass="pre">key=lambda</span><spanclass="pre">x:</span><spanclass="pre">x[1]</span></code> means that we sort on the second element of each tuple (the probability). The <codeclass="docutils literal notranslate"><spanclass="pre">reverse=True</span></code> means that we’ll sort from highest probability to lowest.</p></li>
<li><p><strong>Line 47</strong>:The <codeclass="docutils literal notranslate"><spanclass="pre">random.random()</span></code> call generates a random value between 0 and 1.</p></li>
<li><p><strong>Line 49</strong>: Since the probabilities are sorted from highest to lowest, we loop over them until we find the first one fitting in the random value - this is the action/key we are looking for.</p></li>
<li><p>To give an example, if you have a <codeclass="docutils literal notranslate"><spanclass="pre">probability</span></code> input of <codeclass="docutils literal notranslate"><spanclass="pre">{"attack":</span><spanclass="pre">0.5,</span><spanclass="pre">"defend":</span><spanclass="pre">0.1,</span><spanclass="pre">"idle":</span><spanclass="pre">0.4}</span></code>, this would become a sorted iterable <codeclass="docutils literal notranslate"><spanclass="pre">(("attack",</span><spanclass="pre">0.5),</span><spanclass="pre">("idle",</span><spanclass="pre">0.4),</span><spanclass="pre">("defend":</span><spanclass="pre">0.1))</span></code>, and if <codeclass="docutils literal notranslate"><spanclass="pre">random.random()</span></code> returned 0.65, the outcome would be “idle”. If <codeclass="docutils literal notranslate"><spanclass="pre">random.random()</span></code> returned <codeclass="docutils literal notranslate"><spanclass="pre">0.90</span></code>, it would be “defend”. That is, this AI entity would attack 50% of the time, idle 40% and defend 10% of the time.</p></li>
</ul>
</li>
</ul>
</section>
</section>
<sectionid="adding-ai-to-an-entity">
<h2><spanclass="section-number">12.3. </span>Adding AI to an entity<aclass="headerlink"href="#adding-ai-to-an-entity"title="Permalink to this headline">¶</a></h2>
<p>All we need to add AI-support to a game entity is to add the AI handler and a bunch of <codeclass="docutils literal notranslate"><spanclass="pre">.ai_statename()</span></code> methods onto that object’s typeclass.</p>
<p>We already sketched out NPCs and Mob typeclasses back in the <spanclass="xref myst">NPC tutorial</span>. Open <codeclass="docutils literal notranslate"><spanclass="pre">evadventure/npcs.py</span></code> and expand the so-far empty <codeclass="docutils literal notranslate"><spanclass="pre">EvAdventureMob</span></code> class.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/npcs.py </span>
<p>All the remaining logic will go into each state-method.</p>
<sectionid="idle-state">
<h3><spanclass="section-number">12.3.1. </span>Idle state<aclass="headerlink"href="#idle-state"title="Permalink to this headline">¶</a></h3>
<p>In the idle state the mob does nothing, so we just leave the <codeclass="docutils literal notranslate"><spanclass="pre">ai_idle</span></code> method as it is - with just an empty <codeclass="docutils literal notranslate"><spanclass="pre">pass</span></code> in it. This means that it will also not attack PCs in the same room - but if a PC attacks it, we must make sure to force it into a combat state (otherwise it will be defenseless).</p>
</section>
<sectionid="roam-state">
<h3><spanclass="section-number">12.3.2. </span>Roam state<aclass="headerlink"href="#roam-state"title="Permalink to this headline">¶</a></h3>
<p>In this state the mob should move around from room to room until it finds PCs to attack.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/npcs.py</span>
<p>Every time the AI is ticked, this method will be called. It will first check if there are any valid targets in the room (using the <codeclass="docutils literal notranslate"><spanclass="pre">get_targets()</span></code> helper we made on the <codeclass="docutils literal notranslate"><spanclass="pre">AIHandler</span></code>). If so, we switch to the <codeclass="docutils literal notranslate"><spanclass="pre">combat</span></code> state and immediately call the <codeclass="docutils literal notranslate"><spanclass="pre">attack</span></code> command to initiate/join combat (see the <aclass="reference internal"href="Beginner-Tutorial-Combat-Base.html"><spanclass="doc std std-doc">Combat tutorial</span></a>).</p>
<p>If no target is found, we get a list of traversible exits (exits that fail the <codeclass="docutils literal notranslate"><spanclass="pre">traverse</span></code> lock check is already excluded from this list). Using Python’s in-bult <codeclass="docutils literal notranslate"><spanclass="pre">random.choice</span></code> function we grab a random exit from that list and moves through it by its name.</p>
</section>
<sectionid="flee-state">
<h3><spanclass="section-number">12.3.3. </span>Flee state<aclass="headerlink"href="#flee-state"title="Permalink to this headline">¶</a></h3>
<p>Flee is similar to <em>Roam</em> except the the AI never tries to attack anything and will make sure to not return the way it came.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/npcs.py</span>
<p>We store the <codeclass="docutils literal notranslate"><spanclass="pre">past_room</span></code> in an Attribute “past_room” on ourselves and make sure to exclude it when trying to find random exits to traverse to.</p>
<p>If we end up in a dead end we switch to <em>Roam</em> mode so that it can get back out (and also start attacking things again). So the effect of this is that the mob will flee in terror as far as it can before ‘calming down’.</p>
</section>
<sectionid="combat-state">
<h3><spanclass="section-number">12.3.4. </span>Combat state<aclass="headerlink"href="#combat-state"title="Permalink to this headline">¶</a></h3>
<p>While in the combat state, the mob will use one of the combat systems we’ve designed (either <aclass="reference internal"href="Beginner-Tutorial-Combat-Twitch.html"><spanclass="doc std std-doc">twitch-based combat</span></a> or <aclass="reference internal"href="Beginner-Tutorial-Combat-Turnbased.html"><spanclass="doc std std-doc">turn-based combat</span></a>). This means that every time the AI ticks, and we are in the combat state, the entity needs to perform one of the available combat actions, <em>hold</em>, <em>attack</em>, <em>do a stunt</em>, <em>use an item</em> or <em>flee</em>.</p>
<li><p><strong>Lines 7-13</strong>: This dict describe how likely the mob is to perform a given combat action. By just modifying this dictionary we can easily creating mobs that behave very differently, like using items more or being more prone to fleeing. You can also turn off certain action entirely - by default his mob never “holds” or “uses items”.</p></li>
<li><p><strong>Line 22</strong>: If we are in combat, a <codeclass="docutils literal notranslate"><spanclass="pre">CombadHandler</span></code> should be initialized on us, available as as <codeclass="docutils literal notranslate"><spanclass="pre">self.ndb.combathandler</span></code> (see the <aclass="reference internal"href="Beginner-Tutorial-Combat-Base.html"><spanclass="doc std std-doc">base combat tutorial</span></a>).</p></li>
<li><p><strong>Line 24</strong>: The <codeclass="docutils literal notranslate"><spanclass="pre">combathandler.get_sides()</span></code> produces the allies and enemies for the one passed to it.</p></li>
<li><p><strong>Line 25</strong>: Now that <codeclass="docutils literal notranslate"><spanclass="pre">random_probability</span></code> method we created earlier in this lesson becomes handy!</p></li>
</ul>
<p>The rest of this method just takes the randomly chosen action and performs the required operations to queue it as a new action with the <codeclass="docutils literal notranslate"><spanclass="pre">CombatHandler</span></code>. For simplicity, we only use stunts to boost our allies, not to hamper our enemies.</p>
<p>Finally, if we are not currently in combat and there are no enemies nearby, we switch to roaming - otherwise we start another fight!</p>
</section>
</section>
<sectionid="unit-testing">
<h2><spanclass="section-number">12.4. </span>Unit Testing<aclass="headerlink"href="#unit-testing"title="Permalink to this headline">¶</a></h2>
<blockquote>
<div><p>Create a new file <codeclass="docutils literal notranslate"><spanclass="pre">evadventure/tests/test_ai.py</span></code>.</p>
</div></blockquote>
<p>Testing the AI handler and mob is straightforward if you have followed along with previous lessons. Create an <codeclass="docutils literal notranslate"><spanclass="pre">EvAdventureMob</span></code> and test that calling the various ai-related methods and handlers on it works as expected. A complexity is to mock the output from <codeclass="docutils literal notranslate"><spanclass="pre">random</span></code> so that you always get the same random result to compare against. We leave the implementation of AI tests as an extra exercise for the reader.</p>
</section>
<sectionid="conclusions">
<h2><spanclass="section-number">12.5. </span>Conclusions<aclass="headerlink"href="#conclusions"title="Permalink to this headline">¶</a></h2>
<p>You can easily expand this simple system to make Mobs more ‘clever’. For example, instead of just randomly decide which action to take in combat, the mob could consider more factors - maybe some support mobs could use stunts to pave the way for their heavy hitters or use health potions when badly hurt.</p>
<p>It’s also simple to add a ‘hunt’ state, where mobs check adjoining rooms for targets before moving there.</p>
<p>And while implementing a functional game AI system requires no advanced math or machine learning techniques, there’s of course no limit to what kind of advanced things you could add if you really wanted to!</p>