Skip to content

Tutorial Part 5: NPCs & Basic AI

Estimated time: 45 minutes


Welcome back! In Part 4, you learned how to create rooms and items to build your game world. Now it is time to bring that world to life with Non-Player Characters (NPCs) - the merchants, guards, quest givers, and monsters that players will interact with.

What You Will Learn

By the end of this tutorial, you will:

  1. Understand what NPCs are and their role in a MUD
  2. Create NPC entities with the right components
  3. Configure static dialogue with triggers and responses
  4. Set up AI-powered dialogue for dynamic conversations
  5. Add basic behaviors like patrolling and wandering
  6. Build a complete shopkeeper NPC with inventory
  7. Complete a hands-on exercise: creating a village shopkeeper

What Are NPCs?

Non-Player Characters (NPCs) are any characters in your game that are not controlled by players. They serve many purposes:

  • Atmosphere - Townspeople, wandering travelers, and background characters make the world feel alive
  • Information - NPCs can give hints, lore, directions, and quest objectives
  • Services - Merchants, trainers, healers, and other NPCs provide gameplay services
  • Combat - Monsters and hostile NPCs provide challenges for players
  • Story - Quest givers and key characters drive your game's narrative

Types of NPCs in MAID

MAID supports three approaches to NPC dialogue and behavior:

Type Description Use Case
Static Fixed responses based on keywords Background NPCs, simple vendors
Scripted Conditional dialogue with triggers Quest givers, story characters
AI-Powered Dynamic responses using LLM Rich conversation, complex NPCs

You can mix these approaches - an NPC might use scripted dialogue for quest-critical information but AI dialogue for general conversation.


NPC Entity Structure

Like all game objects in MAID, NPCs are entities with components attached. The combination of components determines what an NPC can do.

+------------------+
|   NPC Entity     |
+------------------+
| id: UUID         |
| tags: [npc]      |
+------------------+
        |
        v
+------------------+    +------------------+    +------------------+
|DescriptionComp   |    |  NPCComponent    |    |PositionComponent |
+------------------+    +------------------+    +------------------+
| name             |    | behavior_type    |    | room_id          |
| short_desc       |    | is_merchant      |    +------------------+
| long_desc        |    | wander_radius    |
| keywords         |    | respawn_time     |
+------------------+    +------------------+
        |
        v
+------------------+    +------------------+
|DialogueComponent |    |InventoryComponent|
+------------------+    +------------------+
| ai_enabled       |    | items            |
| personality      |    | capacity         |
| speaking_style   |    +------------------+
| greeting         |
| farewell         |
+------------------+

Core NPC Components

Every NPC needs at minimum:

Component Purpose Key Fields
DescriptionComponent Display name and description name, short_desc, long_desc, keywords
NPCComponent NPC-specific metadata behavior_type, is_merchant, wander_radius
PositionComponent Location in the world room_id

Additional components for enhanced functionality:

Component Purpose Key Fields
DialogueComponent Conversation configuration ai_enabled, personality, greeting, farewell
InventoryComponent Shop inventory items, capacity
HealthComponent Combat capability current, maximum
CombatComponent Combat stats attack_power, defense

Creating a Basic NPC

Let's create a simple town guard NPC step by step.

Step 1: Create the Entity

from uuid import uuid4

from maid_stdlib.components import (
    DescriptionComponent,
    NPCComponent,
    PositionComponent,
)

# Create the NPC entity
npc_id = world.create_entity()

# Tag it as an NPC
world.add_tag(npc_id, "npc")

Step 2: Add Description

# Add description - how players see this NPC
world.add_component(npc_id, DescriptionComponent(
    name="Town Guard",
    short_desc="A vigilant town guard stands watch here.",
    long_desc=(
        "This guard wears polished chainmail and carries a well-maintained "
        "halberd. His eyes constantly scan the area, alert for any trouble. "
        "A badge on his chest identifies him as a member of the Town Watch."
    ),
    keywords=["guard", "soldier", "watchman", "town guard"],
))

Step 3: Add NPC Configuration

# Add NPC-specific configuration
world.add_component(npc_id, NPCComponent(
    behavior_type="passive",  # Won't attack unless provoked
    is_merchant=False,
    wander_radius=0,  # Stays in place
    respawn_time=300.0,  # 5 minutes if killed
))

