Skip to content

Redwall MUCK Site

Sections
Personal tools
You are here: Home » Members » Riverdale's Home » Code Examples » Teaching » MPI Tutorial 1 -- CD Player
« August 2008 »
Su Mo Tu We Th Fr Sa
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            
 

MPI Tutorial 1 -- CD Player

Using a fairly sophisticated example (a CD player that, over time, 'sings' a song from a list), we pick apart the pieces and begin to learn MPI.

Head First

Note: You can download a PDF of this tutorial here

Let's begin with the code:

                @create CD Player

                @set CD Player=X

                @flock CD player=me

                @act playsong=CD Player==$nothing

                @succ playsong={if:{propdir:{&arg}#}, {if:{or:{not:{prop!:current_song}}, {gt:{subt:{secs},
                {prop!:last_timestamp}}, 5}},You begin playing the song '{store:{&arg},current_song}'. 
                {null:{store:1,current_line}, {delay:1, {lit:{exec!:playSong}}},
                {otell:%n begins playing the song '{&arg}'.}}, There is a song already playing!},
                That is not a song.}

                @set playsong=playSong:{null:{with:l,{prop!:current_line}, {with:s,{prop!:current_song},
                {force:{loc:this}, :: "{prop!:{&s}#/{&l}}"}, {if:{prop!:{&s}#/{inc:l}}, 
                {store:{&l}, current_line}{store:{secs}, last_timestamp}
                {delay:5,{lit:{exec!:playSong}}},{delprop:current_line}{delprop:current_song}}}}}

See notes: [x], [flock], and [nothing] if those things are unfamiliar.

Unless you have previous programming experience or have already seen a lot of MPI (or both), don't expect this code to make much sense immediately. The point of this guide is to throw you right in the deep end, with some code that incorporates some reasonably sophisticated techniques. We will pick this code apart piecemeal, looking at each function individually, and I will provide illustrative examples for more difficult or useful concepts and functions.

What We Will Not Cover:

This "tutorial" will not cover complex string-handling techniques, list-handling, or very much beyond some flow-control basics. Some of these things are covered in the second tutorial, so check there, or page-mail me if you have a more specific question.

Also, the script above does not work with song names containing spaces. This is also covered in the second tutorial.

Pseudocode:

Let's briefly rewrite the MPI code in English, step by step:

                If the property directory <argument>#/ exists, then the song exists, so
                        If there is not a song currently playing
                                        or
                        If it has been more than five seconds since the last song played
                                Send message to player: You begin playing the song '<song name>'
                                Store the song name, <argument> in the property 'current_song'
                                Store 1 as the next line to play from the song
                                Wait 1 second, then execute PlaySong  vv
                        Otherwise
                                Return an error: There is already a song playing!
                Otherwise
                        Return an error: That song does not exist!

                PlaySong: {
                        Store the value of the current line in the variable &l
                        Store the value of the current song in the variable &s
                        Force the CD Player object to say the next line
                                next line = fetch from property &s#/&l
                        Check if this is the last line:
                                If the property &s#/&l+1 (the next line) exists
                                        increment the value stored in current_line 
                                                to the next line number.
                                        store the current timestamp in last_timestamp
                                        wait 5 seconds, then execute PlaySong ^^
                                Otherwise
                                        delete current_line property
                                        delete current_song property
                                        We're done playing the song!
                }

First the Basics

Terms

Functions perform operations based on the arguments you give them. They look like this:

                        {functionname:arg1, arg2,... argn}

*(Defining your own macros and functions is covered briefly in the second tutorial.)*

Variables hold values (numbers, text) while your code is executing and look like this:

                          {&varname}

A string is an ordered list of characters. In other words, a piece of text. Hello world is a string. abc is a string, and so on. In MPI, unlike many languages, you do not need to place strings inside quotation marks. For this reason, string-handling functions are not picky about whether you give them integers or strings or dbrefs.

I use the term object to refer to any rooms, players, actions, exits, and all other items in the MUCK database that possess unique dbrefs.

A dbref is a database reference. Within a MUCK, it is a unique identifier for a particular object on the game at a particular time. There is an important caveat, however: a dbref will not be unique through time. If an object (a player, room, exit, what have you) is recycled, the dbref associated with that object goes into a pool of unused dbrefs (garbage) and can later be reused. To generate a totally unique identifier (one that runs no risk of referring to a different object later), you can combine dbref and creation time, but this requires special permissions. (Exception: you can get the creation time of a player who triggered your command.)

A regname, or registered name, is a way of referring to objects in a more readable (and portable) fashion. They look like $name and are particularly nice for offline scripting. I do not explain them in depth here, but Vaticus' building tutorial does, rather briefly.

An action is an object that performs its @success (or @failure) and associated messages when a nearby player types its name or one of its aliases. An action can have many aliases, like in this example:

                        @action hello;hi;greetings;wave;greet=here==$nothing

An exit is a special type of action that is linked to a room. (In the olden days, it could also be linked to an object with the VEHICLE flag set.) I'll assume you're familiar with this much.

The trigger is the object on which the code is running. In many cases, this is is the action itself. The environment of an object begins with the object's location (or, in the case of an action/exit, the source to which it is attached--here in the above example), then proceeds through the location's location or parent, and so on until the chain ends. If I am sitting in my lovely living room, my environment might look like this:

                        The One Tree
                        |--- Root Parent
                                |--- Software Environment Room
                                        |--- IC Environment Room
                                                |--- Riverdale's Shack
                                                        |--- Riverdale's Living Room
                                                                |--- **Riverdale**

&ARG

{&arg} returns all text that the player typed after the command name. So, if he typed playsong Elevation, {&arg} would be Elevation. You will notice that this takes the format {&varname}, and that it is therefore a variable. It is only special because it is automatically defined; indeed, you can manipulate and reset it like other variables. Like all variables, it will fall out of scope if you perform a {delay}, so store its value in a prop if you need it later. Scope refers to the context within which a variable is defined.

Propdir

{propdir} will return a true value (1) if the directory {&arg}#/ exists on the playsong action. lsedit stores line values in this fashion. So lsedit me=desc would create the desc#/ property directory on me. The sub-properties are named as integers indicating line number. You can see for yourself by typing ex me=desc#/ (assuming that list exists).

NOTE

Whenever the object dbref is omitted in functions like {propdir}, {prop}, {store}, etc., the prop will be searched for on the action executing the code (in this case playsong), then on its location (CD Player), and so on up the environment tree. Thus, {propdir:{&arg#}} is just a more succinct way of writing {propdir:{&arg},this} or {propdir:{&arg}#,playsong}. (Notes on the this keyword below.)

If

{if} is used to conditionally execute code:

                        {if:1,yes,no} --> yes
                        {if:0,yes,no} --> no

Null strings and 0's evaluate as false in boolean/conditional context.

NOTE

If you have programmed before, you will notice that, unlike many languages (including MUF, which is strictly typed), datatypes all appear the same. A string is not enclosed in "s, true and false have no special meaning, there are no floating point numbers, and so on. Since MPI is domain-specific, I suppose this is not much of a problem.

Numeric Comparison

{gt} compares two values, as in {gt:4,2}. If the first is greater, returns 1 (true), otherwise 0 (false). Similar functions: {ge} (greater-than or equal), {eq} (equals, also works with string comparison), {ne} (not equal, ditto), {lt}, {le}

Logical Functions

{or} returns 1 (true) if any of its arguments are true. If the first argument is true, it will skip evaluation of the rest of the arguments (this is noteworthy only if your arguments have side-effects--that is, if they do anything to affect the state of the MU: set props, send {tell}s, or change variables)

Similar functions: {and} (returns true if all arguments are true), {xor} (returns true if JUST ONE argument is true)

The function {not} will return the opposite of its argument. Thus:

                        {not:1} --> 0
                        {not:0} --> 1

Logical functions help you combine conditions and return a boolean (true or false) value based on the truth or falsehood of those conditions. In our code, we use {or} this way:

                        (IF) the property current_song IS NOT set 
                                _OR_ 
                        timestamp - stored timestamp > 5 (THEN...)

To illustrate the use of {and} we can restate it this way:

                        (IF) the property current_song IS NOT set 
                                _AND_ 
                        timestamp - stored timestamp <=5 (THEN...)

or, in MPI:

                        {and:{not:{prop:current_song}}, {le:{subt:{secs},{prop:last_timestamp}},5}}

Math functions:

{subt} subtracts integers:

                        {subt:10,5} --> 5
                        {subt:4,2,1} --> 1

Similar math functions: {add}, {mult}, {div}, {mod} (modulo)

NOTE

MPI does not natively support floating-point values. With some reasonably straightforward techniques, you can fake it if you need to (hint: multiply your values by 10x10^n, where n is the desired precision; see here).

Notification

{tell} and {otell} are functions used to send a message to one player, or to a room of players, respectively. For regular, non-wizbitted players, these functions have some annoying limitations. {otell} will insist on prepending the activating player's name, for example, and {tell} sometimes adds letters to the beginning of the string.

{tell} will send the message (first argument) to a specific player (optional second argument). If the player is not provided, it will assume the player who caused your code to execute (the player who typed playsong, in other words.).

{otell} will send the message (first argument) to a specific room (optional second argument), excluding a particular player (optional third argument). By default, this assumes you want to notify the location of the executing player and that you want to exclude him/her from the notification. So:

                        @succ action={null:{otell:%n is a dingbat.}}

If Alice were to type action, everyone in the room (excepting Alice) would discover the truth of her dingbat origins.

Secs

{secs} returns the current timestamp (seconds since January 1, 1970). We use it here to determine if a song has stopped playing due to an error; if - is greater than 5, we assume it is okay to queue up a new song. This becomes:

                        {gt:{subt:{secs},<stored timestamp>},5}

The Plot Thickens

With

{with} is a function used to create user-defined variables. As I said, {&arg} is special, because it is automatically defined, although you can alter its value with the function {set}. ({&how} is another "special" variable). It takes the format {with:varname,initial value, <code>}. The variable will remain in scope (that is, defined) throughout <code>, but not beyond. It is therefore most useful for making your code slightly more readable and more succinct, and to avoid evaluating the same expression repeatedly.

In the example code, now we can simply use {&l} to refer to the current line rather than typing out {prop:current_line} every time we need the value. The expression (I called it initial value above) will only be evaluated the first time, so if the property current_line changes, the value of {&l} will not. This is generally desirable, but see mpi func if it's not what you want.

Related functions: {set}, {inc} (also used in the example), and {dec}

Null

{null} will suppress the output of a particular function. Since this code is stored in a @success message, anything that returns a value will gum up the output that the player using your action sees, unless you tuck it away behind a {null}.

Properties

{prop} will return the value, if any, stored in a particular property (first argument) on the specified object (optional second argument). Properties are very useful for storing little pieces of data that you'll need to call upon later.

{exec} is a similar function, also seen above and with the same syntax as {prop}, except that it will evaluate MPI code contained in the property. As we will see, {exec} is very useful for recursion, as well as code that you need to run periodically (the CD player is an example of precisely that). Similar functions: {lexec}, {list}

NOTE

There is a crucial difference in the way functions like {prop!} and {exec!} work from {prop} and {exec}. Functions with the bang (!) character will only search on the specified object. Omit it, and the MPI interpreter will crawl up the environment tree until it finds a matching property. In MUF, the difference is illustrated by GETPROPSTR (!) vs. ENVPROPSTR (no !).

This behavior can be very useful, but you should also be aware of it.

Store

{store} stores a value (the first argument) in a property (the second) on an object (optional third argument). This will return the value that you are storing. This is why, in the example, I did this: You begin playing the song {store:{&arg},current_song} : it kills two birds with one stone, both storing and displaying the value of {&arg}. Frequently, however, you will use {store} with its output suppressed ({null}).

Loc

{loc} is a database function: it returns the current location of a specified object. If called on an action/exit, it will return the object to which that action is attached. For an exit, that is the room from which the exit departs.

In the code, {loc:this} illustrates the meaning of the keyword this. Like me, this has a special meaning that relates to who is running the code and where it is running. me refers to the person or object executing the code; this refers to the object on which the code is running. Sometimes these are the same; if you type @mpi {name:this}, you will get the same result as @mpi {name:me}, because you both caused the code to be run and are the object on which the code is running. (@mpi actually creates a temporary hidden property on your player object and executes it with the MUF primitive PARSEPROP, see man parseprop)

Delay

{delay} will wait a certain number of seconds (first argument) before displaying a message (second argument). It is a little odd in its behavior, so it demands some explanation. When {delay} is called, the second argument is evaluated immediately, and then again when it is to be displayed. Here is an example:

                        @mpi {delay:10,\{subt:\{secs\}\,{secs}\}}

What are the backslashes? See footnote [4].

To return to the {delay} code, when the MPI interpreter first sees {delay:10,\{subt:\{secs\}\,{secs}\}}, it translates the second argument into something like this:

                        {subt:{secs}, 1171300854}

because the second {secs} is not escaped, but the first is. When, after waiting 10 seconds, this code is then evaluated again, the {secs} that you initially escaped will become something like this: 1171300864, and the response 10 will appear.

Lit

Manually escaping all your curly braces and commas could get to be a pain, so we use {lit} to do the work for us. Here we use {lit:{exec:playSong}}, so that, after the first evaluation, it becomes simply {exec:playSong}. This code is then run after an interval of 5 seconds. Since playSong is the code calling {exec:playSong}, it is recursing: i.e., the code is invoking itself. A word on this later. When using {lit}, be wary of one thing: unmatched curly braces. Since {lit} tries to be intelligent in determining where it closes, it will expect your open braces to be closed. Thus, {lit: (:{ } probably will not work as expected. Unless you expect it to crash and burn, which is correct: that is precisely what will happen.

Recursion

To perform a loop over time in MPI, you should use recursion. In other words, something like this:

                @set <object>=CodeProperty:{null:{tell:Ping.},{delay:5,{lit:{exec:CodeProperty}}}}

...where the code stored in a property will repeatedly invoke itself (note the last bit, beginning at {delay:...).

Once you execute that MPI code (say, by typing @mpi {exec:CodeProperty,object}), it will loop for a while--probably until the MUCK gets rebooted or goes down. To dequeue a running process, either check @ps and type @kill , or type @kill me if you only have one process in the table.

For simpler tasks, there are a number of helper functions that require an intermediate level of proficiency: {for}, {foreach}, {parse}, {filter}, {fold}, and {while}. Recursing and loops should be familiar to those with programming experience. I will explain some of these functions in a later document or revision.

Add a Song:

        lsedit playsong=Elevation
        .del ^ $
        *Bono wails a bunch of times*

        High, higher than the sun
        You shoot me from a gun
        I need you to elevate me here

        At the corner of your lips
        As the orbit of your hips
        Eclipse
        You elevate my soul

        I've got no self control
        Been living like a mole now
        Going down, excavation
        I and I, in the sky
        You make me feel like I can fly
        So high
        Elevation
        (and so on)
        .end

Endnotes

[x] The X flag makes it so that you can @force or {force} an object to execute a MUCK command. This is necessary here because the code in playSong uses {force} to make the CD Player "play" the lines.

[flock] @flock sets a force-lock on the CD Player. By default, attempts to @force an object will fail, even if it is set XForcible and you own it.

[nothing] $nothing is a global reg-name for the MUF program cmd-stub-nothing. As its name suggests, it does nothing. The reason we link to it is twofold. First, unlinked actions will fail ("You can't go that way."). You can set the @fail message to your MPI code, but then @lock is useless. The second reason is that historically, unlinked actions could be freely @chowned, even if not set CHOWN_OK. This may not longer be the case (I haven't tried recently), but the convention of linking to $nothing to avoid this problem has stuck.

[4] The \ (backslash) character will escape "meaningful" characters, like curly braces. So if you ever want to print something like (:{ in @succ message, do this:

                @succ my_action=You are evil.  (:\{

This is also important to know if you want to put a comma in an argument of a function, but do not want to waste characters typing {lit}. For example: {tell:I am a very foolish\, fond old man!} A third use of the escape character \ is for printing carriage returns. This creates a new line in the output. So {tell:Pray\, do not mock me.\rI am a very foolish\, fond old man\,\rFour score and upward} will print:

                Pray, do not mock me.
                I am a very foolish, fond old man,
                Four score and upward

Created by Riverdale
Last modified 2007-02-13 03:39 PM
 

Powered by Plone

This site conforms to the following standards: