mud/docs/how/dsls.md

47 KiB

MUD Scripting Languages Reference

this document catalogs real-world MUD scripting DSLs with code examples. the goal is to study how different games let builders create interactive objects, NPCs, and behaviors -- to inform our own DSL design later.

for each language, we show real examples from existing MUDs, then implement two test scenarios:

scenario A - talking parrot: an NPC that listens to player speech, remembers what it hears, and randomly repeats things on a timer.

scenario B - tv set: an interactive object with a pull knob (on/off toggle) and spin dial (channel selector). when on, it periodically shows messages from the current channel (weather, news, etc).


1. MOO Verb Language

object-oriented, prototype-based. everything is an object with properties and verbs. fork schedules delayed tasks. :tell() sends messages. :announce_all() broadcasts to the room.

real examples

wind-up toy (classic timer pattern):

@verb toy:wind this none none
@program toy:wind
if (this.wound < this.maximum)
  this.wound = this.wound + 2;
  player:tell("You wind up the ", this.name, ".");
  player.location:announce(player.name, " winds up the ", this.name, ".");
endif
.

@verb toy:drop this none none
@program toy:drop
pass(@args);
if (this.wound)
  this.location:announce_all(this.name, " ", this.startup_msg);
  for x in [1..this.wound]
    fork (x * 15)
      this.location:announce_all(this.name, " ", this.continue_msg);
      this.wound = this.wound - 1;
    endfork
  endfor
endif
.

pet following + command response:

@verb pet:follow any with this
@program pet:follow
if ((player in this.masters) || (!this.masters))
  who = $string_utils:match_player(dobjstr);
  if (who == $failed_match)
    player:tell(this.name, " looks confused...");
  else
    this.tofollow = who;
    this.on_track = 1;
  endif
endif
.

interactive object (kleenex with conditional responses):

@verb kleenex:wipe any with this
@program kleenex:wipe
if (dobjstr == "nose")
  player:tell("Okay. You wipe your nose with the ", this.name, ".");
  player.location:announce(player.name, " has the sniffles.");
else
  player:tell("Okay. You wipe ", dobjstr, " with the ", this.name, ".");
endif
.

random message generation:

first = {"You discover that", "To your surprise,", "Unbelievably,"}[random(3)];
last = {" the cat is alive.", " the cat is dead."}[random(2)];
player:tell(first, last);

parrot implementation

@create $thing named parrot
@property parrot.memories {}
@property parrot.max_memories 10
@property parrot.chatty 1

@verb parrot:setup_listener this none this
@program parrot:setup_listener
" Called when parrot is placed in a room - starts the chatter loop ";
if (this.chatty)
  fork (random(60) + 30)
    this:chatter();
  endfork
endif
.

@verb parrot:chatter this none this
@program parrot:chatter
" Randomly repeats something it heard ";
if (this.location && length(this.memories) > 0)
  phrase = this.memories[random(length(this.memories))];
  this.location:announce_all(this.name, " squawks: \"", phrase, "\"");
endif
if (this.chatty)
  fork (random(60) + 30)
    this:chatter();
  endfork
endif
.

@verb parrot:hear this none this
@program parrot:hear
" Override of standard :hear verb to capture speech ";
pass(@args);
what = args[1];
who = args[2];
if (who != this && length(what) > 0)
  this.memories = {@this.memories, what};
  if (length(this.memories) > this.max_memories)
    this.memories = this.memories[2..$];
  endif
endif
.

@verb parrot:moveto this to any
@program parrot:moveto
pass(@args);
if (valid(this.location))
  this:setup_listener();
endif
.

tv implementation

@create $thing named television
@property tv.is_on 0
@property tv.channel 1
@property tv.channels {{"The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."}, {"Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."}, {"Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."}}
@property tv.broadcasting 0

@verb tv:pull this none none
@program tv:pull
if (this.is_on)
  this.is_on = 0;
  this.broadcasting = 0;
  player:tell("You pull the knob. The ", this.name, " turns off with a click.");
  player.location:announce(player.name, " turns off the ", this.name, ".");
else
  this.is_on = 1;
  player:tell("You pull the knob. The ", this.name, " flickers to life.");
  player.location:announce(player.name, " turns on the ", this.name, ".");
  this:start_broadcast();
endif
.

@verb tv:turn this none none
@verb tv:spin this none none
@program tv:turn
if (!this.is_on)
  player:tell("The ", this.name, " is off.");
  return;
endif
this.channel = (this.channel % length(this.channels)) + 1;
player:tell("You turn the dial. Now on channel ", this.channel, ".");
player.location:announce(player.name, " changes the channel.");
.

@verb tv:start_broadcast this none this
@program tv:start_broadcast
if (!this.broadcasting && this.is_on)
  this.broadcasting = 1;
  fork (15)
    this:broadcast();
  endfork
endif
.

@verb tv:broadcast this none this
@program tv:broadcast
if (this.is_on && valid(this.location))
  msgs = this.channels[this.channel];
  msg = msgs[random(length(msgs))];
  this.location:announce_all("The ", this.name, " shows: ", msg);
  fork (15)
    this:broadcast();
  endfork
else
  this.broadcasting = 0;
endif
.

@verb tv:moveto this to any
@program tv:moveto
this.broadcasting = 0;
pass(@args);
if (this.is_on && valid(this.location))
  this:start_broadcast();
endif
.

2. LPC (LPMud)

C-like, file-based OOP. key hooks: create() (init), init() (add_action for commands), heart_beat() (timer tick). objects inherit from standard library bases.

real examples

NPC scripting system (Lima):

create_script("lunch", ({
    step(SCRIPT_ACTION, "say Well, time for some lunch."),
    step(SCRIPT_WAIT, 5),
    step(SCRIPT_TRIGGER, "The elevator door opens.", "go northwest"),
    step(SCRIPT_DESC, "Harry, leaning against the elevator panel."),
    step(SCRIPT_ACTION, "emote stands up.@@say Guess it's time."),
}));

say-response NPC (Nightmare-Residuum):