Step 4: Place in the World

# Place the NPC in a room
world.add_component(npc_id, PositionComponent(
    room_id=town_square_id,
))

# Register with the room
world.place_entity_in_room(npc_id, town_square_id)

Complete Example

Here is the complete code to create a basic NPC:

from uuid import uuid4

from maid_stdlib.components import (
    DescriptionComponent,
    NPCComponent,
    PositionComponent,
)


def create_basic_npc(
    world,
    name: str,
    short_desc: str,
    long_desc: str,
    keywords: list[str],
    room_id: UUID,
    behavior_type: str = "passive",
) -> UUID:
    """Create a basic NPC entity.

    Args:
        world: The game world
        name: NPC's display name
        short_desc: One-line description
        long_desc: Full description
        keywords: Words that match this NPC
        room_id: Room to place the NPC in
        behavior_type: passive, hostile, friendly, merchant

    Returns:
        UUID of the created NPC
    """
    npc_id = world.create_entity()
    world.add_tag(npc_id, "npc")

    world.add_component(npc_id, DescriptionComponent(
        name=name,
        short_desc=short_desc,
        long_desc=long_desc,
        keywords=keywords,
    ))

    world.add_component(npc_id, NPCComponent(
        behavior_type=behavior_type,
    ))

    world.add_component(npc_id, PositionComponent(room_id=room_id))
    world.place_entity_in_room(npc_id, room_id)

    return npc_id


# Usage
guard_id = create_basic_npc(
    world,
    name="Town Guard",
    short_desc="A vigilant town guard stands watch here.",
    long_desc="This guard wears polished chainmail...",
    keywords=["guard", "soldier", "watchman"],
    room_id=town_square_id,
    behavior_type="passive",
)

Dialogue Systems

MAID provides three levels of dialogue sophistication. You can use any combination based on your needs.

Static Dialogue

The simplest approach uses fixed greeting, farewell, and fallback responses:

from maid_stdlib.components import DialogueComponent

world.add_component(npc_id, DialogueComponent(
    ai_enabled=False,  # No AI - static responses only
    greeting="Halt! State your business in town.",
    farewell="Move along, citizen.",
    fallback_response="I'm just here to keep the peace.",
))

When AI is disabled, the NPC responds with: - greeting when first addressed - farewell when conversation ends - fallback_response for anything else

Scripted Dialogue with Triggers

For more complex interactions, you can define keyword triggers:

from maid_classic_rpg.models.npc.behavior import DialogueLine, NPCDialogue

# Define dialogue lines with triggers
guard_dialogue = NPCDialogue(
    npc_id=guard_id,
    greeting="Halt! State your business in town.",
    farewell="Move along, citizen. Stay out of trouble.",
    default_response="I don't have time for idle chat.",
    lines=[
        DialogueLine(
            trigger="trouble",
            response="There's been some strange activity near the old mill. "
                     "Stay away from there after dark.",
        ),
        DialogueLine(
            trigger="curfew",
            response="Curfew begins at sundown. Anyone caught outside "
                     "after dark without a lantern will be questioned.",
        ),
        DialogueLine(
            trigger="directions",
            response="The marketplace is to the east. The temple is north. "
                     "The inn is west if you need lodging.",
        ),
        DialogueLine(
            trigger="captain",
            response="Captain Aldric runs the Town Watch. You'll find him "
                     "at the barracks near the north gate.",
            conditions={"reputation_guards": "neutral"},  # Requires neutral+ rep
        ),
    ],
)

Dialogue triggers can have conditions that must be met before the response is given. Common conditions include:

Condition Description
quest_state Player's progress in a quest
reputation_* Standing with a faction
has_item Player possesses a specific item
time_of_day Current in-game time

AI-Powered Dialogue

For rich, dynamic conversations, enable AI dialogue with a configured DialogueComponent:

from maid_stdlib.components import DialogueComponent

