Abstracting & Documenting Systems Softcode

Submitted by Eratl on Fri, 2006-07-21 18:10

Hi folks! I'm going to take a minute to show off some code I recently wrote for my latest project, a MUSH set in Terry Pratchett's Discworld (obligatory plug).

This article is targeted at beginning/intermediate programmers who are beginning to get into coding more complex things like economy systems, chargen systems, and so on. I'd love to hear any feedback.

Often, most or even all of the softcode on a MUSH is done by one person. Whether a "Head Coder", the head wiz him/herself, etc, this person often is tasked with designing, coding, and maintaining all systems on the game. While this definitely has some advantages (the old "if you want it done right" principle, for one), it also has disadvantages: if your coder wizard gets bored, busy, or something happens to him/her, you need someone else to add new features, fix bugs, and that sort of thing. This can be a problem, though; most people will tell you it's a lot harder to read someone else's code than read your own. And, even when you're not fixing bugs, it can be a pain to figure out how to work with a system.

For example, let's say I'm running a cyberpunk/futuristic MUSH that uses a D6 system (doesn't matter really, but just for the sake of the argument). My head coder, who's done a great job at writing code, unfortunately is struck by a sudden attack of the RL and can't logon for a couple weeks. But I have a particular player, Bob, who's obviously code-literate, loves the game, and is interested in adding some new things. Bob thinks that, fitting with the game's dark, gritty theme, a drugs & narcotics system would be a cool addition: when you take a drug, some of your character's attributes are temporarily raised while others are temporarily lowered, and after it wears off, they go back to normal.

Obviously, this piece of code has some of its own components - the manufacture/creation of drug objects, perhaps some sort of "addiction" code, etc. But it also needs to tie into the MUSH-wide chargen system - if an attribute is lowered, it needs to be lowered on +roll/+checks and the like.

The problem is that my old head coder's code is really, really, complex. He thought it would be a great idea to code it so that the attributes and skills are stored mixed up in one attribute, and the way this is looked up seems to change depending on where it's being called from (okay, the example is out there, but hopefully you realize the principle that your design decisions aren't always as clear to others as you might like!)

This is where the principle of abstraction comes in. Simply put (and there are a lot more complex definitions out there), abstraction is the process of hiding the details of how something is done from the outside. Ideally, what we'd like in coding our +inject code is something like this (pseudocode):

+inject command:
*notify user*
*notify others in same room*
*raise strength by 2D*
*lower dexterity by 2D*

Our drug object doesn't need to know how the chargen system works. It just needs to know where it can interface with the chargen system.

The good news is, there's no reason we can't design our chargen system to work just like that!

The chargen system should be able to take a request like *raise strength by 2D* and handle it for us. It should know exactly what attributes to manipulate, what SQL tables to update, or whatever - however the data is stored - it shouldn't matter to the drug code. All the drug code needs to know is *raise strength by 2D* or, shall we say, [chargen(attrraise,%#,strength,2)]?

Hopefully the idea of what we're going after is clear. I also thought I'd share how I implemented this sort of functionality in a modular, easy-to-use way.

First, I created an object to hold the chargen() subfunctions - the implementation of attrraise, for example.

@create FUNCTIONS: Chargen
@set FUNCTIONS: Chargen = WIZARD
(because it needs to be able to change things on players)

Now I'm ready to code the attrraise subfunction itself:

&FN.ATTRRAISE FUNCTIONS: Chargen=[switch([t(pmatch(%0))][t(chargen(isattr,%1))][isnum(%2)],0*,#-1 NO VICTIM FOUND,10*,#-1 INVALID ATTRIBUTE,110,#-1 VALUE ARGUMENT MUST BE INTEGER,111,[attrib_set(%0/attributes`%1,[add(chargen(attrget,%0,%1),%2)])])

This code is actually pretty simple - it checks each of its arguments to make sure that they're valid, then sets the victim's new attribute in a MUSH-attribute named attributes`%1. You'll notice also that I used two other chargen() sub-functions: attrget and isattr. These would return the value of the person's attribute in question, and verify whether an inputted attribute really is an attribute or not:

&FN.ATTRGET FUNCTIONS: Chargen=[switch([t(pmatch(%0))][t(chargen(isattr,%1))],0*,#-1 NO VICTIM FOUND,10,#-1 INVALID ATTRIBUTE,1,[get(%0/attributes`%1)])]
&FN.ISATTR FUNCTIONS: Chargen=[t(member(lcstr(get(#27/data.attributes)),lcstr(%0),|))]

Hopefully how each one of those works is clear.

Now we need an actual chargen() function to glue everything together! This @function should take in a sub-function as its first argument, and then pass everything else to that subfunction, if it exists.

I put this on my master @function object:

&FN.CHARGEN FUNCTIONS: Master @functions=[switch([orflags(%@,Wr)][hasattr(#15,fn.%0)],0*,#-1 PERMISSION DENIED,10,#-1 INVALID CHARGEN\(\) SUBFUNCTION,11,[ulocal(#15/fn.%0,%1,%2,%3,%4,%5,%6,%7,%8,%9)])]

#15 is the 'FUNCTIONS: Chargen' object we created earlier. So basically, this function looks for a FN. attribute that matches its first argument, and if it doesn't find it, it errors, but if it does, it runs it and passes arguments. So when you add new functions to #15, they'll automatically get "connected" to chargen(). This is another example of abstraction at work, though a little less obvious.
(Note the use of %@ instead of %# since we're interested in the caller, not the enactor - put another way, we want our drug canister object checked to see if it has a wiz flag, not the player himself!)

Lastly, the step programmers love to hate: documentation. When Bob is attempting to interface with our shiny new, abstracted chargen(), it'd be nice if we wrote down what each sub-function is intended to do - so let's do it!

I chose to document two things: what the arguments incoming to the subfunction are supposed to be, and what it does. I can store all this information in one attribute by putting a | in between the argument list and the description, and separating each argument with a : (so that my argument definitions can have spaces). I'll store this information right on the same object as the FN. definitions, just using a DOC. prefix for the same name:

&DOC.ATTRRAISE FUNCTIONS: Chargen=victim:attribute:value|Raises victim's attribute by value.
&DOC.ATTRGET FUNCTIONS: Chargen=victim:attribute|Returns victim's attribute.
&DOC.ISATTR FUNCTIONS: Chargen=attribute|Returns 1 if input is really an attribute, and 0 if not.

Now I just need something to parse and display it:

@DESCRIBE FUNCTIONS: Chargen=[center(Chargen\(\) Documentation,79,-)]%r%b%b+[repeat(-,73)]+%r[align(2 15 23 33 2,,[ansi(c,SUBFUNCTION)],[ansi(c,ARGUMENTS)],[ansi(c,DESCRIPTION)],,,|)]%r[map(map.doc,[lattr(me/doc.*)], ,%r)]%r%b%b+[repeat(-,73)]+%r[repeat(-,79)]
&MAP.DOC FUNCTIONS: Chargen=[align(2 15 23 33 2,,[ansi(h,[after(%0,DOC.)])],[iter([extract([v(%0)],1,1,|)],dec(inum(0)): [itext(0)],:,%r)],[extract(v(%0),2,1,|)],,,|)]

Note that this code goes off of whatever DOC. attributes it can find, so to document a new subfunction, all we need to do is add a new corresponding DOC. - and now when you look at the object, you can see exactly what each one does and what arguments it takes:

>look FUNCTIONS: Chargen
----------------------------Chargen() Documentation----------------------------
  +-------------------------------------------------------------------------+
  |SUBFUNCTION    |ARGUMENTS              |DESCRIPTION                      |  
  |ATTRGET        |0: victim              |Returns victim's attribute.      |  
  |               |1: attribute           |                                 |  
  |ATTRRAISE      |0: victim              |Raises victim's attribute by     |  
  |               |1: attribute           |value.                           |
  |               |2: value               |                                 |
  |ISATTR         |0: attribute           |Returns 1 if input is really an  |  
  |               |                       |attribute, and 0 if not.         |  
  +-------------------------------------------------------------------------+
-------------------------------------------------------------------------------

Pretty cool!

The great news is, not only is this helpful for enabling code to be worked on by multiple people, it's also really helpful even if you're the only one who will ever touch your code - it forces you to stop and think about how your systems work, eliminates duplicate code, and so on.

Let me know if you have any questions or comments on this article. I'm Eratl on M*U*S*H.