void set_say_response(string match, string response) {
    __SayResponse[match] = response;
}
void handle_say_response(string message) {
    foreach (string match, string response in __SayResponse) {
        if (regexp(message, match)) {
            this_object()->handle_command("say " + response);
            return;
        }
    }
}

periodic random actions (Lima):

void set_actions(int delay, string *actions) {
   delay_time = delay;
   my_actions = actions;
   start_actions();
}

void actions() {
   if (!this_object()->query_target() && sizeof(my_actions))
      do_game_command(choice(my_actions));
   if (my_actions && query_listeners() && find_call_out("actions") == -1)
      call_out("actions", delay_time);
}

room chatter (Discworld):

room_chat(({ 120, 240, ({
    "A frog croaks.",
    "A revolting smell drifts from the rug.",
    "You hear distant thunder."
}) }));

parrot race definition (Discworld):

void setup() {
   set_name("parrot");
   set_long("A vicious, evil-looking parrot.\n");
   add_ac("feather_blow", "blunt", 10);
}
void set_unarmed_attacks(object thing) {
   thing->add_attack("beak", 50, ({ number / 2, 2, number / 2 }), "pierce", "unarmed", 0);
}

room with interactive lever/doorbell (Dead Souls):

void init() {
    ::init();
    add_action("aa_ring", "ring");
}
int aa_ring(string str) {
    write("DONG!\nYou ring a doorbell!\n");
    this_player()->SetProperty("rung_bell", 1);
    return 1;
}
int pre_north() {
    if (!this_player()->GetProperty("rung_bell")) {
        write("Ring the doorbell first!\n");
        return 0;
    }
    return 1;
}

Lima's react scripting mini-language:

@trigger The elevator door opens.
  go northwest
@endtrigger

@doevery 30 to 60
  say Anyone want to buy some fish?
@enddoevery

@if find gold here
  @action $N picks up the gold
  @move gold $self
@else
  say No gold here...
@endif

parrot implementation

// /domains/town/npc/parrot.c
#include <lib.h>

inherit LIVING;

string *memories;
int max_memories;
int next_chat;

void create() {
    ::create();
    set_name("parrot");
    set_short("a colorful parrot");
    set_long("A bright tropical parrot with green and red plumage. "
             "It watches you with intelligent eyes.");
    set_gender("neuter");
    set_race("bird");

    memories = ({});
    max_memories = 10;
    next_chat = 0;

    set_heart_beat(1);
}

void heart_beat() {
    ::heart_beat();

    if (!environment()) return;

    next_chat--;
    if (next_chat <= 0 && sizeof(memories) > 0) {
        string phrase = memories[random(sizeof(memories))];
        do_game_command("say " + phrase);
        next_chat = 30 + random(60); // 30-90 seconds
    }
}

void eventReceiveEmit(string message, object who) {
    string speech;

    // match "Foo says: blah" or "Foo says, \"blah\""
    if (sscanf(message, "%*s says: %s", speech) ||
        sscanf(message, "%*s says, \"%s\"", speech)) {
        if (who != this_object() && speech && strlen(speech) > 0) {
            memories += ({ speech });
            if (sizeof(memories) > max_memories) {
                memories = memories[1..];
            }
        }
    }
}

tv implementation

// /domains/town/obj/television.c
#include <lib.h>

inherit OBJECT;

int is_on;
int channel;
mapping channels;
int next_broadcast;

void create() {
    ::create();
    set_name("television");
    set_short("a vintage television set");
    set_long("An old black-and-white television with a pull knob and a dial. "
             "The screen is " + (is_on ? "glowing" : "dark") + ".");
    set_mass(5000);

    is_on = 0;
    channel = 1;
    channels = ([
        1: ({ "The weather is sunny.",
              "Clouds rolling in.",
              "Expect rain tonight." }),
        2: ({ "Breaking news: mayor resigns!",
              "Sports: home team wins!",
              "Traffic is heavy downtown." }),
        3: ({ "Soap opera: dramatic pause...",
              "Commercial: buy now!",
              "Sitcom: laugh track plays." })
    ]);
    next_broadcast = 0;

    set_heart_beat(1);
}

void init() {
    ::init();
    add_action("do_pull", "pull");
    add_action("do_turn", "turn");
    add_action("do_turn", "spin");
}

int do_pull(string str) {
    if (!str || !id(str)) return 0;

    if (is_on) {
        is_on = 0;
        write("You pull the knob. The television turns off with a click.");
        say(this_player()->query_cap_name() + " turns off the television.");
    } else {
        is_on = 1;
        write("You pull the knob. The television flickers to life.");
        say(this_player()->query_cap_name() + " turns on the television.");
        next_broadcast = 15;
    }

    set_long("An old black-and-white television with a pull knob and a dial. "
             "The screen is " + (is_on ? "glowing" : "dark") + ".");
    return 1;
}

int do_turn(string str) {
    if (!str || !id(str)) return 0;

    if (!is_on) {
        write("The television is off.");
        return 1;
    }

    channel = (channel % sizeof(channels)) + 1;
    write("You turn the dial. Now on channel " + channel + ".");
    say(this_player()->query_cap_name() + " changes the channel.");
    return 1;
}

void heart_beat() {
    ::heart_beat();

    if (!is_on || !environment()) return;

    next_broadcast--;
    if (next_broadcast <= 0) {
        string *msgs = channels[channel];
        string msg = msgs[random(sizeof(msgs))];
        environment()->eventPrint("The television shows: " + msg);
        next_broadcast = 15;
    }
}

3. DG Scripts (CircleMUD/TBAmud)

event-triggered scripts with %variable% substitutions. trigger types: greet, speech, random, command, fight, give, bribe, etc. scripts use if/else/end, wait, switch/case, %load%, %purge%.

real examples

quest NPC (greet trigger):

#1
Quest Offer~
0 g 100
~
if %actor.is_pc% && %direction% == south
  wait 1 sec
  say Can you help me, %actor.name%?
  wait 1 sec
  say An ogre has something of mine.
  say Please, bring me the wings.
