prev | toc | next
 

3.6 Programming Argo Skills

Section 2.6.1 of the Staff Guide, Defining Skills, discussed how to add skills to the Argo database. Simply defining a skill, though, does not implement any coded effects of the skill... It can be used in RP, with commands such as +roll and +prove, but it doesn't really do anything. For some skills, this is perfectly appropriate. For others, though, coded effects add a lot to game play.

This page provides an example of the process with a skill called Appraise Weapon. The skill, we will say, allows users to examine an Argo weapon and make an informed judgment about its value. Since this information is readily available with the +info command, unless the weapon type has been +masked, this is a less than awesome ability, but it would be a realistic skill for someone like an arms dealer to have, and it will let us cover the basics of programming the coded effects of skills. (A more ambitious and interesting project would be to create a generic Appraise skill that can be used on any type of object and requires a number of prerequisite knowledge skills for reliable results.)

The skill's effects will never be invoked unless the skill is defined, so that's probably the place to start. Use +define to define Appraise Weapon as a skill with no prerequisites, no required tools or materials, and an intelligence rating of 8. (In the example spell on the next page, we'll cover tools and materials.)

The skill is now defined. Players can learn it if they want, and use in RP with +roll and +prove. They can also use it as though it were a coded skill... +use appraise weapon on broadsword. But, since there is no program to handle the skill's effects, the event manager would treat it as a `null skill', rolling and displaying results on the user's turn, but not doing anything else.

To create the coded effects, begin by copying the skeleton skill program, asys-xskills, to a new file... call it something like asys-appraiseweapon.

As in our discussion of programming commands, the first step is to search for and replace all instances of `ppp' and `xxx'. The string `ppp' should be replaced with the `Argo name' of the program... `appraiseweapon' in this case. The string `xxx' should be replaced with the name of the skill, `appraise weapon'.

Because our skill name includes a space, one instance of these replacements requires special handling. In main:

      "#usexxx"           ourArg @ smatch if Doxxx        else

If we do a literal replace on Doxxx we will end up with

      "#useappraise weapon"  ourArg @ smatch if Doappraise weapon  else

The space in "#useappraise weapon" is fine, because it's enclosed in a string. We need it in fact, so that the skill name can be matched and found. But the other one is a problem: we're not planning to make a function called weapon; this won't compile... The line should instead read something like:

      "#useappraise weapon"  ourArg @ smatch if DoAppraiseWeapon  else

Again, the skeleton program is set up to handle one thing... one skill in this case. If the program is to handle multiple skills (for example, if we were making a program to handle a group of related skills like Appraise Weapon, Appraise Armor, Appraise Shield, etc.), a few sections of code would need to be duplicated for each skill. In DoInstall, the line,

    #0 "@a/calls/usexxx"     prog setprop

would need to be duplicated for each skill, as would the following line from DoUninstall,

    #0 "@a/calls/usexxx"     remove_prop

and the line in main used to find the right function to execute when the program is called:

      "#usexxx"           ourArg @ smatch if Doxxx        else

A good deal of the code needed to integrate a skill's coded effects into Argo is already provided. Asys-effects, invoked when players use the skill with the +use command, will take care of finding the target object, verifying tools and materials, making sure that the user has an event loop, and so forth. Stateful data gathered by asys-effects and possibly needed by asys-appraiseweapon will be stored in the user's @a/eloop/ directory:

  str /@a/eloop/act:useappraise weapon
  str /@a/eloop/acting:preparing an action
  int /@a/eloop/pid:636
  str /@a/eloop/skill:Appraise Weapon
  ref /@a/eloop/target:Broadsword(#1542)

The pid prop is the process ID of the user's event loop; acting is the action that will be displayed for this user with the +check command. We probably won't need to reference either of these. The other three, though, will be useful. The @a/eloop/act property is the user's current action, stored in a format that lets the events manager and other programs identify the action and act accordingly. It is this prop that lets asys-eventmgr identify and call asys-appraiseweapon; if asys-appraiseweapon handled multiple skills, this prop would let us route execution to the function needed for a given skill. All this is taken care of at this point: asys-effects and asys-eventmgr will set the prop and make the program call; when we replaced "#usexxx" with "#useappraise weapon", we did what was needed for asys-appraiseweapon to route execution appropriately once it is called.

Once we get to a function like DoAppraiseWeapon, we'll need to know the dbref of the weapon to appraise... it's stored in @a/eloop/target, the property used to store the target of any action, whether its a combat attack, spell casting, or — as here — skill use. Our DoAppraiseWeapon function will need to make a skill roll and, if successful, display information about the weapon stored as the target of the skill.

The skeleton program provides two generic functions for determining the succcess or failure of an action, DoUseRoll and DoDefenseRoll. Unless a skill has special requirements for determining success or failure, you can just call DoUseRoll, makes a roll against the skill required by the current action (stored by asys-effects in @a/eloop/skill), and returns "normsucc", "normfail", "critsucc", or "critfail", and stores the amount by which the user made or failed the roll in the ourAttackVal local variable. If applicable (that is, if DoUseRoll returned "normsucc" and the skill something that can be defended against), DoDefenseRoll can then be called... our program won't need to make a defence roll, just a skill roll.

  : DoAppraiseWeapon  (  --  )     (* user tries to appraise a weapon *)
    
    VerifyEvent                  (* make sure called by asys-eventmgr *)
    
                                           (* store target weapon obj *)
    me @ "@a/eloop/target" getprop ourTarget !
    
    DoUseRoll                                   (* roll for appraisal *)
    "normsucc" over smatch if DoApWeapNormSucc else
    "normfail" over smatch if DoApWeapNormFail else
    "critsucc" over smatch if DoApWeapCritSucc else
    "critfail" over smatch if DoApWeapCritFail 
    then then then then
    
    SetWait                                     (* set action to wait *)
  ;

The VerifyEvent statement is a generic security statement that makes sure that the skill is being legitimately invoked, by the events manager... It's included in the skeleton program; you should just leave it there, as is. The SetWait statement is a library function that resets a user's action to `wait'... we stick it at the end of DoAppraiseWeapon so that once the user has appraised the weapon, they'll stop, instead of re-appraising it every turn.

In between these is the code that actually determines what happens when the users tries to appraise a weapon. DoUseRoll will return a success or failure value... we just need to determine which it is and then route to a function that can handle the appropriate outcome.

If the roll succeeds, the user should be shown the value of the weapon. If you examine the properties of an existing Argo weapon, you'll see that there's not a lot of information on the weapon itself:

  str @a/class/melee weapons:1
  str @a/class/swords:1
  str @a/class/weapons:1
  str @a/name:Broadsword
  str @a/version:1.1
  str _/de:{eval:{list:_desc}}
  str _desc#/1:A straight-bladed, double-edged sword.

As little information as possible is kept on Argo objects themselves. Instead, information about types of objects is kept on the realm environment room. The `Argo name' of the object can be used to look information in the realm database. This name is stored on the object in the @a/name property. To determine the value of a weapon, we would need to determine its Argo name, and look up the cost of that object in the realm database:

  : DoApWeapNormSucc  (  --  )         (* notify with value of weapon *)
  
    ourDataObj @ "@a/objects/$object/cost"
    ourTarget @ "@a/name" getpropstr "$object" subst
    getpropstr dup if
      ">>  You appraise the $weapon at $cost."
      ourTarget @ "@a/name" getpropstr "$weapon" subst
      swap atoi ExpressLowestMoney "$cost" subst VerTell
    else
      ">>  The weapon is valueless." VerTell
    then
  ;

VerTell rather than Tell is used to notify the user of the results: VerTell appends a user's verification string — if he has one, and he has verification turned on — to the string being emitted, so that the user may be sure it's the output of Argo rather than a spoof program. Although it's unlikey that someone would be able to guess that the user is appraising the object, and try fake the results with a spoof, and get away with it because the user didn't notice the duplicate output, it's probably best to use VerTell anyway, for consistency with other Argo output... VerTell is used for any notifies that either are prompted by another player or are delayed.

A normal failure can simply tell the user that he is unable to determine the value. Critical success is the same as a normal success, except the string "[CRITICAL SUCCSSS]" appended to the output, so the user has some assurance the information is accurate, and a roll is made so that the user may gain experience points. Critical failure looks like a normal success, but the information is inaccurate... We lie to the player, in other words:

: DoApWeapCritFail  (  --  )           (* notify with incorrect price *)
  
  ourDataObj @ "@a/objects/$object/cost"
  ourTarget @ "@a/name" getpropstr "$object" subst
  getpropstr dup if
    ">>  You appraise the $weapon at $cost."
    ourTarget @ "@a/name" getpropstr "$weapon" subst
    swap atoi 
    random 2 % if      (* toss a coin: either mult or div answer by 2 *)
      2 /
    else
      2 *
    then
    ExpressLowestMoney "$cost" subst VerTell
  else
    ">>  The weapon is valueless." VerTell
  then
;

Our program is pretty simple and not extremely useful... a better Appraisal program would examine the user's skills, the classes of the object, and any skills modified by the object, and implement an algorithm that uses the character's knowledge of objects of this and similar types to modify the chances for success... We won't do all that here, but should point out the basic mechanism for applying such a modification. The DoUseRoll function contains the following line:

  me @ "@a/eloop/genmod" getpropstr atoi +

The @a/eloop/genmod property is for `generic modifiers'... DoUseRoll and other roll-for-success functions add any value held in @a/eloop/genmod to the user's level for the current roll. An example: if a user had an Appraise Weapon skill of 10, but an algorithm such as the hypothetical one discussed above determined that the user should get a +2 chance on the roll, then @a/eloop/genmod should be set to the value of "2"... the result would be that the user succeeds on a roll of 12 or less. Since other parts of Argo also use the genmod prop, you should immediately clear it after setting it and calling DoUseRoll (the once-per-turn upkeep routines in the event manager also clear it, but there's a chance that it could affect a defence roll before this happens).

Programs handling the coded effects of skills aren't directly called from a command, and so don't have a #help option, so we don't need to add a help screen. We should, though, include a header comment and make an entry about the skill in the online manual. Then we're done. Here is the final product.

prev | toc | top | next