JWZ's TADbits (#15: some Lite overlap)

In the course of creating my first (and as yet, only) TADS game I came across a number of pitfalls, gotchas, aha’s, etc. I was too stressed at the time to systematize them and benefit the public with my discoveries, but I’m going to make an attempt to do so now.
Perhaps at some point these can be better catalogued and maybe make their way into a cookbook somewhere (@jnelson ?), but at least for now, I am going to dump them here in the random order that I encounter them as I go back and peruse my files.
I grant that many of these observations may have only a small window of applicability or interest, but for the greater good of TADS I’m going to just dump anything I learned, so I hope somebody can find something useful!

I happen to be sick right now with some unforeseen time on my hands, and that’s why I have time to get this started. Presuming that I can find the time, I hope to make succeeding posts in this thread with more dumps as I slowly wade through my source code and notes files.
This first post is but a nibble of all that may potentially come after, but I’m going to post it now just so I can see the thing started.
(I’m afraid that many, if not most of these will be adv3 specific, but there may be a little bit of Lite overlap.)
Thanks!


RANDOM TADBITS #1

Text printed in afterAction will suppress defaultReports. Standard TAKE, DROP, PUT responses, among several other verbs, and acknowledgments of entering or leaving NestedRooms are all wrapped in defaultReport.
This being the case, if you have "Somewhere, a bell rings. " written in an in-scope afterAction, you will end up with:

>take kiwi
Somewhere, a bell rings.

To avoid this, wrap everything printed in an afterAction in an extraReport macro (unless the message is meant to supplant any possible defaultReports!)


If you use ofKind in a static expression of a class or modify Class property, beware that ofKind will only take into consideration the superclasses of the class you are then dealing with! For instance, this won’t work:

modify Thing
    isCombustible = static ofKind(WoodenItem) || ofKind(ClothItem)
;

because Thing is not a WoodenItem. One way to get around this would be:

modify Thing
    isCombustible = nil
    initializeThing() {
        inherited();
        if(ofKind(WoodenItem) || ofKind(ClothItem))
            isCombustible = true;
    }
;

Out of the box, you can’t define cannotGoThroughClosedDoorMsg on a Door instance to override the default message, the way you can modify most "cannotVerbMsg"s. You have to

modify playerActionMessages
    cannotGoThroughClosedDoorMsg(door) {
        if(door==myDoor) return 'My message. ';
        else return inherited(door);
    }

Alternatively, you could

modify Door
    cannotTravel {
        if (gActor.canSee(self) && !isOpen) {
            if(propDefined(&cannotGoThroughClosedDoorMsg)) 
                reportFailure(cannotGoThroughClosedDoorMsg);
            else
                reportFailure(&cannotGoThroughClosedDoorMsg, self);
        }
        else inherited();
    }
;

and then you could define the messages on each Door object.


Printed text (without conditions) in a beforeAction or roomBeforeAction will appear twice (or more, if you have accompanying Actors!) when the action is directional travel. This is because of the way the library handles travel: there is a TravelAction involved as well as a TravelViaAction. Make sure your text is surrounded by the proper “if’s” in order to be printed.


One of the most elementary lessons for a TADS learner is to remember to call inherited when overriding methods (if you are adding statements rather than replacing the method). Be particularly aware in the situation of creating a Thing-based class and modifying the construct method… if you don’t call inherited the object won’t respond to any vocab words! The normal constructor calls initializeThing which gives vocabulary to the object.

class Apple: Thing 'apple*apples' 'apple' "It's a <<variety>> apple. "
    varieties = ['Winesap','Jonathan','McIntosh']
    variety = nil
    construct {
        local v = rand(varieties);
        variety = v;
        initializeVocabWith(v);
    }
;
// Yikes! Without "inherited", we won't be able to refer to these as "apple"!

The order in which pre-room-title reports appear:
.1. Implicit action announcements
2. Sidekick NPC implicit action announcements
3/4. PushTravel messages
3/4. Messages wrapped in reportBefore
?. GuidedInTravelState.sayDeparting
5. accompanying actor npcTravelDesc
6. travelDesc
7. enteringRoom (text printed within)
I didn’t rigorously test this, but I suspect that PushTravel messages and reportBefore messages will appear in the reverse order that they are reached through code execution. For instance, if a reportBefore occurs both in dobjFor(TravelVia) and enteringRoom in the same command, the message from enteringRoom will appear first because the code reached it last.


roomDaemon is responsible for calling a Room’s atmosphereList. It can be handy, however, to monitor or control other things as well. But one pitfall to avoid is assuming that a given location’s roomDaemon is called on every turn of the game! The only roomDaemon which is called on a given turn is the one for the PC’s location. Therefore roomDaemon is not suitable for monitoring or tracking info about a location that needs to happen in the background as well, i.e. the PC is not there.


If you have an Actor and you want to customize their obeyCommand method, be aware that gDobj is not in effect yet in the action execution cycle. In order to make statements conditional upon the direct object, you need to use getResolvedDobjList, thus:

obeyCommand(issuingActor, action) { 
	if(action.ofKind(SomeAction) && 
            action.getResolvedDobjList().indexOf(someObject))
        return true;
    else return inherited(issuingActor, action);
}

It can be a surprise to find that when you customize an Actor’s specialDesc, your custom desc is not used if that Actor is in a NestedRoom. This is because the call is triggered by showSpecialDescInContents and not the basic showSpecialDesc. For regular Things, showSpecialDescInContents does call their normal specialDesc but for Actors, the method calls listActorPosture. Therefore you have to override one of the involved methods to get the “specialDesc” you want when the Actor is in a NestedRoom.

9 Likes

RANDOM TADBITS #2

Perhaps you would like for an object’s contents to be shown right along with its specialDesc, rather than being posted on a later line with other descs intervening. That is, if you’d prefer

This is the yard.

The wheelbarrow is sitting here. It's carrying a bucket and a ball. 

A mower is out in the grass. 

to

This is the yard.

The wheelbarrow is sitting here.

A mower is out in the grass.

The wheelbarrow contains the bucket and the ball. 

you can make these simple changes:

modify Thing 
    showsCtsInSD = nil 
    showSpecialDesc { 
        inherited; 
        if(showsCtsInSD) 
            examineListContents; 
    }
;
modify Lister 
    contentsListed(obj) { 
        return obj.contentsListed && !obj.showsCtsInSD; 
    }
;
wheelbarrow : [Classes]
    showsCtsInSD = true 
;

If you override the disambigName of an object, and the name doesn’t follow predictable rules concerning being preceded by “a/an”, you will also have to override aDisambigName. For instance, for a disambigName of ‘hour’ the library would produce ‘a hour’ without the second override.


I frequently found that when I defined HelloTopics for certain Actors, they seemed very unsuitable for “implied” greetings, and was annoyed that they were always triggered even when I didn’t enter >GREET ACTOR or >ACTOR, HELLO. You can fix this by making and using a “hello” subclass:

class HelloTopicXpl: HelloTopic matchList = [helloTopicObj] ;
class ByeTopicXpl: ByeTopic matchList = [byeTopicObj] ;

// we have removed impHelloTopicObj from the matchList

There are, to be sure, other ways of getting rid of implied greetings, including tinkering with showGreetingMsg of ConversationReadyState, or impliesGreeting of the topic objects.


Use captureOutput (with the intention of printing the string later) with caution or plenty of testing. I don’t remember my exact scenario any more, but in some set of circumstances captureOutput made a mess when involving defaultReports, defaultDescReports, and certain uses of embedded expressions (double angle bracket expressions).


Be careful with remapTo when in the presence of DistanceConnectors. You can accidentally allow interaction with objects that are supposed to be too distant for that verb. I didn’t do legwork on this, as to whether or not it’s a library bug, or what modifications could improve the behavior. I just noted that it happened to me so I’m passing on the warning.


If using notifyMoveInto, make sure to cater for the newCont parameter being nil.

skunkweed: Thing
    notifyMoveInto(newCont) {
        inherited(newCont);
        newCont.smellsHorrible = true;  //CRASH!
            // first check with "if(newCont)"
            // because it can be nil
    }

It’s easy to get so used to using failCheck that you use it in inappropriate places without being aware of it. (Adv3Lite, I really appreciate how any text in the check method automatically fails the action!) One particularly easy place to do this is in the execAction method of an IAction. failCheck is a Thing method and so has no meaning in an IAction context. You can, however, just use Thing.failCheck (but remembering that is the trick!)

6 Likes

I don’t grok any of this, but I can tell it’s a work of TADSLove. Good work.

Sorry to hear you’re sick. Hope you get well soon.

5 Likes

Yeah, I, too, wish you health. Nice side effect that you have more time for IF, but of course it would be better if you wasn’t ill.

After watching (without envy) all the attention that Inform7 gets in this forum including the many mad scientists I’m glad to see a TADS corner even if it is not yet here.

3 Likes

Thanks, guys! I appreciate it. No worries about the health. It’ll be past soon enough. Other than two somewhat unpleasant nights, it mostly just translates into a period of forced rest and down time.
It actually feels quite fun to leisurely peruse and inspect these old issues now that the period of feverish work and pressure is over!

3 Likes

I like your contribuition; because I’m coding for the IFComp in twine and TADS2, my occasional TADS3 kibitzing have fallen to zero, albeit I have shared a pair of nice TADS2 bits, so your initiative is welcome ! Kudos !

Best regards from Italy,
dott. Piergiorgio.

2 Likes

This is great stuff!

I have a minor quibble with the ofKind example. I’d personally add isCombustible=nil to Thing, and then override on subclasses WoodenItem etc

It makes it easier to rip out one class if you need, and I always worry about initialisation order and dependencies using initializeThing

Of course for more complicated situations, your approach is the way to go.

2 Likes

Thanks, Brett!

I wouldn’t argue with your rationale for the “static ofKind” thing. In my own use case I was trying to define the property isPortable (after the game had already reached a large size) and the class list was quite long (which is why I originally wanted static so it wouldn’t have to reevaluate the expression every time), and also contained some && expressions, so it made more sense to me to try to fell it all in one blow in initializeThing than to hunt down all the individual places where it would apply.
At any rate it’s useful to know that static doesn’t work with ofKind the way a person might be tempted to think!

2 Likes

RANDOM TADBITS #3

OpenableContainer does not automatically call setContentsSeenBy(gActor.visibleInfoTable(), gActor) upon being opened. It supposes that you may want to “look in” the container before contents are considered seen. However, some containers should so patently display their contents upon being opened that you may want to add that line, thus:

chest: OpenableContainer
    dobjFor(Open) {
        action {
            ...
            setContentsSeenBy(gActor.visibleInfoTable(), gActor);
        }
    }

Note that the default openStatus is an uncapitalized string. If for some reason you try to embed it in a desc:

item : Openable, Thing
    someDesc = "Desc of the item. <<openStatus>>"
;
// You'll get "Desc of the item. it's closed."

Use a ‘\^’ if necessary, or modify openStatus.


If it matters to your situation, locations’ atmosphereList messages ought to print before a SensoryEmanation’s “hereWith” messages. Should you wish it to be different, look at misc.t, line 767, where the Daemon that prints sensory messages is given a later-than-default eventOrder value.


It may be tempting to write a noLongerHere message for a SensoryEmanation, worded such as to imply the [sound] source has stopped emanating. But in fact, noLongerHere will also be called if the PC leaves the range of [hearing the sound], so it’s important to write the message appropriately. If it is a [sound] source that can be turned off, and you don’t care to print a message about not hearing it any more because you’ve left, you can do something like:

noLongerHere = "<<if me.canSee(theSoundSource)>>The source seems to have stopped noising. "

Don’t define two DefaultXXXTopics in the same Actor that can be active at the same time! (Two DefaultGiveTopics, etc.) A run-time error will be thrown from findTopicResponse although I didn’t investigate why.


Pitfall alert! If you define a Distant object, any component or contents objects that you define under it with + will not be therefore considered distant; they have to be classed as Distant individually.


I found that a number of times when I used askForDobj or askForIobj in the handler of a custom verb, the library would not actually prompt the player to supply an object, but would pick its own object, one that was by no means clearly more logical than others. It wasn’t till the game was practically done that I found out this behavior could have been controlled: if indeed you want to force the parser to prompt the player for an object, look at the getImpliedObject method of EmptyNounPhraseProd.


Mix-in classes for Thing objects which only add properties and methods can safely subclass from object. But if you want the mix-in class to add vocabWords to the instances, make sure to subclass from VocabObject!

class WoodenItem : object 
    vocabWords = 'wood wooden -'
;       // WON'T WORK!

It seems strange to me that by default, canTouch returns true for Distant objects. You may wish to do as I did and modify:

modify Thing
    canTouch(obj) { 
        if(obj.ofKind(Distant)) return nil;
        else return inherited(obj);
    }
;

“Following” Actors get sent through a TravelConnector before the PC. In the case of enteringRoom:

room1 : Room 
    enteringRoom(traveler) {
        if(traveler==followingPal) 
            "Following pal makes comments upon arrival. ";
    }

the text above will not be seen by the player because the PC will still be in the old room (different sense scope) as the following pal when it’s printed. The above example would probably be better handled by an InitiateTopic, but if you do want to print a string in enteringRoom cued by a following actor, you probably could/should use callWithSenseContext.


Don’t use failCheck in a noteTraversal method! If you have an accompanying actor, that actor will be sent ahead to the new location before it is determined that the PC can’t use the connector, and will be “stranded” past a barrier the PC can’t cross. Use canTravelerPass instead, and the accompanying actor will stay with the PC.

5 Likes

A smaller dump, but this completes my “scratch pad” list of items that were all put in one place… from here on out I’ll have to wade through my source code itself and extract the rest, probably much more slowly.

RANDOM TADBITS #4

With Noises, take into consideration whether you should set isMassNoun or isQualifiedName, or else your transcripts might end up with things like "You can’t do that to a singing. "


It may be useful to realize that whatever a TopicEntry is marked as for isConversational, any AltTopics added under it will copy the same value without having to be defined themselves.


Ordinarily, getEnteredVerbPhrase will return a string with placeholders for the grammatical objects, like: ‘put (dobj) in (iobj)’. Be aware (for testing purposes etc. where you may be making assumptions about what the method returns) that in the case of implicit actions, the parenthesized placeholders are replaced with actual object names! Have not yet dug to the bottom of that, but the answer is probably somewhere around action.t, line 757, where if the condition is not satisfied, literal text is added to the string instead of the “(dobj)” etc. placeholder.


Something to be aware of: if in a certain location or situation you wish to prevent a wide range of actions with one particular message, beforeAction is not going to prove as helpful as you might hope. Again, if the point is that you really want this situation-specific fail message to show, it’s probably not going to in a lot of cases because beforeAction comes after verify, and so a lot of actions will just be failing with stock responses. I can’t remember what prompted me to make this note, or what I did instead, but this approach would be my suggestion, for adv3 perhaps around line 1346 of action.t in doActionOnce before verifyAction is called.


I found a number of NestedRooms or Vehicles in my game where I wasn’t interested in hearing the customary “You are in the [obvious place].” acknowledgment on every EXAMINE or SEARCH. A quick fix is:

modify BasicLocation
    showPCSD = true
;
modify me
    useSpecialDesc = inherited && location.showPCSD 
;
bench : Chair
    showPCSD = nil
;

I’m afraid I don’t recall the exact circumstances of this one. But I believe that if you try to make a Booth a MultiLoc, if there is a situation where an Actor is reported in that Booth by remoteSpecialDesc (probably because of a DistanceConnector), then you will get a run-time error. I never troubleshot the whole thing at the time, and ended up implementing the Booth as two objects rather than a MultiLoc.
[Edit: I couldn’t reproduce the error, but I know it was a hair-pulling experience at the time.]


I seem to recall getting errors by trying to locate MultiInstance objects within Booths (or perhaps any Container). If I get around to recreating that scenario I’ll report more on it.
[Edit: I tried a test scenario and it seemed to work, but I know that at some point there was some complication that drove me to abandon using MultiInstance to populate several similar Booths with similar features, and put those features under each separate Booth with + instead.]

4 Likes

Might’ve been me; I’ve written a lot of code involving that kind of thing.

The thing I’d point out is that you can’t use afterAction() to permit an action that fails in verify(), and you can’t use it to change failure message. But you can fail an action that would otherwise succeed.

What this means is that, in general, “dynamic” things want non-static failures handled in their action() stanza (or in the code for whatever is blocking it) and not in verify().

Or put in slightly different terms verify() should be used for things that are always going to be true, while action() (or external code) is for things that may or may not be true depending on the situation.

2 Likes

Sorry @jbg, the “pre-verify” thing I was trying to remember is actually a recent feature of @Eric_Eve 's mentioned here. And in regards to my tidbit about beforeAction not being suitable for implementing a catch-all fail message, for adv3, implementing something similar to Eric’s preAction routine, perhaps around line 1346 of action.t in doActionOnce before verifyAction is called would be my suggestion.

1 Like

It can be easy to miss that Directions actually have a string representation name property. This property doesn’t appear in the Library Reference Manual because in the source code it’s added by a modify under a #define in en_us.t, along with the backToPrefix property.


RANDOM TADBITS #5: HELPER FUNCTIONS

/*
A simple but handy function for testing, or even behind-the-scenes 
in-game situations. Make the game execute a string as if it were
input just entered at a prompt.
*/

execStr(str) { 
	if(!str) return; 
	local toks = Tokenizer.tokenize(str);
	executeCommand(gPlayerChar, gPlayerChar, toks, true);
}

/*
Get the length of a `LookupTable`, string, `StringBuffer`, 
`List` or `Vector` with one syntax.
*/

len(val) { 
    if(!val) return nil; 
	local arg = dataType(val); 
	if(arg not in (TypeSString, TypeList) && 
			!val.ofKind(Vector) && 
            !val.ofKind(StringBuffer) && 
            !val.ofKind(LookupTable)) {
		"Incorrect argument type for \"len\"\n"; 
		return nil; 
    }
	else if(!val.ofKind(LookupTable)) return val.length();
	else return val.getEntryCount(); 
}

/*
Check if a token is one of the dobj/iobj words, 
with nilobjref protection.
*/

isDobjWord(str) {
	if(gAction && gDobj && gAction.getDobjWords!=nil && 
            gAction.getDobjWords.indexOf(str)) 
        return true;
	return nil;
}

isIobjWord(str) {
	if(gAction && gIobj && gAction.getIobjWords!=nil && 
            gAction.getIobjWords.indexOf(str)) 
        return true;
	return nil;
}

/*
Determine if the player used a specific token in input.
*/

didPlayerType(str) { 
    return gAction && gAction.getOrigText.find(str); 
}

/*
Query whether graphics or sound can be 
used at this juncture in the code.
*/

qGraphics() { 
    return systemInfo(SysInfoInterpClass) == SysInfoIClassHTML && 
            systemInfo(SysInfoPrefImages); 
}
qSounds() { 
    return systemInfo(SysInfoInterpClass) == SysInfoIClassHTML && 
            systemInfo(SysInfoPrefSounds); 
}

/*
Get a random integer within a range.
*/

randRange(min,max) { 
    local range = new Vector(max - min + 1);
	while (min <= max) { 
        range.append(min); 
        ++min; 
    } 
    return rand(range); 
}

/*
For testing situations where you need objects to be 
"known" in order to ASK ABOUT them, etc. this 
function lets you set any amount of args as "known" 
either in-session with a dynamic function or in startup 
code. Or just as good for shorthand in real code 
instead of gSetKnown in multiple statements. 
*/

know(...) { 
    for(local i in 1..argcount) 
        gSetKnown(getArg(i)); 
} 

/*
Get a string representation of the current command's 
`Direction`. It can be useful for checks in `beforeTravel` 
or `PushTravel` actions, or embedded in travel 
messages that can be triggered by more than one 
direction. 
*/

gDir() { 
	if(gAction.propDefined(&getDirection) && gAction.getDirection()!=nil)
		return gAction.getDirection().name;
	else if(gAction.getOriginalAction().propDefined(&getDirection) && 
		    gAction.getOriginalAction().getDirection()!=nil)
		return gAction.getOriginalAction().getDirection().name;
	else if(gAction.parentAction && 
            gAction.parentAction.propDefined(&getDirection) && 
		    gAction.parentAction.getDirection()!=nil)
		return gAction.parentAction.getDirection().name;
	else return nil ; 
}

/*
`gDobj` isn't always in effect at every stage of the 
remapping process. For purposes of writing conditions 
in `maybeRemapTo`, `rDobj` checks `gTentativeDobj` 
if there is no `gDobj`. (Likewise for gIobj.)
`dobjFor(AttackWith)  
   maybeRemapTo( rIobj(SharpWeapon), StabWith, self, IndirectObject)`
*/

rDobj(obj) { 
	if(obj.isClass()) {
		if(gDobj) 
            return gDobj.ofKind(obj);
		return gTentativeDobj && gTentativeDobj.length > 0 &&
			    gTentativeDobj.indexWhich({x:x.obj_.ofKind(obj)}); 
	}
	else if(gDobj) 
        return gDobj==obj;	
	return gTentativeDobj && gTentativeDobj.length > 0 &&
	        gTentativeDobj.iW({x:x.obj_==obj}); 
}
rIobj(obj) { 
	if(obj.isClass()) {
		if(gIobj) 
            return gIobj.ofKind(obj);
		return gTentativeIobj && gTentativeIobj.length > 0 && 
				gTentativeIobj.indexWhich({x:x.obj_.ofKind(obj)}); 
	}
	else if(gIobj) 
        return gIobj==obj;	
	return gTentativeIobj && gTentativeIobj.length > 0 && 
            gTentativeIobj.iW({x:x.obj_==obj}); 
}

/*
Count how many objects the PC can see which 
fulfill the condition or are of the same class. While 
the plurality or actual quantity can be useful to 
find sometimes, this function is also handy for 
finding single objects by being analogous to 
`gPlayerChar.canSee` in the way that `indexWhich` 
is to `indexOf`.
*/

numSeen(func) { 
    local lst = gPlayerChar.visibleInfoTable.keysToList;
	return lst.countWhich(func); 
}

/*
Essentially a `valWhich` from among all objects 
that are in scope for `gActor`.
*/

objWhich(func) { 
	if(gActor==nil) return nil;
	return gActor.connectionTable.keysToList.valWhich(func); 
}
2 Likes

RANDOM TADBITS #6: MISC. FUNCTIONS

If your game, as mine did, includes a lot of descriptions of heights, widths, or other distances, you’ll be loved by the playing public if you include modes for both U.S. standard and metric measurements. From the States, I wrote my descriptions from an inches/miles standpoint and had the game compute metric values if the player selected METRIC mode. (By the way, I’m not aware of anyone that’s played Prince Quisborne and used (or even noticed?) the METRIC mode, so if you are one, I’d like to hear about it!). Usage looks like:

desc = "The wall is <<meas(12,feet,'about')>> tall. "

Which gets you, depending on the selected mode:
"The wall is about twelve feet tall. "
"The wall is around three and half meters tall. "

modify libGlobal
    useStdMeas = true 
;
#define mStd libGlobal.useStdMeas

enum feet, yards, inches, miles; 

meas(int, denom, adj?) { 
	local ret; 
	if(mStd) ret = (adj ? adj + ' ' : '') + spellInt(int) + ' ' + denom;
	else { 
		switch(denom) { 
			case inches: ret = roundMeas(int * 2.54,true) + '  centimeters';  break; 
			case feet: 
				local met = int * 0.3048;
				local rd = roundMeas(met);
				ret = (int > 50 ? spellInt(toInteger(met)) : rd) + 
						' meter<<if !rd.find(R'one$')>>s';  break;
			case yards: ret = spellInt(toInteger(int * 0.914)) + ' meters'; break; 
			case miles: ret = roundMeas(int * 1.609) + ' kilometers'; break;
		} 
	} 
	return ret; 
} 

roundMeas(n,inch?) { 
    local nw = toInteger(n.getWhole()); 
	local whole = spellInt(nw); 
	local frac = n.getFraction(); 
	if(frac < .2) return whole; 
	else if(frac < .33) return 'a little over ' + whole;
	else if(frac < .66) return rand('about ','around ') + whole + (inch ? '':' and a half');
	else if(frac < .8) return 'a little under <<spellInt(nw + 1)>>';
	else return spellInt(nw + 1); 
	} 

VerbRule(metric) 'metric' ('mode'|) ('on'|'off'|) : IAction
    execAction { 
        libGlobal.useStdMeas = !libGlobal.useStdMeas; 
        if(!mStd) "The game is now in metric measurement mode. <<first time>>NOTE: The metric descriptions are automatically generated, and may cause the prose to have a more mechanical feel. ";
        else "The game is now in US customary measurement mode. "; 
    } 
	actionTime = 0 ;

The getTime function returns the weekday as an integer. This function gets you the string:

printWeekday(daynum) { 
	switch(daynum) {
		case 1: return 'Sunday';
		case 2: return 'Monday';
		case 3: return 'Tuesday';
		case 4: return 'Wednesday';
		case 5: return 'Thursday';
		case 6: return 'Friday';
		case 7: return 'Saturday';
		default: return nil;
	} 
}

Timed text is almost universally hated(?), but there can be times where it can be used to good effect. Here’s at least one way of accomplishing it in TADS 3. The delay parameter is milliseconds.

typeOut(str,delay = 20) { 
    local was = (gTranscript ? gTranscript.isActive : nil); 
    if(gTranscript) { 
        gTranscript.flushForInput();
	    gTranscript.deactivate();
    } 
    local lgth = str.length; 
    for(local i in 1..lgth) { 
        "<<str.substr(i,1)>>"; 
        timeDelay(delay); 
    } 
    if(gTranscript) 
        gTranscript.isActive = was; 
}

Dealing with binary? These were my hacks to convert base 10 to binary, or to add binary values.

binConv(x) {
    local vec = new Vector(15); 
    if (x>1) {
		while (x >= 2) {
            vec.prepend(x % 2); x /= 2; 
        }
		vec.prepend(1); 
		return vec.join; 
    }
	else if (x==1) return 1; 
	else return 0; }

addBinary(val1,val2) {      // deals in strings
	local a = new Vector(val1.split()); 
	local b = new Vector(val2.split()); 
	local c = new Vector(16); 
	local carry = nil; 
	for(local i = 15; i >= 1; i -=2) { 
		local wasCarry = carry; 
        local ax = a[i];
		if(carry) { 
            if(a[i]=='1') a[i] = '0'; 
            else { 
                a[i] = '1'; 
                carry = nil; 
            } 
        }
		if(a[i]!=b[i]) c.prepend('1');
		else { 
            c.prepend('0'); 
			if(wasCarry && ax=='1'||wasCarry && b[i]=='1'||b[i]=='1'&& ax=='1') 
                carry = true; 
			else carry = nil; 
        }		
    }
	if(carry) c.prepend('1'); 
	else c.prepend('0'); 
	return c.join(' '); 
}
3 Likes

There’s nothing wrong with rolling your own, but TADS3 provides native methods for converting to and from binary.

  • To get the binary representation of an integer n in a string use sprintf('%b', n)
  • To convert a string str containing the binary representation of an integer into an integer use toInteger(str, 2)

These will probably be substantially more performant than anything implemented in TADS3 (because they have the advantage of executing in native code for the underlying platform instead of running in the T3 VM). Although you’re probably not doing a lot of binary arithmetic this way.

2 Likes

Well noted, @jbg! I’m not sorry, in the end, that I wrote my own, since it was a fun part of my early programming self-education. I never imagined my binary functions were top performers, even in TADS code.

While I’m here, an addendum to the misc. functions tidbit:

Get the opposite of a direction (keyed by dirProp). I used it for dynamically creating Rooms with reciprocal TravelConnectors.

oppDir(dirProp) { 
	switch(dirProp) {
		case &north: return &south; 
		case &east: return &west; 
		case &west: return &east; 
		case &south: return &north; 
		case &northeast: return &southwest; 
		case &southwest: return &northeast;
		case &northwest: return &southeast; 
		case &southeast: return &northwest; 
		case &up: return &down; 
		case &down: return &up;
		default: return nil; 
	} 
}
1 Like

As a stylistic note I’d say that whenever you see that you’ve written a method/function that’s just mapping one value to another value via a big switch statement you should probably consider re-writing it as a data structure. A array (List or Vector in T3) if the key value is an integer and an element’s index value could work as a key, or a table (LookupTable in T3) otherwise.

2 Likes

Just scrolling back through some of these and noticed that this is another one where there’s a builtin mechanism for this in T3.

If you #include <date.h> that gives you the Date class, which gives you access to a bunch of nuts-and-bolts methods for working with dates and times.

You can use Date.formatDate() to get various bits and pieces of information about the date formatted in different ways. For the spelled-out day of week (“Monday”) it’s formatDate('%A'). To get the short version (“Mon”) use formatDate('%a').

There are a bunch of other options, documented in the page on the Date class in the TADS3 System Manual.

Again, nothing wrong with rolling your own. Main argument for using the Date class is that it takes care of all the fiddly tricky bits in working with dates (including things like localization). If all you care about is mapping an integer to a day-of-week string that’s not a big deal, but if you’re messing around with dates/times in general (i.e., if you ever have to compute a date/time interval or display a date in a format other than the one that’s the default on the computer you’re writing the game on) it’ll pay for the cost of learning to use the class.

1 Like

adv3Lite has a nice solution as well. The CompassDirection and ShipboardDirection objects have an opposite property:

northDir: CompassDirection
    dirProp = &north
    // ...
    opposite = southDir
;
4 Likes

No disagreement from me about the theory there. The quaint and curious thing about Prince Quisborne is that it is both the training wheels of my programming self-education and my magnum opus (let no one mistake: I will never attempt to write a game of that magnitude again) at the same time.
There are a host of things in the codebase that, looking back on, I see how I could do differently, but since I got it to work with the knowledge I had at the time I wrote it, I moved on to more pressing things.

5 Likes