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_PROGpercentage (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