end
~

magic eight ball (object command trigger):

#6
Magic Eight Ball~
1 c 2
shake~
switch %random.20%
  case 1
    %send% %actor% Outlook Good
  break
  case 2
    %send% %actor% Outlook Not So Good
  break
  case 3
    %send% %actor% Ask Again Later
  break
done
~

mynah bird (speech mimicry with variable storage!):

#1203
Mynah Bird~
0 d 100
*~
if %actor.is_pc%
  if %c%<20
    eval c 20
    global c
  end
  if !%speech.contains(!)%
    eval c (%c%)+1
    global c
    set %c% %speech%
    global %c%
    eval count %%random.%c%%%
    eval ans %%%count%%%
    wait %random.5%
    say %ans%
  end
end
~

guard bribe trigger:

#5
Guard Bribe~
0 m 1
~
if %amount% >= 10
  if %amount% > 10
    eval change %amount% - 10
    give %change% coin %actor.name%
  end
  say Thank you.
  unlock gateway
  open gateway
  wait 10 sec
  close gateway
  lock gateway
else
  say only %amount% coins, I require 10.
  give %amount% coin %actor.name%
end
~

parrot implementation

the mynah bird example above IS essentially the parrot scenario! it uses global variables %c% (counter) and %1%, %2%, etc (stored phrases). here's an annotated version:

#2001
Talking Parrot~
0 d 100
*~
* Speech trigger - fires on any speech in room
* Uses numbered global variables to store memories
if %actor.is_pc%
  * Initialize memory counter if needed
  if %c%<20
    eval c 20
    global c
  end
  * Only remember non-exclamatory speech
  if !%speech.contains(!)%
    * Increment counter
    eval c (%c%)+1
    global c
    * Store speech in numbered variable (%21%, %22%, etc)
    set %c% %speech%
    global %c%
    * Pick a random memory to repeat
    eval count %%random.%c%%%
    eval ans %%%count%%%
    * Wait random time then repeat it
    wait %random.5%
    say %ans%
  end
end
~

the clever bit: it uses the counter value itself as a variable name. set %c% %speech% expands to something like set 21 hello there, which stores "hello there" in variable %21%. then eval ans %%%count%%% does double-expansion to retrieve it.

tv implementation

#3001
TV Pull Knob~
1 c 2
pull~
* Command trigger on "pull tv"
if %cmd.mudcommand% == pull && pull /= tv
  * Toggle state using context variable
  if %is_on%
    eval is_on 0
    context is_on
    %send% %actor% You pull the knob. The television turns off with a click.
    %echoaround% %actor% %actor.name% turns off the television.
  else
    eval is_on 1
    context is_on
    %send% %actor% You pull the knob. The television flickers to life.
    %echoaround% %actor% %actor.name% turns on the television.
  end
else
  return 0
  halt
end
~
#3002
TV Turn Dial~
1 c 2
turn spin~
* Command trigger on "turn/spin tv"
if !%is_on%
  %send% %actor% The television is off.
  halt
end
* Cycle through channels 1-3
eval channel (%channel% %% 3) + 1
context channel
%send% %actor% You turn the dial. Now on channel %channel%.
%echoaround% %actor% %actor.name% changes the channel.
~
#3003
TV Broadcast~
1 f 100
~
* Random trigger - fires periodically
if !%is_on%
  halt
end
* Select message based on channel
if %channel% == 1
  switch %random.3%
    case 1
      set msg The weather is sunny.
    break
    case 2
      set msg Clouds rolling in.
    break
    case 3
      set msg Expect rain tonight.
    break
  done
else
  if %channel% == 2
    switch %random.3%
      case 1
        set msg Breaking news: mayor resigns!
      break
      case 2
        set msg Sports: home team wins!
      break
      case 3
        set msg Traffic is heavy downtown.
      break
    done
  else
    switch %random.3%
      case 1
        set msg Soap opera: dramatic pause...
      break
      case 2
        set msg Commercial: buy now!
      break
      case 3
        set msg Sitcom: laugh track plays.
      break
    done
  end
end
%echo% The television shows: %msg%
~

4. MUSHcode / Softcode

code stored in object attributes. $pattern:action creates commands. ^pattern:action creates listeners. @listen/@ahear for event reactions. %N = actor name, %0/%1 = captures.

real examples

basic command trigger:

&TURNON TV=$turn tv on:@emit %N turns on the TV.

echo device (monitor trigger):

@set EchoBox=MONITOR
&VA EchoBox=^* says, "*":@emit You hear an echo... %1 %1 %1...

listener reaction:

@listen Guard=*attacks*
@ahear Guard=@emit The guard steps in and shouts, "No fighting here!"

random selection:

&CMD-+CHECK Game=$+check *:@pemit %#=You rolled [rand(1,100)] on %0.

parrot implementation

@create Parrot
@set Parrot=MONITOR
@desc Parrot=A colorful tropical parrot. It watches you with intelligent eyes.

* Store memories as a space-separated list
&MEMORIES Parrot=

* Listen to all speech
&LISTEN Parrot=*
&AHEAR Parrot=@switch/first [strmatch(%0,*says*)]=1,{@va me=%0}

* Capture speech and store it
&VA Parrot=@switch/first [words(%q<1>)]>=[get(me/MAXMEM)]=1,{@trigger me/FORGET},{&MEMORIES me=[get(me/MEMORIES)] [after(%0,says)]};@trigger me/CHATTER

&MAXMEM Parrot=10

* Forget oldest memory when full
&FORGET Parrot=&MEMORIES me=[rest(get(me/MEMORIES))]

* Schedule random chatter
&CHATTER Parrot=@wait [rand(30,90)]=@switch [words(get(me/MEMORIES))]=0,,{@emit [name(me)] squawks: "[extract(get(me/MEMORIES),[rand(1,words(get(me/MEMORIES)))],1)]";@trigger me/CHATTER}

* Start chattering when created
@startup Parrot=@trigger me/CHATTER

tv implementation