world.add_component(npc_id, DialogueComponent(
    ai_enabled=True,

    # Personality drives AI responses
    personality=(
        "A dutiful and formal town guard. Takes his job seriously and "
        "follows regulations to the letter. Slightly paranoid about "
        "outsiders but ultimately fair."
    ),

    # How the NPC speaks
    speaking_style=(
        "Formal, clipped speech. Addresses civilians as 'citizen'. Uses "
        "military terminology. Rarely uses contractions."
    ),

    # NPC's role and knowledge
    npc_role="town guard stationed at the main gate",
    knowledge_domains=[
        "town laws",
        "recent crimes",
        "local landmarks",
        "curfew hours",
    ],

    # Topics to discuss or avoid
    will_discuss=["laws", "directions", "safety", "recent incidents"],
    wont_discuss=["guard deployments", "personal life", "bribes"],

    # Fallback responses
    greeting="Halt, citizen. State your business.",
    farewell="Move along. Stay out of trouble.",
    fallback_response="That's not something I'm at liberty to discuss.",

    # AI settings
    max_response_tokens=120,
    temperature=0.5,  # Lower = more consistent responses
    cooldown_seconds=2.0,
))

For a complete guide to AI dialogue configuration, see the NPC Dialogue Guide.

Dialogue Commands

Players interact with NPC dialogue using these commands:

Command Description Example
talk <npc> <message> Talk to an NPC talk guard about trouble
ask <npc> about <topic> Ask about a topic ask guard about curfew
greet <npc> Send a greeting greet guard
bye <npc> End conversation bye guard

Example player interaction:

> greet guard
Town Guard says, "Halt, citizen. State your business."

> ask guard about trouble
Town Guard says, "There's been some strange activity near the old mill.
Stay away from there after dark."

> ask guard about the mill
Town Guard says, "*glances around nervously* I've already said too much.
Just... stay away from there, citizen. That's official advice."

> bye guard
Town Guard says, "Move along. Stay out of trouble."

Behavior Components

NPCs can have behaviors that make them move and act independently.

NPCComponent Behavior Types

The behavior_type field in NPCComponent determines basic behavior:

Type Description Example
passive Does nothing unless interacted with Shopkeeper
friendly Approaches and helps players Healer NPC
hostile Attacks players on sight Goblin
merchant Offers buy/sell interface Weapon vendor
quest Has quest-related dialogue Quest giver

Patrol Behavior

NPCs can patrol between defined waypoints:

from maid_classic_rpg.models.npc.behavior import NPCState, PatrolState

# Define patrol state
guard_state = NPCState(
    entity_id=guard_id,
    patrol_state=PatrolState.PATROLLING,
    patrol_index=0,
    territory_center=barracks_id,
    territory_radius=5,  # Can move up to 5 rooms from barracks
)

# The BehaviorSystem will move the NPC along the patrol route
# You define the route in your content pack's systems

Wander Behavior

For NPCs that should move randomly:

world.add_component(npc_id, NPCComponent(
    behavior_type="passive",
    wander_radius=3,  # Can wander up to 3 rooms from spawn point
    spawn_point_id=tavern_id,
))

The wander_radius determines how far from the spawn point the NPC can roam. A radius of 0 means the NPC stays in place.

Aggression and Threat Detection

Hostile NPCs track threats using memory:

# NPCState tracks threat memory
state = NPCState(
    entity_id=goblin_id,
    threat_memory={
        str(player_id): 50,  # Player has 50 threat
        str(other_player_id): 25,  # Other player has 25 threat
    },
)

# Get the highest threat target
target_id = state.get_highest_threat_target()
if target_id:
    # Attack the highest threat
    pass

The combat system (covered in Part 6) uses this threat memory to determine NPC targets.


Shopkeeper Example

Let's create a complete merchant NPC with shop inventory.

Step 1: Create the NPC Entity

from uuid import uuid4

from maid_stdlib.components import (
    DescriptionComponent,
    DialogueComponent,
    InventoryComponent,
    NPCComponent,
    PositionComponent,
)


