diff --git a/docs/how/dsls.md b/docs/how/dsls.md new file mode 100644 index 0000000..d34cf14 --- /dev/null +++ b/docs/how/dsls.md @@ -0,0 +1,1958 @@ +# 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