@create Television
@desc Television=An old black-and-white television with a pull knob and a dial. The screen is [switch(get(me/ISON),1,glowing,dark)].

&ISON Television=0
&CHANNEL Television=1

* Channel content
&CH1 Television=The weather is sunny.|Clouds rolling in.|Expect rain tonight.
&CH2 Television=Breaking news: mayor resigns!|Sports: home team wins!|Traffic is heavy downtown.
&CH3 Television=Soap opera: dramatic pause...|Commercial: buy now!|Sitcom: laugh track plays.

* Pull command
&CMD-PULL Television=$pull television:@switch [get(me/ISON)]=1,{&ISON me=0;@oemit %#=%N turns off the television.;@pemit %#=You pull the knob. The television turns off with a click.},{&ISON me=1;@oemit %#=%N turns on the television.;@pemit %#=You pull the knob. The television flickers to life.;@trigger me/BROADCAST}

* Turn/spin dial command
&CMD-TURN Television=$turn television:@switch [get(me/ISON)]=0,{@pemit %#=The television is off.},{&CHANNEL me=[mod(add(get(me/CHANNEL),1),3)];@pemit %#=You turn the dial. Now on channel [get(me/CHANNEL)].;@oemit %#=%N changes the channel.}

&CMD-SPIN Television=$spin television:@trigger me/CMD-TURN

* Broadcast loop
&BROADCAST Television=@switch [get(me/ISON)]=0,,{@emit The television shows: [extract(get(me/CH[get(me/CHANNEL)]),[rand(1,3)],1,|)];@wait 15=@trigger me/BROADCAST}

* Start broadcasting if on
@startup Television=@switch [get(me/ISON)]=1,{@trigger me/BROADCAST}

5. MUF (Multi-User Forth)

stack-based language for TinyMUCK. : word ... ; defines words. me @ pushes current player. notify sends messages.

real examples

: hello-world
    me @ "Hello, world!" notify
;

: whoami
    me @
    "Your name is " me @ name strcat notify
;

: roll-die
    me @
    "You rolled a "
    random 6 % 1 +
    intostr strcat notify
;

parrot implementation sketch

$def MAXMEM 10

: parrot-remember ( str -- )
    me @ location contents  ( str loc-contents )
    begin
        dup while
        dup "parrot" stringpfx if
            dup "memories" array_getitem  ( str parrot memories )
            swap rot  ( memories parrot str )
            swap "memories" 3 pick  ( memories str "memories" parrot )
            3 pick array_appenditem  ( memories parrot )
            "memories" rot getpropstr  ( memories )
            array_count MAXMEM > if
                1 array_delitem  ( trim oldest )
            then
            "memories" swap setprop
        then
        next
    repeat
    pop pop
;

: parrot-chatter ( -- )
    me @ "memories" array_getitem  ( memories )
    dup array_count if
        random over array_count % 1 +  ( memories idx )
        array_getitem  ( phrase )
        me @ location
        "The parrot squawks: \"" 3 pick strcat "\"" strcat
        notify_except
    else
        pop
    then

    30 60 random + sleep
    parrot-chatter  ( recursive loop )
;

: parrot-listen
    begin
        read  ( str )
        dup "says" instr if
            parrot-remember
        else
            pop
        then
    repeat
;

(note: this is illustrative, not production-ready. MUF's event model would need more plumbing for real speech interception.)


6. DIL (DikuMUD Intermediate Language)

C-like compiled bytecode language. wait(SFB_*, condition) blocks for events. heartbeat for timers. labels + goto for loops. exec() runs game commands. self = attached object, activator = triggering player.

real examples

NPC that greets players:

dilbegin hi();
var
code
{
    :start:
    wait(SFB_DONE, command(CMD_AUTO_ENTER)
         and activator.type == UNIT_ST_PC);
    exec("say Hello, " + activator.name + "!", self);
    goto start;
}
dilend

guard daily routine with pathfinding:

dilbegin guardroutine(guardloc : string, dayguard : integer);
var
   sch : intlist;
code
{
   heartbeat := WAIT_SEC*5;
   :start:
   pause;
   if (mudhour == sch.[0])
   {
      exec("wake", self);
      exec("say Time to get going!", self);
      walkto(guardloc);
   }
   goto start;
}
dilend

speaking gem:

dilbegin gem_speak();
code
{
    heartbeat := 90 * PULSE_SEC;
    :start:
    wait(SFB_TICK, TRUE
         and self.outside.type == UNIT_ST_PC
         and self.equip);
    act("$1n says 'You look lovely today!'",
        A_ALWAYS, self.outside, self, null, TO_CHAR);
    goto start;
}
dilend

parrot implementation

dilbegin parrot();
var
    memories : stringlist;
    maxmem : integer;
    count : integer;
    phrase : string;
    u : unitptr;
code
{
    maxmem := 10;
    memories := {};
    heartbeat := PULSE_SEC * 5;

    :start:
    wait(SFB_COM, command("say") and activator.type == UNIT_ST_PC);

    * Capture what was said
    phrase := argument;
    if (length(phrase) > 0)
    {
        addstring(memories, phrase);
        count := length(memories);
        if (count > maxmem)
        {
            memories := delstr(memories, 0);
        }
    }

    goto chatter_wait;

    :chatter_wait:
    wait(SFB_TICK, TRUE);
    pause;
    if (rnd(1, 100) < 15 and length(memories) > 0)
    {
        goto chatter;
    }
    goto chatter_wait;

    :chatter:
    count := length(memories);
    if (count > 0)
    {
        phrase := memories.[rnd(0, count - 1)];
        exec("say " + phrase, self);
    }
    goto chatter_wait;
}
dilend

tv implementation

dilbegin television();
var
    is_on : integer;
    channel : integer;
    ch1 : stringlist;
    ch2 : stringlist;
    ch3 : stringlist;
    current : stringlist;
    msg : string;
code
{
    is_on := 0;
    channel := 1;

    ch1 := {"The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."};
    ch2 := {"Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."};
    ch3 := {"Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."};

    heartbeat := PULSE_SEC * 15;

    :start:
    wait(SFB_CMD, (command("pull") or command("turn") or command("spin"))
         and ("television" in argument or "tv" in argument));

    if (command("pull"))
    {
        if (is_on == 1)
        {
            is_on := 0;
            act("You pull the knob. The television turns off with a click.",
                A_ALWAYS, activator, self, null, TO_CHAR);
            act("$1n turns off the television.",
                A_ALWAYS, activator, self, null, TO_REST);
        }
        else
        {
            is_on := 1;
            act("You pull the knob. The television flickers to life.",
                A_ALWAYS, activator, self, null, TO_CHAR);
            act("$1n turns on the television.",
                A_ALWAYS, activator, self, null, TO_REST);
        }
    }

    if ((command("turn") or command("spin")) and is_on == 1)
    {
        channel := (channel % 3) + 1;
        act("You turn the dial. Now on channel " + itoa(channel) + ".",
            A_ALWAYS, activator, self, null, TO_CHAR);
        act("$1n changes the channel.",
            A_ALWAYS, activator, self, null, TO_REST);
    }

    goto broadcast_check;

    :broadcast_check:
    wait(SFB_TICK, TRUE);
    pause;
    if (is_on == 0)
    {
        goto start;
    }

    * Select current channel content
    if (channel == 1)
    {
        current := ch1;
    }
    else if (channel == 2)
    {
        current := ch2;
    }
    else
    {
        current := ch3;
    }

    msg := current.[rnd(0, length(current) - 1)];
    act("The television shows: " + msg,
        A_ALWAYS, self, null, null, TO_ALL);

    goto broadcast_check;
}
dilend

7. MobProg (Merc/ROM/SMAUG family)

inline scripts on mob definitions. trigger types: greet_prog, speech_prog, rand_prog, fight_prog, give_prog, etc. $n = actor, $i = self. if/else/endif conditionals.

real examples

wizard shop greeting:

>greet_prog 50~
if ispc($n)
  if rand(45)
    say How can I help you? Rings? Wands?
    grin
  else
    smile $n
    say Hello $n, what would you like today?
  endif
endif
~

random idle chatter:

>rand_prog 5~
if rand(51)
  say I know I put that bottle on the 53rd shelf.
  grumble
else
  grumble
  say Adventurers always bothering me.
endif
~

temple guardian (exit trigger):

>exit_prog 100~
if isgood $n
  say Hail!
  if carries $n holy
    mob transfer $n 2301
  else
    say Get a holy symbol first.
  endif
else
  curse $n
  say Get lost, scum!
endif
~

parrot implementation

>speech_prog 100~
* Store speech in mob's memory using mpvariable
* MobProgs don't have dynamic arrays, so we use a circular buffer
if ispc($n)
  if $speech != '!'
    * Increment counter
    if varcmp $i c == ''
      mpvariable c 1
    else
      mpvariable c $c + 1
    endif

    * Store in slot (mod 10)
    eval slot $c % 10
    mpvariable mem$slot $speech

    * Max stored
    if $c > 10
      mpvariable maxmem 10
    else
      mpvariable maxmem $c
    endif
  endif
endif
~

>rand_prog 10~
* Randomly repeat stored phrases
if rand(50)
  if varcmp $i maxmem > 0
    eval slot rnd($maxmem)
    eval phrase $mem$slot
    if $phrase != ''
      say $phrase
    endif
  endif
endif
~

(note: variable handling in MobProgs is implementation-specific. some versions have mpvariable, others don't support dynamic storage at all.)


8. CoffeeMUD Scripts

MobProg-inspired with 60+ event types and 80+ commands. also supports embedded JavaScript via <SCRIPT> blocks.

real examples

trader NPC:

GREET_PROG 75
say Welcome $n! I am a strange trader!
~

FIGHT_PROG 99
IF RAND(15)
    say Think yer TOUGH eh?
ELSE
    IF RAND(15)
        say Take THAT!
    ENDIF
ENDIF
~

GIVE_PROG all
IF GOLDAMT($o < 100)
    say You can do better than THAT.
    drop $o
ELSE
    say THAT's what I LIKE!
    MPOLOAD a nice tunic
    give "a nice tunic" "$n"
ENDIF
~

speech-reactive NPC:

SPEECH_PROG weapon sword mace
IF WORN($n sword)
    say You are wielding a sword!
ELSE
    IF HAS($n sword)
        say You have a sword!
    ENDIF
ENDIF
~

embedded JavaScript:

GREET_PROG 100
<SCRIPT>
    if(ISPC("$n"))
    {
        MPECHO("The ground shakes!");
    }
</SCRIPT>
~

parrot implementation (pure CoffeeMUD scripting)

SPEECH_PROG *
IF ISPC($n)
    IF !STRCONTAINS($t !)
        MPSETVAR $i mem_count $<$i.mem_count>+1
        MPSETVAR $i mem_$<$i.mem_count> $t
        IF $<$i.mem_count> > 10
            MPSETVAR $i mem_count 10
        ENDIF
    ENDIF
ENDIF
~

RAND_PROG 10
IF RAND(50)
    IF $<$i.mem_count> > 0
        MPSETVAR I slot RAND(1 $<$i.mem_count>)
        say $<$i.mem_$<I.slot>>
    ENDIF
ENDIF
~

tv implementation (JavaScript hybrid)

LOAD_PROG 100
<SCRIPT>
    MPSETVAR("$i", "is_on", "0");
    MPSETVAR("$i", "channel", "1");
    MPSETVAR("$i", "ch1_1", "The weather is sunny.");
    MPSETVAR("$i", "ch1_2", "Clouds rolling in.");
    MPSETVAR("$i", "ch1_3", "Expect rain tonight.");
    MPSETVAR("$i", "ch2_1", "Breaking news: mayor resigns!");
    MPSETVAR("$i", "ch2_2", "Sports: home team wins!");
    MPSETVAR("$i", "ch2_3", "Traffic is heavy downtown.");
    MPSETVAR("$i", "ch3_1", "Soap opera: dramatic pause...");
    MPSETVAR("$i", "ch3_2", "Commercial: buy now!");
    MPSETVAR("$i", "ch3_3", "Sitcom: laugh track plays.");
</SCRIPT>
~

FUNCTION_PROG pull_trigger
IF $<$i.is_on> == 1
    MPSETVAR $i is_on 0
    MPECHO $n turns off the television.
    TELL $n You pull the knob. The television turns off with a click.
ELSE
    MPSETVAR $i is_on 1
    MPECHO $n turns on the television.
    TELL $n You pull the knob. The television flickers to life.
ENDIF
~

FUNCTION_PROG turn_trigger
IF $<$i.is_on> == 0
    TELL $n The television is off.
ELSE
    MPSETVAR $i channel ($<$i.channel> % 3) + 1
    TELL $n You turn the dial. Now on channel $<$i.channel>.
    MPECHO $n changes the channel.
ENDIF
~

RAND_PROG 20
IF $<$i.is_on> == 1
    <SCRIPT>
        var ch = GSTAT("$i", "channel");
        var idx = RAND(1, 3);
        var msg = GSTAT("$i", "ch" + ch + "_" + idx);
        MPECHO("The television shows: " + msg);
    </SCRIPT>
ENDIF
~

9. room.js (JavaScript MOO)

Node.js MOO-like system. verbs are .js files on the filesystem per object. dispatch signature: ({ player, dobj, iobj, verbstr, argstr, dobjstr, prepstr, iobjstr }). run.in() for timers.

real examples

guitar with song playback:

function play({ player }) {
  if (this.location !== player) {
    player.tell("You don't have that.");
    return;
  }
  this.stropheIndex = 1;
  this.playing = player;
  this.playEmote();
}

function playStrophe() {
  const song = this.song.trim().split(/\n\n/);
  let strophe = song[this.stropheIndex - 1];
  this.playing.location.announce(() => strophe, this.playing);
  this.stropheIndex++;
  if (this.stropheIndex > song.length) {
    this.stropheIndex = 0;
    this.playing = null;
    return;
  }
  run.in(this.id + '.playStrophe()', 4000);
}

mouse that flees on examine:

function onExamine(player) {
  player.tell(`The ${this.name} starts panicking!`);
  const exitDirs = Object.keys(this.location.exits);
  if (exitDirs.length === 0) return;
  const picked = exitDirs[Math.floor(Math.random() * exitDirs.length)];
  this.location.goDirection({ player: this, argstr: picked });
}

parrot implementation

// parrot.js
function onCreate() {
  this.memories = [];
  this.maxMemories = 10;
  this.chatting = false;
}

function onHear({ speaker, message }) {
  // listen to other people's speech
  if (speaker === this) return;
  if (message.length === 0) return;

  this.memories.push(message);
  if (this.memories.length > this.maxMemories) {
    this.memories.shift();
  }

  if (!this.chatting) {
    this.startChattering();
  }
}

function startChattering() {
  this.chatting = true;
  this.chatter();
}

function chatter() {
  if (!this.location) {
    this.chatting = false;
    return;
  }

  if (this.memories.length > 0) {
    const phrase = this.memories[Math.floor(Math.random() * this.memories.length)];
    this.location.announce(() => `${this.name} squawks: "${phrase}"`);
  }

  const delay = 30000 + Math.random() * 60000; // 30-90 seconds
  run.in(`${this.id}.chatter()`, delay);
}

function onMove() {
  // restart chattering in new location
  if (this.chatting && this.location) {
    this.chatter();
  }
}

module.exports = {
  onCreate,
  onHear,
  startChattering,
  chatter,
  onMove
};

tv implementation

// television.js
function onCreate() {
  this.isOn = false;
  this.channel = 1;
  this.channels = [
    ["The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."],
    ["Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."],
    ["Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."]
  ];
  this.broadcasting = false;
}

function pull({ player }) {
  if (this.isOn) {
    this.isOn = false;
    this.broadcasting = false;
    player.tell("You pull the knob. The television turns off with a click.");
    this.location.announce(() => `${player.name} turns off the television.`, player);
  } else {
    this.isOn = true;
    player.tell("You pull the knob. The television flickers to life.");
    this.location.announce(() => `${player.name} turns on the television.`, player);
    this.startBroadcast();
  }
}

function turn({ player }) {
  if (!this.isOn) {
    player.tell("The television is off.");
    return;
  }
  this.channel = (this.channel % this.channels.length) + 1;
  player.tell(`You turn the dial. Now on channel ${this.channel}.`);
  this.location.announce(() => `${player.name} changes the channel.`, player);
}

function spin({ player }) {
  this.turn({ player });
}

function startBroadcast() {
  if (this.broadcasting || !this.isOn) return;
  this.broadcasting = true;
  this.broadcast();
}

function broadcast() {
  if (!this.isOn || !this.location) {
    this.broadcasting = false;
    return;
  }

  const msgs = this.channels[this.channel - 1];
  const msg = msgs[Math.floor(Math.random() * msgs.length)];
  this.location.announce(() => `The television shows: ${msg}`);

  run.in(`${this.id}.broadcast()`, 15000);
}

function onMove() {
  this.broadcasting = false;
  if (this.isOn && this.location) {
    this.startBroadcast();
  }
}

module.exports = {
  onCreate,
  pull,
  turn,
  spin,
  startBroadcast,
  broadcast,
  onMove
};

10. Ranvier (YAML + JavaScript)

data-driven: entities defined in YAML, behaviors in JS modules. events connect them.

real examples

NPC definition:

- id: 2
  name: "Rat"
  keywords: ["rat"]
  description: "A fat rat scurries about."
  script: "1-rat"
  items: ["limbo:sliceofcheese"]
  quests: ["limbo:onecheeseplease"]
  attributes:
    health: 100
  damage: "1-7"
  behaviors:
    lootable:
      table:
        pools:
          - "limbo:junk"

script (unique per-NPC):

module.exports = {
  listeners: {
    playerEnter: state => function(player) {
      this.emit('combat', player);
    }
  }
};

parrot implementation

# areas/town/npcs.yml
- id: parrot
  name: "colorful parrot"
  keywords: ["parrot", "bird"]
  description: "A bright tropical parrot with green and red plumage. It watches you with intelligent eyes."
  script: "town-parrot"
  behaviors:
    parrot-mimic:
      maxMemories: 10
// behaviors/npc/parrot-mimic.js
module.exports = {
  listeners: {
    spawn: state => function() {
      this.memories = [];
      this.maxMemories = this.getBehavior('parrot-mimic').maxMemories || 10;
      this.startChattering();
    },

    playerSay: state => function(player, message) {
      if (message.length === 0) return;

      this.memories.push(message);
      if (this.memories.length > this.maxMemories) {
        this.memories.shift();
      }
    }
  }
};

// Add to NPC prototype
NPC.prototype.startChattering = function() {
  const chatter = () => {
    if (!this.room || this.memories.length === 0) {
      setTimeout(chatter, 30000 + Math.random() * 60000);
      return;
    }

    const phrase = this.memories[Math.floor(Math.random() * this.memories.length)];
    B.sayAt(this.room, `${this.name} squawks: "${phrase}"`);

    setTimeout(chatter, 30000 + Math.random() * 60000);
  };

  chatter();
};

tv implementation

# areas/town/items.yml
- id: television
  name: "vintage television"
  keywords: ["television", "tv"]
  description: "An old black-and-white television with a pull knob and a dial."
  script: "town-television"
  behaviors:
    tv-broadcast:
      channels:
        - ["The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."]
        - ["Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."]
        - ["Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."]
  metadata:
    isOn: false
    channel: 0
// commands/pull.js
module.exports = {
  usage: 'pull <item>',
  command: state => (args, player) => {
    const item = state.ItemManager.parseDot(args, player.room.items);
    if (!item) {
      return B.sayAt(player, "Pull what?");
    }

    if (item.hasKeyword('television')) {
      const isOn = item.getMeta('isOn') || false;
      if (isOn) {
        item.setMeta('isOn', false);
        B.sayAt(player, "You pull the knob. The television turns off with a click.");
        B.sayAtExcept(player.room, `${player.name} turns off the television.`, player);
      } else {
        item.setMeta('isOn', true);
        B.sayAt(player, "You pull the knob. The television flickers to life.");
        B.sayAtExcept(player.room, `${player.name} turns on the television.`, player);
        item.emit('startBroadcast');
      }
    }
  }
};

// commands/turn.js
module.exports = {
  aliases: ['spin'],
  usage: 'turn <item>',
  command: state => (args, player) => {
    const item = state.ItemManager.parseDot(args, player.room.items);
    if (!item) {
      return B.sayAt(player, "Turn what?");
    }

    if (item.hasKeyword('television')) {
      if (!item.getMeta('isOn')) {
        return B.sayAt(player, "The television is off.");
      }

      const behavior = item.getBehavior('tv-broadcast');
      const numChannels = behavior.channels.length;
      const currentChannel = item.getMeta('channel') || 0;
      const newChannel = (currentChannel + 1) % numChannels;
      item.setMeta('channel', newChannel);

      B.sayAt(player, `You turn the dial. Now on channel ${newChannel + 1}.`);
      B.sayAtExcept(player.room, `${player.name} changes the channel.`, player);
    }
  }
};

// behaviors/item/tv-broadcast.js
module.exports = {
  listeners: {
    spawn: state => function() {
      if (this.getMeta('isOn')) {
        this.startBroadcast();
      }
    },

    startBroadcast: state => function() {
      const broadcast = () => {
        if (!this.getMeta('isOn') || !this.room) {
          return;
        }

        const behavior = this.getBehavior('tv-broadcast');
        const channel = this.getMeta('channel') || 0;
        const messages = behavior.channels[channel];
        const msg = messages[Math.floor(Math.random() * messages.length)];

        B.sayAt(this.room, `The television shows: ${msg}`);

        setTimeout(broadcast, 15000);
      };

      broadcast();
    }
  }
};

11. Evennia (Python Typeclasses)

Python classes with persistence hooks. at_object_creation() runs once. db.* persists. Scripts for timed behavior.

real examples

NPC reacting to player entry:

class NPC(Character):
    def at_char_entered(self, character, **kwargs):
        if self.db.is_aggressive:
            self.execute_cmd(f"say Graaah! Die, {character}!")
        else:
            self.execute_cmd(f"say Greetings, {character}!")

room that notifies NPCs:

class Room(DefaultRoom):
    def at_object_receive(self, arriving_obj, source_location, **kwargs):
        if arriving_obj.account:
            for item in self.contents:
                if utils.inherits_from(item, "typeclasses.npcs.NPC"):
                    item.at_char_entered(arriving_obj, **kwargs)

parrot implementation

from evennia import DefaultCharacter
from evennia.utils import delay
import random

class Parrot(DefaultCharacter):
    """
    A parrot that listens to speech and randomly repeats it.
    """

    def at_object_creation(self):
        """Called once when object is first created."""
        super().at_object_creation()
        self.db.memories = []
        self.db.max_memories = 10
        self.db.chatty = True

    def at_object_receive(self, obj, source_location, **kwargs):
        """Called when moved to a new location."""
        super().at_object_receive(obj, source_location, **kwargs)
        if self.db.chatty:
            self.start_chattering()

    def hear_speech(self, speaker, message):
        """
        Called by room when someone speaks.
        Hook this into your room's msg() or create a custom at_say hook.
        """
        if speaker == self:
            return

        if message and len(message) > 0:
            self.db.memories.append(message)
            if len(self.db.memories) > self.db.max_memories:
                self.db.memories.pop(0)

    def chatter(self):
        """Randomly repeat something heard."""
        if not self.location:
            return

        if self.db.memories:
            phrase = random.choice(self.db.memories)
            self.location.msg_contents(
                f'{self.get_display_name(looker=None)} squawks: "{phrase}"'
            )

        if self.db.chatty:
            delay(random.randint(30, 90), self.chatter)

    def start_chattering(self):
        """Begin the chatter loop."""
        if self.db.chatty:
            delay(random.randint(30, 90), self.chatter)

to integrate speech capturing, add to your Room typeclass:

from evennia import DefaultRoom

class Room(DefaultRoom):
    def at_say(self, speaker, message, **kwargs):
        """Hook called when someone speaks in the room."""
        # notify all parrot-like objects
        for obj in self.contents:
            if hasattr(obj, 'hear_speech'):
                obj.hear_speech(speaker, message)

tv implementation

from evennia import DefaultObject
from evennia.utils import delay
import random

class Television(DefaultObject):
    """
    An interactive TV with pull knob and turn dial.
    """

    def at_object_creation(self):
        """Called once when object is first created."""
        super().at_object_creation()
        self.db.is_on = False
        self.db.channel = 1
        self.db.channels = {
            1: ["The weather is sunny.", "Clouds rolling in.", "Expect rain tonight."],
            2: ["Breaking news: mayor resigns!", "Sports: home team wins!", "Traffic is heavy downtown."],
            3: ["Soap opera: dramatic pause...", "Commercial: buy now!", "Sitcom: laugh track plays."]
        }
        self.db.broadcasting = False

        # add commands to the object
        self.cmdset.add_default(TvCmdSet, persistent=True)

    def get_display_name(self, looker=None, **kwargs):
        """Add power state to name."""
        name = super().get_display_name(looker, **kwargs)
        if self.db.is_on:
            return f"{name} (glowing)"
        return f"{name} (dark)"

    def start_broadcast(self):
        """Begin broadcasting loop."""
        if self.db.broadcasting or not self.db.is_on:
            return
        self.db.broadcasting = True
        self.broadcast()

    def broadcast(self):
        """Show a message from current channel."""
        if not self.db.is_on or not self.location:
            self.db.broadcasting = False
            return

        messages = self.db.channels.get(self.db.channel, [])
        if messages:
            msg = random.choice(messages)
            self.location.msg_contents(f"The television shows: {msg}")

        delay(15, self.broadcast)

    def at_object_receive(self, obj, source_location, **kwargs):
        """When moved to new location."""
        super().at_object_receive(obj, source_location, **kwargs)
        self.db.broadcasting = False
        if self.db.is_on and self.location:
            self.start_broadcast()


from evennia import CmdSet, Command

class CmdPull(Command):
    """
    Pull the TV knob to toggle power.

    Usage:
        pull television
    """
    key = "pull"

    def func(self):
        tv = self.obj
        if tv.db.is_on:
            tv.db.is_on = False
            tv.db.broadcasting = False
            self.caller.msg("You pull the knob. The television turns off with a click.")
            self.caller.location.msg_contents(
                f"{self.caller.name} turns off the television.",
                exclude=self.caller
            )
        else:
            tv.db.is_on = True
            self.caller.msg("You pull the knob. The television flickers to life.")
            self.caller.location.msg_contents(
                f"{self.caller.name} turns on the television.",
                exclude=self.caller
            )
            tv.start_broadcast()


class CmdTurn(Command):
    """
    Turn the TV dial to change channels.

    Usage:
        turn television
        spin television
    """
    key = "turn"
    aliases = ["spin"]

    def func(self):
        tv = self.obj
        if not tv.db.is_on:
            self.caller.msg("The television is off.")
            return

        num_channels = len(tv.db.channels)
        tv.db.channel = (tv.db.channel % num_channels) + 1
        self.caller.msg(f"You turn the dial. Now on channel {tv.db.channel}.")
        self.caller.location.msg_contents(
            f"{self.caller.name} changes the channel.",
            exclude=self.caller
        )


class TvCmdSet(CmdSet):
    """Commands available on the TV object."""

    def at_cmdset_creation(self):
        self.add(CmdPull())
        self.add(CmdTurn())

patterns across languages

after looking at all these systems, some common patterns emerge:

event-driven architecture: almost every DSL is fundamentally event-driven. greet/speech/command/timer triggers dominate. even the procedural-looking ones (LPC, DIL) use wait() and callbacks.

state storage:

  • prototype systems (MOO, room.js) use object properties directly
  • file-based systems (LPC) use member variables
  • limited systems (DG Scripts, MobProg) get creative with numbered variables or simple flags
  • modern systems (Evennia) use persistent db.* attributes

timer patterns:

  • fork + recursion (MOO)
  • call_out (LPC)
  • wait + goto (DIL)
  • RAND_PROG percentage (MobProg)
  • setTimeout / delay (JavaScript / Python)

command dispatch:

  • verb matching (MOO, room.js)
  • add_action (LPC)
  • command triggers (DG Scripts, CoffeeMUD)
  • $pattern (MUSHcode)
  • typeclass methods (Evennia)

variable substitution: older systems love %actor%, $n, @me style placeholders. newer ones prefer real language variables.

embedded vs external: some systems inline scripts into data files (MobProg, DG Scripts), others separate code from data (LPC, Ranvier). the hybrid approach (YAML data + JS behaviors) seems to win for maintainability.

complexity gradient:

  • simple: MobProg, DG Scripts (limited control flow, mainly if/else)
  • medium: MOO, MUSHcode (more features but still constrained)
  • full language: LPC, DIL, JavaScript, Python (you can do anything)

the parrot problem: most systems can handle it, but the approaches differ:

  • dynamic lists: MOO, LPC, JavaScript, Python do it naturally
  • fixed-size buffers: DG Scripts use numbered globals, very clever
  • impossible: basic MobProg can't really do it without extensions

the tv problem: tests both state management and command handling. simpler systems struggle with the two-verb interaction (pull vs turn), while OOP systems handle it easily.

inspiration for our DSL:

  • lean toward event-driven, not polling
  • make state storage first-class (not a hack)
  • support both inline scripts (for simple stuff) and external modules (for complex behaviors)
  • variable substitution is nice for builders, real variables for power users
  • timer/delay primitives are essential
  • speech/emote/action hooks should be easy