Sharing a system to simplify custom verb responses: TADS 3

(Note: you can start using this system even if you have a half-completed game… )

Hey TADS 3 users… in the course of developing a fairly large-scale adventure game with quite a list of custom verbs, I started looking for ways to simplify the process of giving verb responses to the hundreds of objects that litter my game.
Here’s the scenario: most verbs in the library default to illogical in the verify phase, and most verbs that you create yourself will also default to being illogical in the verify phase. In order to give any action an acknowledgment instead of a dismissal, you have to type a lot of characters to clear the verify phase and get to your text bit:

dobjFor(Verb) { verify() { } action() { "You verb the object 
   and witness some amusing results. "; } }

Even to modify the dismissal message, when you have dozens (or hundreds) of objects that you want to fire off lots of action acknowledgments for, one could wish for something a little terser than cannotVerbMsg = 'You can\'t verb that for custom reasons. '
The system I’ve worked up makes extensive use of some underlying macros, and allows you to define action responses on objects in this simple and concise manner, where ‘n’ disallows an action and ‘k’ allows it:

object: Thing 'vocab' 'name'
    "The object is just sitting there. "
    nOpen = 'That won\'t work because there\'s this really good reason. '
    nEat = 'Don\'t do that or you\'ll die. '
    kJumpOver = 'You wow the onlookers with a spry leap clear over the object. '
    nGoThrough = 'It should be obvious you won\'t fit through the object
    kClean { "Using some spit and a sleeve, you give the object a shine. "
        beenCleaned = true; }
   // etc...

The system is not merely a bunch of abbreviations, it also does important
distinguishing of which action phase to run in. If you have a game object and you define an nTaste = 'Don\'t lick that for whatever reason' property on the object, your object will automatically nix the action in verify, with your message, even though default Thing handling permits dobjFor Taste. If you define a kTurn = 'The object spins around. ' property on the object, verify is automatically cleared and your message will print in the action() phase, even though Thing default disallows Turn in verify.
This (virtually) ensures that your kVerb message will never be incongruously printed if the object is obviously out of reach, nor will the library use preconditions to move your character into position to touch the object (taking something, getting off a chair, whatever…), only to then give you your nVerb message "You aren't actually going to do that, because it's irrational."
When you create your own verbs, you can give Thing class either a kCustomVerb property or an nCustomVerb property, and the default will occur in action or verify, respectively. Game objects can override the custom verb response the same as a library verb… just use either a k… or n… property in the object and it will permit or dismiss that action.

Note, the system is not meant to completely eliminate the need to use dobjFor (for the more complicated action processes)… but it can take a massive chunk out of it. In particular, indirect objects are not likely to use the k… version very often, since by nature they usually either just silently clear verify, or else they carry complex processing (like a Throw verb). But you can still easily modify a dismiss message on an indirect object with the same form:
nCutWith = 'You\'ll hardly cut through anything with a piece of cheese. '

If you’re interested in more info or think you might like to use a similar system, feel free to message me or leave a comment. There’s quite a bit of code and #defines involved which I don’t really want to paste en masse unless someone’s interested, in which case I can pass along what I have. :slight_smile:

PS I’ve done it in adv3, and don’t know whether it could be immediately usable in Lite, but assume that something similar could be done, if Lite treats actions even remotely like adv3 does…

5 Likes

Nice! This seems quite useful and it might actually clean up a lot of boilerplate code so I’m interested in seeing it. Considering an upload to ifarchive as well?

2 Likes

I am also interested. An upload to the archive would be an excellent way to share.

Thank you

1 Like

Hey, great! It’s nice to know this could be useful to someone. However, at the moment I’m packing for a short out-of-town trip. I’ll try to put together a file hopefully Wed or Thu at the latest when I get back. Cheers!

2 Likes

I’m leaving for work, but I’m just going to slap some code on here for now. I’m going to put the code in a post for browsing purposes, but I’ll also upload a couple of text files here with the same code better organized. Bear in mind all the #defines will need to go in a header file that’s #included by every one of your source files. I threw this together really hastily so ask me questions about whatever doesn’t make sense, or if some typos in the prep process cause you compiler errors. Hope it helps!

The N/K verb response sytem

-To any potential users: I didn’t write this system/code in a clean, organized fashion as I would have if I’d planned to write a standalone module/extension; rather, it just sort of materialized in my game as I was trying to find ways of speeding up repetitive tasks. For now I am going to simply post the code in its raw form, and defer uploading it to an archive till such time as I can repackage it into something more consistent and refined.
-Note that the system is primarily intended for those situations where you just want to enliven the “supporting cast” of game world objects with text responses, or perhaps a few simple side effects like flipping a property or setting off a daemon. For your major puzzles and interactive objects, just stick with overriding dobjFor(Verb).

-You may hate the naming conventions I used, so feel free to replace them: I tend to go for really terse, since I’m just programming for myself.

USAGE GUIDE:

This text presupposes you’ve read my introduction of this system in the IntFiction.org thread I posted. Carrying on from there:

We’ll start with your custom game verbs – these are the most straightforward.
The system is macro-based, and you’ll have to know a few “codes”, to choose the kind of preconditions you want for the verb.
The only preconditions I’ve incorporated into the system (so far) are touchObj, objVisible, objHeld and an empty precondition set. For any others, including combinations, you’ll have to write your own precondition method in the usual way (or expand on the macro code yourself).

The macros are:

pnK(name) - for no preconditions
pTnK(name) - uses touchObj
pHnK(name) - uses objHeld
pVnK(name) - uses objVisible

I also use short macros for those property or class names that are so commonly used that it’s not too hard to remember their initials. I’ve included them in my list of #defines, and you can use or not use them as you like. A few you might see in this explanatory text include:
acT → DefineTAction acTI → DefineTIAction vR → VerbRule vP → verbPhrase
dF → dobjFor iF → iobjFor mT → modify Thing
tN → theName fC → failCheck rA → replaceAction mI → moveInto

Here’s a sample custom verb definition using the macros:

   acT(JumpOn) ; vR(JumpOn) 'jump''on' singleDobj : JumpOnAction vP = 'jump/jumping (on what)' ;
      mT dF(JumpOn) pTnK(JumpOn)
      nJumpOn = 'You decide you\'ve got better ideas to entertain. '
      ;

Because all Things will initially have nJumpOn defined (and no kJumpOn defined), ‘jump on thing’ will be dismissed in the verify phase, without consideration to preconditions. However, if you define

puddle: Thing
   kJumpOn = 'You jump in the puddle and make a big splash. ' ;
statue: Thing
   nJumpOn = 'The statue is way too high to jump on. ' ;

the statue will simply use your new message, still disallowing the action in verify, while the library will make sure you can touch the puddle before printing your message, because you used the pTnK macro (touch). If you had used the pnK macro, there are no preconds and your message would be printed if the object is in scope at all.

-Important note: nVerb must be a SQstring, or a method that returns a SQstring (it will be passed to the library illogical macros). kVerb can be a SQstring, or it can simply be a method that triggers some side effects and either prints a DQstring OR returns a SQstring. e.g:

   acT(Grab) ...
      mT dF(Grab) pTnK(Grab)
      kGrab { if(isPortable) rA(Take,self);
         else "Grabbing hold of the immovable object has no significant result. "; } ;

-A word about naming. The name which you supply as an argument to the macro (in this case Grab) does not have to match the name of the corresponding Action exactly. It does have to match the n… and k… properties that are used in order to work. The macro looks for properties which are literally the argument name with an ‘n’ or ‘k’ pasted in front. This isn’t much of an issue for TActions, but could matter for TIActions, which we’ll look at next.

TIActions are almost invariably named after the pattern VerbPrep: for instance CutWith, PutUnder,etc. The naming convention I was using in my game followed this pattern: for any TI verb VerbPrep, the direct object macro would use nVerb/kVerb, while the indirect object macro would use nVerbPrep/kVerbPrep. Example:


acTI(PlantIn) ...
   mT dF(PlantIn) pTnK(Plant)
      iF(PlantIn) pTnK(PlantIn)
      nPlant = 'There\'s no point burying <<thatObj>> in the dirt. '
      nPlantIn = 'You probably won\'t reap any crops from <<tN>>. ' ;

class Plantable: Thing
    kPlant { "You plant <<tN>> in <<gIobj.tN>>. "; moveInto(gIobj); 
           /*start a growing daemon etc.*/; } ;
seed: Plantable ; 
soilPatch: Container, Fixture
    kPlantIn { }	// by defining this property, this object will pass verifyIobjPlantIn()
clayPot: Thing
    nPlantIn = 'There\'s no soil in the pot, so you can\'t plant anything in it. '
    nPlant = 'Why would you bury the clay pot? '  ;

This works fine for most things where you’ll always be using direct and indirect object for a given Action. For instance, in this PlantIn example, we’re assuming that if the player types ‘plant seed’, the library will prompt ‘what do you want to plant it in?’
But you may have situations where a TAction and a TIAction have some overlap. Say that you have a RubWith verb, but also have a separate Rub verb, where the PC’s hands are implied to do the rubbing. Then you have


acT(Rub) ...
   mT dF(Rub) pTnK(Rub)
      kRub = 'You rub <<tN>>. '
acTI(RubWith)
   mT dF(RubWith) pTnK( ? )	
      iF(RubWith) pTnK(RubWith)
      n? = 'msg'
      nRubWith = 'msg'

In this situation I used RubXWith, because this was the exception and I was used to the pattern I’d already started with. However, then you have to keep a mental note or a cheatsheet of which verbs don’t follow the standard pattern exactly, when you’re defining responses on bunches of objects. You may prefer for any and all TI verbs, to always mark the macro as iobj or dobj, perhaps:


acT(Rub) ...
   mT dF(Rub) pTnK(Rub)
acTI(RubWith)
   mT dF(RubWith) pTnK(RubWithDj)	
      iF(RubWith) pTnK(RubWithIj)

In any case, as long as distinctions are made, the same object can define:


object: Thing
   nRub = 'You don\'t want to rub <<tN>> with your hand. '
   nRubWithDj = 'You should\'t rub this object with <<gIobj.tN>>. '
   nRubWithIj = 'There\'s a reason you shouldn\'t rub <<gDobj.tN>> with this object. ' ;

-Note that most of the time, indirect objects will only use the macro to customize their dismissal message, or perhaps define an empty kVerb { } method just to clear verify. Any complicated handling on an indirect object should probably be handled in an ordinary iobjFor block.

-Using the macro doesn’t give direct access to the check() method. However, there’s no reason you can’t define:

  kVerb { if(condition) fC('You can\'t do that while condition is true. ');  // failCheck
       else "You verb the object. "; }

This leaves dealing with library verbs, which is currently the most unrefined part of this system. Unfortunately at the time of writing I haven’t made this part all neat and tidy for public presentation… until I get around to that, you’ll just have to work with what I have and modify or add to it as you wish.
Certain library verbs do not participate in the macro system because I didn’t expect to override them frequently enough to make it worthwhile. Other very common verbs that almost always carry handling like Take, Open, etc. also do not participate in the system because of their handling, but I wrote simple name substitution macros so that messages can be defined in a consistent style. That is to say: nTake is simply another name for the cannotTakeMsg property, kTake stands in for okayTakeMsg. In this case, however, the macros aren’t defining when and where the messages get printed, they simply print when the library would otherwise print the corresponding message.
Currently, then, when it comes to library verbs, you will either need to extend my partial list to suit your needs, or else make sure you know which of the verbs actually participate in the macro system.

This is a list of participating library verbs with the corresponding macro name I used. If the macro starts with an ‘l’, it just means the Action name and the dobj macro name are the same. Note that if you want to use different naming conventions, different preconditions, whatever, you’ll have to alter the list below. The 50s and 20s have to do with logicalRank, I was mostly trying to mimic how the library handles the verbs.


modify Thing 
	lpTnK(Attack)
	lpTnK(Break)
	dF(BurnWith) pTnK(Burn)
	dF(CleanWith) pTnK(Clean)
	iF(CleanWith) pHnK(CleanWith)  // hold something to clean with it
	lpTnK(Climb)
	lpTnK(ClimbDown)
	lpTnK(ClimbUp)
	dF(CutWith) pTnK50(Cut)
	iF(CutWith) pHv nK20(CutWith) // we'll show an illogical nCutWith sooner than
					// an illogical nCut 
	dF(DigWith) pTnK(Dig)
	lpTnK(Drink)
	lpTnK(Eat)
	lpTnK(JumpOff)
	lpTnK50(Kiss)
	lpTnK(Lock)		
	lpTnK50(Move)
	dF(MoveWith) pTnK50(MoveXWith)
	lpTnK50(Push)
	lpTnK50(Pull)
	lpTnK50(Turn)
	dF(TurnWith) pTnK(Turn)     
	iF(TurnWith) pHnK(TurnWith)
	lpTnK(Unlock)

We have to define these properties so that the above verbs can make use of the nVerb/kVerb system. If you want to override library default response messages, you can replace the &cannot… with your own message, or you can just override the properties in playerActionMessages like normal. You can also change the n or k before any of these, if you want default dismiss/acknowledge to be different from the library’s defaults. Any lib verbs that you add to the system, you’ll need to define a corresponding nVerb or kVerb property of Thing for a default (and modify any differing subclasses as applicable).


	nBreak = &shouldNotBreakMsg
	nBurn = &cannotBurnMsg
	nClean = &cannotCleanMsg
	nCleanWith = &cannotCleanWithMsg
	kCut = &cutNoEffectMsg
	nCutWith = &cannotCutWithMsg
	nClimb = &cannotClimbMsg
	nClimbUp = &cannotClimbMsg
	nClimbDown = &cannotClimbMsg
	nDig = &cannotDigMsg
	nDigWith = &cannotDigWithMsg
	nDrink = &cannotDrinkMsg
	nEat = &cannotEatMsg
	nFollow = &notFollowableMsg
	nJumpOff = &cannotJumpOffMsg
	kAttack = &uselessToAttackMsg 
	kKiss = &cannotKissMsg		
	nLock = &cannotLockMsg
	kMove = &moveNoEffectMsg
	kMoveXWith = &moveNoEffectMsg
	kPull = &pullNoEffectMsg
	kPush = &pushNoEffectMsg
	nTurn = &cannotTurnMsg
	nTurnWith = &cannotTurnWithMsg
	nUnlock = &cannotUnlockMsg
;
modify Fixture
	nTake = cannotTakeMsg
	nMove = cannotMoveMsg
	nMoveXTo = cannotMoveMsg
	nMoveXWith = cannotMoveMsg
	dF(Push) pTnK50(Push)
	dF(Pull) pTnK50(Pull)
	dF(Move) pTnK50(Move)
	dF(MoveWith) pTnK50(MoveXWith)
	dF(MoveTo) pTnK50(MoveXTo)		;
modify CustomFixture
	nMove = nTake			;
modify Immovable
	nTake = cannotTakeMsg
	nMove = cannotMoveMsg
	nMoveXTo = cannotMoveMsg
	nMoveXWith = cannotMoveMsg
	dF(Push) pTnK50(Push)
	dF(Pull) pTnK50(Pull)
	dF(Move) pTnK50(Move)
	dF(MoveWith) pTnK50(MoveXWith)
	dF(MoveTo) pTnK50(MoveXTo)		;
modify CustomImmovable
	nMove = nTake				;
modify TravelPushable
	nMove = &cannotMovePushableMsg		;
modify Food
	dF(Eat) { action { moveInto(nil); defaultReport(&okayEatMsg); } }
	kEat = &okayEatMsg						;

Below is a list of macros referring to message properties that I hadn’t incorporated into the macro system. You can use them, not use them, or modify them as you wish. They’re just typing shortcuts, but you may have to keep in mind what’s what. For instance you couldn’t define a kDetach property on an object and expect it to work: it would never be called by anything, unless you added the Detach verb to the macro system in the above list, or unless you #defined kDetach to be a substitute for okayDetachFromMsg. I didn’t add it to the system since attachment/detachment is fairly complex and is better handled with normal overrides.
Don’t paste this code into your files in this order… this is just for browsing purposes. The #defines need to appear in the right order to work. I’m planning to upload separate files with the code…


#define nAttach cannotAttachMsg
#define nAttachTo cannotAttachToMsg
#define nBoard cannotBoardMsg
#define nBurnWith cannotBurnWithMsg
#define nClose cannotCloseMsg
#define nDetach cannotDetachMsg
#define nDetachFrom cannotDetachFromMsg
#define nDoff notDoffableMsg
#define nEnter cannotEnterMsg
#define nGetOffOf cannotGetOffOfMsg	
#define nGetOutOf cannotUnboardMsg	
#define nGo cannotGoThatWayMsg
#define nGoThrough cannotGoThroughMsg
#define nAttackWith notAWeaponMsg		
#define nKissA cannotKissActorMsg
#define nLI nothingInsideMsg  	// def. LookIn response
#define nLU nothingUnderMsg	// LookUnder
#define nLB nothingBehindMsg	//
#define nLT nothingThroughMsg	//
#define nLTP nothingThroughPassageMsg
#define nLieOn cannotLieOnMsg
#define nMoveC cannotMoveComponentMsg
#define nMoveWith cannotMoveWithMsg
#define nOpen cannotOpenMsg
#define nOpenLocked cannotOpenLockedMsg
#define nPour cannotPourMsg
#define nPushTravel cannotPushTravelMsg
#define nPut cannotPutMsg
#define nPutIn notAContainerMsg
#define nPutInR cannotPutInMsg(obj)
#define nPutOn notASurfaceMsg
#define nPutInOn(str) nPutIn = iobjMsg(str) nPutOn = iobjMsg(str)
#define nPutOnR cannotPutOnMsg(obj)
#define nPutUnder cannotPutUnderMsg
#define nPutUnderR cannotPutUnderMsg(obj)
#define nPutBehind cannotPutBehindMsg
#define nPutBehindR cannotPutBehindMsg(obj)
#define nPutC cannotPutComponentMsg
#define nSitOn cannotSitOnMsg
#define nStandOn cannotStandOnMsg
#define nTake cannotTakeMsg
#define nTakeC cannotTakeComponentMsg
#define nUnlockWith cannotUnlockWithMsg
#define nWear notWearableMsg
#define kBurn okayBurnMsg
#define kClose okayCloseMsg
#define kDrop okayDropMsg
#define kOpen okayOpenMsg
#define kPutIn okayPutInMsg
#define kPutOn okayPutOnMsg
#define kTake okayTakeMsg
#define kOff okayNotStandingOnMsg

#define nIntang notWithIntangibleMsg
#define nHere notHereMsg
#define	nImp notImportantMsg
#define tooFull tooFullMsg
#define tooLarge tooLargeForContainerMsg

// The rest of the necessary code and some extras:

/* To direct multiple verbs to the same response message:
obj: Thing
   nBoard = 'Don\'t get on that. '
   mapN(Board,StandOn,SitOn,LieOn,Climb)
      //nStandOn,nClimb,etc. will all redirect to nBoard, the first argument of mapN macro
*/

#define mapN(to,from...) from#foreach: n##from = n##to : :
#define mapK(to,from...) from#foreach: k##from = k##to : :
#define mapdF(to,from...) from#foreach: dF(from) adF(to) : :
#define mapiF(to,from...) from#foreach: iF(from) aiF(to) : :
#define mapProp(obj,prop...) prop#foreach: prop = obj.prop : :
#define mapINH(vb...) vb#foreach: dF(vb) INH : :
#define mapINHI(vb...) vb#foreach: iF(vb) INH : :
#define maprT(obj,df...) df#foreach: dF(df) rT(df,obj) : :
#define mapPuts(str) nPutIn = iobjMsg(str) \
	mapN(PutIn,PutOn,PutUnder,PutBehind)
#define mapMounts(str) nBoard = str \
	mapN(Board,StandOn,SitOn,LieOn,Enter,Climb)

#define mapExits(to,from...) from#foreach: from aE(to) : :


#define pTnK(z) { preCond = [touchObj] verify() { nK(z)
#define pTnK50(z) { preCond = [touchObj] verify() { nK50(z)
#define pHnK(z) { preCond = [objHeld] verify() { nK(z)
#define pVnK(z) { preCond = [objVisible] verify() { nK(z)
#define pnK(z) { preCond = [] verify() { nK(z)
#define lpTnK(z) dF(z) { preCond = [touchObj] verify() { nK(z)
#define lpTnK50(z) dF(z) { preCond = [touchObj] verify() { nK50(z)
#define lpHnK(z) dF(z) { preCond = [objHeld] verify() { nK(z)
#define lpVnK(z) dF(z) { preCond = [objVisible] verify() { nK(z)
#define lpnK(z) dF(z) { preCond = [] verify() { nK(z)


#define nKWOA(z) if(!propDefined(&k##z)) { \
		if(propDefined(&n##z)) illogical(n##z); \
		else illogical('That\'s not happening. '); } \
	else if(!overrides(self,Thing,&k##z) && propDefined(&n##z)) \
		illogical(n##z)

#define nK(z) nKWOA(z); } \
	action() { say(k##z); } }

#define nK20(z) if(!propDefined(&k##z)) { \
			if(propDefined(&n##z)) illogical(rank:20,n##z); \
			else illogical(rank:20,'That\'s not happening. '); } \
		else if(!overrides(self,Thing,&k##z) && propDefined(&n##z)) \
			illogical(rank:20,n##z); } \
	action() { say(k##z); } }

#define nK50(z) nKWOA(z); else LR(50,''); } action { say(k##z); } }


#define acI DefineIAction
#define acLit DefineLiteralAction
#define acLitT DefineLiteralTAction
#define acT DefineTAction
#define acTI DefineTIAction
#define acTop DefineTopicAction
#define acTopT DefineTopicTAction
#define vP verbPhrase
#define vR VerbRule
#define INH { preCond = (inherited) remap { inherited; } verify() { inherited(); } \
	check() { inherited(); } action() { inherited(); } }
#define IL illogical
#define ILA illogicalAlready
#define ILN illogicalNow
#define ILS illogicalSelf
#define LR logicalRank
#define fC failCheck
#define rA replaceAction
#define mI moveInto
#define rT remapTo
#define dF dobjFor
#define iF iobjFor
#define adF asDobjFor
#define aiF asIobjFor
#define aE asExit

modify IllogicalVerifyResult	// * so iobj ILs can trump dobj ILs when desired by rank:20 *
	construct(rank:=30, msg, [params]) {  if (dataType(msg) == TypeObject && msg.oK(MessageResult))
        { messageText_ = msg.messageText_;
            messageProp_ = msg.messageProp_;
            return; }
		 if (dataType(msg) == TypeProp)
            messageProp_ = msg;
		messageText_ = resolveMessageText(gAction.getCurrentObjects(), msg, params); 
		resultRank = rank; }
	;
1 Like

I re-uploaded these files as of 3-14-21 after being notified that I’d missed including definitions for some macros I use in my personal code…
nkHeader(update).txt (4.8 KB) nk(update).txt (3.3 KB)

2 Likes

Thank you for sharing.

I am looking forward to seeing if this will help improve my code.

1 Like

I’d be interested to know if you ever gave it a try and everything worked for you as expected, and if all the instructions made enough sense…

The instructions are well detailed. I am in the early stage of converting a game from I6 to TADS 3. TADS already has more built in functionality than I6. Your custom verb response system may make it easier to implement. I am still too much of a novice to tell for sure at this point. I will give further feedback and I move through the conversion and further development.

Thank you.

Gotcha… good luck!!

Hi! If you have downloaded the nk files already, you may want to re-download them from the thread… Tomas notified me that there were a few of my own macros embedded in there that I hadn’t #defined, and so I changed those things and re-uploaded new files to that same post.

1 Like