# 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): ```moo @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: ```moo @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): ```moo @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: ```moo 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 ```moo @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 ```moo @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): ```lpc 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): ```lpc 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): ```lpc 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): ```lpc room_chat(({ 120, 240, ({ "A frog croaks.", "A revolting smell drifts from the rug.", "You hear distant thunder." }) })); ``` parrot race definition (Discworld): ```lpc 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): ```lpc 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 ```lpc // /domains/town/npc/parrot.c #include 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 ```lpc // /domains/town/obj/television.c #include 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 ```forth : 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 ```forth $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: ```dil 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: ```dil 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: ```dil 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 ```dil 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 ```dil 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 ` ~ ``` ### 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_$> ENDIF ENDIF ~ ``` ### tv implementation (JavaScript hybrid) ``` LOAD_PROG 100 ~ 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 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: ```javascript 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: ```javascript 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 ```javascript // 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 ```javascript // 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: ```yaml - 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): ```javascript module.exports = { listeners: { playerEnter: state => function(player) { this.emit('combat', player); } } }; ``` ### parrot implementation ```yaml # 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 ``` ```javascript // 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 ```yaml # 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 ``` ```javascript // commands/pull.js module.exports = { usage: 'pull ', 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 ', 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: ```python 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: ```python 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 ```python 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: ```python 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 ```python 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