def create_shopkeeper(
    world,
    name: str,
    description: str,
    room_id: UUID,
    shop_type: str = "general goods",
) -> UUID:
    """Create a merchant NPC with shop inventory.

    Args:
        world: The game world
        name: Shopkeeper's name
        description: Description of the shopkeeper
        room_id: Room where the shop is located
        shop_type: Type of goods sold

    Returns:
        UUID of the created shopkeeper entity
    """
    npc_id = world.create_entity()
    world.add_tag(npc_id, "npc")
    world.add_tag(npc_id, "merchant")

    # Description
    world.add_component(npc_id, DescriptionComponent(
        name=name,
        short_desc=f"{name} tends the shop here.",
        long_desc=description,
        keywords=["shopkeeper", "merchant", "vendor", name.lower()],
    ))

    # NPC configuration
    world.add_component(npc_id, NPCComponent(
        behavior_type="merchant",
        is_merchant=True,
        wander_radius=0,  # Stays at the shop
    ))

    # Position
    world.add_component(npc_id, PositionComponent(room_id=room_id))
    world.place_entity_in_room(npc_id, room_id)

    # Shop inventory
    world.add_component(npc_id, InventoryComponent(
        capacity=100,  # Large capacity for shop goods
        weight_limit=10000.0,  # High limit for shops
    ))

    # Dialogue
    world.add_component(npc_id, DialogueComponent(
        ai_enabled=True,
        personality=(
            f"A friendly shopkeeper who takes pride in their {shop_type}. "
            "Eager to make sales but fair in pricing. Knows a bit about "
            "everything they sell."
        ),
        speaking_style=(
            "Warm and welcoming. Uses merchant's patter like 'finest quality' "
            "and 'best prices in town'. Occasionally name-drops satisfied "
            "customers. Becomes more enthusiastic when discussing their wares."
        ),
        npc_role=f"shopkeeper selling {shop_type}",
        knowledge_domains=[
            shop_type,
            "prices",
            "local customers",
            "trade routes",
            "item quality",
        ],
        will_discuss=["wares", "prices", "bargains", "local news"],
        wont_discuss=["suppliers", "profit margins", "competitors"],
        greeting=f"Welcome, welcome! Looking for {shop_type}? You've come to the right place!",
        farewell="Come back soon! Tell your friends about us!",
        fallback_response="Hmm, I'm not sure about that. But let me show you some fine wares!",
        max_response_tokens=120,
        temperature=0.7,
    ))

    return npc_id


# Create the shopkeeper
blacksmith_id = create_shopkeeper(
    world,
    name="Tormund",
    description=(
        "Tormund is a burly man with arms like tree trunks, the result of "
        "decades at the forge. Soot stains his leather apron, and his hands "
        "are calloused but steady. Despite his intimidating size, his eyes "
        "are kind and he greets customers with a warm smile."
    ),
    room_id=smithy_id,
    shop_type="weapons and armor",
)

Step 2: Stock the Shop

from maid_stdlib.components import ItemComponent


def add_shop_item(
    world,
    shop_id: UUID,
    item_name: str,
    item_type: str,
    value: int,
    description: str,
) -> UUID:
    """Add an item to a shop's inventory.

    Args:
        world: The game world
        shop_id: The shopkeeper entity ID
        item_name: Name of the item
        item_type: Type (weapon, armor, etc.)
        value: Price in gold
        description: Item description

    Returns:
        UUID of the created item
    """
    item_id = world.create_entity()
    world.add_tag(item_id, "item")

    world.add_component(item_id, DescriptionComponent(
        name=item_name,
        short_desc=f"A {item_name.lower()} for sale.",
        long_desc=description,
        keywords=[item_name.lower()],
    ))

    world.add_component(item_id, ItemComponent(
        item_type=item_type,
        value=value,
        quality="common",
    ))

    # Add to shop inventory
    shop_inventory = world.get_component(shop_id, InventoryComponent)
    shop_inventory.add_item(item_id)

    return item_id


# Stock the blacksmith's shop
add_shop_item(
    world, blacksmith_id,
    "Iron Sword",
    "weapon",
    50,
    "A well-balanced iron sword suitable for any adventurer.",
)

add_shop_item(
    world, blacksmith_id,
    "Steel Shield",
    "armor",
    75,
    "A sturdy steel shield that can block most attacks.",
)

add_shop_item(
    world, blacksmith_id,
    "Chainmail Armor",
    "armor",
    150,
    "Interlocking metal rings provide solid protection.",
)

Step 3: Shop Interaction

Players interact with shops using buy/sell commands:

> look
The Smithy
Heat radiates from the forge where Tormund works metal into weapons...

You see here:
  Tormund tends the shop here.

> greet tormund
Tormund says, "Welcome, welcome! Looking for weapons and armor? You've
come to the right place!"

