Skip to content

Redwall MUCK Site

Sections
Personal tools
You are here: Home » Members » Riverdale's Home » Code Examples » Teaching » MPI Tutorial 2 -- CD Player
« December 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 2 -- CD Player

Part two of the CD Player tutorial: code formatting, MPI macros, using {parse} loops, and some additional features.

Here's the code we already had from the first tutorial :

        @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}}}}}

New Tasks:

  • Reformat the MPI to make it more readable.
  • Move the song-lists into a dedicated prop-directory to avoid conflicts with other lists that might be on the playsong action.
  • Add some MPI macros.
  • Add support for song names with spaces.
  • Demonstrate how to list available songs.

Reformatting

Reformatting your code to make it more readable is useful both because it makes it easier to read (and therefore change) when you come back to it later on and because it makes debugging easier. You can quickly determine where you've left out a curly brace, or where you've made a logical error. Different people format their MPI in different ways, but the basic technique is the same: the more deeply "nested" a piece of code is, the greater the initial indent.

Here's a formatted version of our code:

                 1      lsedit playsong=succ
                 2      .del ^ $
                 3      {if:
                 4       {propdir:{&arg}#}, 
                 5              {if: 
                 6               {or:{not:{prop!:current_song}}, {gt:{subt:{secs},{prop!:last_timestamp}}, 5}},
                 7                      You begin playing the song '{store:{&arg},current_song}'. 
                 8                      {null:
                 9                              {store:1,current_line}, 
                10                              {delay:1,
                11                                      {lit:{lexec:doPlay}}
                12                              },
                13                              {otell:begins playing the song '{&arg}'.}
                14                      }
                15              , 
                16                      There is a song already playing!
                17              }
                18      ,
                19              That is not a song.
                20      }
                21      .end

                22      lsedit playsong=doPlay
                23      .del ^ $
                24      {null:
                25              {with:l,{prop!:current_line}, 
                26              {with:s,{prop!:current_song},
                27                      {force:{loc:this}, :: "{prop!:{&s}#/{&l}}"},
                28                      {if:
                29                       {prop!:{&s}#/{inc:l}}, 
                30                              {store:{&l}, current_line}
                31                              {store:{secs}, last_timestamp}
                32                              {delay:5, 
                33                                      {lit:{lexec:doPlay}}
                34                              }
                35                      ,
                36                              {delprop:current_line}
                37                              {delprop:current_song}
                38                      }
                39              }
                40              }
                41      }
                42  .end

This is not the prettiest reformatting job, but it does help tease out the logical structure from what otherwise looked like a haphazard jumble of commas, curly braces, and flat prose. Notice that {exec!} has become {lexec}. {lexec} is like {exec}, except that it takes a list name as an argument rather than a property name. Before executing the MPI code, it will remove the newlines and both the leading and trailing spaces from each line.

As you'll notice, I also added line numbers. Don't put those in the code.

To get this code to execute when we type playsong we do this:

                @succ playsong={lexec:succ}

Relocating Songs

You may have realized that by making these lists on the playsong action, any old user could easily have the CD Player sing your lovely MPI aloud by typing playsong playSong or playsong succ. This is, clearly, undesirable. So let's put your songs in their own prop directory. This is straightforward: instead of using lsedit playsong=short_songname, use something like this: lsedit playsong=songs/short_songname. If you want to move songs you've already stored, use the mv command: mv playsong=short_songname,playsong=songs/short_songname.

We have to adjust lines 4, 27, and 29 to accommodate this adjustment. In fact, since these three lines share some code, let's learn a nice trick.

MPI Macros

Macros in MPI are useful in helping improve the readability of your code, and for making important bits of it easier to reuse. They are stored in the special _msgmacs/ prop directory. Note that macros will run with the permissions of whatever the currently executing object is, and not with the permissions of the object on which the macro is stored, so it is effectively like substituting in a chunk of code.

Here is an example of a few MPI macros:

                @set playsong=_msgmacs/songsd:songs/
                @set playsong=_msgmacs/songdir:{songsd}{:1}#/
                @set playsong=_msgmacs/songline:{prop!:{songdir:{:1}}{:2}}

Macros will accept up to 9 arguments, which can be referenced in the manner shown above: {:n} where n is a number from one to nine. In the example of {songdir}, it accepts one argument--the name of the song--and returns the name of the directory where the lines of the song are stored. {songline}, despite its romantic, Chatwin-esque name, will simply fetch the given line of the given song.

With the macros, we rewrite the following lines:

                 4       {propdir:{songdir:{&arg}}}, 

                27                      {force:{loc:this}, :: "{songline:{&s},{&l}}"},

                29                       {songline:{&s},{inc:l}},

While you may not see the advantage to rewriting line 4 in this way, it can be useful to centralize configuration changes like this: now if you want to move the songs to a different directory in the future, you can change that in one place.

Macros can also be useful for builders, since they are available whenever the object on which they are stored is in the environment of another trigger object. I wrote code several years ago for my region on Redwall for implementing user-configurable text wrapping. This demonstrates how region-wide macros might be useful.

Listprops

Let's make another macro that returns a list of the songs in the directory:

                @set playsong=_msgmacs/songs:{listprops:{songsd}}

Note that as we build more code on this base, the use of the old macros becomes more clear. We no longer have to worry about where we've put the songs every time we would have originally typed out songs/.

{listprops} does exactly what it says: it lists props. Without an object, it behaves just like {prop}, {store}, and a whole host of others (this was covered in tutorial 1). Given an object as a second argument it will look on that object, and given a third, it will only match sub-properties that match a given wildcard pattern.

The properties will be the full name of the property: for example songs/elevation# instead of just elevation#, and the list will be newline-delimited, like lists in MPI are by default. (Unlike many list functions, {listprops} does not offer the option of specifying a delimiter for the returned values. There is probably a good reason for that.)

Basic String Manipulation

Remember that we want to allow song names to contain spaces. To achieve this, we should look at a couple of string manipulation functions.

First of all, we should note that property names cannot (note: as far as I know) contain spaces in them. This poses a problem for our current way of doing things: if you cannot even store the song in a list, the CD Player is useless.

An easy workaround for this is to use underscores in place of spaces. To add the song "Bad Moon Rising", we would then type lsedit playsong=Bad_Moon_Rising, and so on. Of course, if we leave the code as it is, players would have to type Bad_Moon_Rising to hear CCR do their thing.

Enter {subst}. This function, whose name is short for "substitute", lets us replace all occurrences of a particular character or string with something else. If, for example, you want to get rid of all vowels in a string, you cans use several of these in conjunction:

                @mpi {subst:{subst:{subst:{subst:{subst:I like to eat apples and banunus,a,*},e,*},i,*},o,*},u,*}
                Result: I l*k* t* **t *ppl*s *nd b*n*n*s

Note that {subst} is case-sensitive. To catch that pesky uppercase I in the above example, you could use the function {tolower} to make the whole string lowercase, but that may not always be desirable, since all characters in the string would be affected.

For our CD Player, we'll only need to make one substitution each time we deal with a song name:

                @mpi {subst:song_name,_, }
                Result: song name

Since we'll be doing this a lot, let's make another macro:

                @set playsong=_sep:_
                @set playsong=_msgmacs/songname:{subst:{subst:{subst:{:1},{songsd},},{prop:_sep}, },#,}
                @set playsong=_msgmacs/songsubst:{subst:{:1}, ,{prop:_sep}}

This will convert the name of a song directory into human-readable form. Let's adjust the macro songdir that we made earlier:

                @set playsong=_msgmacs/songdir:{songsd}{songsubst:{:1}}#/

This reverses the transformation we are performing on the song name. {songdir:Ooh La La} will now return songs/Ooh_La_La#/. Even if you pass it the argument Ooh_La_La, the returned value will be correct.

More Listprops and Some Matching

It's also annoying that players should be forced to enter the entire name of the desired song. Is there no better way?

In fact there, is. I mentioned in the section on the function {listprops} that it accepts a third argument, for specifying a pattern against which properties are matched. So say your /songs/ directory looks like this:

                /songs/Bad_Moon_Rising#/
                /songs/Beautiful_Day#/
                /songs/Beauty_Mark#/
                /songs/Dancing_Queen#/
                /songs/Elevation#/
                /songs/Volare#/

We can try a few things with '{listprops}':

                @mpi {listprops:/songs/,playsong,B*#}

MPI has a few, rather basic tools for string matching. The asterisk (*) matches anything (including a null string). The only requirement, then, is that the song name begin with a B, and that the prop ends in an asterisk (so we know it's a list). From the above command, we could get:

                /songs/Bad_Moon_Rising#/
                /songs/Beautiful_Day#/
                /songs/Beauty_Mark#/

Typing:

                @mpi {listprops:/songs/,playsong,Beauty*#}
                Result: /songs/Beautiful_Day#/
                /songs/Beauty_Mark#/

Narrows us down to U2 and Rufus.

You will notice that we are also laying the groundwork for another addition to the CD Player: listing the songs on the CD Player. In fact, let's spice things up a bit:

                @name playsong=playsong;listsongs

                @succ playsong={lexec:{&cmd}}

                mv playsong=succ#,playsong=playsong#

                lsedit playsong=listsongs
                .del ^ $
                        {center:Songs:,40,-}\r
                        {songname:{listprops:{songsd},this,{&arg}*#}}\r
                        {center:,40,-}
                .end

                @desc CD Player={lexec:listsongs,playsong}

The variable {&cmd} is another special one. It is automatically defined with the name of the specific alias the player used to trigger the action. Here, we've used it to decide whether to execute the code for playsong or the code for listsongs. Interestingly, I don't see much MPI code that makes use of this, while MUF programs routinely use the command variable for a similar purpose.

The second line from the listsongs code could end up being more generally useful, so let's pull it out into its own macro:

                @set playsong=_msgmacs/matchsongs:{listprops:{songsd},this,{songsubst:{:1}}*#}

Adjust the line in listsongs appropriately.

Recall that the original point of this whole listprops/matching exercise was to allow players to type just a small piece of the name of song. Before we get to that, we need to cover some list-handling functions. After we do that, we can also improve upon our rather primitive listprops action.

List Handling Functions

Now is a good time to introduce some list handling functions in MPI. The first one we will discuss, {parse} is very useful, and, appropriately, we will be making use of it very shortly.

Parse

{parse} takes between three and five arguments: {parse:variable, list, expression, input separator, output separator}. It will perform the same operation (as specified in your expression argument) on every item in the list you pass to it. We can use it to, say, reformat a list with line numbers in front:

                        @mpi {with:i,0,{parse:line, {list:desc}, {right:{inc:i},3}: {&line}}}

The function {right}, by the way, pads a string (of text) to a desired width by adding spaces (or a character you specify) to the left of your string, thereby right-aligning that string.

In the code example, {parse} systematically goes through each item in the list with the name desc and executes your expression, with the current item being processed as the stored value of the variable line. So if your desc list looked like this:

                        "The time has come," the walrus said,
                        "To talk of many things.
                        Of shoes and ships and ceiling wax,
                        Of cabbages and kings."

The output would add the numbers 1 through 4 to the beginning of each line in its output:

                          1: "The time has come," the walrus said,
                          2: "To talk of many things.
                          3: Of shoes and ships and ceiling wax,
                          4: Of cabbages and kings."

Before returning to the CD Player, let's look at a couple more list-handling functions.

Filter and Commas

{filter} takes the same arguments as {parse}, except that the expression argument should evaluate to either a true (non-null, non-zero) or false (null/zero) value. If it evaluates true, the list item currently being examined will be included in the output list. If false, the item is excluded.

Let's try an example.

Say you have a list of player names: Otter Dumble Vaticus Romaq Nekenyu. For some unspecified reason, we only want names that contain the letter "u", and we want the names to be in the format name1, name2, and name3. To achieve the latter, we will use {commas}.

First step:

                        @mpi {filter:who,Otter Dumble Vaticus Romaq Nekenyu,{smatch:{&who},*u*}, ,\r}

From this we get Dumble\rVaticus\rNekenyu as a return value. Notice the final two arguments in {filter}. These demonstrates the use of separators. The input separator (a single space) determines that the items the want to filter are separated by spaces. The output separator (\r--a newline) means that the returned list is separated by newlines. This is necessary, because {commas} does not allow you to specify an input separator, so it requires newline separators.

Using {commas} in its most basic form is quite easy: just pass it a newline-delimited list of values. Thus:

                        @mpi {commas:{filter:who,Otter Dumble Vaticus Romaq Nekenyu,{smatch:{&who},*u*}, ,\r}}

Will return Dumble, Vaticus and Nekenyu. If you're annoyed by the omission of the serial comma (and, in fact, I am), you can set the final separator yourself: E.g., {commas:<list>,\, and }.

Sublist

{sublist} is a function that allows you to extract a range of items from a list. It takes two to four arguments: {sublist:list, start item, end item, separator}, the last two arguments obviously being optional. One of its many applications is to create actions in MPI that take multiple arguments, since it lets you specify the list separator:

                        @mpi {sublist:arg1=arg2=arg3,2,2,=} is argument 2
                        -Result: arg2 is argument 2

Is nice? High five?

We'll use {sublist} now to finally implement song name matching:

                        @set playsong=_msgmacs/matchsong:{sublist:{matchsongs:{:1}},1}

This returns the first song that matches an argument. Let's adjust playsong to reflect this change:

                        lsedit playsong=playsong
                        .del ^ $
                        {with:song,{matchsong:{&arg}},
                                {if:
                                 {&song}, 
                                        {if: 
                                         {or:
                                          {not:{prop!:current_song}}, 
                                          {gt:{subt:{secs},{prop!:last_timestamp}}, 5}},
                                                You begin playing the song '{store:
                                                        {set:arg,
                                                                {songname:{&song}}
                                                        },
                                                        current_song
                                                }'
                                                {null:
                                                        {store:1,current_line}, 
                                                        {delay:1,
                                                                {lit:{lexec:doPlay}}
                                                        },
                                                        {otell:begins playing the song '{&arg}'.}
                                                }
                                        , 
                                                There is a song already playing!
                                        }
                                ,
                                        That is not a song.
                                }
                        }
                        .end

Notice that we change the value of {&arg} in this code to avoid repeating some code later on, when the identical operation would otherwise be performed. Provided you don't need the original value of the argument, altering it is totally non-evil.

Back to Listprops

Here is the code we had written for 'listprops':

                        {center:Songs:,40,-}\r
                        {songname:{listprops:{songsd},this,{&arg}*#}}\r
                        {center:,40,-}

Since we know about {parse} now, we can do much more complex things with our song list: numbering, formatting, and, if we'd like, we can display the first lyric for each song using the {songline} macro we defined earlier:

                        lsedit playsong=listsongs
                        .del ^ $
                        {center: Songs ,40,-}\r
                        {with:i,0,
                                {parse:song,
                                        {matchsongs:{&arg}},
                                        {right:{inc:i},3}. {songname:{&song}} -- {strip:{songline:{songname:{&song}},1}}...
                                }
                        }\r
                        {center:,40,-}
                        .end

(The {strip} function strips off extra leading and trailing spaces.)

Typing listsongs Beaut now gives us a view something like this:

                        ----------------- Songs ----------------
                          1. Beautiful Day -- The heart is a bloom...
                          2. Beauty Mark -- I never had it, I never wanted it...
                        ----------------------------------------

Created by Riverdale
Last modified 2007-02-20 08:16 PM
 

Powered by Plone

This site conforms to the following standards: