MUD on Urbit

NPC AI & World Simulation

research Doc 11

A comprehensive reference covering mob artificial intelligence, world simulation mechanics, dynamic content generation, mob programming systems, and spawn/respawn architecture as implemented across the MUD tradition.


Table of Contents

  1. Mob AI Patterns
  2. Advanced Mob AI
  3. World Simulation
  4. Dynamic Content
  5. Mob Programs (Mobprogs)
  6. Spawn and Respawn Systems

1. Mob AI Patterns

Traditional MUD mob behavior is driven by act flags – bitfield flags set on each mobile that determine its baseline behavior. These flags originated in DikuMUD and were refined through Merc, ROM, SMAUG, and their descendants.

1.1 Core Behavior Flags

FlagNumeric (Envy)Behavior
SENTINEL2Mob stays in its room; never wanders
SCAVENGER4Picks up objects from the room floor
AGGRESSIVE32Attacks players within its level range on sight
STAY_AREA64Will not leave its area’s vnum range
WIMPY128Flees when HP drops below ~20%
AGGRESSIVE_GOODAttacks good-aligned characters only
AGGRESSIVE_NEUTRALAttacks neutral-aligned characters only
AGGRESSIVE_EVILAttacks evil-aligned characters only

1.2 Aggro (Aggressive Behavior)

Aggro is the most fundamental combat AI pattern. When a player enters a room containing an aggressive mob, the mob initiates combat.

Variations:

  • Level-range aggro: Only attacks players within a configurable level range (prevents newbie slaughter or pointless attacks on high-level players)
  • Alignment-based aggro: Selective aggression based on good/neutral/evil alignment
  • Race-based aggro: Attacks specific races (e.g., orcs attack elves)
  • Faction-based aggro: Aggro driven by reputation/faction standing

Pseudocode:

on_player_enters_room(player):
    if mob.has_flag(AGGRESSIVE):
        if player.level in mob.aggro_range:
            if not mob.is_fighting():
                mob.attack(player)

1.3 Assist Behavior

Mobs help other mobs in combat. CoffeeMUD defines the most granular taxonomy:

BehaviorDescription
BrotherHelperAssists identical mobs or those from the same spawn room
RaceHelperAssists mobs of the same race
AlignHelperAssists mobs of the same alignment
ClanHelperAssists mobs belonging to the same clan
MOBHelperDefends any non-player mob under attack
CombatAssisterDefends mobs matching a configurable mask
GuardDefends any attacked mob or player in the room

Typical implementation: Each combat tick, non-fighting mobs in the room scan for allies in combat. If a matching ally is found, the mob joins the fight on their side.

on_combat_tick():
    for each mob in room:
        if mob.is_fighting():
            continue
        for each combatant in room.fighting_list:
            if combatant.is_ally(mob) and combatant.is_defending():
                mob.attack(combatant.attacker)
                break

1.4 Wander (Mobile Behavior)

Mobs without the SENTINEL flag move randomly through connected rooms at regular intervals. CoffeeMUD’s Mobile behavior adds:

  • Frequency control: How often the mob attempts to move
  • Locale restrictions: Only wander through certain sector types (forest, city, etc.)
  • Leash limits: Maximum distance from home room
  • Patrolling: Follow a fixed path of room IDs or cardinal directions (Patroller behavior)

LPC approach (from Discworld MUD docs): Place an identification function on your mobs (e.g., query_is_area_monster() returns 1), then add exit checks in boundary rooms that block mobs with that tag from leaving.

Aardwolf Lua approach (entry trigger):

local lastroom = self.lastroom
if lastroom ~= nil then
   if self.room.sector ~= SECT_RIVER
      and self.room.sector ~= SECT_LAKE then
      rgoto(lastroom)  -- snap back if wrong sector
   end
end

1.5 Sentinel

The simplest behavior: the mob does not move. Used for:

  • Shopkeepers who must stay at their counter
  • Guards at a specific post
  • Quest NPCs that players need to find reliably
  • Boss mobs in their lair

1.6 Scavenger

The mob picks up objects from the room floor. Typically runs on the same tick as wander behavior. Useful for:

  • Janitor mobs that clean up abandoned equipment
  • Thieves who pocket loose items
  • Animals that eat food items left on the ground

1.7 Memory and Grudges

Mobs can remember players who attacked them and react on future encounters:

ROM/SMAUG mobprog approach:

MOB REMEMBER $n    -- remember the attacker as $q

Later, on a greet trigger:

if istarget $n
    say You again! I haven't forgotten what you did!
    MOB KILL $n
endif

Aardwolf Lua approach:

remember(ch)      -- store ch as self.target
adddelay(300)     -- set a timer

-- later, in delay trigger:
if self.target ~= nil then
    local victim = getmob(self.target.name, nil, LP_PLRONLY)
    if victim ~= nil then
        kill(victim)
    end
end
forget()          -- clear memory

1.8 Flee / Wimpy

When a mob’s HP drops below a threshold (typically 20%), it attempts to flee combat and move to a random adjacent room.

Implementation layers:

  1. Flag-based: The WIMPY act flag triggers automatic flee at 20% HP
  2. Trigger-based: The HPCNT trigger fires during combat when HP falls below a percentage, allowing scripted responses (casting heal, calling for help, surrendering, changing tactics)
  3. Behavioral: CoffeeMUD’s FightFlee behavior adds configurable timing parameters

Practical constraints (from EQ/MUD hybrid designs):

  • Fleeing NPCs must have active aggro on someone; clearing aggro stops the flee
  • NPCs mid-cast do not flee until the cast completes
  • If the NPC regenerates above ~30% HP, it stops fleeing and re-engages
  • Multiple hpcnt triggers should be listed in increasing order (40% fires before 20%)

1.9 Shop / Quest-Giver Behavior

Shop and quest NPCs combine several patterns:

Shopkeeper:

  • SENTINEL flag (stays put)
  • Buy/sell command handlers
  • Inventory management on area reset
  • Price calculation based on item type, condition, and charisma
  • CoffeeMUD: ShopKeeper interface with configurable buy/sell types, pricing, and prejudice masks

Quest-Giver:

  • Speech/give triggers that detect quest items or keywords
  • State tracking (quest not started / in progress / complete)
  • Reward distribution (XP, gold, items)

Aardwolf Lua example (bribe-triggered quest reward):

-- bribe trigger: fires when gold given exceeds threshold
if isplayer(ch) then
  if self.gold > 3000 then
    emote("smiles broadly.")
    say("You may well be the one fated to restore the Balance...")
    local newobj = oload("onslaught-15")
    give(newobj, ch)
  else
    emote("thank", ch)
    say("Generous, but not enough to prove your commitment.")
  end
end

Ranvier YAML-based quest NPC:

- id: rat
  keywords: ['rat']
  name: 'Rat'
  level: 2
  script: '1-rat'
  items: ['limbo:sliceofcheese']
  quests: ['limbo:onecheeseplease']
  attributes:
    health: 100
    speed: 2.5
  damage: 1-7

2. Advanced Mob AI

2.1 Scripted Behavior Systems

Mobprogs (ROM/SMAUG)

The original mob scripting system, using a command-based language similar to what players type. Triggers fire on game events and execute a sequence of MOB commands with simple if/else/endif control flow. See Section 5 for full details.

Lua (Aardwolf)

Aardwolf replaced mobprogs with a full Lua compiler integrated into the MUD engine. The system exposes five global variables:

  • self – the mob running the program
  • ch – the character that triggered the program
  • obj – relevant object (if any)
  • room – the room (for room progs)
  • mud – global MUD state (time of day, season, player count, etc.)

Lua provides real loops, functions, string manipulation, math, and table data structures – a massive improvement over mobprog if/else chains.

LPC (LPMud Family)

In LPC MUDs (Genesis, Discworld, Dead Souls), NPCs are full objects written in the LPC language. Behavior is implemented via:

  • Inheriting from /std/monster and overriding methods
  • The heartbeat system: heart_beat() is called every ~1 second on registered objects, driving healing, combat, wandering, and AI decisions
  • The reset system: reset() is called periodically by the driver, allowing rooms to respawn monsters
// Basic LPC monster creation in a room's reset()
void reset() {
    object ob;
    ob = new("/std/monster");
    ob->set_name("goblin");
    ob->set_level(5);
    ob->set_aggressive(1);
    ob->move(this_object());
}

Event-Driven Scripting (Ranvier)

Ranvier uses Node.js event emitters. All entities (NPCs, items, rooms, areas) can have scripts attached:

Unique script (one per entity):

'use strict';
module.exports = {
  listeners: {
    playerEnter: state => (room, player) => {
      // react to player entering
    },
    hit: state => (damage, target, attacker) => {
      // custom combat logic
    },
  },
};

Behavior (reusable, configurable):

'use strict';
module.exports = {
  listeners: {
    playerEnter: state => (config, room, player) => {
      // config comes from the NPC's YAML definition
      if (config.hostile) {
        // attack logic
      }
    }
  }
};

Multiple behaviors can be attached to a single NPC, composed from the YAML definition:

- id: 3
  name: "Training Dummy"
  behaviors:
    lootable:
      table:
        pools:
        - "limbo:junk"
        - "limbo:sliceofcheese": 25
    aggressive:
      range: 3

2.2 State Machines

Finite State Machines (FSMs) are the most common AI architecture in games. A mob exists in exactly one state at a time, with transitions triggered by inputs.

Basic enum approach:

enum MobState { IDLE, PATROL, CHASE, FIGHT, FLEE, RETURN }

on_tick(mob):
    switch mob.state:
        case IDLE:
            if player_in_range(mob):
                mob.state = CHASE
            elif mob.has_patrol_route:
                mob.state = PATROL

        case PATROL:
            move_along_route(mob)
            if player_in_range(mob):
                mob.state = CHASE

        case CHASE:
            move_toward(mob, mob.target)
            if adjacent_to(mob, mob.target):
                mob.state = FIGHT
            elif distance(mob, mob.target) > mob.leash_range:
                mob.state = RETURN

        case FIGHT:
            execute_combat(mob)
            if mob.hp_percent < 20:
                mob.state = FLEE
            elif mob.target.is_dead:
                mob.state = RETURN

        case FLEE:
            move_away_from(mob, mob.target)
            if mob.hp_percent > 30:
                mob.state = FIGHT
            elif safe_distance(mob):
                mob.state = RETURN

        case RETURN:
            move_toward(mob, mob.home_room)
            if mob.at_home:
                mob.state = IDLE
                mob.hp = mob.max_hp

Advanced FSM variants:

  • Hierarchical State Machines: Superstates contain substates. An “OnGround” superstate handles jumping for both “Standing” and “Crouching” substates, reducing duplication.
  • Pushdown Automata: A stack of states. Push a new state (e.g., “casting spell”) on top; when it completes, pop it and resume the previous state (e.g., “fighting”). Solves the “what was I doing before?” problem.
  • Concurrent State Machines: Separate FSMs for independent concerns (movement vs. combat stance vs. dialogue state). Reduces state explosion from N*M combinations to N+M states.

2.3 Behavior Trees

Behavior trees became the industry standard after Halo 2 (2004) because they scale better than FSMs for complex behavior.

Core node types:

  • Selector (OR): Try children left-to-right, succeed on first success
  • Sequence (AND): Run children left-to-right, fail on first failure
  • Decorator: Modify child behavior (invert, repeat, cooldown)
  • Leaf: Actual actions or conditions
Root (Selector)
├── Sequence: "Fight"
│   ├── Condition: enemy_visible?
│   ├── Condition: hp > 20%?
│   ├── Action: move_to_enemy
│   └── Action: attack
├── Sequence: "Flee"
│   ├── Condition: hp <= 20%?
│   └── Action: flee_to_safety
├── Sequence: "Patrol"
│   ├── Condition: has_patrol_route?
│   └── Action: follow_route
└── Action: idle

Advantages over FSMs:

  • State transition logic is centralized in the tree structure, not dispersed across states
  • Highly modular – subtrees can be reused across mob types
  • Easy to extend without rewriting existing logic

2.4 Group / Pack AI

Assist chains: When one mob is attacked, nearby mobs of the same type join combat (the BrotherHelper pattern). ROM implements this via the ASSIST flag family.

Pack behavior patterns:

  • Alpha/follower: One mob leads, others follow. If the alpha dies, the next-highest-level mob becomes alpha.
  • Coordinated attacks: Pack members flank or surround the target. One mob tanks while others deal damage.
  • Summoning: A mob under attack calls for reinforcements via MOB MLOAD or equivalent.
  • Retreat signal: When the alpha flees, the pack flees.

Mobprog example (kill trigger – calling for help):

-- kill trigger on a guard mob
if mobhere guard
    mob echoaround $n The guard shouts for backup!
    mob force guard kill $n
endif
mob kill $n

2.5 Boss Mechanics

Boss mobs combine multiple AI systems:

Phase transitions (using hpcnt triggers):

-- Phase 1: Normal combat
hpcnt 75:
    say "You think you can defeat me?"
    mob cast 'fireball' $n

-- Phase 2: Enraged
hpcnt 50:
    say "NOW YOU FACE MY TRUE POWER!"
    mob cast 'sanctuary' self
    -- increase damage output, change attack pattern

-- Phase 3: Desperate
hpcnt 25:
    say "I will take you all with me!"
    mob mload minion_vnum    -- summon adds
    mob mload minion_vnum
    mob cast 'earthquake' $n

Common boss patterns:

  • Minion summoning: Load additional mobs at HP thresholds
  • AoE attacks: mob damage all min max hits everyone in the room
  • Protection checks: Require specific items to survive special attacks
  • Enrage timers: After N minutes, boss does dramatically more damage
  • Room mechanics: Transfer players, lock doors, change room descriptions

Aardwolf Lua boss example (protection check):

-- fight trigger on a god-level boss
if isplayer(ch) then
   echoat(ch, "@CThe presence of the Gods sends out unimaginable energy...")
   if wears(ch, "onslaught-23") then
      echoat(ch, "@WThe energy passes harmlessly around you.@w")
   else
      damage(ch, 30000, 40000, LP_SEEALL)  -- instant kill without ward
   end
end

2.6 Reactive / Adaptive AI

Threat tables: Track damage dealt by each player. Mob targets the highest-threat player (tank/healer management).

Learning AI: Track player strategies across encounters. If players always use fire spells, the mob begins casting fire resistance.

LLM integration (CoffeeMUD): Modern CoffeeMUD integrates with LLMs via LangChain4J. The MPLLM scriptable feature supplements MudChat’s pattern-matching with AI-generated responses, allowing NPCs to hold freeform conversations while still executing game commands.

2.7 Dialog Trees

Pattern-matching (MudChat): CoffeeMUD’s MudChat behavior matches player speech against patterns in a chat.dat file and returns canned responses. QuestChat extends this with quest-state-aware responses.

Keyword-based (Mobprogs): Speech triggers match keywords in player dialogue:

-- speech trigger on an NPC
speech tell me about the quest
    say "The dragon has stolen the king's crown..."
    say "Bring it back and you shall be rewarded."

speech yes
    say "Brave soul! Head north to the Dragon's Lair."
    mob oload quest_map
    give map $n

Aardwolf Lua dialog (speech triggers with state):

-- speechexact trigger matching "yes"
if isplayer(ch) then
   if carries(ch, "quest-token") then
      say("You already have a task. Complete it first.")
   else
      say("Very well. Take this token and find the lost artifact.")
      local token = oload("quest-token")
      give(token, ch)
   end
end

Ranvier approach: Dialog is handled through quest definitions and NPC script events. The playerInteract event fires when a player talks to an NPC, and the script checks quest state via the quest manager.


3. World Simulation

3.1 Area Resets / Repops

Area resets are the heartbeat of a MUD’s world persistence. At regular intervals, areas “repop” – restoring mobs, objects, and door states to their defined defaults.

CircleMUD/ROM Reset System

Zone header format:

#<zone_vnum>
<zone_name>~
<top_room_vnum> <lifespan_minutes> <reset_mode>

Reset modes:

ModeBehavior
0Never reset (lifespan ignored)
1Reset after lifespan AND zone is empty of players
2Reset immediately on lifespan expiry (most common)

Reset commands:

CmdFormatPurpose
MM <if> <mob_vnum> <max_exist> <room_vnum>Load mob into room
OO <if> <obj_vnum> <max_exist> <room_vnum>Load object on ground
GG <if> <obj_vnum> <max_exist>Give object to last-loaded mob
EE <if> <obj_vnum> <max_exist> <equip_pos>Equip object on last-loaded mob
PP <if> <obj_vnum1> <max_exist> <obj_vnum2>Put object inside container
DD <if> <room_vnum> <exit_num> <state>Set door state (0=open, 1=closed, 2=locked)
RR <if> <room_vnum> <obj_vnum>Remove/purge object from room

The if-flag creates dependency chains: if set to 1, the command only executes if the previous command succeeded. This creates conditional sequences like “load mob, THEN give it a sword, THEN equip it with armor.”

The max_existing argument caps how many copies of a mob/object can exist in the entire world. If the cap is reached, the reset command silently skips.

Example reset block:

M 0 3010 5 3001    -- Load guard (max 5) in room 3001
E 1 3020 5 16      -- Equip guard with sword (wield position)
E 1 3021 5 3       -- Equip guard with shield (shield position)
G 1 3022 10        -- Give guard a key
M 0 3011 3 3002    -- Load shopkeeper (max 3) in room 3002
O 0 3050 1 3003    -- Load treasure chest in room 3003
P 1 3051 1 3050    -- Put gold inside chest
D 0 3004 2 2       -- Lock the south door of room 3004
S                  -- End of zone commands

SMAUG INSTAZONE

SMAUG provides the INSTAZONE command that automatically generates reset lists from the current world state: place mobs in their rooms wearing their equipment, then run INSTAZONE and it writes all the M/E/G/O/P commands for you.

Ranvier Repop Trigger

Ranvier supports room-level repop triggers that fire on area reset:

add repop [prog_id] [optional_percent] [optional_actor]

3.2 Weather Systems

MUD weather systems range from cosmetic text to gameplay-affecting mechanics.

Algorithmic approach:

  • Use sine waves keyed to in-game date for annual temperature curves (peak in summer, trough in winter)
  • Layer smaller sine waves for daily variation (warmer at noon, cooler at midnight)
  • Add random perturbation within bounds for unpredictability
  • Seed a PRNG with the in-game date for deterministic, reproducible weather

Gameplay integration:

  • Fog reduces visibility between rooms (can’t see mob descriptions in adjacent rooms)
  • Rain extinguishes torches (Nemesis MUD implemented wet torches that dry later)
  • Temperature affects healing rates or spell effectiveness
  • Storms block outdoor travel or flight
  • Snow accumulation changes room descriptions and movement costs

Pseudocode:

on_weather_tick():
    for each area in world:
        base_temp = sine(day_of_year / 365 * 2pi) * seasonal_amplitude
        daily_mod = sine(hour / 24 * 2pi) * daily_amplitude
        area.temperature = base_temp + daily_mod + random(-3, 3)

        if area.temperature < FREEZE_THRESHOLD:
            area.precipitation_type = SNOW
        else:
            area.precipitation_type = RAIN

        area.precipitation_chance = calculate_from_pressure(area)
        if random(100) < area.precipitation_chance:
            area.weather_state = PRECIPITATING
            apply_weather_effects(area)

3.3 Day/Night Cycles

Most MUDs run an accelerated clock (typically 1 real minute = 1 MUD hour, so a full day passes in ~24 real minutes).

Effects:

  • Room descriptions change (torch-lit at night, sunlit by day)
  • Nocturnal mobs become active; diurnal mobs sleep
  • Shops close at night (CoffeeMUD’s GateGuard locks doors at night)
  • Visibility reduces outdoors at night (need light source)
  • Certain spells or abilities are stronger/weaker based on time

Aardwolf Lua time access:

local current_hour = mud.hour  -- current in-game hour
if current_hour >= 20 or current_hour <= 5 then
    -- nighttime behavior
    echoat(ch, "The creature's eyes glow in the darkness.")
end

Mobprog time check:

if hour $i == 6
    say The sun rises! Time to open the shop.
    mob open north
    mob unlock north
endif
if hour $i == 20
    say Closing time!
    mob close north
    mob lock north
endif

3.4 Seasonal Events

Implementation patterns:

  1. Calendar-driven: Check the in-game date against a schedule. During event windows, swap area descriptions, load special mobs, activate event quests.

  2. Timer-driven: An AI director tracks time elapsed and triggers events at intervals. Events can scale based on player population.

  3. Condition-driven: Events trigger when world state meets criteria (e.g., total monster kills exceed a threshold, triggering an invasion).

Aardwolf Lua date handling:

local now = mud.unixtime()
local xmas = mud.unixtime("07:00 12/25/2025")
local diff = xmas - now

if diff > 0 and diff < 60*60*24*7 then
    -- Christmas week: activate holiday content
    say("Holiday cheer fills the air!")
end

3.5 Economy Simulation

MUD economies are notoriously difficult to balance. The core problem: money hemorrhages out of monsters (faucets) faster than it leaves the game (sinks), causing perpetual inflation.

Faucets (money enters the game):

  • Monster drops / loot
  • Quest rewards
  • Selling items to NPC shops (infinite demand at fixed prices)

Sinks (money leaves the game):

  • Buying from NPC shops
  • Training costs / skill purchases
  • Equipment repair
  • Death penalties (gold lost on corpse)
  • Taxes / tithes
  • Consumables (potions, scrolls, food)

Alter Aeon’s feedback control system:

  • Track total currency in the game in real-time
  • Dynamically adjust monster drop rates and shop prices based on total money supply
  • Tax players holding over 1 million gold at 2% of excess per period
  • Result: total currency fluctuates by less than 10% over two years

Shattered World’s loans-based economy:

  • Currency equation: Bank Loans = Bank Deposits + Currency in Circulation
  • All shops, banks, and businesses are player-owned and operated
  • Owners set their own buy/sell prices
  • Currency enters through player loans secured against property
  • The economy self-adjusts based on the number of players willing to borrow

Common design rules:

  • If an item is endlessly generated by the game, shops should refuse to buy it (or buy at near-zero prices)
  • Hard-to-get quest items have intrinsic value; common drops don’t
  • Administrators should participate in the economy as players, not use unlimited creation powers
  • Player-to-player trading should be the primary exchange mechanism for valuable items

4. Dynamic Content

4.1 Procedural Quest Generation

Template-based approach: Define quest templates with variable slots:

Template: FETCH_QUEST
  giver: {npc matching criteria}
  target_item: {item matching criteria}
  target_location: {area matching criteria}
  reward: {scaled to player level}
  narrative: "{giver.name} asks you to retrieve {item.name}
              from {location.name}."

NPC desire-driven approach: NPCs have dynamic desire models with parameters like satisfaction, wealth, and friendship. Quest generation draws from unmet NPC needs:

  • A hungry NPC generates a “bring food” quest
  • A threatened NPC generates a “kill the bandits” quest
  • A wealthy NPC generates a “deliver this package” quest

Parameters change dynamically based on quest completion, world state, and time passage, creating emergent quest chains.

StoryWorld framework: Generates procedural quests rooted in world logic – completing or failing quests permanently affects the world state, making the quest system feel consequential rather than arbitrary.

GenMUD approach: A fully generated world where NPCs are given names, society affiliations, and professions. Side quests generate procedurally based on local events and historical context. The world runs dynamic simulations even when no players are present.

4.2 Random Encounters

Zone-based encounter tables:

encounter_table "dark_forest":
    30%  - 2-4 wolves (level 5-8)
    20%  - 1 bear (level 10)
    15%  - 1-3 bandits (level 6-9)
    10%  - 1 dire wolf (level 12)
     5%  - 1 forest spirit (level 15, quest-giver)
    20%  - nothing

on_player_moves(player, room):
    if room.sector == FOREST:
        roll = random(100)
        encounter = lookup(encounter_table["dark_forest"], roll)
        if encounter:
            spawn_encounter(encounter, room)

Scaling encounters: Adjust encounter difficulty based on player level, party size, or recent activity. High-traffic areas can have depleted encounter rates (the monsters have been scared off).

4.3 World Events

Types:

  • Scheduled: Holiday events, seasonal festivals, monthly tournaments
  • Triggered: Player actions cause world-state changes (killing a faction leader starts a war)
  • Random: Periodic invasions, natural disasters, wandering boss spawns
  • Cascading: One event triggers others (drought -> famine -> refugee NPCs in cities -> new quests)

AI Director pattern: An overarching system monitors world state and player activity, deciding when to trigger events:

ai_director_tick():
    if time_since_last_event > MIN_EVENT_GAP:
        tension = calculate_tension(player_activity, world_state)
        if tension > EVENT_THRESHOLD:
            event = select_appropriate_event(tension, world_state)
            trigger_event(event)
            reset_tension()

4.4 Player-Triggered Events

Kill-chain events: Killing N mobs of a type triggers a response:

on_mob_death(mob):
    area.kill_count[mob.type] += 1
    if area.kill_count[mob.type] >= INVASION_THRESHOLD:
        trigger_invasion(area, mob.faction)
        area.kill_count[mob.type] = 0

Discovery events: Entering a hidden room, finding a rare item, or solving a puzzle triggers world-state changes.

Faction tipping points: When a player’s faction reputation crosses a threshold, NPCs throughout the world change behavior (new dialog options, shop access, quest availability).

4.5 Faction / Reputation Systems

Core architecture:

Reputation Tiers:
    WAR      [-1000, -600)   -- Kill on sight
    HOSTILE  [-600,  -200)   -- Will not interact
    NEUTRAL  [-200,   200)   -- Default standing
    FRIENDLY [ 200,   600)   -- Discounts, new quests
    ALLIANCE [ 600,  1000]   -- Full benefits, allied troops

Reputation changes:

  • Killing faction members: large negative
  • Completing faction quests: positive
  • Completing rival faction quests: negative (cross-faction penalty)
  • Trading with faction merchants: small positive
  • Passive decay: reputation slowly drifts toward neutral over time

Advanced features:

  • Propagation: Gaining rep with faction A gives partial rep with allies, partial negative with enemies
  • Diminishing returns: Repeated identical actions give less reputation over time
  • Faction relationship matrix: An N x N matrix defining how factions relate to each other (allied, neutral, hostile, at war)

Gameplay impact:

  • Area access (certain zones require minimum faction standing)
  • NPC behavior changes (aggro mobs become friendly, or vice versa)
  • Exclusive vendors with unique inventory
  • Allied NPCs assist the player in combat
  • Faction-specific quest lines unlock

5. Mob Programs (Mobprogs)

5.1 ROM/SMAUG Mobprogs

Mobprogs originated as a snippet for Merc/ROM that became standard in SMAUG and its descendants. They provide a command-based scripting language where mobs react to game events.

Trigger Types

TriggerWhen It FiresArgument
ACTAny act() message appears in roomkeyword phrase
SPEECHPlayer speaks matching phrasekeyword phrase
RANDOMEach PULSE_MOBILE tickpercent chance
GREETPlayer enters room (mob must see them)percent chance
GRALLPlayer enters room (always fires)percent chance
ENTRYMob enters a new roompercent chance
EXITPlayer leaves through specific exit (mob must see)direction
EXALLPlayer leaves through exit (always fires)direction
GIVEObject given to mobobject keyword or “all”
BRIBEGold given exceeds thresholdminimum gold
KILLPlayer initiates attack (fires once)percent chance
FIGHTEach PULSE_VIOLENCE during combatpercent chance
HPCNTMob HP below threshold during combatHP percentage
DEATHMob dies (before corpse created)percent chance
DELAYDelay timer expirespercent chance
SURRPlayer surrenderspercent chance

Variables

VariableExpansion
$i / $IMob’s name / short description
$n / $NTriggering character’s name / name with title
$t / $TSecondary target name / description
$r / $RRandom PC in room / description
$q / $QRemembered target / description
$o / $OPrimary object name / description
$p / $PSecondary object name / description
$j/$eHe/she/it (mob/trigger char, nominative)
$k/$mHim/her/it (mob/trigger char, objective)
$l/$sHis/hers/its (mob/trigger char, possessive)

Control Flow

if {check} {argument} [{operator} {value}]
[or {check} ...]
[and {check} ...]
    {commands}
[else
    {commands}]
endif

Operators: ==, !=, >, <, >=, <=

break / end – exit the entire program immediately.

If-Checks (Selection)

Condition checks on characters:

  • isnpc $n / ispc $n – NPC or player?
  • isgood $n / isneutral $n / isevil $n – alignment
  • isimmort $n – level > LEVEL_HERO
  • level $n > 10 – level comparison
  • hpcnt $n < 50 – HP percentage
  • class $n == 'warrior' – class check
  • race $n == 'elf' – race check
  • clan $n == 'shadow' – clan check
  • sex $n == 1 – gender (0=neuter, 1=male, 2=female)
  • align $n > 500 – alignment value

Room/world checks:

  • hour $i == 12 – game time
  • people == 3 – total characters in room
  • players == 2 – player count in room
  • mobhere guard – mob present (by name or vnum)
  • objhere sword – object present
  • rand 25 – 25% random chance

Inventory checks:

  • carries $n 'quest_item' – character has item
  • wears $n 'magic_ring' – character wearing item
  • has $n 'weapon' – character has item type

MOB Commands

Communication:

  • MOB ECHO / ECHOAT / ECHOAROUND – room messages
  • MOB ASOUND – message to adjacent rooms
  • MOB ZECHO – message to entire zone
  • MOB GECHO – global message

Loading/Destroying:

  • MOB MLOAD <vnum> – create NPC
  • MOB OLOAD <vnum> [level] [room|wear] – create object
  • MOB PURGE [target] – remove NPC/object
  • MOB JUNK <object> – destroy from inventory
  • MOB REMOVE <victim> <vnum|all> – strip and destroy equipment

Combat:

  • MOB KILL <victim> – attack
  • MOB FLEE – unconditional retreat
  • MOB CAST <spell> <target> – cast without mana cost
  • MOB DAMAGE <victim|all> <min> <max> [lethal] – direct damage

Movement:

  • MOB GOTO <location> – silent teleport
  • MOB TRANSFER <victim|all> [location] – move character(s)
  • MOB GTRANSFER – transfer with group
  • MOB AT <location> <command> – act in another room

Memory/Timing:

  • MOB REMEMBER <victim> – set $q target
  • MOB FORGET – clear target
  • MOB DELAY <seconds> – set delay trigger timer
  • MOB CANCEL – clear timer

Control:

  • MOB FORCE <victim|all> <command> – compel action
  • MOB CALL <vnum> [victim] [target1] [target2] – call subroutine (max 5 deep)

Complete Example

-- Area file trigger assignment:
M act 1000 pokes you in the ribs.~

-- Program #1000:
if isnpc $n
  chuckle
  poke $n
  break
else
  if level $n <= 5
   or isgood $n
    tell $n I would rather you didnt poke me.
  else
    if level $n > 15
      scream
      say Ya know $n. I hate being poked!!!
      if mobhere guard
        mob force guard kill $n
      endif
      kill $n
      break
    endif
    slap $n
    shout MOMMY!!! $N is poking me.
  endif
endif

This program: chuckles and pokes back at NPCs, warns low-level/good players, calls guards and attacks high-level players, and shouts for help against mid-level players.

5.2 Lua in Aardwolf

Aardwolf’s Lua system is a full replacement for mobprogs, providing a real programming language with access to MUD internals.

Architecture

Five global variables provide access to the MUD:

  • self – the mob executing the program
  • ch – the triggering character (nil for timer/random triggers)
  • obj – relevant object
  • room – room reference
  • mud – global state (time, season, online count)

Trigger Types

Aardwolf Lua supports all legacy mobprog triggers plus additions:

Character triggers: act, bribe, death, delay, entry, exit, exall, fight, give, greet, grall, hpcnt, kill, listen, random, speech, speechexact, tell, tellexact, transfer, timer

Room triggers: command, after, repop, cmdabbrev

Key additions over mobprogs:

  • timer – fires every N seconds (minimum 60), with fixed timing unlike random’s probabilistic check
  • cmdabbrev – intercepts actual MUD commands with abbreviation awareness. Can block commands by returning true.
  • command – arbitrary custom commands in rooms (e.g., “pick apple”, “search rubble”)
  • after – runs a MUD command first, then executes the prog

Function Library (Selection)

Test functions:

  • affected(ch, "flag") – check affect
  • carries(ch, "item", opts) – inventory check
  • isplayer(ch) – player vs NPC
  • mobexists("key", [room]) – mob existence check
  • wearsobj(ch, "type") – equipment check

Action functions:

  • say(), emote(), echo(), echoat() – communication
  • rgoto(), transfer(), summon() – movement
  • oload(), mload() – creation (return references!)
  • kill(), damage(), cast() – combat
  • giveexp(), giveqp() – rewards
  • remember(), forget(), adddelay() – memory/timing
  • call("progid") – invoke another Lua program

Practical Examples

Race-gated door (cmdabbrev on “open”):

if charg == nil then return false end
if room.south.open == 1 then return false end

local i = string.find("south", "^" .. string.lower(charg))
if i ~= nil then
   if ch.race ~= RACE_GIANT then
      send(ch, "Only a giant is strong enough to clear the way South.")
      return true   -- block the command
   else
      return false  -- allow normal open
   end
else
   return false
end

Multi-stage quest with delays:

-- death trigger on a boss mob
if isplayer(ch) then
   if self.target == nil then
      say("You have done it! By slaying the beast, you have saved us...")
      remember(ch)
      adddelay(4)
      purgeobj("all", LP_WORNONLY)
      purgeobj("all", LP_CARRIEDONLY)
   end
end

-- delay trigger (4 seconds later):
if self.target ~= nil then
   echoat(self.target, "The world shimmers as reality reforms...")
   giveexp(self.target, 50000)
   transfer(self.target, "onslaught-1")
   forget()
end

Room cleanup when empty:

-- random trigger on a boss room controller
if room.playercount == 0 then
   if mobexists("onslaught-58") then
      purgemob("onslaught-58", LP_SEEALL + LP_WORLD + LP_ALLMOBS)
   end
   self.hp = self.maxhp  -- full heal when no one is around
end

5.3 LPC Mob Scripting

In LPC MUDs, there is no separate “mobprog” language. NPCs are LPC objects that inherit from a base class (e.g., /std/monster) and override methods.

Key mechanisms:

  • heart_beat() – called every ~1 second on objects with heartbeats enabled. This is where combat, healing, wandering, and AI decisions happen.
  • reset() – called periodically by the driver. Rooms use this to respawn monsters.
  • init() – called when a living object enters the environment. Used to add attack commands, check aggro, etc.

Performance note: Objects with heartbeats are the major CPU hog in LPC MUDs. Minimize the number of objects with active heartbeats. Disable heartbeats on idle mobs and re-enable when players are present.

// Simple LPC monster in a room
inherit "/std/room";

void reset() {
    // Only respawn if the mob isn't already here
    if (!present("goblin")) {
        object mob = clone_object("/monsters/goblin");
        mob->move(this_object());
    }
}

// In /monsters/goblin.c:
inherit "/std/monster";

void create() {
    ::create();
    set_name("goblin");
    set_short("a sneaky goblin");
    set_long("A small, green-skinned creature with sharp teeth.\n");
    set_level(5);
    set_race("goblin");
    set_aggressive(1);
    set_heart_beat(1);
}

void heart_beat() {
    ::heart_beat();  // handle combat, healing

    // Custom wander behavior
    if (!query_attack() && random(10) < 2) {
        // 20% chance to wander each heartbeat
        string *exits = environment()->query_exits();
        if (sizeof(exits)) {
            command(exits[random(sizeof(exits))]);
        }
    }
}

5.4 Ranvier Event-Driven Scripting

Ranvier takes a modern approach: everything is an event. There are no “trigger types” in the mobprog sense – you listen for any event the engine emits.

Script file structure:

// scripts/npcs/areas/limbo/1-rat.js
'use strict';

module.exports = {
  listeners: {
    // fires when the NPC is hit
    hit: state => (damage, target, attacker) => {
      if (target.getAttribute('health') < target.getMaxAttribute('health') * 0.2) {
        // flee at 20% HP
        state.CommandManager.get('flee').execute(null, target, state);
      }
    },

    // fires when a player enters the room
    playerEnter: state => (room, player) => {
      state.ChannelManager.get('say').send(state, target, 'Squeak!');
    },

    // fires when the NPC dies
    deathblow: state => (victim, killer) => {
      // custom death behavior
    }
  }
};

Behaviors are the reusable equivalent of mob flags:

# npcs.yml
- id: wolf
  name: "Gray Wolf"
  behaviors:
    ranvier-aggro:
      range: 3
    ranvier-wander:
      interval: 30
      restricted: true
    lootable:
      table:
        pools:
          - "forest:wolf-pelt": 50
          - "forest:wolf-fang": 25
// behaviors/npcs/ranvier-wander.js
'use strict';

module.exports = {
  listeners: {
    updateTick: state => (config) => function () {
      if (this.isInCombat()) return;

      const room = this.room;
      const exits = room.getExits();
      if (!exits.length) return;

      const randomExit = exits[Math.floor(Math.random() * exits.length)];
      // move the NPC
      state.CommandManager.get(randomExit.direction).execute(null, this, state);
    }
  }
};

6. Spawn and Respawn Systems

6.1 Reset-Based Spawning (Traditional)

The traditional MUD spawn system is the area reset, a periodic process that restores an area to its designed state.

How it works:

  1. A timer counts down for each area (typically 3-15 minutes real-time)
  2. When the timer expires, the reset mode is checked:
    • Mode 1: Only reset if no players are in the area
    • Mode 2: Always reset
  3. The engine iterates through the area’s reset command list (M, O, G, E, P, D, R commands)
  4. Each command checks its max_existing cap before executing
  5. If a mob/object already exists at its designated location and hasn’t been killed/looted, the reset skips that entry

Key property: Resets are additive, not destructive. They fill in what’s missing, not replace what’s there. A mob that wandered to a different room is not teleported back – a new copy may spawn in its designated room if the max cap allows.

6.2 Reset Command Chains

The if-flag system creates equipment dependencies:

M 0 5001 1 5100     -- Load orc chief in room 5100 (max 1 in world)
E 1 5010 1 16       -- IF chief loaded: equip battle axe (wield)
E 1 5011 1 17       -- IF axe equipped: equip shield (hold)
G 1 5012 1          -- IF shield equipped: give dungeon key
M 0 5002 4 5101     -- Load orc grunt in room 5101 (max 4)
E 1 5013 4 16       -- IF grunt loaded: equip rusty sword
O 0 5020 1 5102     -- Load chest in room 5102 (always, max 1)
P 1 5021 1 5020     -- IF chest loaded: put gold inside
D 0 5103 0 2        -- Set north door of 5103 to locked (always)

6.3 Dynamic Spawn Rates

More sophisticated systems adjust spawn rates based on game conditions:

Player density scaling:

spawn_interval = base_interval * (1 / max(1, players_in_area))

More players = faster respawns to prevent resource starvation.

Feedback-controlled spawning (Alter Aeon approach):

  • Track global resource levels (total gold, total items of each type)
  • If resources are below target, increase drop rates and spawn rates
  • If resources exceed target, decrease rates
  • Apply diminishing returns to prevent exploitation

Time-of-day modulation:

  • Nocturnal mobs spawn more frequently at night
  • Undead spawn rates increase during in-game midnight
  • Market NPCs only appear during daytime hours

6.4 Rare / Boss Timers

Boss and rare mob respawns use different mechanics than trash mobs:

Fixed timer: Boss respawns exactly N minutes after death. Players can calculate and camp the spawn.

Window timer: Boss respawns randomly within a time window (e.g., between 4-8 hours after death). Prevents precise camping while maintaining predictability.

Condition-based: Boss spawns when specific conditions are met:

on_area_tick():
    if boss_is_dead and
       kill_count["trash_mob_type"] >= KILL_THRESHOLD and
       time_since_death >= MIN_RESPAWN:
        spawn_boss()
        reset_kill_count()

Triggered spawn: Boss appears in response to player action (using an item, completing a quest, reaching a specific room during an event).

6.5 Instanced vs. Shared World

Shared world (traditional MUD model):

  • All players share one copy of every area
  • Mobs exist once; first-come, first-served
  • Competition for spawns drives social dynamics (camping, kill-stealing)
  • Reset system keeps things populated
  • Boss access is competitive

Instanced dungeons (MMORPG-inspired):

  • A new copy of the area is created for each group that enters
  • Every group gets fresh mobs and bosses
  • No competition for spawns
  • More predictable difficulty scaling
  • Higher server resource cost (multiple copies of everything)
  • Less social friction, but also less emergent social gameplay

Hybrid approaches:

  • Instance only boss encounters, keep the open world shared
  • Create instances only when the area is overcrowded
  • Time-limited instances that merge back into the shared world after completion

Implementation considerations for a new MUD:

  • Shared world is simpler to implement and more true to the MUD tradition
  • Instancing requires tracking per-group state: mob HP, loot tables, reset state, door states
  • Instanced content benefits from stronger narrative design (each run tells a story)
  • Shared world benefits from emergent social dynamics (guild cooperation, spawn negotiation)

6.6 Spawn Architecture Pseudocode

class AreaResetManager:
    areas: Map<AreaId, AreaState>

    on_tick():
        for area in areas:
            area.timer -= tick_interval
            if area.timer <= 0:
                if area.reset_mode == RESET_ALWAYS:
                    execute_resets(area)
                    area.timer = area.lifespan
                elif area.reset_mode == RESET_WHEN_EMPTY:
                    if area.player_count == 0:
                        execute_resets(area)
                        area.timer = area.lifespan

    execute_resets(area):
        last_mob = null
        for cmd in area.reset_commands:
            success = false
            if cmd.if_flag and not last_success:
                continue

            switch cmd.type:
                case 'M':
                    if world.count(cmd.mob_vnum) < cmd.max_existing:
                        last_mob = spawn_mob(cmd.mob_vnum, cmd.room)
                        success = true
                case 'E':
                    if last_mob and world.count(cmd.obj_vnum) < cmd.max:
                        obj = create_obj(cmd.obj_vnum)
                        last_mob.equip(obj, cmd.position)
                        success = true
                case 'G':
                    if last_mob and world.count(cmd.obj_vnum) < cmd.max:
                        obj = create_obj(cmd.obj_vnum)
                        last_mob.inventory.add(obj)
                        success = true
                case 'O':
                    if world.count(cmd.obj_vnum) < cmd.max_existing:
                        obj = create_obj(cmd.obj_vnum)
                        room.add_object(obj)
                        success = true
                case 'D':
                    room = get_room(cmd.room_vnum)
                    room.set_door_state(cmd.exit, cmd.state)
                    success = true

            last_success = success

Sources

Mob AI and Behavior Flags

Mobprogs and Scripting

LPC

Ranvier

State Machines and Behavior Trees

Reset and Spawn Systems

Economy

Procedural Generation and Dynamic Content

Dialog and AI Integration