> list
Tormund's Shop - Weapons and Armor
==================================
  Iron Sword         50 gold
  Steel Shield       75 gold
  Chainmail Armor   150 gold

> buy sword
You purchase an Iron Sword for 50 gold.
Tormund says, "Fine choice! That blade will serve you well."

> ask tormund about the sword
Tormund says, "Ah, that's one of my standard blades. Good iron, properly
tempered. *taps the edge* Sharp enough to shave with, sturdy enough to
block a goblin's club. Perfect for someone just starting out."

Exercise: Create a Village Shopkeeper

Now it is your turn! Create a general store shopkeeper for a small village. Follow these requirements:

Requirements

  1. NPC Name: Martha
  2. Location: Village General Store
  3. Shop Type: General goods (rope, torches, rations, etc.)
  4. Personality: Motherly, chatty, knows everyone in the village
  5. Inventory: At least 5 different items

Step 1: Create the NPC

Connect to your game and use builder commands or write code:

Using builder commands:

> @goto #village_store_id
Village General Store
A cozy shop filled with everyday supplies...

> @create npc Martha
Created NPC 'Martha' with ID: martha-uuid...

> @describe martha A plump, cheerful woman in her fifties with rosy cheeks and flour-dusted hands. She bustles about the shop, always ready with a warm smile and the latest village gossip.

> @set martha/NPCComponent.is_merchant = true
> @set martha/NPCComponent.behavior_type = "merchant"

Using code:

martha_id = create_shopkeeper(
    world,
    name="Martha",
    description=(
        "A plump, cheerful woman in her fifties with rosy cheeks and "
        "flour-dusted hands. She bustles about the shop, always ready "
        "with a warm smile and the latest village gossip."
    ),
    room_id=village_store_id,
    shop_type="general goods",
)

Step 2: Configure Dialogue

Set up Martha's AI dialogue to match her personality:

from maid_stdlib.components import DialogueComponent

# Replace the default dialogue component
world.add_component(martha_id, DialogueComponent(
    ai_enabled=True,
    personality=(
        "A warm, motherly shopkeeper who has lived in the village her "
        "whole life. Knows everyone and their business (but shares gossip "
        "gently). Loves to chat and often gives advice to young adventurers. "
        "Worries about travelers heading into danger."
    ),
    speaking_style=(
        "Warm and chatty. Uses endearments like 'dear' and 'dearie'. "
        "Often sighs or tuts with concern. Tends to ramble and bring up "
        "tangentially related topics. Offers unsolicited motherly advice."
    ),
    npc_role="village general store owner",
    knowledge_domains=[
        "village gossip",
        "villagers and families",
        "local history",
        "practical supplies",
        "weather and seasons",
        "home remedies",
    ],
    secret_knowledge=[
        "knows about the old ruins to the north that nobody talks about",
        "her son went adventuring and never returned",
    ],
    will_discuss=["supplies", "villagers", "weather", "local news", "advice"],
    wont_discuss=["her son", "the old ruins", "her prices being too high"],
    greeting="Oh, hello dearie! Come in, come in! What can I get for you today?",
    farewell="You take care now! And eat something - you look too thin!",
    fallback_response="*tuts* Now, I'm not sure about that, dearie. But speaking of which...",
    max_response_tokens=140,
    temperature=0.8,  # More chatty/varied responses
))

Step 3: Add Shop Inventory

Stock Martha's store with useful adventuring supplies:

# Define shop items
shop_items = [
    ("Hemp Rope", "misc", 5, "Fifty feet of sturdy hemp rope."),
    ("Torch", "misc", 1, "A wooden torch that burns for about an hour."),
    ("Rations", "consumable", 2, "A day's worth of travel food."),
    ("Waterskin", "misc", 3, "A leather waterskin that holds a day's water."),
    ("Bedroll", "misc", 10, "A warm bedroll for camping."),
    ("Lantern", "misc", 15, "An oil lantern with a shuttered hood."),
    ("Healing Salve", "consumable", 25, "A poultice that heals minor wounds."),
]

for name, item_type, value, desc in shop_items:
    add_shop_item(world, martha_id, name, item_type, value, desc)

Step 4: Test Your NPC

Connect to the game and interact with Martha:

> go store

Village General Store
Shelves line the walls of this cozy shop, packed with everything an
adventurer might need. The smell of dried herbs mingles with leather
and rope.

