Color Guide:
Red indicates a command (or commands) you enter into the MUCK.
Navy indicates text returned by the MUCK, usually a result of a command you typed.
|
|
Loops-- Introduction and Applications
by Silver of A Bug's Muck
In this tutorial I'm going to teach you about looping structures in MUF, and show some useful applications. Many other resources are available for learning loops, so my focus will be on practical MUF applications. When you're done with this tutorial, you'll be able to scan all the objects in the database, understand how to use the CONTENTS, EXITS and NEXT functions, and list the properties of an object. This tutorial assumes you're familiar with basic MUF primitives and can already write simple but useful MUF programs.
Contents
Loop Basics
I'll start with a simple example to illustrate basic looping techniques: we'll write MUF code to pop the entire stack. This is a typical application of a loop, counting down from some positive integer to 0. When removing everything from the stack, you'll want to get the number of items on the stack, and start popping until you're left with 0 items on the stack. You can use a variable to keep track of your loop counter, but it's just as easy to keep the counter on the top of the stack and pop it when you're done with it, which removes reliance upon external variables. Here's the code for popping the entire stack:
depth (Get the number of items on the stack and )
( place on top of the stack as a counter)
begin dup while (Loop start: continue until counter reaches 0)
swap pop (Pop first non-counter item on stack)
1 - (Decrement counter)
repeat (Jump back to 'begin')
pop (Remove counter)
Let's assume a stack has 4 elements: 'yellow' 'green' 'blue' 'purple' with 'purple' being on top. As our program executes, the stack looks like this before each BEGIN statement is executed:
'yellow' 'green' 'blue' 'purple' 4
'yellow' 'green' 'blue' 3
'yellow' 'green' 2
'yellow' 1
0
This code uses a BEGIN-REPEAT loop, often called a while-loop in other languages. The MUF version of a while-loop looks like this:
BEGIN <block 1> WHILE
<block 2>
REPEAT
Everything between BEGIN and REPEAT is repeated until a condition becomes false. That's where WHILE comes in. The WHILE statement pops the top of the stack and evaluates it for a true-false condition. In MUF, any integer greater than 0 is considered 'true'. Usually the code in block 1 determines what is to be evaluated by WHILE. The stuff in block 2 is the point of the loop, the code we want to repeat for the required task.
The first time our loop executes, we start by duplicating the top of the stack, 4, and use WHILE to test if it is true. It's greater than 0, so it's 'true' and we're allowed to execute the stuff between WHILE and REPEAT. We switch the top of the stack with what's below it, 'purple', and remove 'purple. Next, and most importantly, we decrement (subtract one) from the number on top of the stack, which is our loop counter. At this point we jump back to BEGIN and start over.
Can you guess what would happen if we forgot to decrement the loop counter? Eventually we'd get to a point where 4 was the only item on the stack, and when we tried to do a SWAP the program would throw an error because there's only one item on the stack.
When the loop counter on top of the stack reaches 0, WHILE interprets it as false, and execution jumps to the statement immediately following REPEAT, exiting the loop. In our case we no longer need the loop counter, so we pop it, yielding an empty stack.
Another way to write the same program is to use the BEGIN-UNTIL form of looping:
depth (Get the number of items on the stack and )
( place on top of the stack as a counter)
begin (Start of the loop)
swap pop (Pop first non-counter item on stack)
1 - (Decrement counter)
dup not until (If counter isn't 0 jump back to 'begin')
pop (Remove counter)
In this case, UNTIL has a function similar to WHILE-- it tests to see if the top of the stack is true (greater than 0), and if not it jumps execution back to the start of the loop. But there's a danger to this form of loop. Can you spot it? Hint: What happens if DEPTH returned 0? The loop portion of the BEGIN-UNTIL form always executes at least once, which forces us to do a little extra work for the case when DEPTH returned 0, indicating an already empty stack:
depth (Get the number of items on the stack and )
( place on top of the stack as a counter)
dup if (Are there any items to remove?)
begin (Start of the loop)
swap pop (Pop first non-counter item on stack)
1 - (Decrement counter)
dup not until (If counter isn't 0 jump back to 'begin')
then
pop (Remove counter)
When there are no other considerations, I prefer the BEGIN-REPEAT form because the looping condition is always checked before a loop is executed, a kind of built-in safety-check.
Sometimes it's useful to be able to break out of a loop at some point other than at a WHILE or REPEAT. You can do this with the BREAK statement. For example:
depth (Get the number of items on the stack and )
( place on top of the stack as a counter)
begin dup while (Loop start: continue until counter reaches 0)
swap pop (Pop first non-counter item on stack)
1 - (Decrement counter)
dup 5 = if (If 5 items remain on the stack...)
break ( stop looping, leaving 5 items on the stack)
then
repeat (Jump back to 'begin')
pop (Remove counter)
Here we have a special test to see if the stack has 5 items. If it does, we want to stop popping the stack immediately rather than let the loop continue. The BREAK statement exits the loop and resumes execution with the first statement following the loop's terminating REPEAT or UNTIL statement. In this case it will resume execution at POP, which removes the loop counter from the stack.
There are other MUF statements concerned with loops, but these will get you going. If you're comfortable with loop basics, let's move on to some useful applications.
Scanning the Object Database
Let's use a loop to do something powerful. When doing system programming for a muck, it's often useful to loop through the object database, scanning for objects that meet certain criteria. For example, you might want to find all puppets. Looping through the database is simple, but it took me a while to figure it out because when I started fooling with MUF I didn't know how the object database is organized, so let's start there.
The object database is a little like a stack of objects. Think of the first object, #0, as the bottom of the stack, and the object with the largest dbref as the top of the stack. The DBTOP primitive returns a number equal to the largest dbref + 1. It's essentially pointing to the next available object position. If the largest dbref is #3154, then DBTOP would return #3155.
If DBTOP returns #3155, this doesn't mean there are 3154 objects in your muck's database. When an object is recycled, its dbref is returned to a pool of available dbrefs and its type is changed to a special 'garbage' type used only by the system. For example, say you have a very simple set of 6 objects in your muck, shown here with their dbrefs and types:
#0 room
#1 player
#2 garbage
#3 room
#4 garbage
#5 thing
DBTOP would return #6, meaning the largest dbref in the database is #5. But, the database actually contains only 4 valid objects. Two have been recycled and returned to the available dbref pool. The next time someone creates an object, the muck will re-use one of the two free dbrefs from the deleted pool before wasting space by creating a new object with a dbref of #6.
When you check a dbref with the OK? primitive, one of the things OK? does is make sure the dbref doesn't represent a garbage object. When you loop through the object database, always check each dbref with OK? before doing anything else with it.
Let's use this information to write some code that loops through the entire object database:
Method #1:
#0 (top of stack is bottom DB position)
begin dup dbtop dbcmp not while (current position <= top of DB?)
dup ok? if (It's not garbage?)
(do stuff) (Do something with the object)
then
1 + (increment position in object database)
repeat
pop (Remove the object DB position counter)
Method #2:
dbtop (Get greatest object dbref)
begin
1 - (Decrement position in object database)
dup ok? if (It's not garbage?)
(do stuff) (Do something with the object)
then
dup not until (Keep looping until we've processed object #0)
pop (Remove the object DB position counter)
These methods illustrate two ways for looping through the object database. Method #2 is more efficient, since it doesn't fetch DBTOP and compare it with the current position on every loop iteration. Since even the simplest muck database has an object #0 and #1, we're guaranteed our loop counter will always start with a number greater than 0.
Processing the entire database can take a while particularly with large mucks, so writing efficient code is a good idea. Consider this code that searches for puppets:
dbtop (Get greatest object dbref)
begin
1 - (Decrement position in object database)
dup ok? if (It's not garbage?)
dup thing? over "Z" flag? and if
(do stuff) (Do something with the puppet)
then
then
dup not until (Keep looping until we've processed object #0)
pop (Remove the object DB position counter)
We can improve on speed of this code by doing this:
dbtop (Get greatest object dbref)
begin
1 - (Decrement position in object database)
dup ok? if (It's not garbage?)
dup "Z" flag? if
dup thing? if
(do stuff) (Do something with the puppet)
then
then
then
dup not until (Keep looping until we've processed object #0)
pop (Remove the object DB position counter)
By testing first for the "Z" flag, you limit further testing to potential puppet objects. We don't do the THING? test unless we have to. We check for "Z"-flagged objects first, because there will likely be a lot less "Z"-flagged objects than 'thing' objects to test.
Here's another situation to watch out for when optimizing code, especially loops:
dup red_function over blue_function or if
(do something)
then
MUF will execute both the red and the blue functions before evaluating the IF statement. If red_function returned true, then it's pointless to execute blue_function because the OR condition is already satisfied. This will generally run faster:
dup red_function if
(do something)
else
dup blue_function if
(do something)
then
then
Looping Through Contents
MUF coding often involves getting the contents of a room or character and doing something with them, but the mechanics of the CONTENTS function isn't obvious, at least it wasn't to me. Building a loop to list an object's contents is easier if you understand what's going on.
Objects have hidden values which only the system can manipulate. Two of these concern us here; I'll call them '.contents' and '.next'. The .contents property of an object holds a dbref representing the first object it contains. If a player is holding a single object with the dbref #2198, then the player's .contents will have the value 2198. If an object does not contain other objects, then .contents is set to -1.
Now here's where things get a little tricky. If you are holding three objects, you might expect there to be three values for .contents on your player. But there aren't. There's only one, representing the first of the three. To find the others, we have to go to the first held object and look up another hidden value, .next, which contains the dbref of the next object you are holding. This continues until one of the objects you're holding has a -1 in the .next property, indicating that's the end of your contents list.
Let's look at an example. Silver holds a lightsaber, a labcoat, and a pair of glasses. Here's how Silver's contents might appear in the database:
Silver(#1184).contents:2933
Silver(#1184).next:2714
lightsaber(#2933).contents:-1
lightsaber(#2933).next:3508
labcoat(#3508).contents:323
labcoat(#3508).next:3512
glasses(#3512).contents:-1
glasses(#3512).next:-1
Silver's .contents points to the first item he is holding, the lightsaber. The lightsaber's .next property indicates the labcoat is the next item in Silver's contents list. The labcoat's .next tells us that the glasses are next in the list. There is a -1 in the .next for the glasses, indicating that's the end of Silver's content list. Note that Silver himself has a .next value pointing to the next person or object (#2714) in the room he is in. Also note that the lightsaber and glasses have no contents, but the labcoat contains at least one item.
So let's apply this stuff to MUF coding. The CONTENTS function retrieves the value in an object's .contents property, the dbref of the first item in the object's contents list. In the above example, #1184 CONTENTS will return #2933. The NEXT function retrieves an object's .next value. For example, #2933 NEXT will return #3508. So if you wanted a function that loops through the contents list for any object, here's one way to do it:
: get_contents (d --)
contents (Get the dbref of the first content)
begin dup ok? while (If the dbref is #-1, we're done)
dup (do something) (Duplicate the object dbref to do something with it)
next (Use the object's dbref to get the next content in the list)
repeat (Keep going until we hit -1)
pop (Remove the -1)
;
The important thing to remember is not to throw away the dbref of the current object because NEXT needs it to find the next object in the contents list.
If all you want to do is learn if an object has any contents, just push the object's dbref on the stack, do a CONTENTS and test for a true result with IF. For example, ME @ CONTENTS IF (do something) THEN will perform some task if the user is holding one or more items.
The EXITS function works the same way as CONTENTS, it simply returns the first exit in a list of exits. You'd apply NEXT to get subsequent exits in the room. Of course this can also be applied to actions.
: get_exits (d --)
exits (Get the dbref of the first content)
begin dup ok? while (If the dbref is #-1, we're done)
dup (do something) (Duplicate the exit dbref to do something with it)
next (Use the exits's dbref to get the next exit in the list)
repeat (Keep going until we hit -1)
pop (Remove the -1)
;
Note that since you can't directly manipulate the hidden values associated with content chains, there's no way to re-order contents or exits other than changing their locations and returning them to the original location in the sequence desired.
Listing Properties
Looping through the properties of an object is a little like using a loop to find contents. The NEXTPROP function works much like CONTENTS/NEXT, except properties are sorted in alphanumeric order so there's no need for any weird dbref chains. For arguments, NEXTPROP requires an object dbref and a string representing an initial property. When you call NEXTPROP, it simply gets the next ordered property on the object. If there are no more properties, it returns a null string, "".
Here's a function that takes an object dbref and an initial property and finds all properties immediately under that property. For example, if you give it an object dbref and "/", it will find all property names at the root level that your permissions allows you to see.
: get_properties (d s --)
( Loop through the properties on an object. To start with the first property, )
( feed the function: dbref "/". If you just want a sub-property, then feed )
( the function like: dbref "_myprop". )
over swap nextprop (Get the first property in the object's list)
begin dup strlen while (Loop until we find a null property)
dup (Duplicate the property; we'll need it to get the next one)
me @ swap notify (Do something with the property-- here I simply print it)
over swap nextprop (Get the next property on the object)
repeat
pop pop (Remove the null property and object dbref)
;
Null strings have a length of 0, so we use STRLEN as an efficient way to test for the end of our loop. Note that you must keep a copy of the object dbref and the current property on the stack, because NEXTPROP uses these to fetch the next property.
Say you examined a character and found these properties:
ex #1184=/**
dir /_/:(no value)
str /_/de:An ant scientist named Silver.
dir /_prefs/:(no value)
str /_prefs/fly?:no
str /_prefs/silly?:yes
Now, assume you call get_properties like this: #1184 "/" GET_PROPERTIES. You might expect to get a result like this:
/_
/_/de
/_prefs
/_prefs/fly?
/_prefs/silly?
But you won't. You'll get this:
/_
/_prefs
What? Where'd the others go? Properties are containers that can hold either a value or a list of properties. (Actually they can do both, but we won't worry about that.) In the above example, the fly? and silly? properties are contained within the _prefs property. When we ran GET_PROPERTIES, we didn't ask for _prefs's properties, we asked for /'s properties. To get the properties fly? and silly?, we could write: #1184 "/_prefs/" GET_PROPERTIES. (You can drop the leading "/"-- NEXTPROP will assume it if not provided.)
Think you're good? How would you revise GET_PROPERTIES to find all properties on an object, including those contained in other properties? Hint #1: Use PROPDIR? to identify properties that contain other properties. Hint #2: The problem has an easy solution.
That's it for my tutorial on loops and MUF applications. I hope you were able to find something useful here. Good luck with your coding!
This tutorial was written for Willow of A Bug's Muck --July 2003