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:
- Understand what NPCs are and their role in a MUD
- Create NPC entities with the right components
- Configure static dialogue with triggers and responses
- Set up AI-powered dialogue for dynamic conversations
- Add basic behaviors like patrolling and wandering
- Build a complete shopkeeper NPC with inventory
- 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¶
- NPC Name: Martha
- Location: Village General Store
- Shop Type: General goods (rope, torches, rations, etc.)
- Personality: Motherly, chatty, knows everyone in the village
- 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 |