Martha tends the shop here.

> greet martha
Martha says, "Oh, hello dearie! Come in, come in! What can I get for
you today?"

> ask martha about the village
Martha says, "*wipes hands on apron* Oh, it's a quiet little place,
been here all my life. The Hendersons run the farm to the west - lovely
family. Old Tom at the inn serves the best stew in three counties, if
you ask me. *lowers voice* Though I hear there's been strange lights
near the old ruins lately. Not that anyone talks about that..."

> ask martha about the ruins
Martha says, "*pauses and looks away* I... I'm not sure about that,
dearie. But speaking of which, have you got enough rations for your
journey? You look like you could use a good meal!"

> list
Martha's Shop - General Goods
=============================
  Hemp Rope        5 gold
  Torch            1 gold
  Rations          2 gold
  Waterskin        3 gold
  Bedroll         10 gold
  Lantern         15 gold
  Healing Salve   25 gold

> buy torch 3
You purchase 3 Torches for 3 gold.
Martha says, "Good thinking, dearie. You never know when you'll need
a bit of light. The nights have been darker lately, or so it seems..."

Congratulations!

You have created a fully functional shopkeeper NPC with:

  • A complete entity with all required components
  • AI-powered dialogue with personality and constraints
  • A stocked inventory of items for sale
  • Natural conversation flow with hints of secrets

What's Next?

You now know how to:

  • Create NPC entities with the right components
  • Configure static, scripted, and AI-powered dialogue
  • Set up basic behaviors like patrol and wander
  • Build merchant NPCs with shop inventory
  • Use dialogue commands to interact with NPCs

In the next tutorial, you will learn about combat and skills - how to give NPCs the ability to fight and how to create engaging combat encounters.

Coming up in Part 6: Combat & Skills

  • Combat components and stats
  • Attack and defense mechanics
  • Skills and abilities
  • Creating combat encounters
  • Death and respawning

< Previous: Part 4 - Rooms & Items Back to Tutorial Index Next: Part 6 - Combat & Skills >


Quick Reference

NPC Creation Template

from maid_stdlib.components import (
    DescriptionComponent,
    DialogueComponent,
    NPCComponent,
    PositionComponent,
)

npc_id = world.create_entity()
world.add_tag(npc_id, "npc")

world.add_component(npc_id, DescriptionComponent(
    name="NPC Name",
    short_desc="One-line description",
    long_desc="Full description",
    keywords=["keyword1", "keyword2"],
))

world.add_component(npc_id, NPCComponent(
    behavior_type="passive",  # passive, hostile, friendly, merchant
    is_merchant=False,
    wander_radius=0,
))

world.add_component(npc_id, PositionComponent(room_id=room_id))
world.place_entity_in_room(npc_id, room_id)

DialogueComponent Fields

Field Type Default Description
ai_enabled bool True Enable AI dialogue
personality str "" Core personality traits
speaking_style str "" How the NPC speaks
npc_role str "" Occupation/role
knowledge_domains list[str] [] Topics NPC knows about
secret_knowledge list[str] [] Hidden information
will_discuss list[str] [] Topics to discuss freely
wont_discuss list[str] [] Topics to avoid
greeting str "Hello." Initial greeting
farewell str "Goodbye." Farewell message
fallback_response str "" Default response
max_response_tokens int 150 Max AI response length
temperature float 0.7 AI creativity (0.0-1.0)
cooldown_seconds float 2.0 Time between responses

NPCComponent Fields

Field Type Default Description
behavior_type str "passive" Behavior pattern
is_merchant bool False Can buy/sell items
wander_radius int 0 Movement range
spawn_point_id UUID None Where NPC spawns
respawn_time float 300.0 Seconds until respawn
faction_id str None Faction membership

Dialogue Commands

Command Description
talk <npc> <message> Talk to NPC
ask <npc> about <topic> Ask about topic
greet <npc> Send greeting
hello <npc> Alias for greet
bye <npc> End conversation
conversations List active conversations

Temperature Guidelines

Value Effect Use For
0.3-0.4 Very consistent, formal Guards, officials
0.5-0.6 Consistent but natural Merchants, craftsmen
0.7-0.8 Balanced variety Most NPCs
0.9-1.0 Creative, unpredictable Mystics, tricksters